Skip to main content

Loki logql-range/v1alpha1

Implemented by the loki adapter since 0.38.0: range, metric and label reads

Rendered from adapters/loki/contracts/logs/v1alpha1/semantics.md by connectors-docs; the source owns every rule on this page.

  • Status: proposed; the unpaged library binding of §11.3 is implemented, without executable composition.
  • Family: datasources. Siblings: records, series, relational (in service v1alpha1).
  • Recorded: 2026-09-08; ownership relocated 2026-09-09.
  • Shared dependency: datasource.logs/v1alpha1. This file owns all Loki-specific semantics.

1. Identity​

FieldValue
Contractdatasource.logs/v1alpha1
Profilelogql-range
Shared with recordsprovenance, completeness, cursor binding rules (docs/design.md:434-446); not the record page shape

A log read returns a bounded observation of the provider's available logs, with explicit selection, order and truncation. Loki preserves native LogQL and returned stream labels. A result proves neither archival retention, absence of late arrivals, a provider-wide snapshot nor continuity across provider rotation/restart.

Loki continuation pages one immutable retained observation. It never reissues a provider query with an adjusted timestamp edge. The selected provider API supplies neither occurrence identity nor a tie cursor; timestamp/text/label hashes cannot distinguish two genuine identical entries. A saturated provider response therefore ends with a truthful non-resumable partial result after its retained entries have been delivered. The provider evidence record identifies the exact sources and selection limits.

3. Types​

Input, logql-range:

{ "query": "{app=\"api\"} |= \"error\"", "start_unix_ns": "1788822000000000000", "end_unix_ns": "1788825600000000000", "direction": "backward", "limit": 200, "collection_limit": 1000, "max_line_bytes": 8192 }

Only query and start are required. Direction defaults to backward; limit and collection_limit default to 1,000; max_line_bytes defaults to 8,192. limit is the public page size; collection_limit is the single provider query's entry cap. Both are integers in 1–1,000 with limit <= collection_limit; max_line_bytes is an integer in 1–8,192. This explicit distinction permits small public pages while keeping total collection bounded.

Resume input is exactly {"cursor":"opaque value"}. Mixed initial/resume fields, explicit null aliases, unknown members, provider interval/step/since overrides and caller origin/route/header selectors are InvalidInput. Resume cannot change the original query, time window, page size or projection.

Output:

{
"lines": [ { "timestamp_unix_ns": "1788825599000000000", "stream": { "app": "api" }, "line": "…", "line_truncated": false, "redacted": false, "source": null } ],
"selection": { "kind": "loki-range", "start_unix_ns": "…", "end_unix_ns": "…", "direction": "backward" },
"order": "timestamp-backward",
"complete": false,
"truncation": { "causes": ["provider_limit"], "occurrences_dropped": null, "stream_groups_dropped": null },
"next_cursor": null,
"provenance": { "instance": "…", "resource": "loki:…", "observed_at_unix_ms": 0, "source_revision": null }
}
FieldRule
timestamp_unix_nscanonical nonnull decimal string; conversion preserves actual source precision
streamreturned Loki labels
sourcenull
redacted / line_truncatedrequired Boolean flags for configured redaction and UTF-8-safe line clipping, independently
selection / orderclosed profile variants below; no fabricated universal window
completesource exhaustion AND no omitted occurrences AND final retained page; line-content clipping is separate (§5)
truncationall applicable causes, plus exact counts only where the total is known (§5)
next_cursorLoki only, iff another retained offset exists; null at a terminal partial result even when unseen provider entries may remain

Loki selection is {kind:loki-range,start_unix_ns,end_unix_ns,direction} and uses order=timestamp-forward/backward. An output label changed by native LogQL is a value, not proof of an original selector or authority. Safe errors and byte accounting are in §5.

4. Rules​

Current host principal, visible operation, source/connection, scope and applicable credential/route evidence are admitted before dispatch. Known revocation and current binding fences also govern publication and result disclosure. These are host ordering guarantees, not an atomic transaction with future provider permission changes. No stale-on-error, cross-principal cache reuse, live follow or automatic read redispatch is selected. F15 owns any later refresh/retry profile under its original shared budget.

4.1 Provider tenant binding​

The direct Loki connection owns its admitted provider tenant. When required by the deployment, configure http.extra_headers: {"X-Scope-OrgID":"tenant-a"} under the shared HTTP header rules. tenant_header was an earlier descriptive label, not a supported key. No caller input may choose or override the tenant. Header placement and revision fencing are future binding obligations; current HttpConfig does not implement the extension.

