Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/ourios-core/src/otlp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,28 @@ pub enum Body {
/// the miner allocates or reuses the
/// `(severity_number, scope_name, BodyKind::Structured)`
/// sentinel template id per §6.1 *Template-key composition*.
///
/// **Wire-export round-trip rule (RFC 0003 implementer note).**
/// The *target* on-disk encoding for `MinedRecord.body` on
/// `Structured` rows is OTLP-canonical JSON per RFC 0005 §3.3.
/// The *current* miner implementation (in
/// `ourios-miner::cluster::ingest_structured`) writes the
/// `Debug` rendering of the decoded `AnyValue` (`format!(
/// "{any_value:?}")`) as an interim placeholder — the
/// canonicalisation PR replaces it before any wire-export
/// path lands. Either way, the future OTLP exporter MUST
/// decode the stored bytes back into the matching `AnyValue`
/// variant (the `opentelemetry_proto::tonic::common::v1::
/// any_value::Value` enum — `KvlistValue`, `ArrayValue`,
/// `IntValue`, `DoubleValue`, `BoolValue`, `BytesValue`,
/// `StringValue`) — *not* emit the stored bytes as
/// `AnyValue::StringValue` carrying the raw text. The latter
/// shortcut is lossy: receivers (e.g. Grafana / Loki) render
/// `StringValue` as text rather than walking the structured
/// tree, and "Body MUST support `AnyValue` to preserve the
/// semantics of structured logs" (OpenTelemetry Logs Data
/// Model §Body) is then violated end-to-end. RFC 0003 will
/// pin this as part of the exporter contract.
Structured(AnyValue),
}

Expand Down
74 changes: 66 additions & 8 deletions crates/ourios-core/src/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use std::sync::{Arc, Mutex};

use crate::audit::ParamType;
use crate::otlp::KeyValue;
use crate::tenant::TenantId;

