rskit-logging 0.2.0-alpha.2

Structured logging setup using tracing — JSON in production, pretty in dev
Documentation

rskit-logging

Production-ready structured logging built on the tracing ecosystem.

CI crates.io docs.rs License: MIT MSRV: 1.91

Features

  • Owns its configuration vocabulary — LoggingConfig / LogFormat / LogOutput are plain serde data with no tracing dependency
  • Structured JSON / pretty console output
  • Sensitive data masking (on by default)
  • Rate-based log sampling (burst + thereafter)
  • Per-module log level overrides via EnvFilter
  • OpenTelemetry Logs bridge (OTLP export, behind otlp feature flag)
  • Unified log schema (consistent across gokit, pykit, rskit)
  • Drop-based guard ensures all buffered logs are flushed on shutdown
  • RUST_LOG env-filter support

Feature flags

Feature Default Description
setup The subscriber-building layer: init_logging*, LoggingGuard, masking, sampling, per-module levels, and context helpers. Pulls in the tracing/tracing-subscriber stack.
otlp OpenTelemetry Logs bridge with OTLP export (implies setup).

Disable default features (default-features = false) to depend on only the tracing-free configuration vocabulary (LoggingConfig / LogFormat / LogOutput) — useful for configuration crates that compose the vocabulary without linking the subscriber stack.

Installation

[dependencies]
rskit-logging = "0.2.0-alpha.2"

# Vocabulary only — no tracing subscriber stack
rskit-logging = { version = "0.2.0-alpha.2", default-features = false }

# With OTLP export support
rskit-logging = { version = "0.2.0-alpha.2", features = ["otlp"] }

Quick Start

use rskit_logging::{LoggingResult, init_logging};
use rskit_logging::LoggingConfig;

fn main() -> LoggingResult<()> {
    let cfg = LoggingConfig::default();
    let _guard = init_logging(&cfg)?;
    // _guard must stay alive for the duration of the program

    tracing::info!(service = "my-svc", "server started");

    // Sensitive data is automatically redacted when using masking init
    // (see Masking section below)
    Ok(())
}

Configuration

rskit-logging owns the logging configuration vocabulary (LoggingConfig / LogFormat / LogOutput). These are plain serde types, so configuration crates such as rskit-config re-export and compose them without pulling in the subscriber stack. All logging options come from LoggingConfig:

logging:
  level: info           # trace | debug | info | warn | error
  format: json          # json | console

Init Functions

Function Description
init_logging(cfg) -> LoggingResult<LoggingGuard> Basic init from LoggingConfig
init_logging_env() Init from RUST_LOG only (no config needed)
init_logging_with_masking(cfg, masking_cfg) -> LoggingResult<LoggingGuard> Sensitive data masking
init_logging_with_options(cfg, sampling, module_levels, masking) -> LoggingResult<LoggingGuard> Sampling + module overrides
init_logging_full(setup) -> LoggingResult<LoggingGuard> Full init with all features (requires otlp feature)

Full Configuration Example

use std::collections::HashMap;
use rskit_logging::{
    init_logging_full,
    LoggingSetup, MaskingConfig, SamplingConfig,
};
use rskit_logging::otlp::OtlpConfig;  // requires "otlp" feature
use rskit_logging::LoggingConfig;

fn main() -> rskit_logging::LoggingResult<()> {
    let cfg = LoggingConfig {
        level: "info".into(),
        format: rskit_logging::LogFormat::Json,
        ..Default::default()
    };

    let sampling = SamplingConfig {
        enabled: true,
        initial_rate: 100,
        thereafter_rate: 10,
    };

    let mut module_levels = HashMap::new();
    module_levels.insert("sqlx".to_string(), "warn".to_string());
    module_levels.insert("rdkafka".to_string(), "off".to_string());

    let otlp = OtlpConfig {
        enabled: true,
        endpoint: "http://otel-collector:4317".to_string(),
        protocol: "grpc".to_string(),
        ..Default::default()
    };

    let masking = MaskingConfig {
        enabled: true,
        ..Default::default()
    };

    let setup = LoggingSetup::new(&cfg, "my-service", "production", "1.0.0")
        .with_sampling(&sampling)
        .with_module_levels(&module_levels)
        .with_masking(&masking)
        .with_otlp(&otlp);
    let _guard = init_logging_full(setup)?;

    tracing::info!(service = "my-service", "started");
    Ok(())
}

Masking

Masking is enabled by default in MaskingConfig. The DefaultMasker operates at the output layer via MaskingMakeWriter, redacting sensitive data from complete log lines before they reach any sink.

Setup