A mediated connection consumes the shared mediated-HTTP capability and requires a parent route that establishes the configured provider tenant. It sends no direct header override through that capability and refuses a missing guarantee without fallback. Loki knows no concrete parent implementation.

4.2 Native LogQL scope​

Configuration selects query_scope.required_equalities, a duplicate-free array of exact {label,value} pairs. Missing scope configuration is invalid; an explicitly admitted empty array means the entire configured provider tenant is allowed. Valid provider label names and nonempty required values avoid empty-label/missing-label ambiguity.

A reviewed parser conforming to Loki v3.7.0 must accept the complete native log-selector or log-pipeline expression and inspect every initial stream selector. Each must contain every required equality with the exact decoded label/value and equality operator. Extra matchers and native filters/parsers/formatters remain supported. A regex, text inside a string, a pipeline label filter or a rewritten output label cannot discharge an initial equality. Metric/sample syntax is not a log query: literal/vector expressions also implement the provider's LogSelectorExpr interface, so interface membership alone is insufficient. The selected root must actually be a stream selector or pipeline.

The native parser evidence retains the exact AST/parser sources used to distinguish those roots. It supplies source evidence, not an implemented or verified local parser.

Parse failure, unsupported/non-log syntax or exceeding 16 KiB UTF-8, 4,096 syntax nodes or depth 64 is InvalidInput. A valid log query missing the required scope is Forbidden. Neither dispatches. The admitted original query bytes are URL-encoded and forwarded unchanged: no reserialization, injected filters, fixed-query substitute or remote parsing probe. A missing/unverified parser cannot advertise the scoped profile. The same predicate applies through mediation and to declared verification queries.

4.3 Loki collection and ordering​

Input nanoseconds are canonical 0 or [1-9][0-9]*, at most 9,223,372,036,854,775,807. Reject signs, whitespace, leading zeroes, decimal/exponent forms and overflow. Resolve omitted end once from the trusted receiver clock; require start < end and checked end-start <=86,400,000,000,000. The single GET fixes both exact decimal times, direction, limit=collection_limit and JSON output. No provider default time or sampling interval is used. Start is inclusive and end exclusive in both directions.

Validate the entire bounded JSON response before publishing. Require successful status, resultType=streams, string labels, valid two-element timestamp/text tuples, in-window representable timestamps and provider order within each group. Reject duplicate JSON keys, missing result type, metric results, malformed entries, unsupported tuple extensions, wrong ordering/window and counts above collection_limit as Unavailable. No malformed-entry skipping or incomplete-JSON prefix may establish an observation. This selected plain Loki success format has no generic partial field; unknown warning/partial extensions must fail safely instead of being ignored.

Retain the first 500 provider stream groups in response order, counting all omitted groups and occurrences from the fully validated response. Groups with identical returned labels remain separate groups for this bound; native pipelines can make their labels equal. Flatten retained groups with private (group ordinal,entry ordinal) coordinates. Sort by timestamp in the requested direction; ties use ascending coordinates. Never deduplicate equal timestamp/labels/text, even after redaction or clipping. Coordinates identify positions in this captured observation only.

Apply any admitted deterministic redaction once, flag changes, and then take the longest UTF-8 prefix within max_line_bytes. Clipping shortens content, not the occurrence count. Intern group labels and retain the immutable resulting occurrences, index and original private context under the storage ceiling. If the whole observation cannot fit, return Unavailable before the first page; do not drop stored pieces while claiming complete continuation.

Provider exhaustion requires a complete successful validated response with fewer than collection_limit occurrences, no known partial execution, and a provider/proxy binding verified to honor that effective limit and surface incomplete execution. A hidden smaller limit invalidates this proof. Exact-cap responses remain conservative partial even if exactly that many source entries existed. A validated empty response can establish exhaustion under the same conditions. Stats and emitted page size cannot establish it.

An unverified effective limit or incomplete-execution guarantee refuses the read as Unavailable. A smaller emitted count cannot repair that missing binding proof.

4.4 Retained paging and current authority​

