# Operations Logging Guide
This guide covers configuring, consuming, and troubleshooting eggserve's structured logging output.
## Configuration
### CLI flags
```sh
eggserve --log-format json /path/to/root # JSON Lines (machine-consumable)
eggserve --log-format text /path/to/root # Text (human-readable)
eggserve --log-format none /path/to/root # Silent (fatal startup diagnostics only)
```
All output goes to **stderr**. stdout is reserved for serving content.
### Python API
```python
from eggserve.server import HTTPServer, SimpleHTTPRequestHandler
handler = SimpleHTTPRequestHandler
server = HTTPServer(("127.0.0.1", 8000), handler)
```
The Python server logs to stderr via the CLI's structured logging. The server does not accept observer callbacks; operational events are emitted to stderr by the Rust runtime.
## JSON Lines Schema
Every line is a self-contained JSON object:
```json
{
"schema_version": 1,
"severity": "INFO",
"event": "connection_accepted",
"timestamp": "2026-07-22T10:00:00.123Z",
"message": "connection accepted",
"connection_id": 42,
"fields": []
}
```
### Required fields
| `schema_version` | number | Always `1` |
| `severity` | string | `DEBUG`, `INFO`, `WARN`, `ERROR` |
| `event` | string | Stable event kind name (snake_case) |
| `timestamp` | string | RFC 3339 format: `YYYY-MM-DDTHH:MM:SS.mmmZ` |
| `message` | string | Human-readable description |
### Optional fields
| `connection_id` | number | Unique per-process connection identifier |
| `request_seq` | number | Request sequence number within connection |
| `fields` | array | Structured key-value pairs |
### Fields array
Each element is an object with a single key-value pair. Values preserve their type:
```json
"fields": [
{"declared_bytes": 1048576},
{"limit_bytes": 524288},
{"error_kind": "WouldBlock"}
]
```
## Event Reference
### Process lifecycle
| `process_starting` | INFO | Server binary starting |
| `root_initialized` | INFO | Root directory opened and pinned |
| `listener_ready` | INFO | Accept loop bound and polling |
| `shutdown_requested` | INFO | Graceful shutdown initiated |
| `draining_started` | INFO | Draining in-flight connections |
| `forced_shutdown_started` | WARN | Drain deadline exceeded |
| `shutdown_complete` | INFO | Server stopped |
### Connection lifecycle
| `connection_accepted` | DEBUG | New TCP connection accepted |
| `connection_rejected` | DEBUG | Admission limit reached |
| `tls_handshake_success` | DEBUG | TLS handshake completed |
| `tls_handshake_failure` | WARN | TLS handshake failed |
| `tls_handshake_timeout` | WARN | TLS handshake timed out |
| `header_timeout` | WARN | Header read timed out |
| `body_read_timeout` | WARN | Body read timed out |
| `parser_rejection` | DEBUG | HTTP framing rejection |
| `keep_alive_closed` | DEBUG | Keep-alive connection closed cleanly |
| `connection_total_timeout` | WARN | Total connection lifetime exceeded |
| `client_disconnect` | DEBUG | Client disconnected |
| `connection_panic` | ERROR | Handler panic contained |
### Request/service
| `file_not_found` | DEBUG | Path resolved but file not found |
| `file_denied` | DEBUG | Access denied (dotfile/symlink/policy) |
| `file_error` | WARN | File stream I/O error |
| `dotfile_denied` | DEBUG | Dotfile access denied |
| `symlink_denied` | DEBUG | Symlink access denied |
| `root_escape_denied` | WARN | Path escapes root |
| `body_policy_rejection` | DEBUG | Request body rejected by policy |
| `incomplete_body_close` | DEBUG | Connection closed with unconsumed body |
| `service_invocation_suppressed` | WARN | Service call skipped (e.g., timeout already fired) |
| `service_timeout` | WARN | Handler timed out (504) |
| `service_error` | ERROR | Handler returned error |
| `request_completed` | INFO | Request fully processed |
| `directory_listing_limit` | WARN | Directory listing entry or size limit reached |
| `blocking_worker_saturation` | WARN | Blocking worker pool fully utilized |
### Operational faults
| `listener_transient_error` | DEBUG/WARN | Retryable accept error |
| `listener_persistent_error` | ERROR | Fatal accept error |
| `resource_exhaustion` | ERROR | File descriptor exhaustion |
| `log_sink_failure` | ERROR | Logging backend failed |
## Operational Counters
`global_counters().snapshot()` provides a point-in-time snapshot:
| `connections_accepted` | TCP connections accepted |
| `connections_rejected` | Rejected by admission limit |
| `active_connections` | Currently active |
| `active_file_streams` | Currently streaming file responses |
| `parser_rejects` | HTTP parsing failures |
| `body_rejections` | Request bodies rejected by policy |
| `header_timeouts` | Header read timeouts |
| `body_read_timeouts` | Body read timeouts |
| `connection_total_timeouts` | Total connection lifetime timeouts |
| `bytes_sent` | Total bytes sent |
| `graceful_shutdowns` | Clean shutdowns |
| `forced_shutdowns` | Shutdowns with timeout |
| `listener_errors` | Accept loop errors |
| `dropped_log_events` | Events dropped due to sink failures |
## Troubleshooting
### Log flooding from listener errors
Repeated accept errors (e.g., file descriptor exhaustion) are rate-limited:
- First occurrence is always emitted
- Subsequent identical errors emit a summary every 10 occurrences
- Counter resets on successful accept or different error kind
### Python server logging
The Python server delegates logging to the Rust runtime's stderr log sink. There is no Python observer callback; operational events are emitted to stderr in the same structured format as the CLI.
### Log sink failures
If a log sink panics, `CompositeLogSink` catches the panic, increments `dropped_log_events`, and emits a `log_sink_failure` event through the remaining sinks. The server continues operating.
### JSON parse errors
If `event_to_json` output fails to parse, file a bug. The output is guaranteed to be valid UTF-8 JSON. Control characters in messages are escaped (`\n`, `\t`, `\u0000`-style).