---
title: Configure Logging
---
# Configure Logging
Configure structured logging with multiple output formats and integrate with log aggregation systems.
#### Before you begin
- SMG [installed](index.md#install)
- Completed the [Getting Started](index.md) guide
- Log aggregation system (optional): Elasticsearch, Loki, or similar
---
## Configuration Options
SMG supports flexible logging configuration via CLI flags or environment variables. The gateway writes its logs to stdout, and also to files when `--log-dir` is set. Timestamps are in UTC.
### CLI Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--log-level` | `info` | Log level for SMG's own crates: `debug`, `info`, `warn`, `error` |
| `--log-json` | `false` | Output logs as JSON |
| `--log-dir` | None | Directory for daily log files (enables file logging); created at start-up when missing — an existing path must be a writable directory (see [File Logging](#file-logging)) |
| `--log-mm-timing` | `false` | Log per-request multimodal timing at INFO (see [Multimodal Timing Logs](#multimodal-timing-logs)) |
### Environment Variable
Without `RUST_LOG`, SMG builds its log filter from `--log-level`: SMG's own crates log at that level and every dependency (hyper, tonic, and so on) at `warn`. Setting `RUST_LOG` replaces that filter entirely, and `--log-level` is then ignored; a `RUST_LOG` value that fails to parse is itself ignored, and `--log-level` applies. A target that matches no `RUST_LOG` directive is not logged at all, so start the value with a base level:
```bash
# Everything at debug, dependencies included
RUST_LOG=debug smg launch --worker-urls http://worker:8000
# Dependencies at warn, SMG at info, routing policies at debug
RUST_LOG=warn,smg=info,smg::policies=debug smg launch --worker-urls http://worker:8000
```
A directive matches every target that starts with it, so `smg` covers the gateway's `smg::...` targets and crates such as `smg_mesh` and `smg_rl`. Other SMG crates, such as `tool_parser`, `reasoning_parser`, `llm_tokenizer`, and `llm_multimodal`, log under their own names and follow the base level unless you name them.
---
## Log Levels
| Level | Description | Use Case |
|-------|-------------|----------|
| `error` | Error conditions only | Production (minimal) |
| `warn` | Warnings and errors | Production (recommended) |
| `info` | Informational messages, including a line for each request and response | Production (verbose) |
| `debug` | Debug information, including routing decisions | Development, troubleshooting |
`trace` is not accepted by `--log-level`; set it through `RUST_LOG`. With OpenTelemetry tracing enabled, keep SMG at `info` or more verbose: the request spans are INFO-level, and the same filter applies to them. At `warn` and `error` the request span is filtered out, so log lines also carry no span fields such as `request_id`.
### Set Log Level
```bash
# Via flag
smg launch --worker-urls http://worker:8000 --log-level debug
# Via environment
RUST_LOG=info smg launch --worker-urls http://worker:8000
```
---
## Output Formats
### Plain Text (Default)
Human-readable lines: UTC timestamp, level, the enclosing request span with its fields, target, source location, and message. On stdout the lines carry ANSI color codes; files written with `--log-dir` do not.
```
2026-09-24 10:30:45 INFO http_request{method=POST uri=/v1/chat/completions version=HTTP/1.1 module="smg" request_id="chatcmpl-4qD9pXvR2mT7sLk0aZ3nB8wE" status_code=200 latency=1523}: smg::response: model_gateway/src/middleware/logging.rs:138: finished processing request
```
### JSON Format
Machine-readable format for log aggregation, one JSON object per line:
```bash
smg launch --worker-urls http://worker:8000 --log-json
```
Output (pretty-printed here):
```json
{
"timestamp": "2026-09-24 10:30:45",
"level": "INFO",
"message": "finished processing request",
"target": "smg::response",
"filename": "model_gateway/src/middleware/logging.rs",
"line_number": 138,
"span": {
"latency": 1523,
"method": "POST",
"module": "smg",
"request_id": "chatcmpl-4qD9pXvR2mT7sLk0aZ3nB8wE",
"status_code": 200,
"uri": "/v1/chat/completions",
"version": "HTTP/1.1",
"name": "http_request"
},
"spans": [
{
"latency": 1523,
"method": "POST",
"module": "smg",
"request_id": "chatcmpl-4qD9pXvR2mT7sLk0aZ3nB8wE",
"status_code": 200,
"uri": "/v1/chat/completions",
"version": "HTTP/1.1",
"name": "http_request"
}
]
}
```
The event's own fields (`message` and any structured fields) sit at the top level. Fields of the enclosing spans are under `span` (the innermost span) and `spans` (all of them, outermost first), so the request ID is at `span.request_id`. `latency` is in microseconds.
---
## Request and Response Logs
Every request that matches a route on the main listener is logged inside an `http_request` span that carries `method`, `uri`, `version`, and `request_id`, plus `status_code` and `latency` (microseconds) once the response is ready:
| Target | Level | Message | When |
|--------|-------|---------|------|
| `smg::request` | INFO | `started processing request` | A request arrives |
| `smg::response` | INFO | `finished processing request` | Any response below 400 |
| `smg::response` | WARN | `request failed with client error` | A 4xx response |
| `smg::response` | ERROR | `request failed with server error` | A 5xx response |
| `smg::response` | ERROR | `response stream failed after the head was sent` | The response body failed after streaming began |
The logging layer writes one ERROR line per 5xx response; releases before v1.10.0 also logged a duplicate `tower_http` failure line for it (smg-project/smg#2124). A request to an unknown path gets a bare `404` and is not logged.
### Health Probe Logs
Requests to `/health`, `/readiness`, and `/liveness` log both lines at DEBUG, whatever the status; the response line reads `finished probe request` (smg-project/smg#2124). A `503` from `/readiness` while workers are still loading is an expected state, so polling probes no longer fill the log with ERROR lines. Probes served on the dedicated `--health-check-port` listener bypass the logging layer entirely.
### Other Structured Logs
| Target | Level | Message | When |
|--------|-------|---------|------|
| `smg::policies::*` | DEBUG | Routing decisions | See [Routing Decision Logs](#routing-decision-logs) |
| `smg::audit` | INFO | `control_plane_audit` | Control-plane requests, when control-plane authentication is configured. The Rust binary logs them by default (turn off with `--disable-audit-logging`); the pip `smg launch` logs them only with `--control-plane-audit-enabled` |
| `smg_rl` | INFO | `rl.proxy`, `rl.fanout` | RL control plane calls, with `--enable-rl` |
---
## Routing Decision Logs
At DEBUG, the routing policies log one structured line per selection decision, naming the branch taken and the worker chosen, so you can see why a request landed on a given worker (smg-project/smg#2210). The lines are off at the default `info` level. Turn them on for the policy targets only:
```bash
RUST_LOG=warn,smg=info,smg::policies=debug smg launch --worker-urls http://worker:8000 --policy cache_aware
```
| Message | Target | Fields |
|---------|--------|--------|
| `Cache-aware selection` | `smg::policies::cache_aware` | `index` (`tree` or `hash`), `branch`, `worker`, `model_id`; tree decisions add `matched_ratio` and `threshold`, hash decisions add `level` |
| `Sticky routing decision` | `smg::policies::registry` | `source` (`rid` or `header`), `key`, `branch`, `worker`, `model_id` |
| `Prefix-hash selection` | `smg::policies::prefix_hash` | `branch`, `level`, `worker`, `model_id` |
Branch values:
- **Cache-aware, tree index**: `tree_match`, `spill`, `expected_wait_fallback`, `first_healthy_fallback`, the same values as `smg_cache_aware_policy_branch_total`.
- **Cache-aware, hash index**: `hash_hit`, `hash_spill`, `short_request` (shorter than the smallest `--cache-boundaries` value), `expected_wait_fallback`, `kv_pressure_expected_wait`.
- **Cache-aware under KV-cache imbalance**: `kv_pressure_expected_wait`; this line has no `index` field.
- **Sticky sessions** (`--routing-key-override`): `occupied_hit`, `occupied_miss`, `vacant`, `cap_respill`, `no_healthy_workers`. `key` is the internal map key, which combines the model, the PD leg, and the routing key.
- **Prefix hash**: `ring_hit`, `load_balance_walk`, `fallback_least_load`, `no_routing_key`, `no_healthy_workers`.
When cache-aware routing uses KV events, it logs `Event-driven routing: overlap match` (`branch` is `event_hit` or `event_spill`) or `Event-driven routing: no overlap, expected-wait fallback`, each with `worker` and `model_id`.
See the [Metrics Reference](../reference/metrics.md#routing-policy-metrics) for the matching decision counters.
---
## Multimodal Timing Logs
`--log-mm-timing` logs per-request multimodal timing at INFO (smg-project/smg#2625). Each line's message starts with `smg_mm_timing`, followed by the stage:
| Message | Contents |
|---------|----------|
| `smg_mm_timing process_multimodal_plan` | Media counts (`image_count`, `audio_count`, `video_count`, `video_frame_count`), per-phase times (`media_fetch_decode_ms`, `config_lookup_ms`, `preprocess_ms`, `token_expand_ms`, `total_ms`), and token counts before and after placeholder expansion (`original_tokens`, `expanded_tokens`) |
| `smg_mm_timing mm_tensor_payload_inline`, `smg_mm_timing mm_shm_write` | Tensor transport: inline sends and shared-memory writes |
| `smg_mm_timing assemble_tokenspeed`, `smg_mm_timing assemble_tokenspeed_item`, `smg_mm_timing tokenspeed_shm_write_direct` | TokenSpeed request assembly |
| `smg_mm_timing video_decode_backend`, `smg_mm_timing video_tempfile_write` | Video decoding (target `llm_multimodal::media`) |
```bash
smg launch --worker-urls grpc://worker:50051 --model-path Qwen/Qwen2.5-VL-7B-Instruct --log-mm-timing
```
The older `SMG_LOG_MM_TIMING` environment variable (`1`, `true`, `yes`, or `on`) still turns the logs on when the flag is absent, but it is deprecated: SMG logs a warning, and support ends in the next minor release. v1.11.0 also stopped logging where each request's media is processed (smg-project/smg#2594); that is counted by `smg_mm_processing_total` instead. See [Multimodal](../concepts/architecture/multimodal.md) for the processing pipeline.
---
## File Logging
Enable persistent log files with automatic daily rotation.
### Enable File Logging
```bash
smg launch \
--worker-urls http://worker:8000 \
--log-dir /var/log/smg \
--log-level info
```
### Features
- **Same content as stdout**: plain text or JSON (with `--log-json`), without ANSI colors
- **Daily rotation**: a new file starts at midnight UTC
- **File naming**: `smg.YYYY-MM-DD`, with no extension
- **No deletion**: old files are kept; see [Log Rotation](#log-rotation)
- **Non-blocking I/O**: a background thread writes the files; if it falls more than 128,000 lines behind, new file lines are dropped rather than slowing requests
- **Directory creation**: SMG creates the directory if it does not exist
### Log Directory Requirements
An existing `--log-dir` must be a directory SMG can write. A missing one is created at start-up, so it must sit under a directory SMG can write; a relative path resolves against the working directory.
Any other path — an existing file, a directory SMG cannot write, a path under a file or under an unwritable directory — is refused at startup with a configuration error naming `log_dir`, the path, and the reason, before the gateway binds its port.
Refusing to start is newer than v1.12.0. In v1.12.0 and earlier, a directory SMG cannot create is reported on stderr as `Failed to create log directory: ` and SMG runs with logging disabled entirely, stdout included; an existing directory SMG cannot write its log file to makes startup panic with `initializing rolling file appender failed`.
### Example Directory Structure
```
/var/log/smg/
├── smg.2026-09-22
├── smg.2026-09-23
└── smg.2026-09-24
```
---
## Docker Logging
### Basic Configuration
```yaml title="docker-compose.yml"
services:
smg:
image: lightseekorg/smg:latest
command: >
--worker-urls http://worker:8000
--log-json
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"
```
### Production Configuration
```yaml title="docker-compose.yml"
services:
smg:
image: lightseekorg/smg:latest
command: >
--worker-urls http://worker:8000
--log-level info
--log-json
--log-dir /var/log/smg
volumes:
- smg-logs:/var/log/smg
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
volumes:
smg-logs:
```
The image runs as the non-root user `smg` (UID 65532), so `--log-dir` must be writable by that user. `/var/log/smg` does not exist in the image, so Docker creates the new named volume owned by root; change its owner to UID 65532 before the first start, or SMG refuses to start with a configuration error naming `log_dir` as not writable (see [Log Directory Requirements](#log-directory-requirements)).
---
## Kubernetes Logging
### Basic Pod Logging
```bash
# Follow logs
kubectl logs -n inference -l app=smg -f
# Previous container logs
kubectl logs -n inference -l app=smg --previous
# All containers in pod
kubectl logs -n inference --all-containers
```
### Deployment Configuration
```yaml title="smg-deployment.yaml"
apiVersion: apps/v1
kind: Deployment
metadata:
name: smg
spec:
template:
spec:
containers:
- name: smg
args:
- --worker-urls=http://worker:8000
- --log-level=info
- --log-json
```
To turn on debug output for one area only, set `RUST_LOG` on the container (for example `warn,smg=info,smg::policies=debug`); it replaces the `--log-level` filter.
---
## Log Aggregation
### Grafana Loki
```yaml title="promtail-config.yaml"
server:
http_listen_port: 9080
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: smg
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: smg
action: keep
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
pipeline_stages:
- json:
expressions:
level: level
target: target
request_id: span.request_id
- labels:
level:
target:
```
### Elasticsearch + Fluentd
```yaml title="fluentd-config.yaml"
@type tail
path /var/log/containers/smg*.log
pos_file /var/log/fluentd-smg.pos
tag smg.*
@type json
@type parser
key_name log
@type json
time_key timestamp
time_format %Y-%m-%d %H:%M:%S
utc true
@type elasticsearch
host elasticsearch
port 9200
index_name smg-logs
include_timestamp true
```
SMG's `timestamp` has second precision and no time-zone suffix; it is always UTC. The filter above parses it as the event time, and `include_timestamp` stores that time as `@timestamp`.
---
## Log Queries
### Loki/LogQL
LogQL's `json` parser flattens nested fields with `_`, so the request ID is `span_request_id`.
```logql
# All SMG logs
{app="smg"}
# Error logs only
{app="smg"} | json | level="ERROR"
# Specific request
{app="smg"} | json | span_request_id="chatcmpl-4qD9pXvR2mT7sLk0aZ3nB8wE"
# Server errors returned to clients
{app="smg"} | json | target="smg::response" | level="ERROR"
# Cache-aware decisions that spilled away from the prefix holder (needs smg::policies=debug)
{app="smg"} | json | message="Cache-aware selection" | branch="spill"
# Multimodal timing (needs --log-mm-timing)
{app="smg"} |= "smg_mm_timing"
```
### Elasticsearch/Kibana
```json
{
"query": {
"bool": {
"must": [
{ "match": { "target": "smg::response" } },
{ "match": { "level": "ERROR" } }
],
"filter": [
{ "range": { "@timestamp": { "gte": "now-1h" } } }
]
}
}
}
```
---
## OpenTelemetry Integration
When OpenTelemetry is enabled, the gateway's request events are logged at a higher level:
| OTEL Enabled | Event Level | Behavior |
|--------------|-------------|----------|
| Yes | INFO | Exported to OTLP collector |
| No | DEBUG | Not exported (local only) |
These are the HTTP routers' `Sending request`, `Sending concurrent requests`, and `Received concurrent requests` events, which are attached to the request's trace. Other log lines are not sent to the collector; only the request spans and these events are exported. See [OpenTelemetry Tracing](monitoring.md#opentelemetry-tracing).
```bash
# Enable OTEL with logging
smg launch \
--worker-urls http://worker:8000 \
--enable-trace \
--otlp-traces-endpoint localhost:4317 \
--log-level info
```
---
## Request Correlation
### Request ID Propagation
Every request that matches a route gets an ID. SMG takes it from the first of these request headers that is present: `x-request-id`, `x-correlation-id`, `x-trace-id`, `request-id` (replace the list with `--request-id-headers`). Otherwise it generates one with an OpenAI-style prefix (`chatcmpl-`, `cmpl-`, `gnt-`, `resp-`, `msg_`, or `req-`) followed by 24 random letters and digits. The ID is returned in the `x-request-id` response header and recorded as `request_id` on the request's `http_request` span, so it appears on the lines logged while the request is handled, as long as SMG logs at `info` or more verbose.
```bash
# Send request with custom ID
curl -H "X-Request-ID: my-trace-123" \
-H "Content-Type: application/json" \
http://localhost:30000/v1/chat/completions \
-d '{"model": "llama", "messages": [{"role": "user", "content": "Hi"}]}'
```
Backend (engine) request ids derive from the same id, so gateway and worker
logs correlate: gRPC requests are dispatched as `{request-id}-{uuid}` (the
suffix keeps retries and tool-loop iterations unique), and the response body
`id` matches. A protocol-level `rid` field (chat, completions, messages,
generate, embeddings, classify) overrides this and is used verbatim outside
PD mode.
### Trace a Request
```bash
# Find all logs for a request
kubectl logs -n inference -l app=smg | grep "my-trace-123"
# In Loki
{app="smg"} | json | span_request_id="my-trace-123"
```
---
## Log Rotation
### File Retention
SMG starts a new file at midnight UTC but never deletes old ones. Prune them on a schedule, for example from cron:
```bash
# Delete SMG log files older than 7 days
find /var/log/smg -name 'smg.*' -mtime +7 -delete
```
### Docker
```yaml
logging:
driver: json-file
options:
max-size: "100m"
max-file: "5"
```
---
## Verification
```bash
# Check log output
smg launch --worker-urls http://worker:8000 --log-level debug | head -20
# Verify JSON format (the startup banner before the logs is plain text, so skip non-JSON lines)
smg launch --worker-urls http://worker:8000 --log-json | jq -R 'fromjson?'
# Test log level filtering: no INFO lines should appear (stop with Ctrl-C)
smg launch --worker-urls http://worker:8000 --log-level warn | grep INFO
# Check file logging
smg launch --worker-urls http://worker:8000 --log-dir /tmp/smg-logs &
ls -la /tmp/smg-logs/
```
---
## Troubleshooting
??? question "No logs appearing"
1. Check log level is not too restrictive:
```bash
smg launch --log-level debug ...
```
2. The gateway logs to stdout, not stderr (the Python launchers may print their own messages to stderr):
```bash
smg launch --worker-urls http://worker:8000 | head
```
3. Check RUST_LOG isn't overriding:
```bash
unset RUST_LOG
smg launch --log-level info ...
```
4. An unusable `--log-dir` does not silently disable logging: SMG refuses to start with a configuration error naming `log_dir`, so a gateway that is running still logs to stdout. v1.12.0 and earlier behaved differently; see [Log Directory Requirements](#log-directory-requirements).
??? question "Logs not in JSON format"
1. Ensure `--log-json` flag is set:
```bash
smg launch --log-json --worker-urls http://worker:8000
```
2. Lines printed before logging is set up are plain text: the startup banner goes to stdout ahead of the JSON logs, and startup errors, such as a refused `--log-dir`, go to stderr
??? question "File logs not appearing"
1. Check directory exists and is writable:
```bash
mkdir -p /var/log/smg
chmod 755 /var/log/smg
```
2. Verify `--log-dir` flag is set correctly
3. Files are named `smg.YYYY-MM-DD` with no `.log` extension, so a `*.log` glob does not match them
??? question "Log aggregator not receiving logs"
1. Verify JSON format is enabled
2. Check network connectivity to aggregator
3. Verify log format matches parser expectations: span fields such as `request_id` are nested under `span`, and `timestamp` uses `%Y-%m-%d %H:%M:%S` in UTC
---
## What's Next?
- [Monitoring](monitoring.md) — Metrics, tracing, and alerting
- [Configuration Reference](../reference/configuration.md) — Full CLI options