The host's logical ReadCursorCachePort owns this bounded ephemeral observation and context; it is not another provider entity or discovery publication. Offset zero starts the initial page. A page is the longest consecutive slice fitting the fixed public limit and complete serialized response ceiling, including repeated labels, escaping, selection, cursor and provenance. Reserve sufficient cursor/envelope bytes when deciding the slice. If even metadata plus one occurrence cannot fit, refuse without a cursor. Only a verified empty observation yields an empty page; a nonterminal page is never empty.

A next cursor exists iff a later retained offset exists. It authenticates an unpredictable observation reference and offset, bound to instance, operation/profile/contract/projection revision, connection and configured provider authority/tenant, direct or mediated source and parent/route/credential/private fences, principal/tenant/scope/policy, exact query/window/direction, collection/page/line limits and redaction configuration. No credential, origin or private route locator is exposed in the opaque token. Current admission precedes disclosure of cursor validity and cached content; then every context coordinate must match. A cursor grants no authority.

Repeating an admitted cursor returns the same slice without provider I/O, with a strictly advancing next offset when present. All pages preserve the original provider observation time and source_revision:null. Each request has its own ordinary handling deadline but cannot extend the fixed observation expiry, at most 300 seconds from initial observation. Expiry, eviction, restart/key loss or changed context is StaleCursor after current admission; unavailable authority/cache infrastructure is Unavailable. Never regenerate a view, resolve a fresh now, query a different edge or fall back to direct access. A new initial read is a separate observation and may repeat or change entries.

Cross-request query caching remains disabled by default; the retained observation is the explicitly selected exception needed for its own continuation. Current known revocation and binding fences govern publication and disclosure, including every cached page. Successful earlier admission or unchanged query text cannot override them.

5. Limits and completeness​

BoundValueSource
Loki occurrences / stream groups1,000 / 500 per observationselected from old projection
Line bytes8 KiB maximum after redaction, before result JSON escapingold projection ceiling
Loki labels<=128 labels/group, <=128 UTF-8 bytes/name, <=4 KiB/value and <=32 KiB serialized group; violation refuses, never clips labelsselected metadata bound
Loki provider responseindependently <=4 MiB encoded entity and decoded/decompressed JSON; <=16 KiB headersselected bounded full decode
Retained observation<=4 MiB accounted storage including interned labels, occurrences/index and private contextselected ephemeral retention
Retained log pool<=64 MiB / 256 observations per service instance, <=16 MiB / 64 observations per admitted principal/tenant partition; smaller configured limits allowedselected host capacity
Public response<=4 MiB including all framing, escaping and cursorordinary service binding
Window / relative selector<=24 h where declared in the selected inputselected profile ceiling
Provider workone log GET; no retry/followselected call budget
Deadlineexisting 20 s outer / 15 s shared provider budget, including admitted mediated workordinary service binding
Cursor TTL<=300 s from the original observation, never renewed by pagesselected retention

These are receiver ceilings, not vendor defaults or measured throughput. Finite reads/cancellation do not prove immediate remote cancellation or bound a provider's internal scan/CPU work. Deployment-side query limits remain independently admitted requirements.

Count any admitted mediated metadata/permission work inside the same 15 s provider / 20 s execution budgets, including at-most-5 s connects; the selected route binding must declare its own finite call budget. No activation/identity probe or refresh is silently added to the read. Retention admission accounts backing allocations and context/index overhead atomically across concurrent requests; expire first, then least-recently-used eviction or refuse a new observation if it still cannot fit. Eviction makes affected cursors stale and never regenerates or extends their observations.

complete = source_exhausted AND no_occurrences_omitted AND final_retained_page. Line clipping is separate and may coexist with complete:true.

Truncation causes are duplicate-free in this order: provider_limit, provider_partial, stream_limit, page_limit, response_bytes, source_bytes, line_bytes. The selected Loki binding uses these independent predicates:

CauseTrigger
provider_limitThe fully validated provider occurrence count equals collection_limit
stream_limitThe fully validated response contains more than 500 stream groups, even if an omitted group is empty
page_limitA retained remainder exists and the emitted page contains limit occurrences
response_bytesA retained remainder exists and appending its next occurrence to this page, with the required envelope/cursor for that candidate page, would exceed 4 MiB
line_bytesAt least one emitted line was clipped, including clipping after admitted redaction

Evaluate page_limit and response_bytes independently; both appear when both predicates hold. Provider/group causes describe the whole observation and persist on every page; page/result/line causes are recomputed for the emitted page. No provider_partial or source_bytes success is selected here: unknown partial extensions and incomplete or oversized source JSON refuse under §4.3. Retained remainders are not dropped occurrences. A terminal incomplete observation may have no cursor while provider_limit/stream_limit persists.

