GreptimeDB
GreptimeDB (Apache-2.0) is a single-binary observability database. It ingests all three signals over OTLP/HTTP under a /v1/otlp prefix, stores them as SQL tables, and reads them back over SQL, PromQL and a Jaeger-compatible API on the same HTTP port. The plugin's dashboard adapter uses all three read paths.
Signals: traces + metrics + logs. Deployment: local (single container) · Kubernetes via the official Helm chart · GreptimeCloud. Cost: OSS, no account.
One real hermes turn through type: greptime into the bundled compose stack (GreptimeDB v1.2.1, 2026-10-11) stored 7 spans, 28 log records and 27 metric tables, and the dashboard-verify harness read every signal back through the adapter. See Verified behavior.
Quick start
docker compose -f docker-compose/greptime/docker-compose.yaml up -d
Then declare the backend (the origin is enough; the type adds the OTLP prefix):
backends:
- type: greptime
endpoint: http://localhost:4398 # the bundled stack; upstream listens on 4000
capture_logs: true
The startup banner reads ✓ GreptimeDB at http://localhost:4398/v1/otlp/v1/traces. Open http://localhost:4398/dashboard for GreptimeDB's own Traces, Logs and Metrics views, or query it:
curl -s -X POST --data-urlencode "sql=SELECT span_name, duration_nano FROM opentelemetry_traces ORDER BY timestamp" \
'http://localhost:4398/v1/sql?db=public'
Why a dedicated type: greptime
The three signals need different headers: GreptimeDB rejects a trace batch without X-Greptime-Pipeline-Name: greptime_trace_v1 (400 Pipeline is required for this API) and rejects a log batch that carries it (400 Unsupported pipeline for logs). The generic OTLP type has one header set for all signals, so it cannot drive GreptimeDB. type: greptime asks the plugin to:
- Complete
endpointto<origin>/v1/otlp/v1/traces(a bare origin, the/v1/otlpprefix or any of the three full URLs all work) and derive the metrics and logs URLs from it. - Send the trace pipeline header on traces only,
X-Greptime-DB-Nameon every signal whendbis set, and Basic auth fromuser/passwordwhen they resolve. - Enable all three signals by default, with cumulative metrics (GreptimeDB stores cumulative and delta points as sent).
- Enable the dashboard adapter: traces through the Jaeger-compatible API, metrics and logs through SQL.
Configuration reference
| Field | Meaning |
|---|---|
endpoint | Required. The GreptimeDB HTTP origin, e.g. http://localhost:4398 (bundled stack) or http://<host>:4000. Also via OTEL_GREPTIME_ENDPOINT. |
db / db_env | Optional database for all three signals (X-Greptime-DB-Name; fallback OTEL_GREPTIME_DB). Unset means the server default, public. The dashboard reads the same database. |
user / user_env, password / password_env | Optional Basic auth, for a server started with --user-provider (fallbacks OTEL_GREPTIME_USER / OTEL_GREPTIME_PASSWORD). Omitted when either is missing. |
headers | Extra headers on every signal, e.g. x-greptime-hints: ttl=7d to age data out. A X-Greptime-Pipeline-Name here replaces the default trace pipeline and goes on traces only. |
metrics / logs / traces | Per-signal toggles; all three default on. |
query_port, query_path, search_fetch, logs_table, metrics_match, service_name | Dashboard read path, see below. |
No metric-temporality preset. GreptimeDB rejects exponential histograms, so keep the default metrics_histogram: explicit.
Single backend (env vars)
export OTEL_GREPTIME_ENDPOINT=http://localhost:4398
# export OTEL_GREPTIME_DB=public
export OTEL_PROJECT_NAME=hermes-otel-greptime
What you'll see
- Traces: one row per span in
opentelemetry_traces, with every attribute flattened to a column ("span_attributes.gen_ai.usage.input_tokens", quoted in SQL because of the dots),parent_span_idnull on theagentroot,span_kind,span_status_code,duration_nano,span_eventsas JSON. The companion tablesopentelemetry_traces_servicesand_operationsfeed the Jaeger-compatible API. - Metrics: one table per metric in Prometheus spelling (
hermes_token_usage_total,gen_ai_client_token_usage_sum/_count/_bucket),greptime_timestampin milliseconds,greptime_value, one TAG column per label (model,token_type,service_instance_id, …). - Logs:
opentelemetry_logswithtimestamp(ns),severity_text/severity_number,body,scope_name(the Python logger),trace_idandspan_idfor records written inside a span, andlog_attributesas JSON (hermes.session_id,code.file.path, …).
Dashboard adapter
The Hermes dashboard's OTel tab reads GreptimeDB on the same port the plugin writes to:
- Traces through the Jaeger-compatible API under
/v1/jaeger(query_path). It speaks the Jaeger v1 grammar (service,operationexact,tagsas JSON,start/endin µs,minDurationwith a unit) and returns typed tags, so the Jaeger adapter does the work. Every search names a service (service_name, else the plugin'sresource_attributes.service.name, elsehermes-agent). Becauselimit=Nkeeps N arbitrary traces rather than the newest, the adapter asks forsearch_fetch(default 500) and cuts the page itself. - Metrics through SQL on the metric tables: raw samples with a five-minute look-behind so counters are turned into per-bucket increases with first-sample counting (GreptimeDB's PromQL route answers a range-vector instant query with an empty body, so SQL is the only way to get raw samples).
group_bynames an attribute; its column is the dotted name with underscores. The metric list comes from PromQL'slabel/__name__/valuesroute, narrowed bymetrics_match. - Logs through SQL on
opentelemetry_logs(logs_table): trace, span, logger, severity, text, session and event filters, the last two throughjson_get_string(log_attributes, '["hermes.session_id"]')(a bracket path, because a bare dotted path would descend into nested objects). status = okand free text cannot be expressed on the trace search (the Jaeger grammar has no negative match and no body search).- No per-trace "open in" link: the embedded dashboard has no documented deep link to one trace.
ui_urlon the entry only sets the Settings tab's link to the UI.
Verified behavior
Read back after one turn (project_name: greptime-363, GreptimeDB v1.2.1):
| Check | Result |
|---|---|
| Spans | 7 in opentelemetry_traces: agent (root), llm.nvidia/…, three api.nvidia/…, tool.terminal, tool.read_file; all SPAN_KIND_INTERNAL, STATUS_CODE_OK |
| Logs | 28 rows; the ones written inside a span carry trace_id and span_id; hermes.session_id in log_attributes |
| Metrics | 27 tables: hermes_token_usage_total, hermes_model_usage_total, hermes_session_*, hermes_tool_duration_milliseconds_{sum,count,bucket}, gen_ai_client_token_usage_*, gen_ai_client_operation_duration_seconds_*, gen_ai_execute_tool_duration_seconds_*, gen_ai_agent_token_usage_*, hermes_prompt_cache_*, hermes_message_count_total |
| Headers | traces without the pipeline header → 400; logs with it → 400; metrics and logs without it → 200 |
| Jaeger API | service required; start / end in µs, tags JSON, operation exact, minDuration=4s and 4000ms both accepted; unknown id → 404; limit truncates arbitrarily |
| PromQL | query_range and label/__name__/values work with the plugin's names; a range-vector instant query returns an empty body |
| Dashboard verify harness | fresh four-turn batch: every trace, metric and log check passed once the adapter over-fetched (search_fetch) |
| Resident memory | about 95 MB |
Caveats
- Pin the image. v1.3 introduces
greptime_trace_v2(opt-in, a separate table); the type sendsgreptime_trace_v1. - Schema grows with attributes. Every new attribute key becomes a column;
Int64may widen toFloat64. Fine for a laptop; mind it for a long-lived shared instance. - No retention by default. Add
x-greptime-hints: ttl=7dunderheaders:ordown -v. - Port. The bundled stack publishes 4398, not upstream's 4000, because dev servers often own 4000 (a Node server on
[::1]:4000makeslocalhost:4000hang while the container answers on127.0.0.1). - Exponential histograms are rejected; delta points are stored as sent (no conversion), so leave the plugin's cumulative default.
Troubleshooting
greptime requires endpoint— noendpoint:and noOTEL_GREPTIME_ENDPOINT; there is no default host.400 Pipeline is required for this APIin the debug log — the entry istype: otlp, nottype: greptime; or aheaders:block replacedX-Greptime-Pipeline-Namewith something GreptimeDB does not know.400 Unsupported pipeline for logs— the trace pipeline header reached/v1/otlp/v1/logs. Either the entry istype: otlpwith the header underheaders:, or atype: greptimeentry setsX-Greptime-Pipeline-Nameunderheaders:(entry headers go to every signal). Remove the header fromheaders:; the type sendsgreptime_trace_v1on traces by itself.- Metrics tab empty in the dashboard — the metric tables are created on the first export; a one-shot run exports once at exit. Widen the window; the adapter counts a series' first sample.