https://otlp.lumenstat.com is an OpenTelemetry Collector that accepts OTLP over HTTP. It turns server spans into per-route request counts and latency histograms, keeps three HTTP latency metric series, and drops everything else. Everything on this page is read from apps/collector/config.yaml and the writer in apps/writer/src.
Endpoint
Any other path on
otlp.lumenstat.com returns 404 at the edge.
Protocol
In the current pilot
otlp.lumenstat.com is served through Cloudflare, which sees requests in transit. The production design serves it directly from EU servers. See Privacy.
Header
The project’s ingest key from the Setup page:
scf_ingest_ followed by 32 URL-safe characters (24 random bytes, base64url). Lumenstat stores only its SHA-256 hash.- The collector’s
resource/tenantprocessor deletes anyscf.ingest_keyresource attribute the client sent, then inserts the header value asscf.ingest_key. Theotlpreceiver runs withinclude_metadata: trueon both protocols, which makes the request header available to the processor. - This happens before spanmetrics aggregates, so spans from different keys never share a series.
- The writer hashes the key and looks it up. The key must not be revoked and its tenant must be
active. Any tenant field in the payload, such as atenant.idresource attribute, is ignored. - Lookups are cached for 60 seconds, so a key revoked in the database can keep working for up to a minute.
Requests without a valid key
The collector does not check the key. A request with a missing, unknown or revoked key gets the same success response as a valid one. Its spans are aggregated like any other, and the writer then drops every resulting data point with the reasonunknown_ingest_key. Nothing is stored and the sender sees no error. Use Is data arriving? on Setup to confirm a key works.
Checking ingest keys at the edge, instead of only in the writer, is planned for Milestone 2. See the roadmap.
Accepted metric series
The collector’sfilter/http-server-latency processor and the writer’s ACCEPTED_METRICS keep these histogram series and nothing else.
- Units. The writer converts
s,ms,usandnsto milliseconds using the metric’sunit. An empty or unknown unit falls back to the default in the table. - Temporality. The collector converts cumulative series from SDKs to delta with its
cumulativetodeltaprocessor. The writer drops any point that still arrives as cumulative. - Routes from SDK metrics. HTTP server metrics carry
http.routebut not the raw path. A metric point withouthttp.routeis stored under the route{unknown}. - Both sources for one service. If a service sends both SDK metrics and spans, the read side uses only the SDK metrics for that service in the requested time range, so no request is counted twice. A service that switches between the two within one range is counted from its metrics only.
spanmetrics
The traces pipeline isotlp receiver → resource/tenant → spanmetrics connector. It has no exporter, so spans are not stored or forwarded.
Dimensions
spanmetrics adds its default dimensionsservice.name, span.name, span.kind and status.code, plus these configured ones:
Both the current and the older HTTP semantic conventions work. The writer reads
span.kind to keep server spans and ignores span.name and status.code. Of the resource attributes, it reads only service.name and scf.ingest_key.
How attributes become stored fields
Histogram buckets
Every latency histogram is stored as 15 counts on one fixed set of bounds in milliseconds:
These are the bucket boundaries OpenTelemetry recommends for
http.server.request.duration, converted from seconds, and the ones the collector’s spanmetrics uses. An SDK histogram with the default boundaries maps one to one.
- Other boundaries. Each source bucket is counted in the fixed bucket that holds its upper bound. A source histogram with no boundaries is placed at its mean.
- Why fixed. Counts on the same boundaries add up without loss. p50, p95 and p99 stay accurate to the bucket when Lumenstat sums several instances of a service or a long time range.
- Percentiles. The read side interpolates linearly inside the bucket that holds the requested rank and rounds to 0.1 ms. A percentile that falls in slot 14 reads 10,000 ms.
Storage
The writer adds each point to one row ofscf_api.api_metric_minute:
The primary key is every column from
tenant_id to bucket_start. A point for an existing row adds its count, duration and bucket counts to it. The schema is in apps/writer/db/migrations/001_init.sql.