Occurrences_dropped and stream_groups_dropped are exact nonnegative totals only when fully observed evidence establishes them, otherwise null for each affected total. Provider saturation leaves unseen totals unknown, not zero; an exact local omission count is not the total including unknown provider losses. Line clipping alone drops zero occurrences. Redaction has its own flag and does not guarantee secret-free content.

Safe base errors apply. Provider 400 for an admitted native query is InvalidInput without raw provider text. Exhausting the original execution or shared provider budget is Timeout, before or during collection; timeout is not clean EOF or a byte-cutoff success. Other failed, malformed, oversized or interrupted provider observations are Unavailable. No raw provider errors, query text, origins or credentials enter ordinary diagnostics.

6. Conformance scenarios​

  • 1,001 occurrences at timestamp T, collection_limit=1,000 and public limit=200: retain every returned occurrence including identical lines, deliver five pages, finish incomplete with no cursor and unknown unseen totals. No timestamp-edge query follows.
  • 999 source occurrences under collection_limit=1,000 and public limit=200: four full pages and one 199-entry page; only the final page is complete when nothing was omitted. Repeating a cursor returns its identical slice.
  • Exact-cap source is conservatively partial; validated empty source can be complete. A provider returning 1,800 entries against a requested cap of 1,000 is a protocol failure, not a basis for promising recovery of 800 unseen entries.
  • 501 fully observed groups below the occurrence cap: preserve 500 groups, count known omissions, report stream_limit and final incomplete. Byte/page/line/provider causes accumulate without relabeling unknown losses as zero.
  • Fixture line of 20 KiB → line_truncated: true, line is an 8 KiB UTF-8-safe prefix.
  • Window exceeding the maximum → InvalidInput, no dispatch.
  • Loki 400 (bad LogQL) → InvalidInput; the error message contains no provider body text.
  • Tenant header configured → present on the fixture request; caller input cannot set or override it.
  • Required equality appears only in quoted line text, regex or a pipeline filter → scope refusal before dispatch. A native admitted selector/pipeline remains byte-for-byte unchanged.
  • Metric result, missing type, malformed tuple, wrong window/order, oversized JSON or hidden lower provider cap cannot become complete empty logs.
  • Expiry, eviction, policy/credential/parent-route/redaction changes or key loss refuse cursor reuse without a requery. A denied caller learns no private cursor status.

These are specification scenarios, not implemented fixture results. Detailed dispositions and executable shape evidence must record their actual verification boundary; ESS shape acceptance does not execute provider, ordering or authority decisions.

The F12 disposition and scenario record maps every initial finding to these rules and records manual expected outcomes. The model check record states the separate executed ESS boundary.

7. Compatibility​

  • New profile binding; no old wire contract for logs existed beyond operation results.
  • Service compatibility is authoritative for the binding. Log schemas can inhabit the ordinary result slot, but require explicit profile support; framing compatibility does not confer log or cursor semantics on existing typed consumers. Tenant headers remain receiver configuration.

8. SDK and host obligations​

ObligationWhere
LogLine, LogRead public codecsfuture connectors-contracts binding; the current crate has no such implemented types
Immutable observation retention and admitted offset cursorfuture SDK/host binding; the existing crates/connectors-sdk/src/lib.rs:161 helper authenticates a token but proves neither retention, progress, exhaustion nor scope
Native LogQL admission, bounded decoders and current disclosure fencesfuture adapter/host binding; no implementation provided by this contract
Receiver-owned http.extra_headers placement and validation (§4.1)future HttpConfig / HttpCapability binding; current host configuration does not implement this extension

9. ESS entities​

LogQL range selection, direction and required selector equalities are typed in the Loki-owned model. Shared collection/page facts and decisions have no shared ESS model; the proposed one is in history at commit 6498a881c. Adapter predicates and runtime enforcement are not supplied by these private shapes.

Entity / valueNotes
OperationDeclaration.profile = logql-rangevalue
Streams, linesnot entities; payload

Proposed provider-independent collection/page facts and completeness decisions have no shared ESS model today; native selections remain adapter-owned as above. Provider grammar, full context equality, occurrence preservation/order, decoding, clocks, numeric/byte bounds and atomic current publication remain explicitly UNMAPPED binding obligations. No persistent provider entity, parser, public codec or cache implementation is introduced.

