openapi: 3.0.3 info: title: LynxDB API version: 1.0.0 description: | Splunk-power log analytics in a single binary. Zero dependencies. SPL2-compatible. ## Design Principles 1. **One URL — one resource.** `POST /query` does a query. `POST /ingest` ingests. 2. **Streaming-first.** Large-data endpoints support NDJSON via `Accept: application/x-ndjson`. 3. **Errors are UX.** Every error carries `code`, `message`, and optionally `suggestion` and `docs_url`. 4. **Single auth model.** Bearer token. Optional. Off by default. 5. **Zero required headers.** `Content-Type` defaults to `application/json`. `curl` just works. ## Authentication Off by default. When enabled via `lynxdb server --auth`: ``` Authorization: Bearer ``` ## Response Envelope ```json // Success — "data" present, "error" absent {"data": {...}, "meta": {"took_ms": 89}} // Error — "error" present, "data" absent {"error": {"code": "INVALID_QUERY", "message": "...", "suggestion": "..."}} ``` No `"status": "ok"`. HTTP status codes carry that. contact: name: LynxDB url: https://lynxdb.io license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0 servers: - url: http://localhost:3100/api/v1 description: Local development tags: - name: Ingest description: Send log events into LynxDB - name: Query description: Search, aggregate, and analyze logs using SPL2 - name: Live description: Real-time log streaming and histogram - name: Schema description: Field discovery and value stats - name: Server description: Health checks and server status - name: Saved Queries description: Persist and reuse SPL2 queries - name: Materialized Views description: Precomputed projections and aggregations for query acceleration - name: Config description: Runtime configuration management - name: Compatibility description: Elasticsearch and OpenTelemetry compatibility endpoints # SECURITY security: - BearerAuth: [] - {} # PATHS paths: # INGEST /ingest: post: operationId: ingest tags: [Ingest] summary: Ingest events description: | Primary structured ingest endpoint. Accepts only a top-level JSON array of event-envelope objects. - Each item must contain `event` plus optional `time`, `source`, `sourcetype`, `host`, `index`, and `fields`. - Use `/ingest/raw` for newline-delimited text or NDJSON. - Use `/es/_bulk` for Elasticsearch bulk payloads. parameters: requestBody: required: true content: application/json: schema: type: array items: $ref: '#/components/schemas/StructuredIngestEvent' examples: batch: summary: Batch of events value: - event: "request started" source: "api" fields: trace_id: "abc123" - event: "request completed" source: "api" fields: trace_id: "abc123" duration_ms: 45 responses: '200': description: All events accepted content: application/json: schema: $ref: '#/components/schemas/IngestResponse' example: data: accepted: 3 failed: 0 '207': description: Partial failure — some events accepted, some rejected content: application/json: schema: $ref: '#/components/schemas/IngestPartialResponse' example: data: accepted: 2 failed: 1 errors: - index: 1 code: "PARSE_ERROR" message: "Invalid JSON at line 2" '401': $ref: '#/components/responses/Unauthorized' '413': $ref: '#/components/responses/PayloadTooLarge' '429': $ref: '#/components/responses/RateLimited' /ingest/bulk: post: operationId: ingestBulk tags: [Ingest] summary: Elasticsearch-compatible bulk ingest description: | Accepts Elasticsearch `_bulk` API format for zero-config migration from Filebeat, Logstash, Vector, Fluentd. Prefer `/es/_bulk`; `/ingest/bulk` is an alias to the same handler. - `_index` is accepted but mapped to `_source` tag (LynxDB is single-index by design). - `_type` is ignored. - Response mimics ES bulk shape for client compatibility. requestBody: required: true content: application/x-ndjson: schema: type: string example: | {"index": {"_index": "logs"}} {"message": "hello from filebeat", "@timestamp": "2026-02-14T12:00:00Z"} {"index": {"_index": "logs"}} {"message": "another event"} responses: '200': description: Bulk ingest result (ES-compatible shape) content: application/json: schema: $ref: '#/components/schemas/ESBulkResponse' example: took: 12 errors: false items: - index: _id: "01JKNM3VXQP..." status: 201 - index: _id: "01JKNM4ABCD..." status: 201 '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/RateLimited' # QUERY /query: post: operationId: queryPost tags: [Query] summary: Execute SPL2 query description: | Core search endpoint. Executes any SPL2 pipeline including search, aggregation, and management commands. **Execution modes** (controlled by `wait` parameter): | `wait` value | Behavior | Response | |---|---|---| | `null` (default) | **Sync.** Block up to `query.sync_timeout` (30s). | `200` with results, or `202` + job if it exceeds the timeout | | `0` | **Async.** Return immediately. | `202` with job handle | | `N` (seconds) | **Hybrid.** Wait up to N seconds. | `200` if done in time, `202` + job otherwise | Hybrid mode (`wait: 5`) is ideal for Web UI — fast queries return instantly, slow ones degrade to async with progress tracking. **Response `data.type` determines rendering:** - `events` → log viewer (raw events) - `aggregate` → table (stats results) - `timechart` → chart (time-series) - `view_created` → MV creation confirmation (when query contains `| materialize`) - `job` → async job handle (when `wait` is set and query didn't complete in time) **MV acceleration:** when the query planner detects a Materialized View that covers the query, `meta.accelerated_by` is present in the response. **SPL2 management commands** also flow through this endpoint: - `| materialize "name"` — create MV - `| from mv_name` — read from MV - `| views` — list MVs - `| dropview "name"` — delete MV requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/QueryRequest' examples: search: summary: Basic log search (sync, default) value: q: "level=error" from: "-1h" limit: 100 aggregation: summary: Aggregation query (sync) value: q: "source=nginx status>=500 | stats count by uri | sort -count | head 10" from: "-1h" to: "now" hybrid: summary: Hybrid mode — wait up to 5s, then fall back to async value: q: "* | stats count by source, status" from: "-30d" wait: 5 async: summary: Async — return job handle immediately value: q: "* | stats dc(user_id) by source" from: "-90d" wait: 0 timechart: summary: Time-series chart value: q: "level=error | timechart count span=5m" from: "-6h" mv_query: summary: Query a materialized view value: q: "| from mv_errors_5m | where source=\"nginx\" | sort -count | head 10" mv_create: summary: Create a materialized view value: q: "level=error | stats count, avg(duration) by source, time_bucket(timestamp, '5m') AS bucket | materialize \"mv_errors_5m\" retention=90d" responses: '200': description: | Query completed synchronously. Returned when: - `wait` is `null` (default sync mode) and query completes in-request - `wait` is `N > 0` (hybrid mode) and query completes within N seconds content: application/json: schema: oneOf: - $ref: '#/components/schemas/QueryEventsResponse' - $ref: '#/components/schemas/QueryAggregateResponse' - $ref: '#/components/schemas/QueryTimechartResponse' - $ref: '#/components/schemas/QueryViewCreatedResponse' examples: events: summary: Raw events result value: data: type: events events: - _id: "01JKNM3VXQP..." _timestamp: "2026-02-14T14:52:01.234Z" _source: nginx level: error status: 502 uri: "/api/v1/users" method: GET duration_ms: 12 total: 1247 has_more: true meta: took_ms: 89 scanned: 12400000 query_id: "qry_7f3a..." aggregate: summary: Aggregation result value: data: type: aggregate columns: ["uri", "count"] rows: - ["/api/v1/users", 1247] - ["/api/v1/orders", 893] - ["/health", 412] total_rows: 42 meta: took_ms: 34 scanned: 12400000 query_id: "qry_8b2c..." accelerated: summary: MV-accelerated result value: data: type: aggregate columns: ["source", "count"] rows: - ["nginx", 142847] - ["api-gw", 89234] total_rows: 5 meta: took_ms: 3 scanned: 142847 query_id: "qry_d4e1..." accelerated_by: view: mv_errors_5m original_scan: 12400000 speedup: "~400x" partial: summary: Partial result (MV still backfilling) value: data: type: aggregate partial: true columns: ["source", "count"] rows: - ["nginx", 95231] - ["api-gw", 42100] meta: took_ms: 4 scanned: 95231 query_id: "qry_f2a1..." accelerated_by: view: mv_errors_5m status: backfilling coverage_percent: 66.7 timechart: summary: Time-series result value: data: type: timechart interval: "5m" columns: ["_time", "count"] rows: - ["2026-02-14T14:00:00Z", 42] - ["2026-02-14T14:05:00Z", 87] - ["2026-02-14T14:10:00Z", 156] meta: took_ms: 45 scanned: 12400000 query_id: "qry_9c1d..." view_created: summary: Materialized view created value: data: type: view_created view: name: mv_errors_5m query: "level=error | stats count, avg(duration) by source, time_bucket(timestamp, '5m') AS bucket" kind: aggregation retention: "90d" status: backfilling version: 1 backfill: total: 12400000 processed: 0 percent: 0 meta: took_ms: 12 '202': description: | Query accepted for async execution. Returned when: - `wait: 0` (async mode) — always returns 202 immediately - `wait: N` (hybrid mode) — query didn't complete within N seconds Response contains a job handle with current progress. Use `GET /query/jobs/{id}` to poll or `GET /query/jobs/{id}/stream` for real-time SSE updates. content: application/json: schema: $ref: '#/components/schemas/QueryJobResponse' examples: async_immediate: summary: Async — job just started value: data: type: job job_id: "qry_9c1d4e" status: running progress: phase: parsing elapsed_ms: 0 hybrid_fallback: summary: Hybrid — timed out after 5s, shows progress so far value: data: type: job job_id: "qry_7f3a2b" status: running progress: phase: scanning_segments segments_total: 128 segments_scanned: 24 rows_read_so_far: 847291 elapsed_ms: 5000 '400': $ref: '#/components/responses/InvalidQuery' '429': $ref: '#/components/responses/RateLimited' get: operationId: queryGet tags: [Query] summary: Execute SPL2 query (GET convenience) description: GET variant for simple queries. Use POST for complex or long queries. parameters: - name: q in: query required: true schema: type: string example: "level=error" - name: from in: query schema: type: string default: "-15m" example: "-1h" - name: to in: query schema: type: string default: "now" - name: limit in: query schema: type: integer default: 1000 maximum: 50000 - name: format in: query schema: type: string enum: [json] default: json description: Optional response format selector. Only `json` is accepted. responses: '200': description: Query result content: application/json: schema: oneOf: - $ref: '#/components/schemas/QueryEventsResponse' - $ref: '#/components/schemas/QueryAggregateResponse' - $ref: '#/components/schemas/QueryTimechartResponse' '400': $ref: '#/components/responses/InvalidQuery' /query/stream: post: operationId: queryStream tags: [Query] summary: Execute query with NDJSON streaming description: | Returns an NDJSON stream for large result sets, exports, and piping. **This is different from job SSE** (`GET /query/jobs/{id}/stream`): - `/query/stream` — NDJSON export of results. One event per line. For `curl | jq`, data pipelines. - `/query/jobs/{id}/stream` — SSE progress tracking. For Web UI real-time updates. - Response: `Transfer-Encoding: chunked`, one JSON object per line. - Last line is always `{"__meta": {...}}` — stream summary. - Supported request fields: `q`/`query`, `from`/`earliest`, `to`/`latest`, and `variables`. - `limit`, `offset`, `wait`, `profile`, and `format` are rejected with `400`. - Client disconnect = cancellation. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/QueryStreamRequest' example: q: "level=error" from: "-24h" responses: '200': description: NDJSON event stream content: application/x-ndjson: schema: type: string example: | {"_id":"01JKN...","_timestamp":"2026-02-14T14:52:01Z","level":"error","message":"timeout"} {"_id":"01JKN...","_timestamp":"2026-02-14T14:51:58Z","level":"error","message":"refused"} {"__meta":{"total":8432,"scanned":12400000,"took_ms":342}} '400': $ref: '#/components/responses/InvalidQuery' /query/explain: get: operationId: queryExplain tags: [Query] summary: Parse and explain query without executing description: | Returns the parsed pipeline, estimated cost, fields involved, and MV acceleration availability. Powers autocomplete, red-underline validation, and query planner UI. parameters: - name: q in: query required: true schema: type: string example: "source=nginx | stats count by uri" responses: '200': description: Query explanation content: application/json: schema: $ref: '#/components/schemas/QueryExplainResponse' examples: valid: summary: Valid query with MV acceleration value: data: parsed: pipeline: - type: search filters: - field: source op: "=" value: nginx - type: stats aggregations: - fn: count group_by: ["uri"] result_type: aggregate estimated_cost: low uses_full_scan: false fields_read: ["source", "uri"] fields_produced: ["uri", "count"] acceleration: available: true view: mv_nginx_parsed reason: "MV covers filter (source=nginx) and GROUP BY (uri) with count aggregate" estimated_speedup: "~200x" is_valid: true invalid: summary: Invalid query with suggestion value: data: is_valid: false errors: - position: 24 length: 6 message: "Unknown command 'staats'" suggestion: stats /query/jobs: get: operationId: listJobs tags: [Query] summary: List active and recent query jobs description: | Returns all running jobs and recently completed jobs (kept for `job_ttl`, default 5 minutes). Useful for Web UI job manager and debugging. The optional `status` filter accepts canonical values (`running`, `done`, `error`, `canceled`) and the aliases `complete`, `failed`, and `cancelled`. parameters: - name: status in: query schema: type: string enum: [running, done, error, canceled, complete, failed, cancelled] description: Filter by job status responses: '200': description: Job list content: application/json: schema: $ref: '#/components/schemas/JobListResponse' example: data: jobs: - job_id: "qry_9c1d4e" status: running query: "* | stats dc(user_id) by source" created_at: "2026-02-14T14:50:00Z" - job_id: "qry_7f3a2b" status: done query: "level=error | stats count by source" created_at: "2026-02-14T14:48:12Z" /query/jobs/{jobId}: get: operationId: getJob tags: [Query] summary: Get job status, progress, and results description: | Poll this endpoint to track query progress. Response shape depends on `status`: - `running` → `progress` - `done` → `results` - `error` → `error` - `canceled` → cancellation error payload parameters: - name: jobId in: path required: true schema: type: string example: "qry_9c1d4e" responses: '200': description: Job status and results content: application/json: schema: $ref: '#/components/schemas/QueryJobResponse' examples: running: summary: Running value: data: type: job job_id: "qry_9c1d4e" status: running query: "* | stats dc(user_id) by source, status" progress: phase: scanning_segments segments_total: 128 segments_scanned: 64 rows_read_so_far: 423000000 elapsed_ms: 18400 done: summary: Completed — final results value: data: type: job job_id: "qry_9c1d4e" status: done query: "* | stats dc(user_id) by source, status" created_at: "2026-02-14T14:50:00Z" completed_at: "2026-02-14T14:50:37Z" results: type: aggregate columns: ["source", "status", "dc(user_id)"] rows: - ["nginx", 200, 1784682] - ["nginx", 404, 85762] - ["api-gw", 200, 1224088] - ["api-gw", 500, 12044] total_rows: 14 meta: took_ms: 37200 scanned: 84700000000 error: summary: Failed — error details value: data: type: job job_id: "qry_d4e1f2" status: error query: "* | stats count by uri" error: code: QUERY_MEMORY_EXCEEDED message: "Query exceeded 512 MB memory limit at 20% scan. Too many unique 'uri' values (>2M) for GROUP BY." suggestion: "Add a filter to reduce cardinality, or increase max_query_memory_mb in /config." '404': $ref: '#/components/responses/NotFound' delete: operationId: cancelJob tags: [Query] summary: Cancel a running query job description: | Cancels a running job. The response always returns the current terminal status plus a `canceled` flag that tells you whether this request changed the job state. parameters: - name: jobId in: path required: true schema: type: string example: "qry_9c1d4e" responses: '200': description: Current job state after the cancel attempt content: application/json: examples: cancelled: summary: Successfully cancelled value: data: job_id: "qry_9c1d4e" status: canceled canceled: true completed_at: "2026-02-14T14:50:18Z" already_done: summary: Job already finished value: data: job_id: "qry_9c1d4e" status: done canceled: false completed_at: "2026-02-14T14:50:37Z" '404': $ref: '#/components/responses/NotFound' /query/jobs/{jobId}/stream: get: operationId: streamJob tags: [Query] summary: SSE stream of job progress and final results description: | Server-Sent Events stream for real-time job tracking. Preferred over polling for Web UI. **Event types:** | Event | When | Data | |---|---|---| | `progress` | Every ~1s while running | Progress snapshot plus optional preview rows | | `complete` | Query finished | Final results | | `failed` | Query errored | Error object | | `canceled` | Job was cancelled | Progress at cancellation | SSE (not WebSocket) because this is a unidirectional server→client stream. SSE auto-reconnects, passes through proxies, and works with the native `EventSource` browser API. ```javascript const es = new EventSource("/api/v1/query/jobs/qry_xxx/stream"); es.addEventListener("progress", (e) => updateProgressBar(JSON.parse(e.data))); es.addEventListener("complete", (e) => { showFinalResults(JSON.parse(e.data)); es.close(); }); es.addEventListener("failed", (e) => { showError(JSON.parse(e.data)); es.close(); }); ``` parameters: - name: jobId in: path required: true schema: type: string example: "qry_9c1d4e" responses: '200': description: SSE event stream content: text/event-stream: schema: type: string example: | event: progress data: {"phase":"scanning_segments","scanned":24,"segments_total":128,"percent":18.75,"events_matched":847291,"elapsed_ms":5000,"eta_ms":21666} event: complete data: {"data":{"type":"aggregate","columns":["source","count"],"rows":[["nginx",712345],["api-gw",356789]],"total_rows":5},"meta":{"took_ms":23100,"scanned":10400000000}} '404': $ref: '#/components/responses/NotFound' # LIVE /tail: get: operationId: liveTail tags: [Live] summary: SSE live tail with full SPL2 pipeline description: | Server-Sent Events stream for real-time log tailing with full SPL2 pipeline support. Uses the same query engine as `POST /query`, but in streaming mode. **Two phases:** 1. **Catchup** — replays the last `count` matching events from storage (time range: `from` to now). Each event is sent as `event: result`. Phase ends with `event: catchup_done`. 2. **Live** — streams new events from the EventBus through the SPL2 pipeline in real time. Events are sent as `event: result`. Heartbeat every 15s as `event: heartbeat`. **Supported SPL2 commands** (streaming, event-by-event): `search`, `where`, `eval`, `fields`, `table`, `rename`, `rex`, `fillnull`, `head`, `bin`. Commands requiring full materialization (`stats`, `sort`, `join`, `dedup`, `timechart`, etc.) are rejected with `422`. ```javascript const es = new EventSource("/api/v1/tail?q=search+ERROR+|+where+status>500&count=100&from=-1h"); es.addEventListener("result", (e) => appendRow(JSON.parse(e.data))); es.addEventListener("catchup_done", (e) => showLiveIndicator()); es.addEventListener("heartbeat", (e) => updatePing()); es.addEventListener("error", (e) => showError(JSON.parse(e.data))); ``` SSE (not WebSocket) because this is a unidirectional server-to-client stream. SSE auto-reconnects, passes through proxies, and works with the native `EventSource` browser API. parameters: - name: q in: query required: true schema: type: string description: SPL2 query (streaming commands only) examples: keyword: summary: Simple keyword search value: "search ERROR" filtered: summary: Filter with eval and projection value: "search index=main ERROR | where status > 500 | eval sev=upper(level) | fields _time, _raw, sev" - name: count in: query schema: type: integer default: 100 minimum: 0 maximum: 10000 description: Number of historical events to replay during catchup phase - name: from in: query schema: type: string default: "-1h" description: Catchup lookback window (relative or ISO 8601) example: "-1h" responses: '200': description: SSE event stream content: text/event-stream: schema: type: string example: | event: result data: {"_time":"2026-02-14T14:51:58Z","_raw":"level=ERROR status=502 uri=/api/users","status":502} event: result data: {"_time":"2026-02-14T14:52:01Z","_raw":"level=ERROR status=503 uri=/api/orders","status":503} event: catchup_done data: {"count":2} event: result data: {"_time":"2026-02-14T14:52:15Z","_raw":"level=ERROR status=500 uri=/api/pay","status":500} event: heartbeat data: {"ts":"2026-02-14T14:52:30Z"} '400': description: Parse error — invalid SPL2 query content: application/json: example: error: "Unknown command 'staats'" suggestion: stats '422': description: Query contains unsupported commands for tail content: application/json: example: error: "unsupported commands for tail: stats (aggregation and stateful commands require full materialization)" unsupported: ["stats"] /histogram: get: operationId: histogram tags: [Live] summary: Time-bucketed event counts description: | Powers the timeline bar chart in the Web UI. Fires on every filter change — must be blazing fast. Separate from `/query` because it runs independently and concurrently. parameters: - name: q in: query schema: type: string description: SPL2 filter expression (search part only, before pipes) example: "level=error" - name: from in: query required: true schema: type: string example: "-1h" - name: to in: query schema: type: string default: "now" - name: buckets in: query schema: type: integer default: 60 description: Target number of buckets. Server picks the best interval (1s, 5s, 1m, 5m, 1h). responses: '200': description: Histogram data content: application/json: schema: $ref: '#/components/schemas/HistogramResponse' example: data: interval: "1m" buckets: - time: "2026-02-14T14:00:00Z" count: 42 - time: "2026-02-14T14:01:00Z" count: 87 - time: "2026-02-14T14:02:00Z" count: 156 total: 8432 meta: took_ms: 12 # SCHEMA /fields: get: operationId: listFields tags: [Schema] summary: List all fields with types and stats description: Essential for autocomplete and the Fields sidebar in the Web UI. parameters: - name: from in: query schema: type: string description: Restrict stats to time range start example: "-1h" - name: to in: query schema: type: string description: Restrict stats to time range end - name: prefix in: query schema: type: string description: Filter fields by name prefix (for autocomplete) example: "sta" - name: source in: query schema: type: string description: Fields seen from a specific source example: "nginx" responses: '200': description: Field list with stats content: application/json: schema: $ref: '#/components/schemas/FieldsResponse' example: data: fields: - name: "_timestamp" type: datetime count: 847000000 coverage: 1.0 - name: level type: string count: 847000000 coverage: 1.0 top_values: - value: info count: 612000000 - value: error count: 142000000 - value: warn count: 93000000 - name: status type: integer count: 423000000 coverage: 0.50 min: 200 max: 504 top_values: - value: 200 count: 380000000 - value: 404 count: 21000000 - name: duration_ms type: float count: 423000000 coverage: 0.50 min: 0.1 max: 30001.0 avg: 145.3 p50: 42.0 p99: 3200.0 /fields/{name}/values: get: operationId: fieldValues tags: [Schema] summary: Top values for a specific field description: Powers "Quick Stats" sidebar and value autocomplete in the search bar. parameters: - name: name in: path required: true schema: type: string example: status - name: from in: query schema: type: string example: "-1h" - name: to in: query schema: type: string - name: limit in: query schema: type: integer default: 10 maximum: 100 responses: '200': description: Top field values content: application/json: schema: $ref: '#/components/schemas/FieldValuesResponse' example: data: field: status type: integer values: - value: 200 count: 89421 percent: 72.3 - value: 404 count: 18234 percent: 14.7 - value: 500 count: 9123 percent: 7.4 - value: 502 count: 4521 percent: 3.7 - value: 504 count: 2401 percent: 1.9 unique_count: 14 total_count: 123700 meta: from: "2026-02-14T13:52:00Z" to: "2026-02-14T14:52:00Z" /sources: get: operationId: listSources tags: [Schema] summary: List log sources with volume stats responses: '200': description: Source list content: application/json: schema: $ref: '#/components/schemas/SourcesResponse' example: data: sources: - name: nginx event_count: 423000000 last_event: "2026-02-14T14:52:01Z" first_event: "2026-01-15T00:00:01Z" rate: 1200.5 storage_bytes: 5200000000 - name: api-gateway event_count: 312000000 last_event: "2026-02-14T14:52:00Z" first_event: "2026-01-15T00:00:02Z" rate: 890.2 storage_bytes: 4100000000 # SERVER /status: get: operationId: serverStatus tags: [Server] summary: Server info, storage, event stats, and health responses: '200': description: Full server status content: application/json: schema: $ref: '#/components/schemas/StatusResponse' example: data: version: "0.4.0" uptime_seconds: 1234567 storage: used_bytes: 13300000000 total_bytes: 53700000000 usage_percent: 24.8 events: total: 847000000 today: 1200000 ingest_rate: 2340.5 queries: active: 12 avg_duration_ms: 45 jobs: running: 2 queued: 0 max_concurrent: 32 views: total: 3 active: 2 backfilling: 1 storage_bytes: 2267789702 retention: policy: "7d" oldest_event: "2026-02-07T00:00:01Z" health: healthy /health: get: operationId: healthCheck tags: [Server] summary: Minimal health check for load balancers description: Returns `200` when healthy, `503` when not. No envelope — trivially parseable. responses: '200': description: Healthy content: application/json: schema: type: object properties: status: type: string enum: [ok] example: status: ok '503': description: Unhealthy content: application/json: example: status: unhealthy # SAVED QUERIES /queries: get: operationId: listSavedQueries tags: [Saved Queries] summary: List saved queries responses: '200': description: Saved query list content: application/json: schema: $ref: '#/components/schemas/SavedQueriesResponse' example: data: queries: - id: sq_abc123 name: "High 5xx rate" q: "source=nginx status>=500 | stats count by uri | sort -count" from: "-1h" created_at: "2026-02-10T12:00:00Z" updated_at: "2026-02-14T08:00:00Z" post: operationId: createSavedQuery tags: [Saved Queries] summary: Create a saved query requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SavedQueryInput' example: name: "High 5xx rate" q: "source=nginx status>=500 | stats count by uri | sort -count" from: "-1h" responses: '201': description: Created content: application/json: schema: $ref: '#/components/schemas/SavedQueryResponse' /queries/{id}: put: operationId: updateSavedQuery tags: [Saved Queries] summary: Replace a saved query parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SavedQueryInput' responses: '200': description: Updated content: application/json: schema: $ref: '#/components/schemas/SavedQueryResponse' '404': $ref: '#/components/responses/NotFound' delete: operationId: deleteSavedQuery tags: [Saved Queries] summary: Delete a saved query parameters: - name: id in: path required: true schema: type: string responses: '204': description: Deleted # MATERIALIZED VIEWS /views: get: operationId: listViews tags: [Materialized Views] summary: List all materialized views description: Returns every MV with operational status, storage, lag, and backfill progress. responses: '200': description: MV list content: application/json: schema: $ref: '#/components/schemas/ViewsListResponse' example: data: views: - name: mv_errors_5m kind: aggregation query: "level=error | stats count, avg(duration) by source, time_bucket(timestamp, '5m') AS bucket" retention: "90d" status: active version: 1 rows: 142847 segments: 12 storage_bytes: 12582912 lag_ms: 1200 created_at: "2026-02-12T10:00:00Z" last_event: "2026-02-14T14:52:01Z" - name: mv_5xx_hourly kind: aggregation query: "source=nginx status>=500 | stats count, p95(duration) by uri, time_bucket(timestamp, '1h') AS hour" retention: "365d" status: backfilling version: 1 rows: 6720 segments: 3 storage_bytes: 348160 lag_ms: null backfill: total: 12600000 processed: 8400000 percent: 66.7 eta_seconds: 134 created_at: "2026-02-14T14:30:00Z" last_event: null post: operationId: createView tags: [Materialized Views] summary: Create a materialized view (or trigger rebuild) description: | REST equivalent of `| materialize`. If name already exists, triggers a **versioned rebuild**: old version keeps serving queries, new version builds in background, atomic swap on completion. Send `If-None-Match: *` to get `409 Conflict` if the view already exists (prevents accidental rebuilds). parameters: - name: If-None-Match in: header schema: type: string description: 'Set to `*` to prevent rebuild of existing view' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ViewInput' examples: aggregation: summary: Aggregation MV value: name: mv_errors_5m q: "level=error | stats count, avg(duration) by source, time_bucket(timestamp, '5m') AS bucket" retention: "90d" projection: summary: Projection MV value: name: mv_access q: "source=nginx | extract timestamp, method, uri, status, size, duration" retention: "14d" partition_by: "date(timestamp)" cascading: summary: Cascading MV (built on another MV) value: name: mv_errors_1h q: "| from mv_errors_5m | stats sum(count) AS count by source, time_bucket(bucket, '1h') AS hour" retention: "365d" responses: '201': description: MV created, backfill started content: application/json: schema: $ref: '#/components/schemas/ViewResponse' example: data: name: mv_errors_5m kind: aggregation query: "level=error | stats count, avg(duration) by source, time_bucket(timestamp, '5m') AS bucket" retention: "90d" status: backfilling version: 1 backfill: total: 12400000 processed: 0 percent: 0 '200': description: Existing MV — versioned rebuild triggered content: application/json: example: data: name: mv_errors_5m kind: aggregation query: "level=error | stats count, avg(duration), p99(duration) by source, time_bucket(timestamp, '5m') AS bucket" retention: "90d" status: rebuilding version: 2 previous_version: version: 1 status: active backfill: total: 12400000 processed: 0 percent: 0 '409': description: Conflict — view exists and `If-None-Match` was set content: application/json: example: error: code: ALREADY_EXISTS message: "View 'mv_errors_5m' already exists. Remove If-None-Match header to trigger rebuild." /views/{name}: get: operationId: getView tags: [Materialized Views] summary: Get MV details, column schema, and stats parameters: - name: name in: path required: true schema: type: string example: mv_errors_5m responses: '200': description: Full MV details content: application/json: schema: $ref: '#/components/schemas/ViewDetailResponse' example: data: name: mv_errors_5m kind: aggregation query: "level=error | stats count, avg(duration) by source, time_bucket(timestamp, '5m') AS bucket" retention: "90d" status: active version: 1 columns: - name: source type: string encoding: dictionary - name: bucket type: timestamp encoding: delta-of-delta - name: count type: int64 encoding: delta-varint - name: "avg(duration)" type: float64 derived_from: ["_sum_duration", "_count_duration"] group_by: ["source", "bucket"] aggregations: ["count", "avg(duration)"] stats: rows: 142847 segments: 12 segments_pending_merge: 3 storage_bytes: 12582912 compression_ratio: 18.4 lag_ms: 1200 ingest_rate: 234.5 oldest_event: "2026-01-15T00:00:01Z" newest_event: "2026-02-14T14:52:01Z" source_view: null created_at: "2026-02-12T10:00:00Z" '404': $ref: '#/components/responses/NotFound' patch: operationId: patchView tags: [Materialized Views] summary: Update mutable MV properties description: | Only `retention` and `paused` can be changed without rebuild. To change `q`, `partition_by`, or `group_by`, use `POST /views` with the same name (triggers rebuild). parameters: - name: name in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ViewPatchInput' examples: change_retention: summary: Change retention value: retention: "30d" pause: summary: Pause MV pipeline value: paused: true responses: '200': description: Updated content: application/json: schema: $ref: '#/components/schemas/ViewResponse' '404': $ref: '#/components/responses/NotFound' delete: operationId: deleteView tags: [Materialized Views] summary: Delete MV and all its data description: | Atomic deletion. If the view has cascading dependents, returns `409`. Use `?force=true` to delete the view and all dependents. parameters: - name: name in: path required: true schema: type: string - name: force in: query schema: type: boolean default: false description: Delete view and all cascading dependents responses: '204': description: Deleted '404': $ref: '#/components/responses/NotFound' '409': description: View has dependents content: application/json: example: error: code: HAS_DEPENDENTS message: "Cannot delete 'mv_errors_5m': view 'mv_errors_1h' depends on it." dependents: ["mv_errors_1h"] suggestion: "Delete dependent views first, or use ?force=true to delete all." /views/{name}/backfill: get: operationId: viewBackfill tags: [Materialized Views] summary: Backfill/rebuild progress parameters: - name: name in: path required: true schema: type: string responses: '200': description: Backfill status content: application/json: examples: in_progress: summary: Backfill in progress value: data: status: backfilling version: 1 total: 12600000 processed: 8400000 percent: 66.7 rate: 62500 eta_seconds: 67 started_at: "2026-02-14T14:30:00Z" cursor: "seg-004:offset-847291" errors: 0 idle: summary: No backfill running value: data: status: idle last_completed: "2026-02-12T10:04:23Z" '404': $ref: '#/components/responses/NotFound' # CONFIG /config: get: operationId: getConfig tags: [Config] summary: Current runtime configuration responses: '200': description: Configuration content: application/json: example: data: listen: "localhost:3100" data_dir: "~/.lynxdb/data" retention: "7d" auth_enabled: false otlp_enabled: false syslog_enabled: false max_query_memory_mb: 512 patch: operationId: patchConfig tags: [Config] summary: Update runtime configuration description: Only runtime-adjustable fields. Fields requiring restart are flagged in `meta.restart_required`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ConfigPatch' example: retention: "30d" max_query_memory_mb: 1024 responses: '200': description: Updated config content: application/json: example: data: listen: "localhost:3100" data_dir: "~/.lynxdb/data" retention: "30d" auth_enabled: false otlp_enabled: false syslog_enabled: false max_query_memory_mb: 1024 meta: restart_required: [] # COMPATIBILITY /es/_bulk: post: operationId: esBulk tags: [Compatibility] summary: Elasticsearch _bulk API description: | Drop-in compatible with Filebeat, Logstash, Vector, Fluentd. `_index` maps to `_source` tag. `_type` is ignored. ```yaml # Filebeat config: output.elasticsearch: hosts: ["http://lynxdb:3100/api/v1/es"] ``` requestBody: required: true content: application/x-ndjson: schema: type: string responses: '200': description: ES-compatible bulk response content: application/json: schema: $ref: '#/components/schemas/ESBulkResponse' /es/{index}/_doc: post: operationId: esIndexDoc tags: [Compatibility] summary: Elasticsearch single-doc ingest parameters: - name: index in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LogEvent' responses: '201': description: Document indexed content: application/json: example: _id: "01JKNM3VXQP..." result: created /es/: get: operationId: esClusterInfo tags: [Compatibility] summary: Fake ES cluster info (Filebeat handshake) responses: '200': description: Minimal cluster info for client compatibility content: application/json: example: name: lynxdb cluster_name: lynxdb version: number: "8.0.0" build_flavor: default tagline: "LynxDB — Splunk-power log analytics in a single binary" /otlp/v1/logs: post: operationId: otlpLogs tags: [Compatibility] summary: OpenTelemetry OTLP log ingest description: | Accepts OTLP HTTP logs in JSON format. Requests with `Content-Type: application/x-protobuf` are rejected with `415 Unsupported Media Type`. ```yaml # OTEL Collector config: exporters: otlp_http: endpoint: http://lynxdb:3100/api/v1/otlp encoding: json ``` requestBody: required: true content: application/json: schema: type: object responses: '200': description: Accepted # COMPONENTS components: securitySchemes: BearerAuth: type: http scheme: bearer description: Optional. Off by default. Enable with `lynxdb server --auth`. parameters: XSource: name: X-Source in: header schema: type: string description: Tag events with a source label (e.g., `nginx`, `api-gateway`) example: nginx XFormat: name: X-Format in: header schema: type: string enum: [json, syslog, clf, raw, auto] default: auto description: Force log format parser. Default `auto` detects format. responses: Unauthorized: description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: AUTH_REQUIRED message: "Authentication is enabled. Provide a Bearer token." docs_url: "https://lynxdb.io/docs/auth" NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: NOT_FOUND message: "Resource not found." InvalidQuery: description: Invalid SPL2 query content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: INVALID_QUERY message: "Unknown field 'stauts'." suggestion: "status" QueryTimeout: description: Query exceeded time limit content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: QUERY_TIMEOUT message: "Query exceeded 30s timeout. Try narrowing the time range or adding filters." PayloadTooLarge: description: Event or batch exceeds size limit content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: PAYLOAD_TOO_LARGE message: "Event exceeds 1 MB limit." RateLimited: description: Too many requests headers: Retry-After: schema: type: integer description: Seconds to wait before retrying content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: RATE_LIMITED message: "Ingest rate limit exceeded. Retry after 1s." schemas: # ── Generic ── ErrorResponse: type: object required: [error] properties: error: type: object required: [code, message] properties: code: type: string description: Machine-readable SCREAMING_SNAKE code example: INVALID_QUERY message: type: string example: "Unknown field 'stauts'" suggestion: type: string example: status docs_url: type: string format: uri Meta: type: object properties: took_ms: type: number scanned: type: integer query_id: type: string # ── Ingest ── LogEvent: type: object additionalProperties: true description: Any JSON object. No required schema. example: message: "GET /api/users 200 12ms" level: info source: nginx status: 200 StructuredIngestEvent: type: object required: [event] properties: event: type: string description: Raw event text stored in `_raw` time: type: number description: Unix timestamp in seconds source: type: string sourcetype: type: string host: type: string index: type: string fields: type: object additionalProperties: true description: Additional scalar fields copied onto the event example: event: "request completed" source: "api" host: "web-01" fields: level: "info" duration_ms: 45 IngestResponse: type: object required: [data] properties: data: type: object required: [accepted, failed] properties: accepted: type: integer failed: type: integer IngestPartialResponse: type: object required: [data] properties: data: type: object required: [accepted, failed, errors] properties: accepted: type: integer failed: type: integer errors: type: array items: type: object properties: index: type: integer code: type: string message: type: string ESBulkResponse: type: object properties: took: type: integer errors: type: boolean items: type: array items: type: object properties: index: type: object properties: _id: type: string status: type: integer # ── Query ── QueryRequest: type: object properties: q: type: string description: SPL2 query string example: "source=nginx status>=500 | stats count by uri" query: type: string description: Alias for `q` earliest: type: string description: Legacy alias for `from` latest: type: string description: Legacy alias for `to` from: type: string description: "Start time: relative (`-1h`, `-7d`) or ISO 8601. Default: `-15m`" default: "-15m" example: "-1h" to: type: string description: "End time: relative (`now`, `-5m`) or ISO 8601. Default: `now`" default: "now" limit: type: integer default: 1000 maximum: 50000 description: Max events to return offset: type: integer default: 0 description: Offset for pagination (tabular results only) format: type: string enum: [json] default: json description: Optional response format selector. Only `json` is accepted on `/query`. wait: type: number nullable: true default: null description: | Controls sync/async behavior: - `null` (default) — **Sync.** Block until query completes. Returns `200` with results. - `0` — **Async.** Return `202` immediately with a job handle. Client polls or subscribes to SSE for progress. - `N` (seconds) — **Hybrid.** Wait up to N seconds. If query completes in time → `200` with results. If not → `202` with job handle and current progress. Best for UI: short queries feel instant, long queries degrade gracefully. example: 5 profile: type: string enum: [basic, full, trace] description: Include richer execution statistics in the response `meta.stats`. variables: type: object additionalProperties: type: string description: Template variables substituted into the query before planning. QueryStreamRequest: type: object properties: q: type: string description: SPL2 query string query: type: string description: Alias for `q` earliest: type: string description: Legacy alias for `from` latest: type: string description: Legacy alias for `to` from: type: string description: "Start time: relative (`-1h`, `-7d`) or ISO 8601." to: type: string description: "End time: relative (`now`, `-5m`) or ISO 8601." variables: type: object additionalProperties: type: string description: Template variables substituted into the query before planning. AcceleratedBy: type: object properties: view: type: string description: Name of the MV used original_scan: type: integer description: Events that would have been scanned without MV speedup: type: string example: "~400x" status: type: string enum: [active, backfilling] coverage_percent: type: number description: Only present when MV is backfilling QueryEventsResponse: type: object required: [data] properties: data: type: object required: [type, events, total] properties: type: type: string enum: [events] events: type: array items: $ref: '#/components/schemas/LogEvent' total: type: integer has_more: type: boolean partial: type: boolean description: Present and `true` when served from a backfilling MV meta: allOf: - $ref: '#/components/schemas/Meta' - type: object properties: accelerated_by: $ref: '#/components/schemas/AcceleratedBy' QueryAggregateResponse: type: object required: [data] properties: data: type: object required: [type, columns, rows] properties: type: type: string enum: [aggregate] columns: type: array items: type: string rows: type: array items: type: array total_rows: type: integer partial: type: boolean meta: allOf: - $ref: '#/components/schemas/Meta' - type: object properties: accelerated_by: $ref: '#/components/schemas/AcceleratedBy' QueryTimechartResponse: type: object required: [data] properties: data: type: object required: [type, interval, columns, rows] properties: type: type: string enum: [timechart] interval: type: string example: "5m" columns: type: array items: type: string rows: type: array items: type: array meta: $ref: '#/components/schemas/Meta' QueryViewCreatedResponse: type: object required: [data] properties: data: type: object required: [type, view] properties: type: type: string enum: [view_created] view: $ref: '#/components/schemas/ViewSummary' meta: $ref: '#/components/schemas/Meta' # ── Query Jobs (async) ── JobProgress: type: object required: [phase] properties: phase: type: string enum: [parsing, scanning_buffer, filtering_segments, scanning_segments, executing_pipeline] description: Current execution phase segments_total: type: integer description: Total segments considered for the query segments_scanned: type: integer description: Segments scanned so far segments_dispatched: type: integer description: Segments dispatched to workers segments_skipped_index: type: integer segments_skipped_time: type: integer segments_skipped_stats: type: integer segments_skipped_bloom: type: integer segments_skipped_range: type: integer buffered_events: type: integer rows_read_so_far: type: integer format: int64 description: Rows read so far elapsed_ms: type: number description: Wall-clock time since job started QueryJobResponse: type: object required: [data] description: | Returned as `202` from `POST /query` when the query continues asynchronously, and as `200` from `GET /query/jobs/{id}`. properties: data: type: object required: [type, job_id, status] properties: type: type: string enum: [job] job_id: type: string example: "qry_9c1d4e" status: type: string enum: [running, done, error, canceled] query: type: string description: The original SPL2 query created_at: type: string format: date-time completed_at: type: string format: date-time description: Present when `status` is `done` progress: $ref: '#/components/schemas/JobProgress' results: description: Final query results. Present only when `status` is `done`. Same shape as sync `200` response `data`. oneOf: - type: object properties: type: type: string enum: [events] events: type: array items: $ref: '#/components/schemas/LogEvent' total: type: integer has_more: type: boolean - type: object properties: type: type: string enum: [aggregate] columns: type: array items: type: string rows: type: array items: type: array total_rows: type: integer - type: object properties: type: type: string enum: [timechart] interval: type: string columns: type: array items: type: string rows: type: array items: type: array error: description: Present when `status` is `error` or `canceled` type: object properties: code: type: string example: QUERY_MEMORY_EXCEEDED message: type: string suggestion: type: string meta: $ref: '#/components/schemas/Meta' JobListResponse: type: object required: [data] properties: data: type: object required: [jobs] properties: jobs: type: array items: type: object properties: job_id: type: string status: type: string enum: [running, done, error, canceled] query: type: string created_at: type: string format: date-time QueryExplainResponse: type: object required: [data] properties: data: type: object properties: parsed: type: object properties: pipeline: type: array items: type: object result_type: type: string enum: [events, aggregate, timechart] estimated_cost: type: string enum: [low, medium, high] uses_full_scan: type: boolean fields_read: type: array items: type: string fields_produced: type: array items: type: string acceleration: type: object properties: available: type: boolean view: type: string reason: type: string estimated_speedup: type: string is_valid: type: boolean errors: type: array items: type: object properties: position: type: integer length: type: integer message: type: string suggestion: type: string # ── Schema ── FieldsResponse: type: object required: [data] properties: data: type: object required: [fields] properties: fields: type: array items: type: object required: [name, type, count, coverage] properties: name: type: string type: type: string enum: [string, integer, float, boolean, datetime] count: type: integer coverage: type: number description: Fraction of events containing this field (0.0–1.0) min: type: number max: type: number avg: type: number p50: type: number p99: type: number top_values: type: array items: type: object properties: value: {} count: type: integer FieldValuesResponse: type: object required: [data] properties: data: type: object required: [field, type, values] properties: field: type: string type: type: string values: type: array items: type: object properties: value: {} count: type: integer percent: type: number unique_count: type: integer total_count: type: integer meta: type: object properties: from: type: string format: date-time to: type: string format: date-time SourcesResponse: type: object required: [data] properties: data: type: object required: [sources] properties: sources: type: array items: type: object properties: name: type: string event_count: type: integer last_event: type: string format: date-time first_event: type: string format: date-time rate: type: number description: Events per second (current) storage_bytes: type: integer # ── Histogram ── HistogramResponse: type: object required: [data] properties: data: type: object required: [interval, buckets, total] properties: interval: type: string example: "1m" buckets: type: array items: type: object required: [time, count] properties: time: type: string format: date-time count: type: integer total: type: integer meta: $ref: '#/components/schemas/Meta' # ── Status ── StatusResponse: type: object required: [data] properties: data: type: object properties: version: type: string uptime_seconds: type: integer storage: type: object properties: used_bytes: type: integer total_bytes: type: integer usage_percent: type: number events: type: object properties: total: type: integer today: type: integer ingest_rate: type: number queries: type: object properties: active: type: integer avg_duration_ms: type: number views: type: object properties: total: type: integer active: type: integer backfilling: type: integer storage_bytes: type: integer retention: type: object properties: policy: type: string oldest_event: type: string format: date-time health: type: string enum: [healthy, degraded, unhealthy] # ── Saved Queries ── SavedQuery: type: object properties: id: type: string name: type: string q: type: string from: type: string created_at: type: string format: date-time updated_at: type: string format: date-time SavedQueryInput: type: object required: [name, q] properties: name: type: string example: "High 5xx rate" q: type: string example: "source=nginx status>=500 | stats count by uri | sort -count" from: type: string example: "-1h" SavedQueriesResponse: type: object required: [data] properties: data: type: object required: [queries] properties: queries: type: array items: $ref: '#/components/schemas/SavedQuery' SavedQueryResponse: type: object required: [data] properties: data: $ref: '#/components/schemas/SavedQuery' # ── Materialized Views ── ViewSummary: type: object required: [name, kind, query, status] properties: name: type: string kind: type: string enum: [projection, aggregation] query: type: string retention: type: string status: type: string enum: [active, backfilling, rebuilding, paused, error] version: type: integer rows: type: integer segments: type: integer storage_bytes: type: integer lag_ms: type: integer nullable: true backfill: $ref: '#/components/schemas/BackfillProgress' previous_version: type: object properties: version: type: integer status: type: string created_at: type: string format: date-time last_event: type: string format: date-time nullable: true BackfillProgress: type: object properties: total: type: integer processed: type: integer percent: type: number eta_seconds: type: integer ViewColumn: type: object required: [name, type] properties: name: type: string type: type: string enum: [string, int64, float64, timestamp, boolean] encoding: type: string enum: [dictionary, delta-of-delta, delta-varint, gorilla, bitpacked] derived_from: type: array items: type: string description: Internal state columns for computed aggregates (e.g., avg = sum/count) ViewInput: type: object required: [name, q] properties: name: type: string pattern: '^mv_[a-z0-9_]+$' description: "Must start with `mv_`. Lowercase, underscores, alphanumeric." example: mv_errors_5m q: type: string description: SPL2 pipeline (without `| materialize`) example: "level=error | stats count, avg(duration) by source, time_bucket(timestamp, '5m') AS bucket" retention: type: string description: "Duration string. Default: same as index retention." example: "90d" partition_by: type: string description: Partition strategy expression example: "date(timestamp)" ViewPatchInput: type: object properties: retention: type: string example: "30d" paused: type: boolean ViewResponse: type: object required: [data] properties: data: $ref: '#/components/schemas/ViewSummary' ViewDetailResponse: type: object required: [data] properties: data: allOf: - $ref: '#/components/schemas/ViewSummary' - type: object properties: columns: type: array items: $ref: '#/components/schemas/ViewColumn' group_by: type: array items: type: string aggregations: type: array items: type: string stats: type: object properties: rows: type: integer segments: type: integer segments_pending_merge: type: integer storage_bytes: type: integer compression_ratio: type: number lag_ms: type: integer ingest_rate: type: number oldest_event: type: string format: date-time newest_event: type: string format: date-time source_view: type: string nullable: true description: Parent MV name for cascading views ViewsListResponse: type: object required: [data] properties: data: type: object required: [views] properties: views: type: array items: $ref: '#/components/schemas/ViewSummary' # ── Config ── ConfigPatch: type: object properties: retention: type: string example: "30d" max_query_memory_mb: type: integer example: 1024 listen: type: string description: Requires restart auth_enabled: type: boolean otlp_enabled: type: boolean syslog_enabled: type: boolean