use rskit_logging::{init_logging_with_masking, MaskingConfig};
use rskit_logging::LoggingConfig;

let cfg = LoggingConfig::default();
let masking = MaskingConfig::default(); // enabled: true
let _guard = init_logging_with_masking(&cfg, &masking)?;

// Sensitive fields are now redacted in output
tracing::info!(password = "hunter2", "user login");
// output: password=[REDACTED]

Default Masked Fields

# Field Name Description
1 password User passwords
2 secret Generic secrets
3 token Generic tokens
4 api_key API keys
5 apikey API keys (alternate)
6 api-key API keys (hyphenated)
7 authorization Auth headers
8 auth_token Authentication tokens
9 access_token OAuth access tokens
10 refresh_token OAuth refresh tokens
11 private_key Private keys
12 ssn Social Security numbers
13 credit_card Credit card numbers
14 card_number Card numbers (alternate)
15 cvv Card verification values
16 pin Personal identification numbers

Value Patterns

These patterns detect sensitive data regardless of field name:

# Pattern Example Input Masked Output
1 Bearer token Bearer abc123def Bearer [REDACTED]
2 JWT eyJhbGci...payload...sig [JWT_REDACTED]
3 AWS Access Key AKIAIOSFODNN7EXAMPLE [AWS_KEY_REDACTED]
4 Credit Card 4111-1111-1111-1234 ****-****-****-1234
5 SSN 123-45-6789 ***-**-****
6 Email user@example.com ***@***.***
7 Hex Secret (32+) a1b2c3d4e5f6... (32+ hex chars) [HEX_REDACTED]

Adding Custom Fields and Patterns

let masking = MaskingConfig {
    enabled: true,
    field_names: vec!["my_internal_token".into(), "employee_id".into()],
    value_patterns: vec![r"MYSVC_[A-Za-z0-9]{32}".into()],
    replacement: "[REDACTED]".into(),
};
let _guard = init_logging_with_masking(&cfg, &masking)?;

Output-Level Masking

Unlike gokit and pykit (which mask at the field level), rskit masks at the output writer level. The MaskingMakeWriter wraps the underlying io::Write and applies both field-name regex patterns (matching JSON "field":"value" and text field=value formats) and value-pattern regexes to complete log lines. This ensures nothing leaks regardless of how fields are formatted.

use std::sync::Arc;
use rskit_logging::masking::{DefaultMasker, Masker, MaskingMakeWriter};

let masker: Arc<dyn Masker> = Arc::new(DefaultMasker::default());
let writer = MaskingMakeWriter::new(std::io::stdout, masker);

Sampling

Sampling reduces log volume in high-throughput services. When enabled, each log level gets an independent counter per one-second window:

  1. Burst — the first initial_rate events per second per level pass through unconditionally.
  2. Thereafter — after the burst, only every thereafter_rate-th event is kept.
use rskit_logging::SamplingConfig;

let sampling = SamplingConfig {
    enabled: true,
    initial_rate: 100,     // allow first 100/sec per level
    thereafter_rate: 10,   // then keep every 10th
};

When to use: Enable sampling on hot-path services producing thousands of log events per second. Leave disabled for low-volume services or during debugging.

The SamplingLayer implements tracing_subscriber::Layer and uses event_enabled() to drop excess events. Counters are protected by parking_lot::Mutex for minimal contention.

Module Levels

Override the global log level for specific modules using tracing_subscriber::EnvFilter directives. Useful for silencing noisy dependencies or enabling debug output for a single crate.

use std::collections::HashMap;
use rskit_logging::init_logging_with_options;
use rskit_logging::LoggingConfig;

let cfg = LoggingConfig::default();

let mut module_levels = HashMap::new();
module_levels.insert("sqlx".to_string(), "warn".to_string());
module_levels.insert("rdkafka".to_string(), "off".to_string());
module_levels.insert("hyper".to_string(), "error".to_string());

let _guard = init_logging_with_options(&cfg, None, Some(&module_levels), None)?;
// Generates filter: "info,hyper=error,rdkafka=off,sqlx=warn"

The build_env_filter() function merges the base level with per-module overrides into a single EnvFilter. When RUST_LOG is set, it takes precedence over config.

use rskit_logging::module_levels::build_env_filter;

let filter = build_env_filter("info", &module_levels);
// Equivalent to: RUST_LOG="info,sqlx=warn,rdkafka=off,hyper=error"

OTLP Export

The OpenTelemetry Logs bridge sends tracing events to an OTLP collector. It uses opentelemetry-appender-tracing to convert every tracing::Event into an OTel log record.

Feature gate: OTLP requires the otlp cargo feature.

[dependencies]
rskit-logging = { version = "0.2.0-alpha.2", features = ["otlp"] }