10. Open decisions​

DecisionDefault taken
Redactionhost filter, opt-in, flagged per line; not a contract guarantee
Window maximum24 h
Durable/archival continuation and live followseparate unselected profiles; finite native queries and retained Loki paging remain selected

11. Metric queries, label discovery and the unpaged binding​

Recorded: 2026-10-08. These profiles close the two limits the 2026-09-09 baseline recorded: §4.2 refuses metric expressions as log queries, and no label discovery was selected. They are typed in the Loki-owned model (MetricSelection, MetricResultType, LabelSelection) and declared in the adapter specification.

OperationContractProfileNative request
logs.query_rangedatasource.logs/v1alpha1logql-rangeGET loki/api/v1/query_range
logs.query_metricdatasource.series/v1alpha1logql-metricGET loki/api/v1/query (instant) or GET loki/api/v1/query_range (range)
logs.labelsdatasource.logs/v1alpha1loki-labelsGET loki/api/v1/labels or GET loki/api/v1/label/<name>/values

Paths are relative to the configured base URL. Every operation is one GET with no retry, under the shared provider bounds of §5: at most 4 MiB of provider body and the existing transport deadline. A body over that bound, a transport failure or a malformed answer is Unavailable; a deadline is Timeout. Provider 400 and 422 are InvalidInput, 401 is Unauthorized, 403 is Forbidden, 429 is RateLimited and any other non-200 status is Unavailable. No provider text, query text, origin or credential enters an error.

Every success envelope must be exactly {"status":"success","data":…}; any other top-level member, including a warnings extension, is Unavailable (§4.3).

11.1 logql-metric​

Input is an instant query {query, time_unix_ns?} or a range query {query, start_unix_ns, end_unix_ns, step_seconds}; the two forms do not mix. query is 1–16,384 bytes. Omitted time_unix_ns is the receiver clock. Timestamps follow §4.3 (canonical decimal nanoseconds); a range requires start < end, end − start ≤ 24 h, step_seconds in 1–86,400 and at most 11,000 points per series ((end − start) / step + 1). A violation is InvalidInput before dispatch. The range request sends start, end and step=<n>s exactly; the instant request sends time.

data.resultType must be vector, matrix or scalar. A streams answer means the query was a log query: InvalidInput, never log lines. Result:

{ "result_type": "matrix", "series": [ { "labels": { "app": "api" }, "samples": [ { "timestamp_unix_ns": "1788825600000000000", "value": "42" } ] } ], "complete": true, "truncation": [], "provenance": { "instance": "…", "profile": "logql-metric", "observed_at_unix_ms": 0 } }

A sample timestamp is the provider's decimal seconds converted exactly to canonical nanoseconds; an exponent form or more than nine fractional digits is Unavailable. A value stays the provider's string (NaN, +Inf included), never reparsed. A scalar is one series with empty labels. Series keep provider order; samples keep provider order within a series. Labels follow the §5 label bounds; a violation is Unavailable.

At most 500 series and 50,000 samples in total are returned. Series past 500 are omitted with cause series_limit; samples past 50,000 are omitted with cause sample_limit, from the first series that reaches it. complete is false exactly when a cause is present. Loki's own series limit answers an error, not a partial result, so a complete answer is exhaustive for the query.

11.2 loki-labels​

Input {label?, start_unix_ns?, end_unix_ns?, query?}. Without label the operation lists label names; with label it lists that label's values. label is a Loki label name ([A-Za-z_][A-Za-z0-9_]*, at most 128 bytes) and is sent as one encoded path segment. query is an optional stream selector of 1–16,384 bytes, forwarded unchanged. Omitted end is the receiver clock; omitted start is end minus six hours, Loki's own default made explicit. The window follows §4.3 and is at most 24 h.

data must be an array of strings, each at most 4 KiB; anything else is Unavailable. Result {label, values, complete, truncation, provenance} keeps provider order and returns at most 10,000 values; past that, cause value_limit and complete: false.

11.3 The implemented binding (2026-10-08)​

