#open-telemetry #observability #mocking #otlp #testing-mocking

dev mock-collector

Mock OpenTelemetry OTLP collector server for testing

9 releases

Uses new Rust 2024

0.2.5 Dec 3, 2025
0.2.4 Dec 3, 2025
0.2.1 Nov 29, 2025
0.1.2 Nov 23, 2025

#123 in Testing

Download history 2/week @ 2025-11-22 80/week @ 2025-11-29 67/week @ 2025-12-06

149 downloads per month
Used in 4 crates

MIT license

120KB
2K SLoC

Mock Collector

Crates.io Documentation CI License

A mock OpenTelemetry OTLP collector server for testing applications that export telemetry data.

Features

  • Multiple Signal Support: Logs, Traces, and Metrics
  • Multiple Protocol Support: gRPC, HTTP/Protobuf, and HTTP/JSON
  • Single Collector: One collector handles all signals - test logs, traces, and metrics together
  • Fluent Assertion API: Easy-to-use builder pattern for test assertions
  • Flexible Matching: Match by body/name, attributes, resource attributes, and scope attributes
  • Severity Level Assertions: Assert on log severity levels (Debug, Info, Warn, Error, Fatal)
  • Count-Based Assertions: Assert exact counts, minimum, or maximum number of matches
  • Async-Ready: Built with Tokio for async/await compatibility
  • Graceful Shutdown: Proper resource cleanup with shutdown signals

Installation

Add to your Cargo.toml (check the badge above for the latest version):

[dev-dependencies]
mock-collector = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Quick Start

gRPC Server

use mock_collector::{MockServer, Protocol};

#[tokio::test]
async fn test_grpc_logging() {
    // Start a gRPC server on port 4317
    let server = MockServer::new(Protocol::Grpc, 4317)
        .start()
        .await
        .unwrap();

    // Your application exports logs here...

    // Assert logs were received
    server.with_collector(|collector| {
        collector
            .expect_log_with_body("Application started")
            .with_resource_attributes([("service.name", "my-service")])
            .assert_exists();
    }).await;

    // Graceful shutdown
    server.shutdown().await.unwrap();
}

HTTP/JSON Server

use mock_collector::{MockServer, Protocol};

#[tokio::test]
async fn test_http_json_logging() {
    let server = MockServer::new(Protocol::HttpJson, 4318)
        .start()
        .await
        .unwrap();

    // Your application exports logs to http://localhost:4318/v1/logs

    server.with_collector(|collector| {
        collector
            .expect_log_with_body("Request processed")
            .with_attributes([("http.status_code", "200")])
            .assert_exists();
    }).await;
}

HTTP/Protobuf Server

use mock_collector::{MockServer, Protocol};

#[tokio::test]
async fn test_http_binary_logging() {
    let server = MockServer::new(Protocol::HttpBinary, 4318)
        .start()
        .await
        .unwrap();

    // Your application exports logs to http://localhost:4318/v1/logs
    // with Content-Type: application/x-protobuf

    server.with_collector(|collector| {
        assert_eq!(collector.log_count(), 5);
    }).await;
}

Testing Traces

The same server automatically supports traces! Simply use the trace assertion API:

use mock_collector::{MockServer, Protocol};

#[tokio::test]
async fn test_traces() {
    // Start server with default settings (gRPC on OS-assigned port)
    let server = MockServer::builder().start().await.unwrap();

    // Your application exports traces to the server...
    // For gRPC: server.addr()
    // For HTTP: http://{server.addr()}/v1/traces

    server.with_collector(|collector| {
        // Assert on spans
        collector
            .expect_span_with_name("GET /api/users")
            .with_attributes([("http.method", "GET")])
            .with_resource_attributes([("service.name", "api-gateway")])
            .assert_exists();

        // Count assertions work too
        collector
            .expect_span_with_name("database.query")
            .assert_at_least(3);
    }).await;
}

Testing Metrics

The same server automatically supports metrics! Simply use the metric assertion API:

use mock_collector::{MockServer, Protocol};

