betterstack-tracing

Crates.iobetterstack-tracing
lib.rsbetterstack-tracing
version0.1.0
created_at2025-10-11 21:49:36.397402+00
updated_at2025-10-11 21:49:36.397402+00
descriptionA tracing layer for sending logs to Betterstack
homepage
repositoryhttps://github.com/gorilla-devs/betterstack-tracing
max_upload_size
id1878594
size136,939
Davide (blarfoon)

documentation

README

betterstack-tracing

A tracing layer for sending logs to Betterstack.

This crate provides a Rust implementation inspired by the slog-betterstack Go library, adapted for Rust's tracing ecosystem.

Features

  • Non-blocking async log sending - Logs are sent in the background without blocking your application
  • Automatic batching - Configurable size and time-based batching for efficient log delivery
  • Gzip compression - Automatically compresses batches for reduced bandwidth usage
  • Size limit validation - Enforces Betterstack API limits (1 MiB per log, 10 MiB per batch)
  • Span context tracking - Automatically includes parent span information for distributed tracing
  • Configurable error handling - Optional error callbacks for monitoring send failures
  • Connection pooling - Reuses HTTP connections for better performance
  • Type-safe configuration - Builder pattern with compile-time validation

Installation

Add this to your Cargo.toml:

[dependencies]
betterstack-tracing = "0.1"
tracing = "0.1"
tracing-subscriber = "0.3"
tokio = { version = "1", features = ["full"] }

Quick Start

use betterstack_tracing::BetterstackLayer;
use tracing_subscriber::prelude::*;

#[tokio::main]
async fn main() {
    // Create the Betterstack layer
    let config = BetterstackLayer::builder("your-betterstack-token")
        .build()
        .expect("failed to create config");

    let betterstack_layer = BetterstackLayer::new(config);

    // Initialize tracing with the Betterstack layer
    tracing_subscriber::registry()
        .with(betterstack_layer)
        .init();

    // Use tracing as normal
    tracing::info!("Application started");
    tracing::error!(error = "connection refused", "Failed to connect");

    // Give logs time to be sent before exiting
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}

Configuration

Builder Options

use std::time::Duration;

let config = BetterstackLayer::builder("your-token")
    // Optional: Custom endpoint (default: https://in.logs.betterstack.com/)
    .endpoint("https://in.logs.betterstack.com/")

    // Optional: HTTP request timeout (default: 10s)
    .timeout(Duration::from_secs(10))

    // Optional: Batch size (default: 10 logs)
    .batch_size(10)

    // Optional: Batch delay (default: 2s)
    .batch_delay(Duration::from_secs(2))

    // Optional: Channel capacity (default: 1000)
    .channel_capacity(1000)

    // Optional: Include span context (default: true)
    .include_span_context(true)

    // Optional: Custom logger name (default: "tracing-betterstack")
    .logger_name("my-app")

    // Optional: Custom logger version (default: crate version)
    .logger_version("1.0.0")

    // Optional: Error callback
    .on_error(|error| {
        eprintln!("Betterstack error: {}", error);
    })
    .build()
    .expect("failed to create config");

let layer = BetterstackLayer::new(config);

Batching Strategy

Logs are automatically batched and sent when either:

  • The batch reaches batch_size logs, OR
  • batch_delay time has elapsed since the last send

This provides a good balance between latency and throughput.

Span Context

When include_span_context is enabled (default), the layer automatically captures the current span hierarchy and includes it in the log payload:

let span = tracing::info_span!("http_request", request_id = "123");
let _enter = span.enter();

tracing::info!("Processing request");
// This log will include the "http_request" span context

Examples

Basic Usage

use betterstack_tracing::BetterstackLayer;
use tracing_subscriber::prelude::*;

#[tokio::main]
async fn main() {
    let token = std::env::var("BETTERSTACK_TOKEN")
        .expect("BETTERSTACK_TOKEN must be set");

    let config = BetterstackLayer::builder(token)
        .build()
        .expect("failed to create config");

    let betterstack_layer = BetterstackLayer::new(config);

    tracing_subscriber::registry()
        .with(betterstack_layer)
        .init();

    tracing::info!("Hello, Betterstack!");

    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}

With Console Output

Combine with the fmt layer to log to both console and Betterstack:

use betterstack_tracing::BetterstackLayer;
use tracing_subscriber::prelude::*;

#[tokio::main]
async fn main() {
    let config = BetterstackLayer::builder("your-token")
        .build()
        .expect("failed to create config");

    let betterstack_layer = BetterstackLayer::new(config);

    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer())  // Console output
        .with(betterstack_layer)                  // Betterstack
        .init();

    tracing::info!("This goes to both console and Betterstack");

    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}

With Spans

use betterstack_tracing::BetterstackLayer;
use tracing_subscriber::prelude::*;

#[tokio::main]
async fn main() {
    let config = BetterstackLayer::builder("your-token")
        .include_span_context(true)
        .build()
        .expect("failed to create config");

    let betterstack_layer = BetterstackLayer::new(config);

    tracing_subscriber::registry()
        .with(betterstack_layer)
        .init();

    let span = tracing::info_span!("http_request",
        request_id = "req-123",
        method = "POST"
    );

    let _enter = span.enter();
    tracing::info!("Handling request");
    // The log will include span context with request_id and method

    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}

See the examples/ directory for more complete examples.

Payload Format

Logs are sent to Betterstack in the following JSON format:

{
  "dt": "2025-10-11T12:34:56.789Z",
  "level": "INFO",
  "message": "log message",
  "logger.name": "betterstack-tracing",
  "logger.version": "0.1.0",
  "target": "my_app::module",
  "file": "src/main.rs",
  "line": 42,
  "custom_field": "value",
  "spans": [
    {
      "name": "http_request",
      "fields": {
        "request_id": "req-123"
      }
    }
  ]
}

All custom fields and span information are included at the root level of the JSON object, not nested under an "extra" key, in compliance with the Betterstack logs API.

Performance

  • Non-blocking: Log calls return immediately, sending happens in background
  • Batched: Reduces HTTP overhead by sending multiple logs per request
  • Backpressure: Bounded channel prevents memory growth under load
  • Connection pooling: Reuses HTTP connections for better performance

If the log channel is full, new logs are dropped to prevent blocking your application.

Error Handling

By default, send errors are silently ignored. You can provide an error callback:

let config = BetterstackLayer::builder("your-token")
    .on_error(|error| {
        eprintln!("Failed to send logs to Betterstack: {}", error);
        // Or send to metrics, etc.
    })
    .build()
    .expect("failed to create config");

Size Limits

The crate automatically enforces Betterstack API size limits:

  • Individual logs: Maximum 1 MiB per log record
    • Logs exceeding this limit are dropped with a warning
    • Logs exceeding 100 KiB generate a debug message
  • Batches: Maximum 10 MiB uncompressed per batch
    • Batches are automatically compressed with gzip (typically 3-4x compression ratio)
    • Oversized batches result in an error

These limits help prevent rejected requests and ensure reliable log delivery.

Graceful Shutdown

The layer automatically flushes pending logs when dropped, with a 5-second timeout. For explicit control:

layer.flush().await;

License

Licensed under the MIT license.

Acknowledgments

Commit count: 0

cargo fmt