greentic-telemetry 0.1.1

Thin telemetry facade for Greentic: tracing/logging/metrics with OTLP + WASM.
Documentation

greentic-telemetry

Structured JSON logging helpers built on top of tracing for Greentic services.

Quickstart

use greentic_telemetry::{init, set_context, shutdown, CloudCtx, TelemetryInit, prelude::*};

fn main() -> anyhow::Result<()> {
    init(
        TelemetryInit {
            service_name: "example-service",
            service_version: "1.0.0",
            deployment_env: "staging",
        },
        &["tenant", "team", "flow", "run_id"],
    )?;

    set_context(CloudCtx {
        tenant: Some("tenant-42"),
        team: Some("growth"),
        flow: Some("onboarding"),
        run_id: Some("run-17"),
    });

    info!("service booted");

    shutdown(); // flush OTLP batches before exiting

    Ok(())
}

Run the included example to view JSON output:

cargo run --example stdout

Environment overview

Variable Description Default
TELEMETRY_EXPORT Export mode (json-stdout, otlp-grpc, otlp-http) json-stdout
OTLP_ENDPOINT Collector endpoint (e.g. http://otel-collector:4317) unset
OTLP_HEADERS Comma separated headers forwarded to the collector unset
TELEMETRY_SAMPLING parent or traceidratio:<ratio> parent
CLOUD_PRESET Cloud preset (aws, gcp, azure, datadog, loki, none) none
PII_REDACTION_MODE off, strict, allowlist off
PII_ALLOWLIST_FIELDS Comma allowlist for PII fields (allowlist mode) unset
PII_MASK_REGEXES Extra regex masks applied to messages & string fields unset

When OTLP configuration fails, the crate logs a warning and keeps emitting JSON to stdout (if enabled).

Runnable examples

cargo run --example stdout          # JSON logs to stdout
cargo run --example nats_propagation
cargo run --example wasm_host_demo
cargo run --example otlp_demo       # requires OTLP endpoint

Context Propagation

Use inject_carrier / extract_carrier_into_span to round-trip span context and the Greentic cloud IDs across message boundaries:

struct Headers(HashMap<String, String>);

impl greentic_telemetry::Carrier for Headers {
    fn set(&mut self, key: &str, value: String) { self.0.insert(key.into(), value); }
    fn get(&self, key: &str) -> Option<String> { self.0.get(key).cloned() }
}

let mut headers = Headers(HashMap::new());

let span = tracing::info_span!("publish");
let _guard = span.enter();
greentic_telemetry::inject_carrier(&mut headers);

// Later, on the consumer side:
let span = tracing::info_span!("handle");
greentic_telemetry::extract_carrier_into_span(&headers, &span);
let _guard = span.enter();

inject_carrier emits W3C traceparent / tracestate headers and the x-tenant, x-team, x-flow, x-run-id identifiers. extract_carrier_into_span restores the span parentage and rehydrates the context so subsequent logs include the inherited IDs. If you already entered the target span, extract_carrier will attempt to apply the context to the current span.

NATS propagation demo

use greentic_telemetry::{
    init, set_context, Carrier, CloudCtx, TelemetryInit, extract_carrier_into_span, inject_carrier, prelude::*,
};
use std::collections::HashMap;

#[derive(Default)]
struct Headers(HashMap<String, String>);

impl Carrier for Headers {
    fn set(&mut self, key: &str, value: String) { self.0.insert(key.to_string(), value); }
    fn get(&self, key: &str) -> Option<String> { self.0.get(key).cloned() }
}

fn main() -> anyhow::Result<()> {
    init(
        TelemetryInit {
            service_name: "nats-demo",
            service_version: "1.0.0",
            deployment_env: "prod",
        },
        &["tenant", "team"],
    )?;

    set_context(CloudCtx {
        tenant: Some("alpha"),
        team: Some("platform"),
        flow: None,
        run_id: None,
    });

    let mut headers = Headers::default();
    {
        let span = info_span!("publish");
        let _guard = span.enter();
        inject_carrier(&mut headers);
        info!(subject = "orders.created", "message published");
    }

    let span = info_span!("consume");
    let _guard = span.enter();
    extract_carrier_into_span(&headers, &span);
    info!("message consumed with propagated context");

    Ok(())
}

Cloud Presets

Set CLOUD_PRESET for quick-start wiring. Presets only prefill defaults—you can still override env vars manually.

Preset Default OTLP_ENDPOINT Notes
aws http://aws-otel-collector:4317 Targets AWS Distro for OpenTelemetry collector.
gcp http://otc-collector:4317 Example for Google Ops Agent’s OTLP receiver.
azure http://otel-collector-azure:4317 Collector forwarding to Azure Monitor exporter.
datadog http://datadog-agent:4317 If DD_API_KEY present, auto-inserts OTLP_HEADERS=DD_API_KEY=....
loki N/A Keeps json-stdout; ship through Vector/Grafana Agent for Loki/Tempo.

TELEMETRY_EXPORT remains respected. If unset, presets select otlp-grpc (except loki, which leaves JSON stdout).

Collector snippets

AWS ADOT sidecar (logs/traces):

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
exporters:
  awsxray:
    local_mode: true
  awscloudwatchlogs:
    log_group_name: /greentic/services
service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [awsxray]
    logs:
      receivers: [otlp]
      exporters: [awscloudwatchlogs]

GCP Ops Agent OTLP collector (forward to Cloud Trace / Logging):

receivers:
  otlp:
    protocols:
      grpc:
exporters:
  googlecloud:
    project: ${PROJECT_ID}
service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [googlecloud]
    logs:
      receivers: [otlp]
      exporters: [googlecloud]

Azure Monitor exporter via standalone collector:

receivers:
  otlp:
    protocols:
      grpc:
exporters:
  azuremonitor:
    instrumentation_key: ${APP_INSIGHTS_KEY}
service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [azuremonitor]
    logs:
      receivers: [otlp]
      exporters: [azuremonitor]

Datadog agent OTLP:

receivers:
  otlp:
    protocols:
      grpc:
exporters:
  otlphttp:
    endpoint: https://api.datadoghq.com
    headers:
      x-api-key: ${DD_API_KEY}
service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlphttp]
    logs:
      receivers: [otlp]
      exporters: [otlphttp]