#[tokio::test]
async fn test_metrics() {
    let server = MockServer::builder().start().await.unwrap();

    // Your application exports metrics to the server...
    // For gRPC: server.addr()
    // For HTTP: http://{server.addr()}/v1/metrics

    server.with_collector(|collector| {
        // Assert on metrics
        collector
            .expect_metric_with_name("http_requests_total")
            .with_attributes([("method", "GET")])
            .with_resource_attributes([("service.name", "api-gateway")])
            .assert_exists();

        // Count assertions work too
        collector
            .expect_metric_with_name("db_query_duration")
            .assert_at_least(1);
    }).await;
}

Testing All Signals Together

One collector handles all three signals simultaneously:

#[tokio::test]
async fn test_all_signals() {
    let server = MockServer::builder().start().await.unwrap();

    // Your app exports logs, traces, and metrics...

    server.with_collector(|collector| {
        // Verify all signals were collected
        assert_eq!(collector.log_count(), 10);
        assert_eq!(collector.span_count(), 15);
        assert_eq!(collector.metric_count(), 5);

        // Assert on logs
        collector
            .expect_log_with_body("Request received")
            .assert_exists();

        // Assert on traces
        collector
            .expect_span_with_name("handle_request")
            .assert_exists();

        // Assert on metrics
        collector
            .expect_metric_with_name("requests_total")
            .assert_exists();
    }).await;
}

Assertion API

Log Assertions

// Assert at least one log matches
collector.expect_log_with_body("error occurred").assert_exists();

// Assert no logs match (negative assertion)
collector.expect_log_with_body("password=secret").assert_not_exists();

// Assert exact count
collector.expect_log_with_body("retry attempt").assert_count(3);

// Assert minimum
collector.expect_log_with_body("cache hit").assert_at_least(10);

// Assert maximum
collector.expect_log_with_body("WARNING").assert_at_most(5);

// Assert on severity levels
use mock_collector::SeverityNumber;

collector
    .expect_log()
    .with_severity(SeverityNumber::Error)
    .assert_count(2);

collector
    .expect_log()
    .with_severity(SeverityNumber::Debug)
    .assert_exists();

// Combine severity with other criteria
collector
    .expect_log_with_body("Connection failed")
    .with_severity(SeverityNumber::Error)
    .with_resource_attributes([("service.name", "api")])
    .assert_exists();

Trace Assertions

Span assertions use the same fluent API:

// Assert at least one span matches
collector.expect_span_with_name("ProcessOrder").assert_exists();

// Assert no spans match (negative assertion)
collector.expect_span_with_name("deprecated.operation").assert_not_exists();

// Assert exact count
collector.expect_span_with_name("database.query").assert_count(5);

// Assert minimum
collector.expect_span_with_name("cache.lookup").assert_at_least(10);

// Assert maximum
collector.expect_span_with_name("external.api.call").assert_at_most(3);

Metric Assertions

Metric assertions use the same fluent API:

// Assert at least one metric matches
collector.expect_metric_with_name("http_requests_total").assert_exists();

// Assert no metrics match (negative assertion)
collector.expect_metric_with_name("deprecated_metric").assert_not_exists();

// Assert exact count
collector.expect_metric_with_name("db_connections").assert_count(1);

// Assert minimum
collector.expect_metric_with_name("cache_hits").assert_at_least(5);

// Assert maximum
collector.expect_metric_with_name("errors_total").assert_at_most(2);

Matching Criteria

All three signals (logs, spans, and metrics) support matching on attributes, resource attributes, and scope attributes:

// Logs
collector
    .expect_log_with_body("User login")
    .with_attributes([
        ("user.id", "12345"),
        ("auth.method", "oauth2"),
    ])
    .with_resource_attributes([
        ("service.name", "auth-service"),
        ("deployment.environment", "production"),
    ])
    .with_scope_attributes([
        ("scope.name", "user-authentication"),
    ])
    .assert_exists();

// Spans (same API!)
collector
    .expect_span_with_name("AuthenticateUser")
    .with_attributes([
        ("user.id", "12345"),
        ("auth.provider", "google"),
    ])
    .with_resource_attributes([
        ("service.name", "auth-service"),
    ])
    .with_scope_attributes([
        ("library.name", "auth-lib"),
    ])
    .assert_exists();