The library binding in adapters/loki/src implements these three operations with this narrower selection, which the shared logs contract admits ("an unpaged result is its final page"):

  • Unpaged. logs.query_range takes limit (1–1,000, default 1,000) as both the public page size and the provider entry cap; collection_limit and cursor are not accepted. next_cursor is always null and no observation is retained (§4.4 is not implemented). The emitted result is bounded to 4 MiB serialized; lines past it are omitted with cause response_bytes.
  • Scope. Configuration requires query_scope.required_equalities and admits only the explicit empty array (§4.2: the entire configured tenant). No reviewed LogQL parser exists, so the scoped form is not advertised and a nonempty array is an invalid configuration.
  • Root classification without a parser. A log query sent to logs.query_metric, or a metric query sent to logs.query_range, is recognised from the provider's resultType after dispatch and refused as InvalidInput. §4.2's pre-dispatch refusal needs the parser.
  • Tenant header. X-Scope-OrgID placement (§4.1) needs the shared HTTP header extension, which the host HTTP port does not carry; the binding sends no tenant header.
  • Exhaustion proof. §4.3 requires a provider binding verified to honour the effective limit. The binding treats a validated answer below limit as exhausted; a deployment whose proxy lowers the limit silently is not detected.
  • Redaction is not applied; redacted is always false.

The executable composition that serves this binding through connections connect and invoke is §11.4.

11.4 The bearer connection (2026-10-08)​

The executable connectors-loki (adapters/loki/src/local.rs) advertises one profile, loki.bearer, and serves the three operations of §11.3 through the local host. Its configuration, profile, entry and identity probe are modelled in spec/ess/domains/connection.yaml; tests/ess_model.rs checks the executable against the schemas and invariants that model generates.

  • Profile. Scheme http_bearer, capability http-bearer, purpose service_account, subject app. The protected entry is {"token": "..."} (1–8,192 visible ASCII bytes, no spaces), sent as Authorization: Bearer <token>. Loki grants no scopes, so the profile requires none and every operation is a read.
  • Identity probe. Connect, repair and revalidate send GET /loki/api/v1/labels with the token. 200 admits it; 401 and 403 refuse it as an invalid credential; 429 and 5xx are unavailable; any other status is a protocol failure. The answer's body is not read. Validation evidence lasts 60 seconds.
  • Identity. Loki has no user or account read, so the identity is the configured connection (identity source configuration): kind loki.connection, subject the configuration's instance. Any token the deployment accepts is the same identity, and a repair cannot detect a token of another tenant or deployment. Use one instance per deployment and tenant.
  • Configuration. The owner-only file connectors-loki-local/1 names instance (1–128 characters of A–Z a–z 0–9 _ . -), an HTTPS base_url of at most 512 characters written in its canonical form (lowercase https:// and host, a path ending in /, no credentials, query or fragment), an optional owner-only PEM ca_file at an absolute path that replaces the system roots (absent, never null), and the explicit query_scope of §11.3. Plaintext HTTP is refused. The configuration revision covers the base URL and the CA file's digest, so a changed CA refuses the cached launch.
  • Not served. loki.anonymous: the local host admits only profiles with at least one entry field and a bearer, basic, mTLS or session scheme, and this binding does not widen that. loki.basic and loki.via_parent are not implemented.

12. Configured line and trace profiles​

Recorded: 2026-10-14. Two reads whose LogQL the operator writes once, in the connection's configuration, as a named profile; the caller names the profile and passes only the values its operation declares. The profiles are typed in the Loki-owned model (LineProfile, TraceProfile, ProfileLinesSelection, CallTraceSelection, ProfileRecord, TraceLine) and carried by connectors_loki.connection.LocalConfiguration; the operations are declared in the adapter specification.

OperationContractProfileNative request
logs.profile_linesdatasource.logs/v1alpha1loki-line-profileGET loki/api/v1/query_range, forward
logs.call_tracedatasource.logs/v1alpha1loki-call-traceGET loki/api/v1/query_range, forward

Both are reads under the bearer connection of §11.4 and follow §11.3: one GET, no retry, unpaged, at most limit (1–1,000, default 1,000) provider entries over a window the caller states with both start_unix_ns and end_unix_ns (§4.3 timestamps, start before end, at most 24 hours), the provider answer validated whole as §4.3 requires, at most 500 stream groups retained and the emitted answer bounded to 4 MiB. Truncation causes and dropped totals follow §5; complete is false exactly when provider_limit, stream_limit or response_bytes applies. Errors are §11's. A profile name the configuration does not hold is NotFound before dispatch; any other input the operation does not declare is InvalidInput before dispatch.

12.1 Why a profile is not the injected filter §4.2 forbids​

§4.2 governs caller-authored LogQL. There the caller's query bytes are the request, so the adapter forwards them unchanged and may not inject a filter or substitute a fixed query: the caller would no longer get what it asked for, and scope could only be proven by parsing what it wrote. A profile read takes no caller LogQL at all. The adapter composes the whole query itself from structured parts of operator-authored configuration — label matchers with a declared operator, a declared format and declared field keys — and places the caller's one value, a call identifier or a matched field value, only inside a LogQL string literal in a fixed position. There are no caller bytes to preserve, and no parser is needed to know what the query selects: the adapter built its syntax tree and renders it.

A string literal is double-quoted with ", \, newline, carriage return and tab written as \", \\, \n, \r and \t and every other control character as \uXXXX, so no caller text can end the literal or add a matcher, a line filter or a pipeline stage. A matcher value in the configuration is rendered the same way.

12.2 loki-line-profile​

A line profile names a stream selector (1–16 matchers, at least one a nonempty equality), a format and 1–64 typed fields, one of which is the call identifier. The format is logfmt (key=value, key="quoted" with the escapes of §12.1, or a bare key) or a pattern of literal text and <name> captures (<_> discards), matched from the line's start, each capture ending at the next literal's first occurrence and the last running to the line's end. A field reads one key and has a type: string; integer (a signed 64-bit decimal); phone_number (text); or unix_seconds (decimal seconds, at most nine fractional digits), unix_millis or unix_nanos, each answered as canonical decimal nanoseconds. A field may be optional (null when its key is absent) and filterable.

Masking is per field: none answers the value, redact answers "<redacted>", and last_four replaces every character but the last four with * (a value of four characters or fewer is all *). A phone_number field without a declared mask is last_four; every other field without one is none. Only text is masked. The call identifier is the join key a workflow reads, so it is unmasked, required string text.

Input {profile, start_unix_ns, end_unix_ns, limit?, match?}. match maps up to 16 fields the profile declares filterable to an exact value (1–512 characters). With no match the composed query is the selector alone. With matches it is the selector, the format's parser stage — | logfmt, or | pattern "<the configured pattern>" — and one exact label filter | <key>="<value>" per match, in field-name order, combining with AND: Loki answers only lines whose parsed key is exactly the value. A match is never a substring line filter. A filterable field's key is therefore a label name that does not begin with __ (Loki rewrites any other key and keeps __ names for itself) and is no label of the profile's selector (a stream label of the key's name would shadow the parsed value in the filter); a profile breaking either is refused at configuration. The adapter then reads every answered line itself and checks each match again, exactly and before masking.