/// RFC 0001 §6.1 *Body representation* discriminator.
Expand Down Expand Up @@ -69,14 +70,13 @@ pub struct Param {
/// One row the miner emits per ingested line — RFC 0001 §6.1
/// record schema.
///
/// Fields the §6.1 schema names but PR-A defers (everything in
/// `OtlpLogRecord` outside of the mining-output set —
/// `attributes`, `resource_attributes`, `trace_id`, `span_id`,
/// `flags`, `event_name`, etc.) land alongside the Parquet
/// writer, where they actually become observable. PR-A carries
/// only what the cluster has to populate today: the identity
/// triple, the template-key half of `(severity, scope)`, the
/// time field, and the mining outputs.
/// Carries the full §6.1 OTLP-derived envelope plus the mining
/// outputs. The receiver (RFC 0003, post-MVP) populates the
/// OTLP-derived fields from the wire; the miner copies them
/// through unchanged. Until the receiver lands, corpus / bench
/// inputs leave the OTLP envelope at its `OtlpLogRecord::default()`
/// values (zero / `None` / empty `Vec`), which surface as NULL
/// or empty in the corresponding RFC 0005 §3.2 Parquet columns.
Comment on lines +78 to +79
///
/// Records emitted on the parse-failure paths
/// (`body::None`-with-record, empty input, over-cap, degenerate
Expand All @@ -100,10 +100,48 @@ pub struct MinedRecord {
/// so a reader can filter without joining back through the
/// template store.
pub severity_number: u8,
/// `LogRecord.severity_text` — the source's original severity
/// string, when set. Outside the §6.1 template key (the
/// numeric `severity_number` is canonical) but retained
/// per-record for query / display fidelity.
pub severity_text: Option<String>,
pub scope_name: Option<String>,
/// `InstrumentationScope.version` — emitter library version,
/// retained per-record for drift / debugging. Outside the
/// template key.
pub scope_version: Option<String>,
/// Source event time per `OtlpLogRecord.time_unix_nano`. `0`
/// = unknown.
pub time_unix_nano: u64,
/// `LogRecord.observed_time_unix_nano` — collector observation
/// time, when set.
pub observed_time_unix_nano: Option<u64>,
/// `LogRecord.attributes` — per-occurrence structured context.
/// Mirrors RFC 0001 §6.1's `Vec<KeyValue>`; the Parquet writer
/// (RFC 0005 §3.3) encodes this as canonical JSON in the
/// `attributes` `BYTE_ARRAY` column. Empty vec ↔ `[]` on disk
/// per RFC 0005 §3.2.
pub attributes: Vec<KeyValue>,
/// `LogRecord.dropped_attributes_count` — truncation indicator
/// from the receiver.
pub dropped_attributes_count: u32,
/// `Resource.attributes` — source identity (`service.name`,
/// `host.*`, `k8s.*`, ...) copied onto every record under the
/// originating `ResourceLogs` group. Same on-disk encoding as
/// `attributes`.
pub resource_attributes: Vec<KeyValue>,
/// `LogRecord.trace_id` — opaque 16 bytes (W3C Trace Context),
/// when set. RFC 0005 §3.2 stores this as
/// `FIXED_LEN_BYTE_ARRAY(16)` with no logical type — *not*
/// an RFC 4122 UUID.
pub trace_id: Option<[u8; 16]>,
/// `LogRecord.span_id` — opaque 8 bytes, when set.
pub span_id: Option<[u8; 8]>,
/// `LogRecord.flags` — lower 8 bits are W3C trace flags.
pub flags: u32,
/// `LogRecord.event_name` — identifier for structured-event
/// records.
pub event_name: Option<String>,
/// §6.1 *Body representation* fork.
pub body_kind: BodyKind,
/// Masked-parameter slots in template order. Empty for
Expand Down Expand Up @@ -285,8 +323,18 @@ mod tests {
template_id: 7,
template_version: 1,
severity_number: 9,
severity_text: None,
scope_name: Some("lib.auth".to_string()),
scope_version: None,
time_unix_nano: 1_700_000_000_000_000_000,
observed_time_unix_nano: None,
attributes: Vec::new(),
dropped_attributes_count: 0,
resource_attributes: Vec::new(),
trace_id: None,
span_id: None,
flags: 0,
event_name: None,
body_kind: BodyKind::String,
params: vec![Param {
type_tag: ParamType::Num,
Expand All @@ -311,8 +359,18 @@ mod tests {
template_id: 0,
template_version: 0,
severity_number: 0,
severity_text: None,
scope_name: None,
scope_version: None,
time_unix_nano: 0,
observed_time_unix_nano: None,
attributes: Vec::new(),
dropped_attributes_count: 0,
resource_attributes: Vec::new(),
trace_id: None,
span_id: None,
flags: 0,
event_name: None,
body_kind: BodyKind::String,
params: Vec::new(),
separators: Vec::new(),
Expand Down
26 changes: 26 additions & 0 deletions crates/ourios-miner/src/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,14 +508,40 @@ impl MinerCluster {
/// `separators`, `body`, `confidence`, `lossy_flag`) are left
/// at their zero / sentinel defaults; the calling site
/// customises before calling [`Self::emit_record`].
///
/// **Per-record clone cost (deferred optimisation).** The
/// `attributes` and `resource_attributes` vectors are
/// `.clone()`-d once per emitted record. For corpus / bench
/// inputs today the vectors are empty (`Vec::clone` on an
/// empty `Vec` is essentially free), so there's no measured
/// cost. Once the RFC 0003 receiver populates them — and
/// especially `resource_attributes`, which is typically
/// identical across every record in a `ResourceLogs` group —
/// the deep clone becomes a hot-path concern worth measuring.
/// The shape options at that point (per-record-borrowed,
/// `Arc<[KeyValue]>` interning, take-ownership-from-receiver)
/// are RFC 0003 / `ourios-ingester` territory; pinning a
/// shape here would optimise without data. The
/// [`MinedRecord`] field type stays plain `Vec<KeyValue>` for
/// now so it mirrors `OtlpLogRecord`'s shape exactly.
fn record_envelope(record: &OtlpLogRecord, body_kind: BodyKind) -> MinedRecord {
MinedRecord {
tenant_id: record.tenant_id.clone(),
template_id: NO_TEMPLATE,
template_version: 0,
severity_number: record.severity_number,
severity_text: record.severity_text.clone(),
scope_name: record.scope_name.clone(),
scope_version: record.scope_version.clone(),
time_unix_nano: record.time_unix_nano,
observed_time_unix_nano: record.observed_time_unix_nano,
attributes: record.attributes.clone(),
dropped_attributes_count: record.dropped_attributes_count,
resource_attributes: record.resource_attributes.clone(),
trace_id: record.trace_id,
span_id: record.span_id,
flags: record.flags,
event_name: record.event_name.clone(),
body_kind,
params: Vec::new(),
separators: Vec::new(),
Expand Down
10 changes: 10 additions & 0 deletions crates/ourios-miner/src/reconstruct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,18 @@ mod tests {
template_id: 0,
template_version: 0,
severity_number: 0,
severity_text: None,
scope_name: None,
scope_version: None,
time_unix_nano: 0,
observed_time_unix_nano: None,
attributes: Vec::new(),
dropped_attributes_count: 0,
resource_attributes: Vec::new(),
trace_id: None,
span_id: None,
flags: 0,
event_name: None,
body_kind,
params: vec![],
separators: vec![],
Expand Down
Loading