Skip to main content

spectra/
lib.rs

1//! Spectra is a Rust observability library for typed **metrics and counters**, structured
2//! **event logs**, and pluggable storage.
3//!
4//! Wire a backend once with [`Spectra::builder()`], declare schemas with [`spectra_metric!`] and
5//! [`spectra_schema!`], then emit and query through the typed helpers those macros expand.
6//! Storage engines stay behind thin async ports — swap `mem`, `sqlite`, ClickHouse, or
7//! TensorBase without changing emit code.
8//!
9//! ## Features
10//!
11//! - **Typed schema and metric DSL** — declare counters and event tables with classification
12//!   metadata (PII, console safety) via [`spectra_metric!`] and [`spectra_schema!`]. Each
13//!   macro expands inventory registration, typed `*Recorder` / `*Logger` helpers, and
14//!   transport `*Payload` / `*_TOPIC` DTOs.
15//! - **Schema DSL controls** — set emit defaults on the schema: `level`, `default_sample_rate`,
16//!   `coalesce_ms` (gauges), and field `classification` (`pii`, `safe_for_console`). See
17//!   [`spectra_schema!`] and [`spectra_metric!`]. Runtime env/TOML overrides live on
18//!   [`spectra_core::SpectraConfig`].
19//! - **Composable storage** — inject backends on the builder: in-memory (`mem`), durable embedded
20//!   (`sqlite`), or remote (`clickhouse`, `tensorbase`).
21//! - **Emit controls** — global and per-name level gates, sampling, optional request/job buffers,
22//!   and async batched persist (overrides schema defaults).
23//! - **Query API** — read metrics and events through [`SpectraRouter`] with label and time-range
24//!   filters.
25//! - **Direct or distributed wiring** — one process can write storage, or many publishers can
26//!   fan out to a consumer process that owns the database (see
27//!   [Mode 2](#mode-2--distributed-publish-and-consume-two-binaries)).
28//!
29//! *Declarative schemas and typed logging APIs — same surface from embedded to multi-service.*
30//!
31//! # Getting started
32//!
33//! You always emit the same way (`CacheHitsRecorder::record(...)`, etc.). What changes is
34//! **which process writes the database**.
35//!
36//! ## Choose how data reaches storage
37//!
38//! - **[Mode 1 — Direct persist](#mode-1--direct-persist-one-binary)** — one binary emits and
39//!   stores. Start here.
40//! - **[Mode 2 — Distributed](#mode-2--distributed-publish-and-consume-two-binaries)** — many
41//!   app processes *publish* onto a bus; a separate **consumer** binary writes storage.
42//! - **[Mode 3 — Dual-path](#mode-3--dual-path-optional)** — one process both publishes and
43//!   stores (mirroring). Optional.
44//!
45//! After you pick a mode, continue with [schemas](#4-declare-and-link-schemas)
46//! (shared by every mode).
47//!
48//! ## Mode 1 — Direct persist (one binary)
49//!
50//! This process emits metrics/events **and** writes them to storage. There is no second binary
51//! and no message bus.
52//!
53//! ```text
54//! Your app ──emit──► Spectra ──async persist──► mem / SQLite / ClickHouse / TensorBase
55//! ```
56//!
57//! | Backend | Types | Feature | When to use |
58//! |---------|-------|---------|-------------|
59//! | In-memory | [`MemMetricsBackend`] / [`MemEventsBackend`] | `mem` (default) | Local experiments |
60//! | SQLite | [`SqliteMetricsBackend`] / [`SqliteEventsBackend`] | `sqlite` | Durable single host |
61//! | ClickHouse | [`ClickHouseMetricsBackend`] / [`ClickHouseEventsBackend`] | `clickhouse` | Remote analytics |
62//! | TensorBase | [`TensorBaseMetricsBackend`] / [`TensorBaseEventsBackend`] | `tensorbase` | ClickHouse-compatible scale-out |
63//!
64//! **In-memory** (both backends are required):
65//!
66//! ```no_run
67//! # #[cfg(feature = "mem")]
68//! # async fn demo() -> spectra::Result<()> {
69//! use std::sync::Arc;
70//! use spectra::{MemEventsBackend, MemMetricsBackend, Spectra};
71//!
72//! let spectra = Spectra::builder()
73//!     .metrics_backend(Arc::new(MemMetricsBackend::new()))
74//!     .events_backend(Arc::new(MemEventsBackend::new()))
75//!     .embedded()
76//!     .build()?;
77//! # let _ = spectra;
78//! # Ok(())
79//! # }
80//! ```
81//!
82//! **High-throughput DW ingest** — raise L2 batch size on the builder (not env vars):
83//!
84//! ```no_run
85//! # #[cfg(feature = "mem")]
86//! # async fn demo() -> spectra::Result<()> {
87//! use std::sync::Arc;
88//! use std::time::Duration;
89//! use spectra::{MemEventsBackend, MemMetricsBackend, PersistConfig, Spectra};
90//!
91//! let spectra = Spectra::builder()
92//!     .metrics_backend(Arc::new(MemMetricsBackend::new()))
93//!     .events_backend(Arc::new(MemEventsBackend::new()))
94//!     .persist(PersistConfig {
95//!         batch_max: 2048,
96//!         batch_wait: Duration::from_millis(5),
97//!         ..PersistConfig::default()
98//!     })
99//!     .build()?;
100//! // Web / scripts: use try_record_*_now / helpers (enqueue L2).
101//! // Scripts that must exit only after durable writes:
102//! spectra.flush_persist().await?;
103//! # Ok(())
104//! # }
105//! ```
106//!
107//! Canonical path for **web** and **Write Now**: `*_now` → L2 batched persist. Prefer that over
108//! `request_scope`, which drops undrained emits on panic or early exit.
109//!
110//! **ClickHouse** (omit `.embedded()`; constructors are async):
111//!
112//! ```no_run
113//! # #[cfg(feature = "clickhouse")]
114//! # async fn demo() -> spectra::Result<()> {
115//! use std::sync::Arc;
116//! use spectra::{ClickHouseEventsBackend, ClickHouseMetricsBackend, Spectra};
117//!
118//! let url = "http://127.0.0.1:8123"; // or SPECTRA_CLICKHOUSE_URL
119//! let metrics = ClickHouseMetricsBackend::connect(url).await?;
120//! let events = ClickHouseEventsBackend::connect(url).await?;
121//! let spectra = Spectra::builder()
122//!     .metrics_backend(Arc::new(metrics))
123//!     .events_backend(Arc::new(events))
124//!     .build()?;
125//! # let _ = spectra;
126//! # Ok(())
127//! # }
128//! ```
129//!
130//! Runnable: `quickstart`, `quickstart_sqlite`, `quickstart_clickhouse_emit`.
131//! Then jump to [schemas](#4-declare-and-link-schemas).
132//!
133//! ## Mode 2 — Distributed publish and consume (two binaries)
134//!
135//! Use this when many services emit telemetry but you do **not** want each of them to open
136//! ClickHouse (or SQLite) itself. Instead:
137//!
138//! 1. Each app process is a **publisher** — it emits into Spectra, which hands the emit to your
139//!    [`SpectraSink`], which publishes onto a message bus.
140//! 2. A separate **consumer** process (or fleet) subscribes on that bus and writes storage.
141//!
142//! ```text
143//! Publisher binary(ies) ──emit──► SpectraSink ──publish──► bus (e.g. Photon)
144//! Consumer binary       ──subscribe──► decode ──► try_*_now / storage backends
145//! ```
146//!
147//! Spectra does **not** ship a bus. Your host owns that piece. A common choice is
148//! [Photon](https://github.com/unified-field-dev/photon) (`uf-photon` on crates.io).
149//!
150//! ### What you create
151//!
152//! | Piece | Purpose |
153//! |-------|---------|
154//! | Shared schema crate | Same `spectra_*!` modules (`mod`-linked) on both sides |
155//! | Publisher binary | `[[bin]]` or crate that emits; **no** DB writes |
156//! | Consumer binary | Another `[[bin]]` or crate that owns storage |
157//! | Bus | Photon, NATS, Kafka, … — host-provided |
158//!
159//! ### Shared setup (both binaries)
160//!
161//! Put `spectra_schema!` / `spectra_metric!` declarations in a shared crate both binaries depend
162//! on. `mod` each schema file so macros expand helpers, topic DTOs, and inventory registration
163//! (see [§ 4](#4-declare-and-link-schemas)).
164//!
165//! - **Helpers** — typed emit API (publisher uses these)
166//! - **Topic payloads** — `*Payload` / `*_TOPIC` beside each schema (publisher publishes these)
167//! - **Consumer persist** — after decode, call [`try_record_counter_at`] / [`try_log_event_at`]
168//!   with the envelope timestamp (or a storage backend)
169//!
170//! ### Publisher binary
171//!
172//! Add a second binary in your workspace (for example `src/bin/telemetry_publisher.rs`, or a
173//! dedicated crate). In that process:
174//!
175//! 1. Implement [`SpectraSink`]: for each emit, build a topic `*Payload` and publish it on
176//!    your bus. Keep the methods non-blocking (spawn a task, enqueue, or use Photon buffering).
177//! 2. Wire Spectra with `.sink(...).persist_disabled().build()` so this process does **not**
178//!    write the analytics database. (The builder still requires dummy or unused backends.)
179//! 3. Emit with typed helpers exactly as in Mode 1.
180//!
181//! ```ignore
182//! use std::sync::Arc;
183//! use spectra::{
184//!     MemEventsBackend, MemMetricsBackend, Spectra, SpectraSink,
185//! };
186//!
187//! /// Your bus adapter — replace the body with Photon (or another) publish calls.
188//! struct BusPublishSink;
189//!
190//! impl SpectraSink for BusPublishSink {
191//!     fn record_counter(&self, name: &str, labels: &[(&str, &str)], delta: i64) {
192//!         // 1) Map name → schema module *Payload (see topics beside each schema).
193//!         // 2) Publish asynchronously — do not block the emit thread.
194//!         let _ = (name, labels, delta);
195//!     }
196//!     fn record_gauge(&self, name: &str, labels: &[(&str, &str)], value: f64) {
197//!         let _ = (name, labels, value);
198//!     }
199//!     fn log_event(&self, table: &str, fields: &serde_json::Value) {
200//!         let _ = (table, fields);
201//!     }
202//! }
203//!
204//! # async fn boot() -> spectra::Result<()> {
205//! let sink = Arc::new(BusPublishSink);
206//! let _spectra = Spectra::builder()
207//!     .metrics_backend(Arc::new(MemMetricsBackend::new()))
208//!     .events_backend(Arc::new(MemEventsBackend::new()))
209//!     .sink(sink as Arc<dyn SpectraSink>)
210//!     .persist_disabled() // publisher does not open ClickHouse
211//!     .build()?;
212//!
213//! // Same emit API as Mode 1:
214//! // CacheHitsRecorder::record(1, serde_json::json!({"region":"us"}));
215//! # Ok(())
216//! # }
217//! ```
218//!
219//! Runnable sketch (in-memory stand-in for the bus): `quickstart_publish_only`.
220//! API detail: [`SpectraSink`], [`SpectraBuilder::sink`], [`SpectraBuilder::persist_disabled`].
221//!
222//! ### Consumer binary
223//!
224//! Create a **different** binary (for example `src/bin/telemetry_consumer.rs`). This process
225//! owns the database:
226//!
227//! 1. Build Spectra with real backends and **persist left on** (Mode 1-style `.build()`).
228//! 2. Start your bus subscriber (Photon `start_executor` / `#[subscribe]`, etc.).
229//! 3. On each message: deserialize to a `*Payload` or [`MetricEmit`] / [`SpectraEvent`],
230//!    then call [`try_record_counter_at`] / [`try_log_event_at`] with the envelope `ts`
231//!    (or write the storage backend directly).
232//!
233//! ```ignore
234//! use std::sync::Arc;
235//! use spectra::{
236//!     try_record_counter_at, ClickHouseEventsBackend, ClickHouseMetricsBackend, Spectra,
237//! };
238//!
239//! # async fn boot_consumer() -> spectra::Result<()> {
240//! let url = std::env::var("SPECTRA_CLICKHOUSE_URL")?;
241//! let spectra = Spectra::builder()
242//!     .metrics_backend(Arc::new(ClickHouseMetricsBackend::connect(&url).await?))
243//!     .events_backend(Arc::new(ClickHouseEventsBackend::connect(&url).await?))
244//!     .build()?; // persist ON — this process writes storage
245//!
246//! // Pseudocode for your bus callback (Photon subscriber, etc.):
247//! // let emit = decode_metric_emit(bytes)?;
248//! // let labels = label_pairs_from_json(&emit.labels);
249//! // let ts = emit.ts.unwrap_or_else(chrono::Utc::now);
250//! // try_record_counter_at(&emit.name, &labels, emit.delta.unwrap_or(1), ts);
251//!
252//! let _ = spectra; // keep handle alive; run subscriber loop until shutdown
253//! # Ok(())
254//! # }
255//! ```
256//!
257//! Runnable sketch (decode → `try_record_counter_at` without a live broker):
258//! `quickstart_consume_forward`.
259//!
260//! ### Run both
261//!
262//! 1. Start the **consumer** (and your bus / Photon) so subscriptions are ready.
263//! 2. Start one or more **publishers**.
264//! 3. Query storage from the consumer process (or any process that shares the same DB).
265//!
266//! For Photon you typically set `PHOTON_TRANSPORT_KEY` (base64 of 32 bytes) and a broker URL
267//! such as `PHOTON_NATS_URL`. See Photon’s own README for adapter wiring.
268//!
269//! ## Mode 3 — Dual-path (optional)
270//!
271//! Same process both publishes through a [`SpectraSink`] **and** persists to storage. Use when
272//! you want a bus mirror without moving writes to another binary.
273//!
274//! Wire with `.sink(transport).build()` — omit `persist_disabled` so storage still receives emits.
275//!
276//! Runnable: `quickstart_transport`.
277//!
278//! ## 4. Declare and link schemas
279//!
280//! Typed emit helpers expand from the same macros that register schema metadata. Required for
281//! every mode. No `build.rs` or `OUT_DIR` includes.
282//!
283//! ```toml
284//! [dependencies]
285//! spectra = { package = "uf-spectra", git = "https://github.com/unified-field-dev/spectra.git", tag = "v0.1.0", features = ["mem"] }
286//! chrono = { version = "0.4", features = ["serde"] }
287//! serde = { version = "1", features = ["derive"] }
288//! serde_json = "1"
289//! ```
290//!
291//! Typical application layout:
292//!
293//! ```text
294//! my-app/
295//! ├── Cargo.toml
296//! └── src/
297//!     ├── schemas/
298//!     │   ├── mod.rs
299//!     │   ├── cache_hits.rs
300//!     │   └── request_log.rs
301//!     └── main.rs
302//! ```
303//!
304//! ```ignore
305//! // src/schemas/mod.rs — one `mod` line per schema file (links inventory + helpers + topics)
306//! pub mod cache_hits;
307//! pub mod request_log;
308//!
309//! pub use cache_hits::CacheHitsRecorder;
310//! pub use request_log::RequestLogLogger;
311//! ```
312//!
313//! ## 5. Declare metric and event schemas
314//!
315//! Schemas are the typed contracts Spectra stores and queries. Share them across publisher and
316//! consumer binaries in Mode 2 (depend on the same schema crate).
317//!
318//! - **Metric schema** ([`spectra_metric!`]) — a named measurement family; optional `level`,
319//!   `default_sample_rate`, and `coalesce_ms` (gauges).
320//! - **Event schema** ([`spectra_schema!`]) — a typed structured log table with classified
321//!   columns; optional `level` and `default_sample_rate` (`coalesce_ms` is not valid on events).
322//!
323//! Full field tables: [`spectra_metric!`] / [`spectra_schema!`]. Runtime overrides (env/TOML):
324//! [`spectra_core::SpectraConfig`] and the emit-gate table in the crate README.
325//!
326//! ```ignore
327//! // src/schemas/cache_hits.rs
328//! use spectra::spectra_metric;
329//!
330//! spectra_metric! {
331//!     CacheHits {
332//!         store: "default",       // logical store for routing
333//!         name: "cache_hits",     // registry key + query_metrics.metric_name
334//!         version: "0.1.0",
335//!         description: "Counter for cache hit events",
336//!         level: Debug,
337//!         default_sample_rate: 0.1,
338//!     }
339//! }
340//! ```
341//!
342//! ```ignore
343//! // src/schemas/request_log.rs
344//! use spectra::spectra_schema;
345//!
346//! spectra_schema! {
347//!     RequestLog {
348//!         store: "default",
349//!         table: "request_log",   // registry key + query_events.table
350//!         version: "0.1.0",
351//!         description: "Structured request debug events",
352//!         level: Debug,
353//!         default_sample_rate: 0.25,
354//!         fields: [
355//!             message: {
356//!                 r#type: String, // String | i64 | f64 | bool
357//!                 classification: { pii: false, safe_for_console: true },
358//!             },
359//!         ],
360//!     }
361//! }
362//! ```
363//!
364//! Helper names come from the schema identifier:
365//! `CacheHits` → `CacheHitsRecorder`, `RequestLog` → `RequestLogLogger`.
366//! Each expansion also emits `*Payload` and `*_TOPIC` for Mode 2 transport.
367//!
368//! ## 6. Emit metrics and events
369//!
370//! **Mode 1 / Mode 3:** call helpers in the same process that owns (or mirrors) storage.
371//!
372//! **Mode 2:** call helpers only in the **publisher**. The consumer persists with
373//! [`try_record_counter_at`] / [`try_log_event_at`] after decode (pass envelope `ts`).
374//!
375//! ```ignore
376//! use crate::schemas::{CacheHitsRecorder, RequestLogLogger};
377//!
378//! CacheHitsRecorder::record(1, serde_json::json!({ "region": "us-west" }));
379//! RequestLogLogger::log("request handled".to_string());
380//! ```
381//!
382//! This crate's CI demo helpers ([`helpers`]) come from `platform_smoke_*` schemas only —
383//! define product schemas in **your** application.
384//!
385//! ## 7. Query persisted data
386//!
387//! Storage persist is asynchronous. Wait briefly (or poll) before querying.
388//!
389//! **Mode 1 / Mode 3:** query on the same process that wrote storage.
390//!
391//! **Mode 2:** query from the **consumer** (or any process connected to the same database) —
392//! not from the publisher.
393//!
394//! ```no_run
395//! # #[cfg(feature = "mem")]
396//! # async fn demo(spectra: spectra::Spectra) -> spectra::Result<()> {
397//! use spectra_core::{current_emit_ts, EventsQueryFilter, MetricsQueryRange};
398//!
399//! tokio::time::sleep(std::time::Duration::from_millis(80)).await;
400//! let now = current_emit_ts();
401//!
402//! let points = spectra
403//!     .router()
404//!     .query_metrics(MetricsQueryRange {
405//!         metric_name: "cache_hits".into(),
406//!         start: now - chrono::Duration::seconds(5),
407//!         end: now + chrono::Duration::seconds(1),
408//!         label_matchers: vec![],
409//!     })
410//!     .await?;
411//!
412//! let rows = spectra
413//!     .router()
414//!     .query_events(EventsQueryFilter {
415//!         table: "request_log".into(),
416//!         start: Some(now - chrono::Duration::seconds(5)),
417//!         end: Some(now + chrono::Duration::seconds(1)),
418//!         ..Default::default()
419//!     })
420//!     .await?;
421//! # let _ = (points, rows);
422//! # Ok(())
423//! # }
424//! ```
425//!
426//! Prefer row queries. Event chart aggregates (`query_aggregate`) are stubbed on most backends.
427//!
428//! Configuration (emit gates, sampling, buffers) is documented on
429//! [`spectra_core::SpectraConfig`].
430//!
431//! # Notes
432//!
433//! - Enable backend features explicitly (`mem` is on by default; `sqlite`, `clickhouse`, and
434//!   `tensorbase` are optional).
435//! - Product schemas belong in **your application**; this crate registers CI demo
436//!   `platform_smoke_*` schemas only.
437//! - Pin consumption via git tag (for example `v0.1.0`). Configure storage on
438//!   [`Spectra::builder()`], not a global mode enum.
439//! - Event chart aggregates (`spectra_core::EventStorageBackend::query_aggregate`) are stubbed on most backends.
440//! - Schema modules must be `mod`-linked into the binary or `inventory` will not see them.
441//! - Schema macros and helpers call through the `spectra` facade — pin `spectra` as a direct
442//!   dependency (no separate `spectra-core` pin required for the default recipe).
443
444// Allow schema macros to resolve `::spectra::…` inside this crate.
445extern crate self as spectra;
446
447mod schemas;
448
449pub mod helpers;
450pub mod topics;
451
452pub use spectra_core::{
453    self, try_log_event_at, try_log_event_now, try_record_counter, try_record_counter_at,
454    try_record_counter_now, try_record_gauge_at, try_record_gauge_now, ChainedSink, Error,
455    FieldClassification, LoggingKind, MetricEmit, RecordingSink, Result, SchemaFieldMetadata,
456    SchemaMetadata, SchemaMetadataInit, SchemaRegistry, SpectraEvent, SpectraLevel, SpectraRouter,
457    SpectraSink,
458};
459/// Re-export for schema macro `inventory::submit!` expansions.
460pub use spectra_core::inventory;
461pub use spectra_macros::{spectra_metric, spectra_schema};
462pub use spectra_runtime::{
463    PersistConfig, PersistHandle, PersistOverflow, Spectra, SpectraBuilder, StoragePersistSink,
464};
465
466#[cfg(feature = "mem")]
467pub use spectra_backend_mem::{MemEventsBackend, MemMetricsBackend};
468
469#[cfg(feature = "sqlite")]
470pub use spectra_backend_sqlite::{SqliteEventsBackend, SqliteMetricsBackend};
471
472#[cfg(feature = "tensorbase")]
473pub use spectra_backend_tensorbase::{TensorBaseEventsBackend, TensorBaseMetricsBackend};
474
475#[cfg(feature = "clickhouse")]
476pub use spectra_backend_clickhouse::{ClickHouseEventsBackend, ClickHouseMetricsBackend};