Skip to content

Add instrumentation / telemetry #30

@liamwh

Description

@liamwh

Hi there,

Thank you for making this! It's really a great project, well done, and well documented. I've enjoyed checking it out!

One thing I notice that Rusve is clearly missing to make it production ready, which is absolutely essential in a microservices architecture, is telemetry. For example, the observability to follow a single request across services with a trace is very important to understanding the behaviour of the app.

To achieve this, the code would need to be instrumented with an SDK, as well as having a tool to send the traces to, like Grafana Tempo.

If you're interested in adding this to the repo, then I have some resources for you:

There are docker-compose examples for Tempo here. Although I've recently set this up myself in an example repo which you might find more helpful here.

To instrument your Rust code, I can share this which might help get you started:

use opentelemetry_otlp::WithExportConfig;
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;

const OTEL_EXPORTER_OTLP_ENDPOINT_ENV_VAR: &str = "OTEL_EXPORTER_OTLP_ENDPOINT";
const OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT: &str = "http://localhost:4317";

const OBSERVABILITY_SERVICE_NAME_ENV_VAR: &str = "OBSERVABILITY_SERVICE_NAME";
const OBSERVABILITY_SERVICE_NAME_DEFAULT: &str = "rusve-xyz-service";

#[tracing::instrument]
pub async fn configure_observability() -> std::result::Result<(), crate::error::Error> {
    let otel_exporter_endpoint =
        dotenvy::var(OTEL_EXPORTER_OTLP_ENDPOINT_ENV_VAR).unwrap_or_else(|_| {
            tracing::warn!(
                "{} Env var not set, using default",
                OTEL_EXPORTER_OTLP_ENDPOINT_ENV_VAR
            );
            OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT.to_string()
        });

    let observability_service_name = dotenvy::var(OBSERVABILITY_SERVICE_NAME_ENV_VAR)
        .unwrap_or_else(|_| OBSERVABILITY_SERVICE_NAME_DEFAULT.to_string());

    let tracer = opentelemetry_otlp::new_pipeline()
        .tracing()
        .with_exporter(
            opentelemetry_otlp::new_exporter()
                .tonic()
                .with_endpoint(otel_exporter_endpoint),
        )
        .with_trace_config(opentelemetry::sdk::trace::config().with_resource(
            opentelemetry::sdk::Resource::new(vec![opentelemetry::KeyValue::new(
                "service.name",
                observability_service_name.clone(),
            )]),
        ))
        .install_batch(opentelemetry::runtime::Tokio)?;

    // Create a tracing layer with the configured tracer
    let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);

    let filter = tracing_subscriber::EnvFilter::from_default_env();

    cfg_if::cfg_if! {
    if #[cfg(feature="bunyan")] {
            // Create a new formatting layer to print bunyan formatted logs to stdout, pipe into bunyan to view
            let formatting_layer = BunyanFormattingLayer::new(observability_service_name, std::io::stdout);
            let subscriber = tracing_subscriber::Registry::default()
                .with(filter)
                .with(telemetry_layer)
                .with(JsonStorageLayer)
                .with(formatting_layer);
    } else {
            let subscriber = tracing_subscriber::Registry::default()
            .with_filter(filter),
            .with_writer(std::io::stdout)
            .with(telemetry_layer);
        }
    }

    // Use the tracing subscriber `Registry`, or any other subscriber
    // that impls `LookupSpan`

    Ok(tracing::subscriber::set_global_default(subscriber)?)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions