OpenPay (1 crate, ~2940 LOC, 40 tests)
op-statements Statement generation + reconciliation + fee accounting.
Render targets: PDF (template), CSV, JSON, 94-char
NACHA fixed-width. ISO 20022 camt.053 with full
BkToCstmrStmt envelope (GrpHdr/Stmt/Bal/Ntry). BAI2
with 01/02/03/16/49/98/99 records. MT940/MT942
SWIFT with :20:/:25:/:28C:/:60F:/:61:/:86:/:62F:
+ :90C:/:90D: counters. FeeBucket (interchange/scheme/
acquirer/fx/settlement-network/bnpl) + FeeSchedule
with bps + flat rules. Cadence (daily/weekly/monthly/
custom). Two-tier Reconciler (external_id → window
heuristic).
Smart Byte (1 crate, 3032 LOC, 39 tests)
smart-byte-activitypub ActivityPub W3C Rec (2018). AS2 vocab + Public
addressing; Actor (Person/Application/Group/
Organization/Service) with publicKey + shared-inbox;
Object (Note/Article/Image/Video/Tombstone) with
Mastodon extensions (sensitive, CW summary,
conversation, blurhash); 12 Activity types
(Create/Update/Delete/Follow/Accept/Reject/Announce/
Like/Undo/Move/Block/Flag) with side-effect rules;
OrderedCollection + Page; Webfinger (RFC 7033);
HTTP Signatures (cavage draft-12 + RFC 9421);
SHA-256 Digest; Ed25519 sign/verify.
EOC (1 crate, 2759 LOC, 20 tests)
eoc-observability OpenTelemetry-compatible tracing + metrics + logs.
W3C tracecontext + baggage. Span / SpanKind / Status.
Samplers: AlwaysOn/Off/RatioBased/ParentBased. Sync
BatchSpanProcessor + Exporter trait. Counter / Gauge /
Histogram. Prometheus text exposition (HELP/TYPE/
_bucket/_sum/_count/+Inf). StatsD / DogStatsD lines.
GenAI semconv (gen_ai.{operation,system,request.*,
response.*,usage.*}) + EOC extensions (eoc.joules.*,
eoc.co2e_grams, eoc.cost.usd). USD + gCO2e attribution
with PUE. LangSmith + LangFuse export adapters.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
100 lines
3.2 KiB
Rust
100 lines
3.2 KiB
Rust
//! End-to-end span lifecycle: start, attribute, end, batch-export.
|
|
|
|
use eoc_observability::{
|
|
AlwaysOnSampler, BatchSpanProcessor, GenAiAttributes, InMemoryExporter, Sampler,
|
|
SamplingDecision, Span, SpanContext, SpanId, SpanKind, Status, TraceFlags, TraceId,
|
|
};
|
|
use std::sync::Arc;
|
|
|
|
#[test]
|
|
fn full_lifecycle_emits_one_span() {
|
|
let exporter = Arc::new(InMemoryExporter::new());
|
|
let processor = BatchSpanProcessor::new(exporter.clone());
|
|
|
|
// Sampler says yes.
|
|
let sampler = AlwaysOnSampler;
|
|
let trace_id = TraceId([1u8; 16]);
|
|
let decision = sampler.should_sample(None, &trace_id, "llm.generate");
|
|
assert_eq!(decision, SamplingDecision::RecordAndSample);
|
|
|
|
// Build a sampled context.
|
|
let span_id = SpanId([2u8; 8]);
|
|
let ctx = SpanContext::new(trace_id, span_id, TraceFlags::SAMPLED);
|
|
|
|
// Start the span.
|
|
let mut span = Span::new(
|
|
&ctx,
|
|
None,
|
|
"llm.generate",
|
|
SpanKind::Client,
|
|
1_000_000_000,
|
|
);
|
|
|
|
// GenAI attributes.
|
|
let attrs = GenAiAttributes {
|
|
operation_name: Some("chat".to_string()),
|
|
system: Some("openai".to_string()),
|
|
request_model: Some("gpt-4o".to_string()),
|
|
response_model: Some("gpt-4o-2024-08-06".to_string()),
|
|
prompt_tokens: Some(100),
|
|
completion_tokens: Some(200),
|
|
total_tokens: Some(300),
|
|
finish_reasons: Some("stop".to_string()),
|
|
..Default::default()
|
|
}
|
|
.to_attributes();
|
|
for (k, v) in attrs {
|
|
span.set_attribute(k, v);
|
|
}
|
|
|
|
// Joule cost.
|
|
span.set_joule_cost(eoc_core::JouleCost::measured(12_345_000));
|
|
|
|
span.set_status(Status::ok());
|
|
span.end(2_000_000_000);
|
|
assert!(span.is_ended());
|
|
assert_eq!(span.duration_nanos(), 1_000_000_000);
|
|
|
|
processor.on_end(span).expect("on_end");
|
|
processor.force_flush().expect("flush");
|
|
|
|
let collected = exporter.finished_spans();
|
|
assert_eq!(collected.len(), 1);
|
|
let exported = &collected[0];
|
|
assert_eq!(exported.name, "llm.generate");
|
|
assert!(exported.joule_cost.is_some());
|
|
let joule = exported.joule_cost.expect("joule_cost present");
|
|
assert_eq!(joule.microjoules, 12_345_000);
|
|
|
|
// Verify gen_ai.system attribute round-tripped.
|
|
let has_system = exported
|
|
.attributes
|
|
.iter()
|
|
.any(|(k, _)| k == "gen_ai.system");
|
|
assert!(has_system, "gen_ai.system attribute missing");
|
|
}
|
|
|
|
#[test]
|
|
fn batch_processor_respects_max_batch_size() {
|
|
let exporter = Arc::new(InMemoryExporter::new());
|
|
let cfg = eoc_observability::BatchConfig {
|
|
max_batch_size: 2,
|
|
max_queue_size: 10,
|
|
};
|
|
let processor = BatchSpanProcessor::with_config(exporter.clone(), cfg);
|
|
|
|
let trace_id = TraceId([1u8; 16]);
|
|
for i in 0..3u8 {
|
|
let span_id = SpanId([i + 1; 8]);
|
|
let ctx = SpanContext::new(trace_id, span_id, TraceFlags::SAMPLED);
|
|
let mut s = Span::new(&ctx, None, "op", SpanKind::Internal, 0);
|
|
s.end(1);
|
|
processor.on_end(s).expect("on_end");
|
|
}
|
|
// After 2 spans the queue auto-flushed; the third remains pending.
|
|
assert_eq!(processor.pending_count(), 1);
|
|
assert_eq!(exporter.finished_spans().len(), 2);
|
|
|
|
processor.shutdown().expect("shutdown");
|
|
assert_eq!(exporter.finished_spans().len(), 3);
|
|
}
|