Setup

use rskit_logging::otlp::OtlpConfig;

let otlp = OtlpConfig {
    enabled: true,
    endpoint: "http://otel-collector:4317".to_string(),
    protocol: "grpc".to_string(),    // "grpc" | "http"
    headers: HashMap::new(),
};

Full Init with OTLP

let setup = rskit_logging::LoggingSetup::new(&cfg, "my-service", "production", "1.0.0")
    .with_sampling(&sampling)
    .with_module_levels(&module_levels)
    .with_otlp(&otlp);
let _guard = rskit_logging::init_logging_full(setup)?;

Subscriber Stack

When using init_logging_full, the subscriber layers are composed as:

  1. EnvFilter — base level + per-module overrides
  2. SamplingLayer (optional) — rate-based event sampling
  3. Format layer — JSON or console output
  4. OpenTelemetryTracingBridge (optional) — OTLP export

Graceful Shutdown

The LoggingGuard must be held for the lifetime of your service. When dropped, it restores the previous subscriber. When OTLP is enabled, the OtlpProvider::shutdown() method flushes pending records:

fn main() -> rskit_logging::LoggingResult<()> {
    let setup = rskit_logging::LoggingSetup::new(&cfg, "my-service", "production", "1.0.0")
        .with_otlp(&otlp);
    let _guard = rskit_logging::init_logging_full(setup)?;

    // ... application runs ...

    // _guard is dropped here → subscriber restored, OTLP flushed
    Ok(())
}

Unified Schema

All three kits (gokit, pykit, rskit) share the same structured field names, defined in rskit_logging::fields::names:

Field Constant Description
service fields::names::SERVICE Service name
environment fields::names::ENVIRONMENT Deployment environment
version fields::names::VERSION Service version
component fields::names::COMPONENT Logical component
trace_id fields::names::TRACE_ID Distributed trace ID
span_id fields::names::SPAN_ID Span ID within trace
correlation_id fields::names::CORRELATION_ID Cross-service correlation
user_id fields::names::USER_ID User identifier
request_id fields::names::REQUEST_ID HTTP request identifier
duration_ms fields::names::DURATION_MS Duration in milliseconds

Using Field Constants

use rskit_logging::fields::names::*;

tracing::info!(
    { SERVICE } = "order-svc",
    { ENVIRONMENT } = "production",
    { VERSION } = "1.2.3",
    { COMPONENT } = "checkout",
    "order placed"
);

Custom Masker

Implement the Masker trait to provide your own masking logic:

use std::borrow::Cow;
use rskit_logging::masking::Masker;

struct MyMasker;

impl Masker for MyMasker {
    fn mask_value<'v>(&self, key: &str, value: &'v str) -> Cow<'v, str> {
        if key == "internal_id" {
            Cow::Borrowed("***")
        } else {
            Cow::Borrowed(value)
        }
    }

    fn mask_output<'v>(&self, line: &'v str) -> Cow<'v, str> {
        // Apply value-level patterns to complete log lines
        Cow::Borrowed(line)
    }
}

Use with MaskingMakeWriter:

use std::sync::Arc;
use rskit_logging::masking::MaskingMakeWriter;

let masker: Arc<dyn Masker> = Arc::new(MyMasker);
let writer = MaskingMakeWriter::new(std::io::stdout, masker);

Convenience Re-exports

rskit-logging re-exports core tracing macros for convenience:

use rskit_logging::{info, warn, error, debug, trace, instrument};

#[instrument]
fn process_order(id: &str) {
    info!("processing order");
}

API Reference

Function / Type Description
init_logging(cfg) -> LoggingResult<LoggingGuard> Basic subscriber init
init_logging_env() Init from RUST_LOG only
init_logging_with_masking(cfg, masking) -> LoggingResult<LoggingGuard> Output masking
init_logging_with_options(cfg, sampling, modules, masking) -> LoggingResult<LoggingGuard> Sampling + module levels
init_logging_full(setup) -> LoggingResult<LoggingGuard> Full init with OTLP (otlp feature)
LoggingGuard Drop guard — hold for program lifetime
MaskingConfig Masking configuration
DefaultMasker Built-in masker with PII/secret patterns
Masker (trait) Interface for custom maskers
MaskingMakeWriter Writer wrapper that masks output
SamplingConfig Sampling configuration
SamplingLayer Tracing layer for rate-based sampling
ModuleLevelsConfig Per-module level overrides
build_env_filter(level, modules) Build EnvFilter from config
OtlpConfig OTLP export configuration (otlp feature)
OtlpProvider OTel LoggerProvider manager (otlp feature)

See Also