remote_write — whichever your
existing agent already speaks.
The service is regional. Data is stored in the region you wrote it to, and
there is no cross-region read:
Logs
Creating a group, ingesting into it, and the search window that is not
optional.
Metrics
What “Prometheus-compatible” does and does not mean here.
Traces
Span ingest, waterfall reads, and the one retention setting per account.
OTLP
Pointing a collector at us, and the authentication constraint you will
hit first.
Every telemetry endpoint is scoped to one account. Send the account’s handle
in
X-Account-Id; without it the request is rejected before it reaches a
handler. The handle selects which account you are acting on — it is not a
credential, and the IAM check still runs against that account’s resources.Logs
Create the log group first
A log group is the unit of retention, encryption and IAM scope. Records cannot be written to a group that does not exist — an ingest naming an unregistered group has that record rejected, not created for you.name is 1–512 characters of A-Za-z0-9_./#- and is immutable. Renaming a
group would change the CRN every existing policy references, so a rename is a
delete and a recreate.
Hierarchical names pay off in IAM. The name lands in the CRN verbatim, so a
policy can wildcard a whole subtree:
Retention
retention_days accepts one of twenty-two values, or null for never expire:
The closed set is not arbitrariness for its own sake. Retention is part of
the storage partition key, which is what lets expiry drop a whole partition
instead of rewriting one to strip short-lived rows out from between
long-lived ones. With free-form integers the partition count becomes a
function of how many distinct numbers customers happen to type. Twenty-two
options bound it.
clear_retention: true — retention_days: null alone is not enough.
Ingest
log_group, log_stream and body are required per record. log_stream is
free-form and conventionally identifies the producer — a host, a container, a
task. timestamp is optional and defaults to ingest time.
Two limits bound one call: at most 1000 records per batch, and a 4 MiB
request body.
Timestamps you supply are bounded
A caller-suppliedtimestamp is accepted only between 2000-01-01 and
24 hours ahead of the receiving server’s clock. The skew allowance covers a
drifting producer clock and a batching exporter.
The ceiling exists because your timestamp decides both which retention
partition a record lands in and when expiry drops it. A record stamped in
2200 would sit alone in a partition nothing queries and outlive its retention
window by however far ahead it was stamped.
basaltic_account_id and basaltic_org_id
are stripped from whatever you send and set from your signed identity. A
workload with shell access on one of your instances cannot label its logs as
someone else’s.
Search
from and to are required, and the window must be at most 31 days —
an unbounded search would turn into a full-retention scan. Results come back
newest-first with an opaque marker cursor.
trace_id is the useful one when you already have a trace: it pulls the log
lines emitted inside those spans, so you can read a request’s logs and its
waterfall against each other.
Deleting a group
DELETE /v1/log-groups/{id} removes the group’s administrative record. Log
records already written keep their reference to it and keep expiring on their
own schedule — but the group name no longer resolves, so you cannot search them
by group after the delete.
Metrics
What “Prometheus-compatible” means here
Precisely two things, and it is worth being clear about the third:Compatible
Ingest is the real
remote_write protocol — a snappy-compressed
protobuf WriteRequest, byte-for-byte what a Prometheus server sends.Response envelopes are Prometheus’s — status, data.resultType
(matrix or vector) and data.result — with sample values as strings
and timestamps as unix seconds, so an existing chart renderer reads them
unchanged.Not compatible
The query language is not PromQL. There is no
query= expression
parameter. You select a metric, filter it with label matchers, group it
and aggregate it through structured parameters.Joins, histogram_quantile, and arbitrary expressions have no equivalent
here. A Grafana Prometheus data source will not work against these
endpoints.Ingest
204 on success, per the Prometheus specification. Every timeseries
is stamped with your account and organization on the way in, and any tenant
labels the payload already carried are stripped first — a producer cannot claim
another account’s series.
Querying
An instant query returns a vector — the aggregate over one lookback window ending attime:
step:
required
The metric name. One metric per query.
avg | sum | min | max | count | last | rate | increase
How samples collapse inside a bucket. Defaults to
avg when omitted. rate
and increase derive from successive counter samples and are reset-guarded,
so a counter restart does not read as a spike.repeated
Label matchers using PromQL’s four operators —
=, !=, =~, !~. For
example match[]=job="api" and match[]=route=~/v1/.*.repeated
Group-by label names. Omit it and you get one series per distinct label set.
duration
Bucket width on
query_range (defaults to 60s, minimum 1s); lookback
window on query (defaults to 5m).POST with a form-encoded body, which is how
you send a matcher set too long to fit in a URL.
Discovery
GET /v1/metrics/names?start=…&end=… returns the distinct metric names you
emitted in the window. GET /v1/metrics/series?metric=…&start=…&end=… returns
the distinct label sets for one metric. Together they are what a dashboard
builder needs to offer a picker instead of a blank text field.
Metric retention is fixed at 30 days
Unlike logs and traces, metric retention is not per-tenant and not configurable — samples expire 30 days after their timestamp. That is also why the query window is capped at 31 days: a longer window could only ever return a partly-empty range.Traces
Ingesting spans
202 with per-record rejected
and errors.
trace_id is 32 lower-hex characters and span_id is 16, matching the
OpenTelemetry wire format. parent_span_id is empty for a root span.
end_time must not precede start_time. Span timestamps are bounded by the
same ingest window as logs.
kind defaults to INTERNAL and status_code to UNSET.
Reading traces
GET /v1/traces/{trace_id} then returns every span in that trace, ordered by
start_time ascending, so it can be drawn as a waterfall directly.
The 31-day window cap applies here too, and from/to are required.
Trace settings
Trace retention is set once per account, not per log group — one setting decides the fate of everything the account ever traces.There is no server-side sampling control. Trace settings cover retention
and key association, nothing else — the API stores every span you send it.
Sample in your SDK or collector, before the data leaves your workload.
Encryption at rest
A log group and an account’s trace settings each take an optionalkms_key_crn. When one is set, record bodies (and, for spans, the
customer-typed bag of name, status message, attributes, events and links) are
envelope-encrypted under that key before storage.
Indexed fields stay in the clear so search still works without unwrapping
anything: for spans that is service_name, trace_id, span_id, timing and
status_code.
OTLP
The OTLP receiver is a separate host serving the canonical OpenTelemetry paths, so an SDK resolves them from one variable:
Rejections come back in OTLP’s own
partial_success envelope with a
rejected_log_records count and a joined error message, rather than as an HTTP
error — the same per-record semantics as the native ingest, expressed in the
protocol’s shape.
Resolving the log group
OTLP has no log-group concept, so the receiver derives one from resource attributes:
The resolved group still has to exist in the account. Create it before you
point a collector at the receiver, or every record comes back rejected.
Severity uses
severity_text when the SDK set one; otherwise the numeric band
maps per the OpenTelemetry specification — 1-4 TRACE, 5-8 DEBUG, 9-12
INFO, 13-16 WARN, 17-20 ERROR, 21-24 FATAL.
The authentication constraint
The same applies toPOST /v1/metrics/write: a Prometheus server’s
remote_write block has no way to produce this signature on its own. See
Authentication for the full signing procedure.
Permissions
Telemetry actions authorize against the CRN of the thing being touched, so a policy can be scoped to one group or one naming convention. See policies for how the evaluation works.
A batch that touches several log groups is authorized per group. One denied
group fails only its own records — the rest of the batch is written.
Limits
1000 records
Per
POST /v1/logs and per POST /v1/spans.4 MiB
On both the native API and the OTLP receiver.
31 days
Required and capped on
GET /v1/logs, GET /v1/traces, and every metric
query.11 000
(end - start) / step on query_range.2000-01-01 to now + 24h
Applies to log timestamps and to span
start_time / end_time.30 days
Fixed. Log and trace retention are yours to choose.
Troubleshooting
Ingest returns 202 but nothing is searchable
Ingest returns 202 but nothing is searchable
Read
rejected and errors in the 202 body. The most common entry is a
log group that was never created — ingest is strict, and naming an unknown
group rejects that record rather than creating the group.The second most common is a timestamp outside the accepted window. Check
the producer’s clock: anything more than 24 hours ahead of ours is dropped
per record.Creating a log group returns 400 on the name
Creating a log group returns 400 on the name
Names cannot start with
/. app/prod/api is valid; /app/prod/api is
not. Slashes elsewhere in the name are fine.The full rule is 1–512 characters of A-Za-z0-9_./#-, and the first
character cannot be /.retention_days is rejected
retention_days is rejected
Retention is a closed set, not a range: 1, 3, 5, 7, 14, 30, 60, 90, 120,
150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653.
A value like 45 or 3650 is refused even though it falls between valid
entries.
Trace retention keeps reverting to 30 days
Trace retention keeps reverting to 30 days
PUT /v1/trace-settings replaces the whole settings document. Omitting
retention_days — or sending clear_retention: true hoping for
never-expire — resets to the 30-day default, because spans have no
never-expire option.Send the retention you want on every PUT, and read the response back.A metric query returns an empty result
A metric query returns an empty result
Confirm the metric name with
GET /v1/metrics/names for the same window —
a name that was never emitted returns success with an empty result, not an
error. Then check the label set with GET /v1/metrics/series: a matcher
against a label the series does not carry filters everything out.Also check the window against retention. Metrics expire after 30 days, so
a query near the edge of the 31-day cap can be reading past the data.A Grafana Prometheus data source will not connect
A Grafana Prometheus data source will not connect
It cannot. The read endpoints share Prometheus’s response envelope but not
its query language or its URL layout — there is no
/api/v1/query and no
query=<promql> parameter, and the endpoints require request signing that
the data source cannot produce.Ingest is the compatible half: remote_write from Prometheus or an agent
works, given a way to sign the request.A collector gets 401 on every export
A collector gets 401 on every export
OTLP is authenticated exactly like the rest of the API, and a static header
will not satisfy it. You need per-request
BASALTIC-HMAC-SHA256 signing
with a fresh X-Date and X-Nonce, plus X-Account-Id for the account
you are writing to.If the signature is right and you still see 401, check clock skew first —
a signature is valid for five minutes from its X-Date.Next
Authentication
The signing procedure every ingest and query request needs.
Policies
Scoping a policy to a log group subtree.
Regions
Which host to call, and why telemetry is regional.
API reference
Every telemetry operation, with request and response schemas.