No unmasked value reaches the answer, and that includes every count and complete. A record carries no raw line. Nothing is counted about a line before it is known to hold every matched value exactly, so the answer to a match on a value no line holds exactly — a prefix of a masked number, say — is the answer to a match on a value no line holds at all, at every limit: no records, skipped.unreadable 0, the same complete and truncation.

Result:

{ "profile": "fixture-cdr", "records": [ { "timestamp_unix_ns": "1788822090000000000", "call_id": "fx-leg-0001", "fields": { "direction": "inbound", "caller": "********0001", "call_id": "fx-leg-0001" } } ], "skipped": { "unreadable": 1 }, "selection": { "kind": "loki-profile", "start_unix_ns": "…", "end_unix_ns": "…", "direction": "forward" }, "order": "timestamp-forward", "complete": true, "truncation": { "causes": [], "occurrences_dropped": 0, "stream_groups_dropped": 0 }, "provenance": { "instance": "…", "resource": "loki:query_range", "observed_at_unix_ms": 0, "source_revision": null } }

Records are in timestamp order, ties in provider order. skipped.unreadable counts the answered lines that do not read as the profile — the format does not match, a required key is absent, a value is not its type or is over 4 KiB — and, with matches, only those whose format reads and whose matched keys hold every value exactly. An answered line that does not hold every matched value exactly on the adapter's reading, or whose format does not read at all when there are matches, is dropped and counted nowhere: Loki's exact filter should not have answered it, and counting it would tell the caller about a value it did not name. None of these is an omitted occurrence. complete, the truncation causes and the dropped totals describe Loki's answer to the exact query, so they too reflect only lines holding the caller's exact values.

12.3 loki-call-trace​

