| Crates.io | mock-collector |
| lib.rs | mock-collector |
| version | 0.2.7 |
| created_at | 2025-11-23 18:36:40.927694+00 |
| updated_at | 2025-12-30 12:55:49.929536+00 |
| description | Mock OpenTelemetry OTLP collector server for testing |
| homepage | https://github.com/djvcom/mock-collector |
| repository | https://github.com/djvcom/mock-collector |
| max_upload_size | |
| id | 1946845 |
| size | 285,550 |
A mock OpenTelemetry OTLP collector server for testing applications that export telemetry data.
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"] }
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();
}
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;
}
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;
}
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;
}
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;
}
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;
}
// 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();
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 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);
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();
// 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());
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();
The examples/ directory contains complete working examples:
basic_grpc.rs - Getting started with gRPC
wait_for_* methods for async datahttp_protocols.rs - HTTP/JSON and HTTP/Protobuf
/v1/logs, /v1/traces, /v1/metrics)metrics.rs - Metrics collection
assertion_patterns.rs - Comprehensive assertion API
assert_count, assert_at_least, assert_at_most)assert_not_exists)dump() for debuggingRun examples with:
just example basic_grpc
# or: cargo run --example basic_grpc
This library was inspired by fake-opentelemetry-collector but adds:
assert_count(), assert_at_least(), assert_at_most()assert_not_exists() for verifying data doesn't existMockServer::builder().start() or full controlA Nix flake provides a development shell with all required tools:
nix develop
Common tasks are available via just:
just # List all commands
just check # Run tests, clippy, and format check
just test # Run tests
just clippy # Run clippy
just fmt # Format code
just doc-open # Build and open documentation
Licensed under the MIT license.
Contributions are welcome! Please feel free to open issues or submit pull requests.