// Metrics (same API!)
collector
    .expect_metric_with_name("http_requests_total")
    .with_attributes([
        ("method", "POST"),
        ("status", "200"),
    ])
    .with_resource_attributes([
        ("service.name", "api-gateway"),
    ])
    .with_scope_attributes([
        ("meter.name", "http-metrics"),
    ])
    .assert_exists();

Inspection Methods

// Get counts
let log_count = collector.log_count();
let span_count = collector.span_count();
let metric_count = collector.metric_count();

// Get matching items
let log_assertion = collector.expect_log_with_body("error");
let matching_logs = log_assertion.get_all();
let log_match_count = log_assertion.count();

let span_assertion = collector.expect_span_with_name("database.query");
let matching_spans = span_assertion.get_all();
let span_match_count = span_assertion.count();

let metric_assertion = collector.expect_metric_with_name("requests_total");
let matching_metrics = metric_assertion.get_all();
let metric_match_count = metric_assertion.count();

// Clear all collected data (logs, spans, AND metrics)
collector.clear();

// Debug dump all data
println!("{}", collector.dump());

Sharing a Collector

You can share a collector between multiple servers or inspect logs without starting a server:

use std::sync::Arc;
use tokio::sync::RwLock;
use mock_collector::{MockCollector, MockServer, Protocol};

let collector = Arc::new(RwLock::new(MockCollector::new()));

// Start multiple servers with the same collector
let grpc_server = MockServer::with_collector(
    Protocol::Grpc,
    4317,
    collector.clone()
).start().await?;

let http_server = MockServer::with_collector(
    Protocol::HttpJson,
    4318,
    collector.clone()
).start().await?;

// Access the collector directly
let log_count = collector.read().await.log_count();

Examples

The examples/ directory contains complete working examples demonstrating various features:

  • basic_grpc.rs - Getting started with gRPC protocol

    • Starting a gRPC server
    • Sending log records
    • Basic assertion patterns
    • Graceful shutdown
  • http_protocols.rs - HTTP/JSON and HTTP/Protobuf protocols

    • Using HTTP/JSON endpoint
    • Using HTTP/Protobuf endpoint
    • Testing logs and traces
    • Protocol comparison
  • metrics.rs - Testing metrics collection

    • Sending metrics via gRPC
    • Asserting on metric names and attributes
    • All metric types (Sum, Gauge, Histogram)
    • Metric data point matching
  • severity_assertions.rs - Testing log severity levels

    • Asserting on log severity (Debug, Info, Warn, Error, Fatal)
    • Using SeverityNumber enum
    • Combining severity with other criteria
    • Counting logs by severity level
  • shared_collector.rs - Sharing collectors between servers

    • Using a shared collector Arc
    • Running multiple servers (gRPC and HTTP)
    • Direct collector access patterns
  • assertion_patterns.rs - Advanced assertion techniques

    • Count assertions (assert_count, assert_at_least, assert_at_most)
    • Negative assertions (assert_not_exists)
    • Combining multiple criteria
    • Testing all three signals (logs, traces, metrics)
  • debugging.rs - Debugging failed assertions

    • Using dump() to inspect collected data
    • Using count() and get_all() for inspection
    • Understanding assertion failures
    • Troubleshooting tips

Run any example with:

cargo run --example basic_grpc
cargo run --example metrics
# etc.

Comparison with fake-opentelemetry-collector

This library was inspired by fake-opentelemetry-collector but adds:

  • Full Signal Support: Test logs, traces, and metrics in the same collector
  • HTTP Protocol Support: Both JSON and Protobuf over HTTP, not just gRPC
  • Fluent Assertion API: Builder pattern for more readable tests
  • Count Assertions: assert_count(), assert_at_least(), assert_at_most()
  • Negative Assertions: assert_not_exists() for verifying data doesn't exist
  • Scope Attributes: Support for asserting on scope-level attributes
  • Better Error Messages: Detailed panic messages showing what was expected vs what was found
  • Arc-Optimised Storage: Efficient memory usage for resource/scope attributes
  • Builder Pattern: Simple defaults with MockServer::builder().start() or full control

License

Licensed under the MIT license.

Contributing

Contributions are welcome! Please feel free to open issues or submit pull requests.

Dependencies

~9–21MB
~238K SLoC