Loki + Tempo via Vector:

sources:
  otlp_grpc:
    type: otlp
    address: 0.0.0.0:4317
sinks:
  loki:
    type: loki
    inputs: [otlp_grpc]
    endpoint: http://loki:3100
  tempo:
    type: tempo
    inputs: [otlp_grpc]
    endpoint: http://tempo:4317

Metrics

  • Counters, gauges, and histograms are exposed via greentic_telemetry::metrics.
  • When TELEMETRY_EXPORT resolves to an OTLP exporter, measurements are forwarded over the same gRPC channel. With json-stdout, metrics default to no-ops so instrumentation never needs guard clauses.
let requests = greentic_telemetry::metrics::counter("service.requests");
let latency = greentic_telemetry::metrics::histogram("service.request.duration_ms");

requests.add(1.0);
latency.record(elapsed_ms);

Every data point automatically includes service.name, service.version, deployment.environment, and the active cloud context (tenant, team, flow, run_id). If a tracing span is in scope, exemplar hints (trace_id, span_id) ride along so compatible collectors can correlate metrics back to traces.

WASM guests / host tools

The wit/greentic-telemetry.wit package exposes a narrow logging interface that WASM guests can rely on. With wit-bindgen, the guest side becomes:

// wasm_guest.rs
use greentic_telemetry::wasm_guest::{Field, Level, log, span_end, span_start};

pub fn run_tool() {
    let span = span_start("guest-tool", &[Field { key: "tenant", value: "acme" }]);
    log(Level::Info, "initialised guest tool", &[]);
    log(Level::Info, "work complete", &[]);
    span_end(span);
}

A native host can forward the guest calls to tracing:

use greentic_telemetry::wasm_host::{log as host_log, span_end, span_start, Field, LogLevel};

fn simulate_guest() {
    let span = span_start("guest-run", &[Field { key: "team", value: "ops" }]);
    host_log(LogLevel::Info, "guest emitted log", &[]);
    span_end(span);
}

See examples/wasm_host_demo.rs for a runnable version.

PII Redaction

  • Configure PII_REDACTION_MODE=off|strict|allowlist to mask sensitive values before they reach collectors.
  • strict masks common tokens, emails, and phone numbers by default; allowlist keeps only the fields in PII_ALLOWLIST_FIELDS unchanged.
  • Extend masking with PII_MASK_REGEXES (comma-separated regexes) to scrub custom patterns.

OTLP demo

cargo run --example otlp_demo emits a span (demo.operation), structured logs, and metrics (demo.request.count, demo.request.duration_ms). Point TELEMETRY_EXPORT=otlp-grpc and OTLP_ENDPOINT at a collector before running.

Troubleshooting

  • No logs: ensure RUST_LOG includes info (or higher) and that the collector has a logs pipeline when using OTLP.
  • Metrics missing: verify the collector has a metrics pipeline and that it isn’t filtering by resource attributes (service.*, deployment.environment).
  • Context lost: make sure headers survive transport (case sensitivity, lower-case keys for NATS, etc.) and call extract_carrier_into_span before entering the span that should adopt the remote context.
  • Unexpected PII: enable PII_REDACTION_MODE=strict and add custom regexes for service-specific tokens.
  • Snapshot tests: use greentic_telemetry::dev::test_init_for_snapshot() and capture_logs to gather deterministic JSON output with a fixed timestamp.