rfc: 0003 title: OTLP receiver — gRPC and HTTP wire endpoints for OpenTelemetry log ingest status: green author: Jens Holdgaard Pedersen jens@holdgaard.org drafting-assistance: Claude created: 2026-05-13 supersedes: — superseded-by: —
RFC 0003 — OTLP receiver
How to read this document. Sections §§1–4 are the design contract — the what and the why. §5 lists the normative
Given / When / Thenscenarios — the contract the receiver crate is implemented against and tested for. §6 is the precise specification the receiver crate is implemented against. §7 records the alternatives we evaluated and rejected. §8 maps each §5 scenario to the technique that tests it. §9 lists open questions; §10 the references.Cross-references to
CLAUDE.mdsections are in square brackets, e.g.[§3.4], and name the invariant the section must preserve. Cross-references to RFC 0001 use its section numbers directly (e.g. RFC 0001 §6.1).
1. Summary
The Ourios OTLP receiver accepts OpenTelemetry log batches over
gRPC and HTTP, decodes them per the official opentelemetry-proto
schema, derives a tenant_id per ResourceLogs group via an
operator-configured rule (RFC 0001 §6.1 Tenant derivation),
materialises each LogRecord.body into the
Body::String(String) | Body::Structured(AnyValue) fork (the
decoded AnyValue rides through verbatim — canonicalisation
happens once, at ingest, inside the miner, per the amended
§6.4), fans
the batch out into per-tenant streams of OtlpLogRecord, hands
each stream to ourios-miner, and
acknowledges the OTLP request only after the WAL-before-ack
invariant [§3.4] is satisfied. The default wire stack is
tonic (gRPC) + axum/hyper (HTTP) against the official
opentelemetry-proto Rust crate; the alternatives considered and
rejected are embedding rotel as a library and running the OTel
Collector out-of-process.
This RFC is the wire-decode contract that the §6.1 amendment of
RFC 0001 (PR #21) and the §6.2 algorithm rewrite (PR #23) both
implicitly require: the miner takes a structured OtlpLogRecord
that something must produce. RFC 0003 is that something.
2. Motivation
2.1 The OTel-native commitment is not yet implemented
docs/glossary.md (entry OTLP) commits Ourios to OTLP as the
sole ingest contract: “we do not invent our own format.” RFC
0001’s pre-amendment §6.1 record schema and the
MinerCluster::ingest(_, raw: &str) signature treated logs as
flat text strings, which the investigation in
docs/architecture/otlp-log-format.md (PR #20) showed to be
incompatible with that commitment. PRs #21 and #23 amended the
miner’s data model and algorithm to consume structured
OtlpLogRecords. No code yet produces those records. This
RFC specifies the producer.
2.2 The receiver is the boundary that decides what “OTLP” means in practice
OTLP carries a structured LogRecord whose body is
AnyValue (string, bool, int, double, bytes, array, kvlist),
whose attributes are typed, whose Resource lives one container
level up (per ResourceLogs), and whose timestamps and severity
are first-class. Where in the pipeline these wire-level facts
become the in-memory OtlpLogRecord the miner sees is a
load-bearing decision: the receiver is where:
- The wire format (protobuf vs JSON, gRPC vs HTTP) collapses to a single in-memory representation.
tenant_idis derived perResourceLogs(RFC 0001 §6.1 Tenant derivation) and the batch fans out into per-tenant streams.body.kind = Structuredrecords have theirAnyValuebody carried verbatim intoBody::Structured(AnyValue)— per the amended §6.4 the receiver never canonicalises; the miner encodes the tree to the Ourios canonical body encoding at ingest, whose round-tripstored_bytes ↔ AnyValueper RFC 0001 §6.1 Body representation makes thelossy_flag = falsepromise meetable.- The acknowledgement-after-durability sequencing (
[§3.4]) is enforced.
Specifying these decisions in one place — and pinning them explicitly against the OTel spec rather than reinventing them — is what this RFC does.
2.3 Roadmap context
docs/roadmap.md §5 (post-#22) lists “OTLP wire endpoints
(gRPC + HTTP listeners)” as post-MVP: the bench reads OTLP
from disk (a corpus of pre-recorded LogsData), not from the
network, so wire-decode is not on the C2 thesis-gate path. The
record shape is in MVP — the miner consumes OtlpLogRecord
from the corpus reader. This RFC is the spec for the
post-MVP wire layer; landing the spec now (rather than after
the bench) settles the design while the OTLP record shape is
being implemented in the miner, so the receiver’s eventual
implementation has nothing to redesign.
3. Background — OTLP wire formats
3.1 The OTLP message hierarchy
An OTLP log export is a single ExportLogsServiceRequest
message carrying one or more ResourceLogs. (LogsData is the
file-format equivalent message in logs.proto and shares the
same resource_logs: ResourceLogs[] field shape; this RFC uses
ExportLogsServiceRequest throughout, since that is the wire
type both transports decode into.)
ExportLogsServiceRequest
└── resource_logs: ResourceLogs[]
├── resource: Resource # per-source attributes (service.name, host.*, ...)
├── schema_url: string
└── scope_logs: ScopeLogs[]
├── scope: InstrumentationScope # name, version, attributes
├── schema_url: string
└── log_records: LogRecord[] # the actual log entries
A single export request can carry records from multiple
sources (multiple ResourceLogs groups), each with its own
Resource, and within each Resource multiple instrumentation
scopes. The mapping from this hierarchy to per-tenant streams
of records is the receiver’s responsibility (§6.4 below).
3.2 Two transports, three encodings
OTLP is defined for two transports:
- OTLP/gRPC — the canonical transport. Service is
opentelemetry.proto.collector.logs.v1.LogsService, methodExport. Wire encoding: protobuf over HTTP/2. - OTLP/HTTP — POST against the
/v1/logspath. Wire encoding chosen by the client per theContent-Typeheader:application/x-protobuf(recommended by the spec; the same protobuf message as gRPC)application/json(the proto3 JSON mapping with OTLP overrides — hextrace_id/span_id, base64bytes)
The receiver MUST support both transports and all three encodings. The OTel emitter ecosystem is split: SDKs ship with gRPC by default, but the HTTP transport is widely used in constrained environments and as a Collector exporter. Refusing either transport reduces the receiver to a non-compliant subset.
3.3 Backpressure and partial-success in the OTLP response
The OTLP spec defines a partial-success response shape:
ExportLogsServiceResponse
└── partial_success: ExportLogsPartialSuccess (optional)
├── rejected_log_records: int64
└── error_message: string
When set, this signals that the receiver accepted some records but rejected others (e.g., due to rate limiting, validation failures, etc.). The full-failure case uses a transport-level error (gRPC status code, HTTP non-2xx) rather than partial-success.
§6.7 below discusses how Ourios uses (or defers using) this field. For the initial design, the receiver uses all-or- nothing batch semantics — full-success or transport-level error — and reserves partial-success for a future RFC.
4. Background — Existing Rust OpenTelemetry ecosystem
4.1 opentelemetry-proto
The official Rust crate carrying generated bindings for every
opentelemetry-proto message. Tracks upstream proto. Suitable
as the in-memory representation between wire-decode and the
miner. Trivially compatible with both tonic (gRPC) and
prost (raw protobuf, used over HTTP).
4.2 tonic
The de-facto Rust gRPC framework: production-grade, async
(tokio), supports the metadata, deadlines, and streaming
features OTLP relies on. Standard choice for any Rust gRPC
service. The LogsService server trait is generated by the
tonic-build codegen step from the OTLP .proto files.
4.3 axum and hyper
axum is the conventional Rust HTTP framework for service
endpoints, built on hyper and tower. Suitable for the OTLP/
HTTP transport. The endpoint handler decodes the request body
(protobuf or JSON, dispatched on Content-Type) into the same
in-memory ExportLogsServiceRequest representation tonic
produces, and the two transport paths converge into a single
business-logic layer.
4.4 rotel
A Rust-implemented OpenTelemetry Collector. Mature (production
deployments exist), covers receivers, processors, exporters.
Interesting as a possible library to embed (taking just its
OTLP-receiver component) rather than as a separate process —
see §7 for the build-vs-embed analysis. Note: rotel’s public
API is collector-shaped (full pipeline), not “just the receiver”
shaped, which complicates embedding.
4.5 OTel Collector (Go)
The reference collector implementation. Often deployed as a sidecar or daemonset that buffers, batches, and forwards telemetry to backends. For an Ourios deployment, a fronting Collector would terminate OTLP at the Collector and forward to Ourios via some other transport (or via OTLP again). §7 discusses this as a deployment option, not a code dependency.
5. Acceptance criteria
Each scenario carries an id of the form RFC0003.<m> that is
referenced verbatim from each test’s leading doc comment (e.g.
/// Scenario RFC0003.1 — WAL-before-ack.) so the spec↔test
mapping is greppable (per docs/rfcs/README.md Required
sections and docs/verification.md §2.3 — function names are
not part of the contract, the doc-comment line is). Scenarios .1–.11 cover the
invariants and hazards the §6 design touches; .12–.15 pin
behaviour the OTLP spec mandates and that the §9 enrichments
surfaced (empty request, compression, default path,
concurrency).
Scenario RFC0003.1 — WAL-before-ack
[§3.4]
- Given a
Receiverwired to a realWal(opened with defaults) and a single OTLPExportLogsServiceRequestcarrying ≥ 1LogRecord- When the receiver runs its accept path
- Then the transport-level success response (gRPC
OK/ HTTP 2xx) is emitted only after theWal::synccall covering the batch’s frame returnsOk(_)— measured by anAtomicBoolset aftersyncreturnsOk(_)(mirroring RFC0008.1; the probe insidesyncwould already be true mid-call). The response-writer asserts the flag istruebefore sending- And the pre-sync points (
decode,tenant_derive,body_materialise,append) all observe the flag asfalse- And the WAL contains a single
FrameKind::OtlpBatchframe (per RFC 0008 §3.2 + §6.2.3) whose payload decodes (viaprost) to the inputExportLogsServiceRequest— verified post-response by shutting down the receiver (which drops itsWalhandle, per RFC 0008 §3.1’s single-writer architecture — enforced bycrates/ourios-wal/src/lib.rs:162) and then opening a secondWalto replay viaWal::replay, asserting one new frame whose payload round-trips viaprostto the input request. ThisAndis a content/existence check; the before-the-ack ordering is established by theAtomicBoolprobe in the precedingThen+Andclauses, not by the replay (byte equality of the payload to any specific encoding is not required — protobuf has multiple wire encodings that decode to the same message; see RFC0003.2)- And the §6.5 step-5 miner-acceptance precondition for ack also holds: every record in the batch has been handed to
MinerCluster::ingestand accepted before the ack fires (an instrumentedMinerClusterstub records eachingestcall; the response-writer asserts the per-batch accepted-count equals the batch’s record-count before sending)
Scenario RFC0003.2 — Crash-before-ack: at-least-once with retry tolerance
[§3.4]
- Given a receiver wired to a real
Wal, an OTLP client that retries on transport timeout per the OTLP spec retry semantics, and aSIGKILLinjected afterWal::syncreturnsOk(_)but before the success response reaches the wire — i.e. anywhere in the §6.5 window between step 4 (fsync return) and step 6 (ack); both the step-4/5 and step-5/6 gaps reduce to the same duplicate-on-retry contract since the records are already durable- When the receiver process restarts,
Wal::replayruns, and the client retries the timed-out export- Then the post-restart WAL contains the
OtlpBatchframe the killed process had fsync’d before the kill — its payload decodes (viaprost) to anExportLogsServiceRequestsemantically equivalent to the killed process’s input (the RFC0008.2 guarantee this RFC consumes; byte equality of the wire payload is not required, see the secondAndbelow for why)- And the client’s retry is accepted and produces a second
OtlpBatchframe whose payload decodes to anExportLogsServiceRequestsemantically equivalent to the first (sameresource_logsafterprostdecode — the wire bytes need not match, since the client may re-encode on retry: JSON field-ordering / whitespace, switched compression, etc.). This duplication is the at-least-once contract per the OTLP spec’s duplicate-data section (“duplicate data is a deliberate tradeoff for telemetry data”); the receiver implements no de-duplication in this RFC- And no special “retry” marker is appended; the receiver has no dedup key (§9 reserves any future dedup mechanism for a follow-up RFC) and cannot distinguish a retry from an independent batch carrying the same records
Scenario RFC0003.3 — Tenant fan-out
[§3.7]
- Given an OTLP batch containing exactly two
ResourceLogsgroups R_A (service.name = "svc-a") and R_B (service.name = "svc-b"), and an operator-configured tenant-derivation rule keyed onservice.name- When the receiver processes the batch
- Then the
(tenant_id, OtlpLogRecord)pairs accepted by an instrumentedMinerClusterstub fortenant_id_aare exactly those derived from R_A and contain no record derived from R_B- And the symmetric assertion holds for
tenant_id_b- And each emitted
OtlpLogRecord’sresource_attributesreflects the originating Resource verbatim — the receiver does not mix Resource attribute sets across the fan-out
Scenario RFC0003.4 — Tenant resolution failure rejects the entire batch
[§3.7]
- Given an OTLP batch where at least one
ResourceLogs.resourcelacks the attribute named by the operator’s tenant-derivation rule- When the receiver processes the batch
- Then the receiver emits a transport-level error (gRPC
INVALID_ARGUMENT/ HTTP 400) whose payload names the failingResourceLogsindex and the missing attribute key- And no
OtlpBatchframe from the batch is appended to the WAL (asserted by shutting down the receiver — dropping its singleWalhandle per RFC 0008 §3.1’s single-writer architecture (enforced bycrates/ourios-wal/src/lib.rs:162) — and then opening a secondWaland observing viaWal::replaythat frame count and segment offsets are unchanged from the pre-batch snapshot)- And no record from the batch reaches
MinerCluster::ingest— per-Resource partial acceptance is reserved per §6.3
Scenario RFC0003.5 — gRPC ≡ HTTP/protobuf decode equivalence
- Given a byte-equal
ExportLogsServiceRequestprotobuf payload- When the payload is decoded via the
tonicgRPC handler and via theaxumHTTP handler withContent-Type: application/x-protobufindependently- Then the two resulting in-memory
ExportLogsServiceRequestvalues are structurally equal (PartialEq), and every derivedOtlpLogRecordfrom each path is field-for-field equal — includingbody,attributes,resource_attributes,trace_id,span_id, anddropped_attributes_count
Scenario RFC0003.6 — HTTP/JSON ↔ gRPC/protobuf equivalence with OTLP-JSON encoding rules
- Given an
ExportLogsServiceRequestcarrying non-trivialtrace_idandspan_idbytes, a record with abytes-typedAnyValueattribute, and at least one record whoseseverity_numberexercises a non-default enum value- When the payload is serialised as gRPC + protobuf and as HTTP +
application/jsonper the OTLP-JSON mapping (hex-encodedtraceId/spanId, base64-encodedbytes, integer-encoded enums, lowerCamelCase field names) and each is independently decoded by the receiver- Then the two derived
OtlpLogRecordsequences are equal at theAnyValuetree level — no byte-level canonicalisation is asserted at this layer (byte-level equivalence under the Ourios canonical body encoding is the miner’s ingest contract per the amended §6.4)- And the JSON decoder accepts whitespace and field- ordering variation (insignificant per proto3-JSON)
- And the JSON decoder ignores unknown fields anywhere in the request body (top-level, nested, repeated) per the OTLP spec’s “receivers MUST ignore unknown fields” rule (forward-compatibility)
Scenario RFC0003.7 —
Body::Structuredcarries the decodedAnyValueverbatim
- Given a
LogRecordwhosebodyis anAnyValueof a non-string_valuevariant (kvlist_value,array_value,int_value,double_value,bool_value, orbytes_value)- When the receiver materialises the record via
ourios_core::otlp::Body::from_any_valuefrom either transport- Then the resulting
OtlpLogRecord.bodyisSome(Body::Structured(av))whereavis structurally equal to the wire’sAnyValue(no canonicalisation, no reshape, no dropped fields)- And the same equality holds across the three transports, since RFC0003.5 and RFC0003.6 make the per-transport decodes equivalent at the
AnyValuelevel
Scenario RFC0003.8 —
Body::Stringreaches the miner as the unwrappedL_raw
- Given a
LogRecordwhosebodyisAnyValue { value: Some(string_value(s)) }- When the receiver materialises the record
- Then the resulting
OtlpLogRecord.bodyisSome(Body::String(s))wheresis the original UTF-8 string (no wrapping, no quoting, no escaping)- And the value handed to
MinerCluster::ingestequalssbyte-for-byte (asserted by an instrumentedMinerClusterstub that records eachingestcall’s body argument — the receiver’s contract here is the pass-through, not anything about how the miner indexes or short-circuits on it)
Scenario RFC0003.9 — Edge OTLP fields pass through unchanged
- Given a
LogRecordwithseverity_number = 0(UNSPECIFIED), noscope_nameon its enclosingInstrumentationScope, andobserved_time_unix_nano = 0(proto3’s scalar default for an unset field — OTLP’s log-record section spells this out as “the value of 0 indicates unknown”)- When the receiver materialises the record
- Then the derived
OtlpLogRecordcarriesseverity_number = 0(kept as0becauseUNSPECIFIEDis an explicit OTLP value per RFC 0001 §6.1, not absence),scope_name = None, andobserved_time_unix_nano = None— the receiver applies the wire-0→Nonerule forobserved_time_unix_nanospecifically (theOption<u64>typing in RFC 0001 §6.1 exists for this conversion; this scenario is the contract that owns the rule)- And the record is accepted by
MinerCluster::ingestwithout rejection, coalescing, substitution, or any downcast to a “default” value
Scenario RFC0003.10 —
dropped_attributes_countpreserved verbatim
- Given a
LogRecordwhosedropped_attributes_countis42on the wire- When the receiver materialises the record
- Then the resulting
OtlpLogRecord.dropped_attributes_countis exactly42- And the receiver does not recompute the field — it reflects the wire-level claim only, even if a future receiver-side per-attribute truncation step would have dropped further attributes (a hypothetical such step is tracked as a §9 open question)
Scenario RFC0003.11 — Transport-level errors are controlled, not panics
- Given any of:
- a malformed protobuf payload (random bytes that fail
prost::Message::decode),- an over-size request body exceeding the receiver’s configured request-size limit,
- an HTTP request with an unrecognised
Content-Type,- an HTTP
POSTto a path other than the configured/v1/logs(covered jointly with RFC0003.14),- or a gRPC client cancellation mid-decode
- When the receiver handles the request
- Then the receiver emits a controlled transport-level error — gRPC
INVALID_ARGUMENT/RESOURCE_EXHAUSTED/CANCELLEDas appropriate, or HTTP 400 / 413 / 415 / 404 as appropriate- And no part of the receiver panics or restarts; the process remains alive (each arm of the test asserts this after the request)
- And no
OtlpBatchframe is appended to the WAL (the rejected batch never reaches §6.5 step 3, so the persistence unit — the per-export frame — never lands)
Scenario RFC0003.12 — Empty
ExportLogsServiceRequestreturns success without WAL write
- Given an
ExportLogsServiceRequestthat carries zeroLogRecords — covered shapes are (i)resource_logsempty, (ii)resource_logs[i].scope_logsempty for everyi, (iii) everyresource_logs[i].scope_logs[j].log_recordsempty. All three shapes are tested- When the receiver processes the request via either transport
- Then the receiver emits a transport-level success response carrying an
ExportLogsServiceResponsewithpartial_successunset (per the OTLP spec’s otlpgrpc-response and otlphttp-response sections: “servers SHOULD treat empty as success”)- And the receiver does not invoke
Wal::sync, no frame is appended (asserted via a test wrapper around theWalhandle that countsappendandsynccalls), and no record reachesMinerCluster::ingest
Scenario RFC0003.13 — Compression over HTTP: identity and gzip MUST be supported
- Given an HTTP request whose body is the byte-equal
ExportLogsServiceRequestpayload of RFC0003.5, transported withContent-Encoding: identity(or absent) and withContent-Encoding: gzipindependently- When the receiver processes each request
- Then the two derived
OtlpLogRecordsequences are equal — the OTLP spec mandates both encodings, and the receiver’s decode produces semantically identical results- And a request with an unsupported
Content-Encoding(e.g.zstd,br) is rejected with HTTP 415 and a controlled error message;zstdsupport is deferred per §9
Scenario RFC0003.14 — Default
/v1/logspath with configurable override
- Given the HTTP listener bound with the default path configuration
- When a
POSTarrives at/v1/logs- Then the receiver handles it via the OTLP/HTTP code path defined in §6.2
- And a
POSTto any other path returns HTTP 404 (the “wrong path” arm of RFC0003.11)- And when the operator configures an override path (e.g.
/otlp/v1/logs), it replaces/v1/logsas the accepted path without changing any other receiver behaviour (the configurability matches the Collector’s OTLP-receiverpathknob, so deployments that need a non-standard prefix don’t have to front Ourios with a reverse proxy)
Scenario RFC0003.15 — Concurrent
Exportcalls each obey WAL-before-ack independently[§3.4]
- Given N ≥ 2 concurrent gRPC
Exportunary calls submitted to the receiver from independent client connections- When each call’s batch independently traverses the §6.5 sequence
- Then each call’s ack is emitted only after its own batch’s
Wal::syncreturnsOk(_)and its own batch’s records have all been accepted byMinerCluster::ingest— the §3.4AtomicBoolof RFC0003.1 is per in-flight call, not process-global; a per-call probe records both the sync-completion and miner-acceptance ordering before the response-writer sends- And the WAL contains exactly one
OtlpBatchframe per concurrent call (no call’s batch is lost to concurrency, asserted by shutting down the receiver — dropping its singleWalhandle per RFC 0008 §3.1’s single-writer architecture (enforced bycrates/ourios-wal/src/lib.rs:162) — and then opening a secondWalwhoseWal::replayyields N frames whose payloads round-trip to the N inputExportLogsServiceRequests)- And the test does not assert any cross-call ordering — concurrent batches may interleave in the WAL as the tokio runtime chooses, which is consistent with the OTLP spec’s recommendation to support concurrent unary
Exportcalls for throughput
Amendment (served-binary slice, post-
green). Scenarios RFC0003.1–.15 are implemented and weregreen(exercised in-process:axum/tonichandlers via direct call /oneshot, the pipeline + WAL directly). This amendment adds RFC0003.16 — the end-to-end served-binary contract, which the §9 Receiver process model resolution settles — and re-enters the ladder atspecifieduntil RFC0003.16 lands.
Scenario RFC0003.16 — Served binary: both transports bind, a client export round-trips, graceful shutdown
[§3.4]
- Given
ourios-serverstarted with the receiver role enabled (config-toggled per the §9 resolution), the gRPC listener bound on its configured port (default 4317) and the HTTP listener bound on its configured port (default 4318), both sharing oneIngestPipelineover a singleWal- When a real OTLP client exports a non-empty batch (resolvable tenant) over each bound socket — gRPC
Exportand HTTPPOST /v1/logs(application/x-protobuf) — and receives each transport response, and only then is the server signalled to shut down (this scenario pins the steady-state export→ack→shutdown path; in-flight-during- shutdown behaviour is out of scope here)- Then each client receives transport-level success (gRPC
OK/ HTTP 200) only after its batch is durable — the §6.5 WAL-before-ack contract holds end-to-end over a real socket, not just in-process- And the shutdown signal stops the listeners and exits the process cleanly, releasing the single
Walhandle — without dropping it mid-fsync, and no already-acked batch is lost on the way out- And after that clean exit frees the single-writer handle (RFC 0008 §3.1), opening the WAL and running
Wal::replayrecovers each batch’sOtlpBatchframe — the durability check necessarily follows shutdown, since the WAL cannot be reopened while the server holds it
6. Proposed design
6.1 Overall shape
The receiver is a single Rust crate (ourios-ingester per the
target layout in CLAUDE.md §7) exposing two listeners — gRPC
on its own port, HTTP on its own port — that share a single
business-logic layer. The business-logic layer accepts a
decoded ExportLogsServiceRequest and:
- Iterates
ResourceLogs[], derivingtenant_idper Resource via the operator-configured rule (RFC 0001 §6.1 Tenant derivation). - For each
ResourceLogs, iteratesScopeLogs[]andLogRecord[], materialising oneOtlpLogRecordper record. TheOtlpLogRecordis the in-memory shape RFC 0001 §6.1’s amended record table mirrors; it carries the inheritedResourceattributes and theInstrumentationScopename and version as fields, so downstream code never needs to walk back up the OTLP hierarchy. - For each
LogRecord, materialisesbodyinto theBody::String(String) | Body::Structured(AnyValue)fork perourios-core::otlp::Body::from_any_value. No canonicalisation runs at the receiver — the structured branch carries the decodedAnyValueverbatim per the amended §6.4. - Hands each per-tenant stream to
ourios-miner(oneMinerClusterper process; the cluster routes internally pertenant_id). - After the batch has been written to the WAL as a single
OtlpBatchframe with fsync AND every record accepted by the miner, returns a transport-level success.
6.2 Wire stack defaults
- gRPC:
tonic+ theopentelemetry-protocrate’s generatedLogsServiceServertrait. - HTTP:
axumonhyper. A single/v1/logsPOST handler dispatches onContent-Type:application/x-protobuf→prost::Message::decodeinto the sameExportLogsServiceRequesttype the gRPC path produces.application/json→ proto3-JSON decode intoExportLogsServiceRequest. The decode handles whitespace and field-ordering variation natively (proto3-JSON spec); no separate canonicalisation pass — theBody::Structured(AnyValue)carried downstream is transport-agnostic at theAnyValuelevel.
- Both listeners spawn off the same tokio runtime, share a
single instance of the business-logic layer, and bind on
operator-configured ports (defaults 4317 for gRPC and
4318 for HTTP, per the OTel convention; configurable).
The receiver is a role of the
ourios-serverbinary, enabled by config and sharing that binary’s tokio runtime alongside the other roles (e.g. the compaction daemon) — the §9 Receiver process model resolution. The served-binary contract is RFC0003.16.
6.3 Tenant fan-out
Per RFC 0001 §6.1 Tenant derivation, tenant_id is derived
per ResourceLogs group, not per export batch. The receiver:
- Resolves the tenant rule once per
ResourceLogs.resource. - Groups the resulting
OtlpLogRecords bytenant_id(a single batch can produce multiple per-tenant groups). - Hands each group to the miner via
MinerCluster::ingest, which is already per-tenant-routed internally.
If any ResourceLogs.resource fails to resolve to a tenant
under the configured rule, the receiver rejects the entire
batch with a transport-level error naming the failing
ResourceLogs index (the offset in the export’s
resource_logs[]) and the missing attribute key. Per-Resource
partial acceptance is reserved for a future RFC (see §9).
6.4 AnyValue canonicalisation happens once, at ingest
Amendment (PR introducing
ourios-core::otlp::OtlpLogRecord). This subsection originally pinned the receiver as the place that canonicalises structuredAnyValuebodies into OTLP-canonical JSON, with the in-memory record carrying pre-cachedBytes. The amended position carries theAnyValueitself on the in-memory record and defers canonicalisation to the storage layer (Parquet writer, when it lands).
Amendment 2026-06-10 (canonicalisation happens at ingest). The amendment above predated the implementation and placed the single canonicalisation pass at Parquet-write time. The merged implementation runs it at ingest: the miner’s
ingest_structuredencodes theAnyValuethe receiver delivered with the Ourios canonical body encoding (RFC 0001 §6.1 The Ourios canonical body encoding), and the mined record carries those bytes from there — WAL, flush, and the Parquet writer (RFC 0005 §3.3) persist them verbatim. What the amendment above got right is preserved: the receiver still does not canonicalise, and the “mine inner field” optionality is intact because the miner receives the decoded tree — the encode point sits after any future inner-field hook would run. This note reconciles the text below to the implemented behaviour; the rationale is rewritten accordingly.
The receiver hands the miner an OtlpLogRecord whose body, when
present and structured, carries the decoded AnyValue verbatim
(Body::Structured(AnyValue)) — unchanged from the first
amendment. The miner’s §6.2 step-0 short-circuit dispatches on
the discriminator alone; only after taking the structured branch
does ingest_structured encode the tree, once, via
ourios_core::otlp::canonical::encode_any_value (infallible for
every AnyValue the type system admits) into the Ourios
canonical body encoding per RFC 0001 §6.1 Body representation.
The record carries the encoded bytes in its body field from
that point on; the Parquet writer stores them verbatim in the
RFC 0005 §3.3 body column.
Rationale:
- Optionality is not lost. RFC 0001 §6.1 reserves a future
“mine inner field” mode (e.g. mine
body.kvlist["msg"]as the line if present) gated on corpus evidence. That mode needs the structured tree — andingest_structuredreceives the structured tree: the receiver handsBody::Structured(AnyValue)through untouched, so the exact place such a mode would hook in still sees theAnyValue. Only the stored form is the canonical bytes; encoding at ingest forecloses nothing. - Single canonicalisation pass. Whether the body arrived
as gRPC-protobuf or HTTP-JSON, exactly one encode runs —
once per structured record, at ingest. There is one
transport-agnostic encoder (
ourios-core’sotlp::canonical, operating on the decodedAnyValue); neither the receiver nor the writer needs to know a second strategy, and the writer’s contract shrinks to “persist the bytes.” - Miner hot path is unchanged. The §6.2 step-0
short-circuit still inspects only the discriminator
(
Body::Structured(_)vsBody::String(_)) before branching; noAnyValuewalking decides the dispatch. The encode cost scales with body size, but it is paid exactly once per structured record regardless of which layer pays it — moving it to write time would buy no work back, while costing a rework ofMinedRecord, the miner’s snapshot serialisation (RFC 0001 §6.9), and the render path (RFC 0001 §6.6), all of which carry the encodedbodytoday. That churn would purchase only a mode with no RFC and no named consumer.
For Body::String(s), no canonicalisation is ever needed; the
unwrapped string is passed through as L_raw.
6.5 WAL-before-ack sequencing
[§3.4] requires the receiver to acknowledge a non-empty
batch only after the batch’s OtlpBatch frame is durably
written. (The empty-batch fast path of RFC0003.12 is the
explicit exception: no WAL write occurs, and success is
returned without an OtlpBatch frame.) Concrete contract for
the non-empty case:
- Receiver accepts the request and decodes to
ExportLogsServiceRequest. - Receiver fans out to per-tenant
OtlpLogRecordstreams (§6.3); body canonicalisation does not happen here, per the amended §6.4. - Receiver appends the request as a single
FrameKind::OtlpBatchframe (per RFC 0008 §3.2 + §6.2.3) whose payload is a protobuf-encodedExportLogsServiceRequestdecodable viaprost— semantically equivalent to the input, but byte-equal to the wire payload is not required (per RFC0003.1 + .2 the contract is “you can recover the input message,” not “you get the bytes back”). For the gRPC and HTTP/protobuf paths the receiver MAY store the wire bytes verbatim; for the HTTP/JSON path no incoming protobuf bytes exist and the receiver MUST encode the decoded message. - Receiver fsyncs the WAL segment(s) touched.
- Receiver hands records to the miner for templating.
- Receiver returns transport-level success.
The fsync-then-template ordering matters: a crash between (4) and (5) is recoverable (records replay from the WAL; the miner state is reconstructed); a crash between (3) and (4) loses those records but the client retries (no ack was sent); a crash between (5) and (6) is the “the server did the work and the client never heard about it” case, where client retries produce duplicates. This RFC implements no de-duplication: duplicates on retry are the explicit at-least-once contract per RFC0003.2 and §9 #1 (resolved by reference to the OTLP spec’s duplicate-data section). Any future content-hash or request-id dedup is purely additive on top of this baseline.
The receiver itself is post-MVP per roadmap.md §5 — the MVP
bench reads OTLP from the on-disk corpus, bypassing this
component entirely. The receiver therefore cannot be enabled
until ourios-wal lands, and there is no MVP code path that
acks a network request before durability. The
append-then-fsync-then-ack sequence above is the only
contract; no “WAL no-ops” mode exists, since that would
violate [§3.4].
6.6 The OtlpLogRecord in-memory shape
Amendment (PR introducing
ourios-core::otlp::OtlpLogRecord). Body now carries the decodedAnyValuerather than its OTLP-canonical JSON encoding (see amended §6.4).body_kindis derived frombodyrather than stored on the record, since the §6.2 step-0 fork only needs to read the discriminator.
Amendment 2026-06-11 — the effective timestamp is derived downstream, not here. RFC 0005 §3.2 (amendment of the same date) adds a writer-derived
effective_time_unix_nanoParquet column (time_unix_nanowhen non-zero, elseobserved_time_unix_nano.unwrap_or(0)— RFC 0005 §3.2 is the normative derivation). The receiver’s contract is unchanged:time_unix_nanois carried verbatim from the wire including0(RFC 0001 scenario RFC0001.10), and the wire-0→Nonerule forobserved_time_unix_nanostands. No effective-timestamp field is materialised onOtlpLogRecord; the derivation happens at the Parquet writer from the two fields below, and never overwrites either.
The receiver materialises each wire-level LogRecord (plus its
inherited Resource and InstrumentationScope context) into a
single owned struct. The authoritative definition lives in the
ourios-core::otlp module; the sketch below mirrors that
module:
struct OtlpLogRecord {
// Identity / partitioning
tenant_id: TenantId,
// OTLP-derived (per RFC 0001 §6.1)
time_unix_nano: u64,
observed_time_unix_nano: Option<u64>,
severity_number: u8,
severity_text: Option<String>,
scope_name: Option<String>,
scope_version: Option<String>,
attributes: Vec<KeyValue>, // opentelemetry-proto KeyValue
dropped_attributes_count: u32,
resource_attributes: Vec<KeyValue>, // opentelemetry-proto KeyValue
trace_id: Option<[u8; 16]>,
span_id: Option<[u8; 8]>,
flags: u32,
event_name: Option<String>,
// Body — None when the wire delivered no body
body: Option<Body>,
}
enum Body {
String(String),
Structured(AnyValue), // opentelemetry-proto AnyValue
}
// `body_kind()` is a method on OtlpLogRecord that returns
// `Option<BodyKind>` derived from `body`; the discriminator
// is never stored.
enum BodyKind { String, Structured }
The Rust types are informal here; the precise definition lives
in the ourios-core::otlp module — owning the type in
ourios-core (rather than ourios-ingester) lets the miner
take it without depending on the receiver crate, since the
receiver doesn’t yet exist. The shape mirrors RFC 0001 §6.1
column-for-column so the Parquet writer can serialise a slice
of these directly without a translation layer.
6.7 Backpressure (deferred)
The receiver does not apply rate limiting in this initial
design. If the miner or the WAL is the bottleneck, the receiver
holds the request open until the per-tenant queue drains, then
acks. In practice this means OTLP clients see backpressure as
elevated request latency rather than as
partial_success.rejected_log_records. Whether to upgrade
this to explicit partial-success is reserved for a future RFC
(see §9). The full-failure path (transport error) covers the
unresolvable-tenant and malformed-batch cases per §6.3 and
§3.2.
6.8 Out of scope for this RFC
- Metrics + traces ingest. OTel Collector and OTLP define
endpoints for both; Ourios is a logs-only backend per
CLAUDE.md§1. Receiver MAY accept metric/trace requests at the transport layer (returning a deliberateUnimplementedresponse) but this RFC does not specify that path. - mTLS / authn / authz. Production deployment concerns, out-of-band of the OTLP wire contract. A future RFC covers the authentication model (likely token-based per request with the resolved identity feeding the tenant-derivation rule).
- Schema URL handling.
ResourceLogs.schema_urlandScopeLogs.schema_urlare separate OTLP fields and do not appear on theOtlpLogRecordshape in §6.6 — the receiver currently drops them. Rationale: RFC 0001 §6.1’s record schema does not include columns for them, no consumer references them yet, and Ourios does not interpret OTel semantic conventions. Whether to addresource_schema_url/scope_schema_urlfields (or a Parquet column) is tracked as an open question in §9; until then the drop is deliberate, not an oversight. - Compactor / WAL implementation. Specified in the
forthcoming
ourios-walRFC; this RFC’s contract with the WAL is just the append-then-fsync-then-ack sequence in §6.5.
7. Alternatives considered
7.1 Embed rotel as a library
rotel is a production-quality Rust OTel collector. Embedding
it would give us a known-good OTLP receiver implementation
without us building one. Rejected because:
rotel’s public API is collector-shaped (the full receivers→processors→exporters pipeline), not “just the receiver” shaped. Embedding it means embedding the entire pipeline machinery, then building Ourios as one of its exporters. That’s a deployment shape (out-of-process collector) wearing the costume of a code dependency, with the worst of both worlds: the dependency footprint of a full collector and the integration friction of an in- process one.- The OTel-receiver pieces of
rotelare themselves built ontonic+opentelemetry-proto— the same primitives we would use directly. Embeddingroteladds a layer without removing one. - Build-vs-embed parity: our wire-decode layer is small (~a few hundred lines, almost all glue against generated protobuf bindings). The reuse argument doesn’t carry the weight it would for a complex piece of infrastructure.
7.2 Run the OTel Collector out-of-process and have it forward to us
Common deployment shape: a Collector terminates OTLP at the network edge, batches, and forwards to a backend. Rejected as the default because:
- The Collector ACKs the OTLP client before our backend sees
the data, breaking the WAL-before-ack contract
[§3.4]. The only way to recover the contract is for our forwarding protocol from the Collector to be itself durable + ack- after-fsync — at which point that protocol is what we needed to spec, and we are back to writing a receiver. - Adds a deployment dependency (operator must install and configure the Collector) for no signal beyond what a direct receiver provides.
- Configuration drift between the Collector’s input validation and ours becomes a real source of “works in one place, fails in the other” bugs.
That said: the Collector is a perfectly fine deployment option for operators who already run one (e.g., for trace sampling). The receiver in this RFC accepts OTLP from any source, including a Collector forwarder, so the deployment shape is not foreclosed; it just isn’t the default and doesn’t get to be on the WAL-before-ack path.
7.3 Hand-roll the protobuf without opentelemetry-proto
Writing our own protobuf bindings against the OTLP .proto
files. Rejected because the official crate tracks upstream
faithfully and is the canonical Rust binding for the OTLP
messages. Re-implementing risks drift, especially on the
JSON-encoding overrides (hex IDs, base64 bytes) which are
spec-defined but easy to get wrong.
7.4 HTTP-only or gRPC-only
Supporting only one of the two transports. Rejected because
the OTel ecosystem is split: SDK defaults are gRPC, but HTTP
is widely used in constrained environments and is the standard
exporter target for the Collector’s otlphttp exporter.
Refusing either transport reduces the receiver to a non-
compliant subset of OTLP and forces a class of operators to
front Ourios with a converter (e.g., the Collector) — which
re-introduces the WAL-before-ack problem of §7.2.
7.5 Synchronous AnyValue canonicalisation in the miner or the receiver
Amendment 2026-06-10 (canonicalisation happens at ingest). The conclusion this section originally reached — “canonicalise at the storage layer (Parquet writer)” — was superseded by the implementation; see the amended §6.4. The grounds on which variant (a) was rejected dissolved once RFC 0001 §6.1’s 2026-06-09 amendment pinned a single transport-agnostic encoder over the decoded
AnyValue(ourios-core’sotlp::canonical): the miner needs no transport knowledge, and the encode cost is once per structured record regardless of which layer pays it. The original text is preserved below as the record of the evaluation.
Two related alternatives evaluated together:
(a) Canonicalise in the miner. Rejected because the
miner’s hot path benefits from a constant-time write in the
body_kind = Structured short-circuit (RFC 0001 §6.2 step 0);
doing serialisation work there scales with body size on every
structured record. The miner would also need to know the
source transport (the two transports need different
canonicalisation strategies), which is a layering inversion.
(Superseded — see the amendment note above: this is the
implemented design.)
(b) Canonicalise in the receiver before materialising
OtlpLogRecord. This was the original §6.4 stance and was
the basis on which §7.5(a) was rejected. Reversed by the
§6.4 amendment: receiver-side canonicalisation forecloses
the future “mine inner field” mode (RFC 0001 §6.1) which
needs the structured tree, not pre-cached bytes; it also
splits canonicalisation knowledge across two transports
unnecessarily. (Still rejected — the receiver continues to
hand the decoded AnyValue through verbatim. Only the
“canonicalise at the storage layer” conclusion that this
paragraph originally drew is superseded, per the amendment
note above.)
8. Testing strategy
Mapped to the §5 scenarios. Each technique below names the
scenario ids it covers; each test’s leading doc comment
references the same id verbatim
(/// Scenario RFC0003.1 — WAL-before-ack. etc., per
docs/verification.md §2.3) so the spec↔test mapping is
greppable.
-
WAL-before-ack and concurrency (RFC0003.1, RFC0003.15): integration tests against a real
Wal(defaults), with anAtomicBoolordering probe mirroring RFC0008.1 — set afterWal::syncreturns, assertedtrueby the response-writer andfalseby every pre-sync stage. RFC0003.15 spawns N ≥ 2 concurrentExportcalls and uses a per-call probe so the invariant is checked independently per in-flight call. -
Crash-before-ack (RFC0003.2): a child-process harness mirroring
wal_crash_fixture(PR #126) runs a receiver binary wired to a realWal, the parent SIGKILLs betweenWal::syncreturn and ack-emit, the parent restarts the child and re-issues the export, and the assertion is that the post-restart WAL contains twoOtlpBatchframes whose payloads each decode (viaprost) to anExportLogsServiceRequestsemantically equivalent to the input (byte-equality is not required — see RFC0003.2 — but the second frame must round-trip to the same logical request) — the at-least-once contract per the OTLP spec’s duplicate-data section. The test explicitly does not assert dedup; RFC0003.2’s contract is “no loss + safe retry,” not exactly-once. -
Tenant fan-out (RFC0003.3, RFC0003.4): unit tests with a hand-curated two-Resource batch and an instrumented
MinerClusterstub that records every accepted(tenant_id, OtlpLogRecord)pair. Apropteststrategy over tenant-derivation rules asserts the cross-contamination-free invariant for any rule that returnsSomefor both Resources. RFC0003.4 uses a hand-curated batch where one Resource lacks the rule’s attribute key. -
Wire-decode equivalence (RFC0003.5, RFC0003.6): a
propteststrategy generatesExportLogsServiceRequestpayloads across the proto’s value space; each is serialised to gRPC + protobuf, HTTP + protobuf, and HTTP + JSON, decoded by the receiver, and the three resultingOtlpLogRecordsequences are asserted equal at theAnyValuelevel. The RFC0003.6 OTLP-JSON encoding-rule clauses (hex IDs, base64 bytes, integer enums, ignore unknown fields) use hand-curated payloads, since the proptest generator can’t reliably exercise spec-mandated forward-compatibility behaviour. -
Body fork (RFC0003.7, RFC0003.8): table-driven tests over all seven
AnyValuevariants, each asserting thatBody::from_any_valueroutesstring_valuetoBody::String(s)(unwrapped) and every other variant toBody::Structured(av), whereavis structurally equal to the inputAnyValueand the inneroneofis moved, not cloned. -
Edge OTLP cases (RFC0003.9, RFC0003.10): hand-curated
LogRecords exercisingseverity_number = 0,scope_name = None,observed_time_unix_nano = 0, and non-zerodropped_attributes_count. Assertions pin the pass-through semantics on the derivedOtlpLogRecord. -
Transport-level errors + empty request (RFC0003.11, RFC0003.12): table-driven tests over each error arm (malformed protobuf, oversize, unrecognised
Content-Type, wrong path, mid-decode cancellation) and the empty-request success arm. Each assertion pins the response status code, that noOtlpBatchframe is appended to the WAL and no record reaches the miner, and that the receiver process is still alive afterwards. -
Compression and path (RFC0003.13, RFC0003.14): the gzip arm of RFC0003.13 uses
flate2to construct theContent-Encoding: gzipbody; the unsupported-encoding arm asserts HTTP 415. RFC0003.14’s path arm covers the default/v1/logs, a wrong-path 404, and an operator-configured override path producing equivalent behaviour. -
Conformance fuzzing (additive, not bound to a single scenario):
propteststrategies derived from the proto definitions feed random valid batches through the receiver; the only assertion is “no panic; response is either success or a controlled transport-level error” — a backstop against decode paths the hand-curated cases miss. -
Benchmarks (
criterion, inourios-bench): end-to-end latency from request arrival to ack-fires, for both transports, at batch sizes (1, 100, 1 000, 10 000 records per batch). RFC0003.15 throughput at N = 8 concurrent callers. Regressions block merges perCLAUDE.md§6.2. -
Served binary (RFC0003.16): an integration test boots the
ourios-serverreceiver role bound on ephemeral ports (127.0.0.1:0, reading back each OS-assigned port), exports a non-empty batch over each transport with a real client — atonicgRPC client and an HTTP client (reqwest/hyper) — and asserts transport success for each. It then signals shutdown and waits for the server task to join cleanly, which releases the singleWalhandle (RFC 0008 §3.1’s single-writer rule — the WAL cannot be reopened while the server holds it). Only after that join does the test open the WAL andWal::replayit, confirming each batch’sOtlpBatchframe is durable — WAL-before-ack over a real socket, with no acked batch lost on the way out. Unlike RFC0003.1–.15 (in-process: direct handler call /oneshot), this is the only scenario that crosses a real socket.
docs/verification.md §3’s two-loop Red gate applies: the §5
scenarios become #[ignore]d test stubs at red stage, then
get implementations as the receiver crate is built (the same
two-loop pattern RFC 0008 §5 used to drive its red-gate
scenarios — #[ignore]’d stubs first, implementations second).
9. Open questions
-
Retry-induced duplicate suppression.Resolved by §5 / RFC0003.2: a crash between miner-attach (step 5) and ack (step 6) in §6.5 produces duplicates on client retry, and that is the contract. The OTLP spec’s duplicate-data section (“the client may re-send … which may result in duplicate data on the server side. This is a deliberate choice and is considered to be the right tradeoff for telemetry data”) explicitly accepts at-least-once with duplicates; the Collector’s WAL guidance carries the same caveat. The receiver implements no de-duplication in this RFC. If a future RFC introduces a dedup mechanism (content-hash idempotency key, OTel SDK request-id header), it is purely additive — the at-least-once baseline is the floor, not a stop-gap. -
ResourceLogs.schema_url/ScopeLogs.schema_urlpreservation. §6.8 records that schema URLs are currently dropped because no consumer references them and RFC 0001 §6.1’s record schema has no column for them. If a semantic-conventions-aware feature lands later (e.g., schema URL → attribute key mapping),OtlpLogRecordand the Parquet schema will need the two fields added. Tracked here so a future RFC does not re-derive the question. -
Where exactly does canonicalisation cost land?Resolved (2026-06-10 §6.4 amendment, reconciling to the implementation): canonicalisation runs at ingest — the miner’singest_structuredencodes the decodedAnyValuewith the Ourios canonical body encoding (RFC 0001 §6.1) and the record carries the bytes from there; the Parquet writer (RFC 0005 §3.3) persists them verbatim. The receiver carries the decodedAnyValueverbatim and never canonicalises. The cost is once per structured record either way; placing it at ingest keepsMinedRecord, the snapshot format, and the render path on a single stored form. -
dropped_attributes_countsemantics on truncation. Preserve verbatim from the wire (current §6 design), sum across records, or recompute if the receiver itself drops attributes (e.g., for being over the 256-byte limit per RFC 0001 §3.2)? Current design says preserve; a future receiver- side truncation step would need to either recompute or use a separate column. -
Receiver process model.Resolved (served-binary amendment): the receiver is a role of theourios-serverbinary, enabled by config and sharing that binary’s tokio runtime alongside the other roles (e.g. the compaction daemon) — not a separate sidecar. Default ports 4317 (gRPC) / 4318 (HTTP) per §6.1; the end-to-end served contract (bind + client round-trip + graceful shutdown) is RFC0003.16. -
Partial-success response semantics.Resolved (OTLP review): the all-or-nothing batch contract (§6.3 / RFC0003.4) is spec-compliant. OTLP mandates only400 Bad Request+ no client retry for permanently-bad/undecodable input (OTLP/HTTP Bad Data) and does not require accepting a valid subset;partial_success.rejected_log_recordsis supported but optional. We keep whole-batch rejection and deferpartial_successto a future RFC if a concrete operator need surfaces (e.g. one failing tenant in a large multi-tenant batch). On full successpartial_successstays unset — the normal OK path. - Authentication and tenant binding. If the receiver
authenticates the client (mTLS, token), does the
authenticated identity feed into the
tenant_idderivation rule (e.g., as a constraint), or is it purely an access- control check decoupled from tenancy? A future authentication RFC settles this; the open question is flagged here so the tenant-derivation rule’s interface can grow into it. - Multi-line / non-UTF-8 body handling for
Stringbodies. The miner’s tokenize step (RFC 0001 §6.2 step 1) has explicit failure modes (malformed UTF-8, embedded NUL, oversize). Should the receiver pre-validate and reject at the transport level, or pass through and let the miner emit a parse-failure record? Current design: pass through, per-record granularity is the miner’s concern. -
Compression (gzip / zstd over HTTP).Resolved by §5 / RFC0003.13: the OTLP spec mandates that servers supportidentityandgzip; both are required acceptance criteria.zstdandbrare out of scope for this RFC — a request carrying an unsupported encoding is rejected with HTTP 415. A future RFC may addzstdif operator demand surfaces; until then the 415 response is the contract. - Receiver-side OTel telemetry (eating our own dog food). The receiver should itself emit metrics about request rates, decode failures, fan-out latency. Specified where? Likely in the same RFC as the §6.8 telemetry surface (RFC 0001 §6.8); flagged here for tracking.
10. References
- OTLP
logs.proto: https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/logs/v1/logs.proto - OTLP
logs_service.proto: https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/collector/logs/v1/logs_service.proto - OTLP
common.proto(AnyValue, KeyValue): https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/common/v1/common.proto - OpenTelemetry Logs Data Model spec: https://opentelemetry.io/docs/specs/otel/logs/data-model/
- OTLP transport spec (gRPC, HTTP, encodings): https://opentelemetry.io/docs/specs/otlp/
tonic: https://github.com/hyperium/tonicopentelemetry-protoRust crate: https://crates.io/crates/opentelemetry-protoaxum: https://github.com/tokio-rs/axumrotel: https://github.com/streamfold/rotel- OpenTelemetry Collector: https://github.com/open-telemetry/opentelemetry-collector
- Ourios investigation finding:
docs/architecture/otlp-log-format.md - RFC 0001 §6.1 (record schema this RFC produces records for):
docs/rfcs/0001-template-miner.md CLAUDE.md§1 (Ourios is logs-only), §3.4 (WAL-before-ack), §3.7 (multi-tenancy not bolted on), §4 (hazards).