betterstack-tracing 0.1.0

A tracing layer for sending logs to Betterstack
Documentation
# betterstack-tracing

A [tracing](https://docs.rs/tracing) layer for sending logs to [Betterstack](https://betterstack.com).

This crate provides a Rust implementation inspired by the [slog-betterstack](https://github.com/samber/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`:

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

## Quick Start

```rust
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

```rust
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:

```rust
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

```rust
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:

```rust
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

```rust
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:

```json
{
  "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](https://betterstack.com/docs/logs/ingesting-data/http/).

## 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:

```rust
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](https://betterstack.com/docs/logs/ingesting-data/http/):

- **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:

```rust
layer.flush().await;
```

## License

Licensed under the MIT license.

## Acknowledgments

- Inspired by [slog-betterstack]https://github.com/samber/slog-betterstack by [@samber]https://github.com/samber
- Built for the [tracing]https://docs.rs/tracing ecosystem