tracing-better-stack 0.1.0

A tracing-subscriber layer for Better Stack (Logtail) logging
Documentation
// Format-specific integration tests that verify serialization output

mod common;

use common::{BETTER_STACK_ACCEPTED_STATUS, BodyCaptureMatcher, mock_host};
use std::sync::Arc;
use std::time::Duration;
use tracing::{error, info, warn};
use tracing_better_stack::{BetterStackConfig, BetterStackLayer};
use tracing_subscriber::prelude::*;
use wiremock::matchers::{bearer_token, header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

#[cfg(feature = "json")]
#[tokio::test]
async fn test_json_serialization_format() {
    let mock_server = MockServer::start().await;
    let captured_bodies = Arc::new(tokio::sync::Mutex::new(Vec::new()));
    let body_matcher = BodyCaptureMatcher {
        captured: captured_bodies.clone(),
    };

    // Expect JSON content type
    Mock::given(method("POST"))
        .and(path("/"))
        .and(bearer_token("test-token"))
        .and(header("content-type", "application/json"))
        .and(body_matcher)
        .respond_with(ResponseTemplate::new(BETTER_STACK_ACCEPTED_STATUS))
        .expect(1)
        .mount(&mock_server)
        .await;

    let layer = BetterStackLayer::new(
        BetterStackConfig::builder(mock_host(&mock_server), "test-token")
            .batch_size(3)
            .batch_timeout(Duration::from_millis(100))
            .build(),
    );

    let subscriber = tracing_subscriber::registry().with(layer);

    tracing::subscriber::with_default(subscriber, || {
        info!("Test JSON message");
        warn!(field = "value", "Warning with field");
        error!("Error message");
    });

    // Wait for batch to be sent
    tokio::time::sleep(Duration::from_millis(200)).await;

    mock_server.verify().await;

    // Verify JSON format
    let bodies = captured_bodies.lock().await;
    assert_eq!(bodies.len(), 1, "Should have captured one batch");

    // Parse as JSON to verify format
    let body = &bodies[0];
    let json: serde_json::Value = serde_json::from_slice(body).expect("Body should be valid JSON");

    assert!(json.is_array(), "Body should be a JSON array");
    let array = json.as_array().unwrap();
    assert_eq!(array.len(), 3, "Should have 3 log entries");

    // Verify structure of first log entry
    assert!(array[0].get("timestamp").is_some());
    assert!(array[0].get("level").is_some());
    assert!(array[0].get("message").is_some());
}

#[cfg(feature = "message_pack")]
#[tokio::test]
async fn test_msgpack_serialization_format() {
    let mock_server = MockServer::start().await;
    let captured_bodies = Arc::new(tokio::sync::Mutex::new(Vec::new()));
    let body_matcher = BodyCaptureMatcher {
        captured: captured_bodies.clone(),
    };

    // Expect MessagePack content type
    Mock::given(method("POST"))
        .and(path("/"))
        .and(bearer_token("test-token"))
        .and(header("content-type", "application/msgpack"))
        .and(body_matcher)
        .respond_with(ResponseTemplate::new(BETTER_STACK_ACCEPTED_STATUS))
        .expect(1)
        .mount(&mock_server)
        .await;

    let layer = BetterStackLayer::new(
        BetterStackConfig::builder(mock_host(&mock_server), "test-token")
            .batch_size(3)
            .batch_timeout(Duration::from_millis(100))
            .build(),
    );

    let subscriber = tracing_subscriber::registry().with(layer);

    tracing::subscriber::with_default(subscriber, || {
        info!("Test MessagePack message");
        warn!(field = "value", "Warning with field");
        error!("Error message");
    });

    // Wait for batch to be sent
    tokio::time::sleep(Duration::from_millis(200)).await;

    mock_server.verify().await;

    // Verify MessagePack format
    let bodies = captured_bodies.lock().await;
    assert_eq!(bodies.len(), 1, "Should have captured one batch");

    // Parse as MessagePack to verify format (using named fields)
    let body = &bodies[0];
    let value: serde_json::Value =
        rmp_serde::from_slice(body).expect("Body should be valid MessagePack");

    assert!(value.is_array(), "Body should be an array");
    let array = value.as_array().unwrap();
    assert_eq!(array.len(), 3, "Should have 3 log entries");

    // Verify structure of first log entry
    assert!(array[0].get("timestamp").is_some());
    assert!(array[0].get("level").is_some());
    assert!(array[0].get("message").is_some());
}

#[cfg(feature = "json")]
#[tokio::test]
async fn test_json_structured_fields() {
    let mock_server = MockServer::start().await;
    let captured_bodies = Arc::new(tokio::sync::Mutex::new(Vec::new()));
    let body_matcher = BodyCaptureMatcher {
        captured: captured_bodies.clone(),
    };

    Mock::given(method("POST"))
        .and(path("/"))
        .and(bearer_token("test-token"))
        .and(header("content-type", "application/json"))
        .and(body_matcher)
        .respond_with(ResponseTemplate::new(BETTER_STACK_ACCEPTED_STATUS))
        .expect(1)
        .mount(&mock_server)
        .await;

    let layer = BetterStackLayer::new(
        BetterStackConfig::builder(mock_host(&mock_server), "test-token")
            .batch_size(2)
            .batch_timeout(Duration::from_millis(100))
            .build(),
    );

    let subscriber = tracing_subscriber::registry().with(layer);

    tracing::subscriber::with_default(subscriber, || {
        info!(user_id = 123, action = "login", "User logged in");
        error!(
            error_code = "DB_ERROR",
            retry_count = 3,
            "Database connection failed"
        );
    });

    tokio::time::sleep(Duration::from_millis(200)).await;
    mock_server.verify().await;

    let bodies = captured_bodies.lock().await;
    let body = &bodies[0];
    let json: serde_json::Value = serde_json::from_slice(body).unwrap();
    let array = json.as_array().unwrap();

    // Verify fields are properly serialized
    assert_eq!(array[0]["fields"]["user_id"], 123);
    assert_eq!(array[0]["fields"]["action"], "login");
    assert_eq!(array[1]["fields"]["error_code"], "DB_ERROR");
    assert_eq!(array[1]["fields"]["retry_count"], 3);
}

#[cfg(feature = "message_pack")]
#[tokio::test]
async fn test_msgpack_structured_fields() {
    let mock_server = MockServer::start().await;
    let captured_bodies = Arc::new(tokio::sync::Mutex::new(Vec::new()));
    let body_matcher = BodyCaptureMatcher {
        captured: captured_bodies.clone(),
    };

    Mock::given(method("POST"))
        .and(path("/"))
        .and(bearer_token("test-token"))
        .and(header("content-type", "application/msgpack"))
        .and(body_matcher)
        .respond_with(ResponseTemplate::new(BETTER_STACK_ACCEPTED_STATUS))
        .expect(1)
        .mount(&mock_server)
        .await;

    let layer = BetterStackLayer::new(
        BetterStackConfig::builder(mock_host(&mock_server), "test-token")
            .batch_size(2)
            .batch_timeout(Duration::from_millis(100))
            .build(),
    );

    let subscriber = tracing_subscriber::registry().with(layer);

    tracing::subscriber::with_default(subscriber, || {
        info!(user_id = 123, action = "login", "User logged in");
        error!(
            error_code = "DB_ERROR",
            retry_count = 3,
            "Database connection failed"
        );
    });

    tokio::time::sleep(Duration::from_millis(200)).await;
    mock_server.verify().await;

    let bodies = captured_bodies.lock().await;
    let body = &bodies[0];
    let value: serde_json::Value = rmp_serde::from_slice(body).unwrap();
    let array = value.as_array().unwrap();

    // Verify fields are properly serialized
    assert_eq!(array[0]["fields"]["user_id"], 123);
    assert_eq!(array[0]["fields"]["action"], "login");
    assert_eq!(array[1]["fields"]["error_code"], "DB_ERROR");
    assert_eq!(array[1]["fields"]["retry_count"], 3);
}