One application, two modes
A service usually wants a console layer locally and the JSON layer once deployed, chosen by one match rather than duplicated main functions.
APP_MODE (default development) picks the log sink:
development: human-readable console lines;production: Cloud Logging JSON lines on stdout, which a logging agent on GKE, Cloud Run or GCE collects.
Spans go to the Telemetry API through OTLP in both modes, and the logging layer reads the span context they attach, so every entry carries its trace and spanId.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AppMode {
Development,
Production,
}
impl AppMode {
fn from_env() -> Self {
match std::env::var("APP_MODE").as_deref() {
Ok("production") => AppMode::Production,
_ => AppMode::Development,
}
}
}
Option<Layer> implements Layer, so each sink is built in the match and composed unconditionally, without a boxed trait object or two separate subscriber types.
The inactive one is a None that costs nothing:
// `Option<Layer>` implements `Layer`, so both sinks are built from one `match` and composed
// unconditionally below. The inactive one is a `None` that costs nothing.
let (console_layer, json_layer) = match mode {
AppMode::Development => (Some(tracing_subscriber::fmt::layer()), None),
AppMode::Production => (
None,
Some(GcpCloudLoggingLayerBuilder::new(google_project_id).build()?),
),
};
// The OpenTelemetry layer goes before the logging layer: the logging layer reads the span
// context the OpenTelemetry layer attaches, and sees only what is registered ahead of it.
let subscriber = Registry::default()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.with(tracing_opentelemetry::layer().with_tracer(tracer))
.with(console_layer)
.with(json_layer);
tracing::subscriber::set_global_default(subscriber)?;
PROJECT_ID=<your-google-project-id> cargo run --example logging-dev-and-prod --features logs,otlp
PROJECT_ID=<your-google-project-id> APP_MODE=production cargo run --example logging-dev-and-prod --features logs,otlp
The production mode writes to stdout only. Run one sink in production: a service that also sends entries over the network duplicates them on a host that collects stdout.
The traces and logs page has the same switch with the development mode sending its entries over OTLP as well.
Full example available here.