A trace profile names a stream selector (as §12.2), where the call identifier is carried — a stream label, or a key of the line read with a declared format — and the label that names each line's source. Input is {profile, call_id, start_unix_ns, end_unix_ns, limit?} and nothing else; call_id is 1–512 characters of any text. The composed query is the profile's matchers, then <source_label>=~".+", then for a label-carried identifier <label>="<call_id>"; for a line-carried identifier it is followed by the format's parser stage and the exact label filter | <key>="<call_id>", as §12.2 composes a match, never a substring line filter. The key is a label name not beginning with __, and neither the source label nor a label of the selector; a profile breaking that is refused at configuration.

The answer is the call's lines, oldest first with ties in provider order, each with its source, its text clipped to 8 KiB (line_truncated, cause line_bytes) and its timestamp:

{ "profile": "fixture-by-label", "call_id": "fx-leg-0001", "lines": [ { "timestamp_unix_ns": "1788822000100000000", "source": "edge-proxy", "line": "INVITE received", "line_truncated": false } ], "selection": { "kind": "loki-call-trace", "start_unix_ns": "…", "end_unix_ns": "…", "direction": "forward" }, "order": "timestamp-forward", "complete": true, "truncation": { "causes": [], "occurrences_dropped": 0, "stream_groups_dropped": 0 }, "provenance": { "instance": "…", "resource": "loki:query_range", "observed_at_unix_ms": 0, "source_revision": null } }

No line of another call is answered. A retained stream without a nonempty source label, or a label-carried answer whose stream names another call identifier, is not Loki's answer to the composed equality and is Unavailable, never mixed in. For a line-carried identifier the adapter reads every answered line itself: a line is answered only when it reads in the declared format and its key is exactly the identifier. Any other line Loki answered — which its exact filter should not have — is dropped and counted nowhere, so a trace of an identifier no call has answers exactly as a trace of one no line holds anywhere: no lines, the same complete and truncation, nothing about other calls' lines. No unmasked value of another call reaches the answer, counts and complete included.

The adapter answers only lines on which Loki's parser and its own reader agree, and no substring pre-filter stands between them: whatever line Loki's parser extracts with the key exactly the value is fetched, and the reader then decides. The reader reads a pattern as Loki's pattern parser does (from the line's start, each capture to the next literal's first occurrence) and a logfmt value unquoted with the escapes of §12.1. Where Loki's parser is more lenient than the reader on a malformed line, Loki may answer it and the adapter drops it as above; where the two read one line differently, the line is not answered, and that disagreement is the limit of what complete can promise. A stream label outside the selector that has the matched key's name shadows the parsed value in Loki's filter; naming such a key is the operator's error, and the reader's own check keeps any line it lets through out of the answer. One invocation reads one connection; merging the traces of several connections is the workflow's time-ordered join.

12.4 A profile never weakens the configured scope​

Every equality of query_scope.required_equalities must appear among each profile's matchers as an exact = with the same label and value; a profile without one is refused at configuration, before any read. The adapter only adds to what a profile selects: matchers of a selector all hold of every stream it selects, so the source-label matcher, the call equality, the parser stage and every exact label filter narrow and never widen. A trace profile's call label may not appear in its selector or be its source label, so the composed equality is the only matcher on it. The configuration still admits only the empty scope (§11.3), because the native reads of §11 cannot enforce a nonempty one; the profile rule is enforced and tested (tests/profiles.rs, a_profile_cannot_widen_the_configured_stream_scope) so that it already holds when a scope is admitted.

12.5 Configuration​

line_profiles and trace_profiles are optional members of the configuration file of §11.4: absent, or a list of 1–16 profiles with distinct names ([a-z0-9_.-], 1–64 characters); never null. Field names are distinct within a profile ([a-z0-9_], 1–64 characters); label names are Loki's ([A-Za-z_][A-Za-z0-9_]*, at most 128 bytes). A pattern needs at least one named capture, no two adjacent captures and no capture named twice, and a pattern profile's keys are its captures. A matched key — a filterable field's, or a trace profile's line-carried call identifier's — is a label name not beginning with __ and no selector label (for a trace profile, nor its source label); any other key is only read and may be any 1–128 characters. A configured profile is part of the effective configuration and so of its revision: changing one refuses a recorded launch, as a changed CA does. A configuration without profiles keeps the revision it had before profiles existed. The repository holds the profile shape, its validation and synthetic fixtures only; the selectors, keys, formats and masking of a real deployment are the operator's, in that deployment's configuration.