nym-bin-common 1.21.1

Common code for nym binaries
Documentation
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0

use serde::{Deserialize, Serialize};
use std::io::IsTerminal;

// Re-export tracing_subscriber for consumers that need to compose layers
#[cfg(feature = "basic_tracing")]
pub use tracing_subscriber;

#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct LoggingSettings {
    // well, we need to implement something here at some point...
}

// don't call init so that we could attach additional layers
#[cfg(feature = "basic_tracing")]
pub fn build_tracing_logger() -> impl tracing_subscriber::layer::SubscriberExt {
    use tracing_subscriber::prelude::*;

    tracing_subscriber::registry()
        .with(default_tracing_fmt_layer(std::io::stderr))
        .with(default_tracing_env_filter())
}

#[cfg(feature = "basic_tracing")]
pub fn default_tracing_env_filter() -> tracing_subscriber::filter::EnvFilter {
    if ::std::env::var("RUST_LOG").is_ok() {
        tracing_subscriber::filter::EnvFilter::from_default_env()
    } else {
        // if the env value was not found, default to `INFO` level rather than `ERROR`
        tracing_subscriber::filter::EnvFilter::builder()
            .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
            .parse_lossy("")
    }
}

#[cfg(feature = "basic_tracing")]
pub fn default_tracing_fmt_layer<S, W>(
    writer: W,
) -> impl tracing_subscriber::Layer<S> + Sync + Send + 'static
where
    S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
    W: for<'writer> tracing_subscriber::fmt::MakeWriter<'writer> + Sync + Send + 'static,
{
    tracing_subscriber::fmt::layer()
        .with_writer(writer)
        // Use a more compact, abbreviated log format
        .compact()
        // Display source code file paths
        .with_file(true)
        // Display source code line numbers
        .with_line_number(true)
        // Don't display the event's target (module path)
        .with_target(false)
}

#[cfg(feature = "basic_tracing")]
pub fn setup_tracing_logger() {
    use tracing_subscriber::util::SubscriberInitExt;
    build_tracing_logger().init()
}

/// Initialize an OpenTelemetry tracing layer that exports spans via OTLP/gRPC.
///
/// This produces a layer compatible with `tracing_subscriber::registry()` that
/// sends traces to any OTLP-compatible collector (SigNoz, Grafana Tempo, etc).
///
/// Returns both the tracing layer and the [`SdkTracerProvider`] so the caller
/// can invoke [`SdkTracerProvider::shutdown`] for graceful flush on exit.
///
/// # Arguments
/// * `service_name` - The service name reported to the collector (e.g. "nym-node")
/// * `endpoint` - The OTLP/gRPC collector endpoint (e.g. "http://localhost:4317"
///   or "https://ingest.eu.signoz.cloud:443" for SigNoz Cloud)
/// * `ingestion_key` - Optional SigNoz Cloud ingestion key. When provided, it is
///   sent as the `signoz-ingestion-key` gRPC metadata header on every export.
/// * `environment` - Deployment environment label (e.g. "sandbox", "mainnet", "canary").
///   Attached as the `deployment.environment` OTel resource attribute.
/// * `sample_ratio` - Trace sampling ratio in 0.0..=1.0 (e.g. 0.1 = 10% of traces).
///   Used to limit cost when exporting from many nodes; clamped to [0.0, 1.0].
/// * `export_timeout_secs` - Timeout in seconds for each OTLP export batch. Prevents
///   unbounded blocking if the collector is slow or unreachable.
#[cfg(feature = "otel-otlp")]
pub fn init_otel_layer<S>(
    service_name: &str,
    endpoint: &str,
    ingestion_key: Option<&str>,
    environment: &str,
    sample_ratio: f64,
    export_timeout_secs: u64,
) -> Result<
    (
        tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>,
        opentelemetry_sdk::trace::SdkTracerProvider,
    ),
    Box<dyn std::error::Error + Send + Sync>,
>
where
    S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
    use opentelemetry::trace::TracerProvider as _;
    use opentelemetry_otlp::WithExportConfig;
    use opentelemetry_otlp::WithTonicConfig;
    use opentelemetry_sdk::trace::Sampler;
    use std::time::Duration;

    // Validate endpoint URI early to fail with a clear message
    if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
        return Err(format!(
            "invalid OTLP endpoint URI: {endpoint} (must start with http:// or https://)"
        )
        .into());
    }

    let sample_ratio_clamped = sample_ratio.clamp(0.0, 1.0);

    let mut builder = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .with_endpoint(endpoint)
        .with_timeout(Duration::from_secs(export_timeout_secs));

    // Explicitly configure TLS when the endpoint uses HTTPS
    if endpoint.starts_with("https://") {
        builder =
            builder.with_tls_config(tonic::transport::ClientTlsConfig::new().with_native_roots());
    }

    if let Some(key) = ingestion_key {
        let mut metadata = tonic::metadata::MetadataMap::new();
        metadata.insert(
            "signoz-ingestion-key",
            key.parse()
                .map_err(|_| "invalid ingestion key format (value redacted)")?,
        );
        builder = builder.with_metadata(metadata);
    }

    let exporter = builder
        .build()
        .map_err(|e| format!("failed to build OTLP exporter for endpoint {endpoint}: {e}"))?;

    let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
        .with_sampler(Sampler::TraceIdRatioBased(sample_ratio_clamped))
        .with_batch_exporter(exporter)
        .with_resource(
            opentelemetry_sdk::Resource::builder()
                .with_service_name(service_name.to_owned())
                .with_attribute(opentelemetry::KeyValue::new(
                    "deployment.environment",
                    environment.to_owned(),
                ))
                .build(),
        )
        .build();

    opentelemetry::global::set_tracer_provider(tracer_provider.clone());
    let tracer = tracer_provider.tracer(service_name.to_owned());

    Ok((
        tracing_opentelemetry::layer().with_tracer(tracer),
        tracer_provider,
    ))
}

pub fn banner(crate_name: &str, crate_version: &str) -> String {
    format!(
        r#"

      _ __  _   _ _ __ ___
     | '_ \| | | | '_ \ _ \
     | | | | |_| | | | | | |
     |_| |_|\__, |_| |_| |_|
            |___/

             ({crate_name} - version {crate_version})

    "#
    )
}

pub fn maybe_print_banner(crate_name: &str, crate_version: &str) {
    if std::io::stdout().is_terminal() {
        println!("{}", banner(crate_name, crate_version))
    }
}