Missive provides built-in support for monitoring email delivery through tracing (telemetry) and metrics.
Missive uses the tracing crate for structured logging and distributed tracing.
Every email delivery creates a span with relevant context:
missive.deliver { provider="resend", to=["user@example.com"], subject="Welcome!" }
├── DEBUG: Delivering email
└── INFO: Email delivered { message_id="abc123" }
Add a tracing subscriber to your application:
use tracing_subscriber;
fn main() {
// Simple console logging
tracing_subscriber::fmt::init();
// Now all missive operations are logged
}| Span | Level | Fields | Description |
|---|---|---|---|
missive.deliver |
INFO | provider, to, subject | Single email delivery |
missive.deliver_many |
INFO | provider, count | Batch delivery |
Span Fields:
provider- The mailer being used (resend, sendgrid, smtp, etc.)to- List of recipient email addressessubject- Email subject linecount- Number of emails (batch only)message_id- Provider's message ID (on success)
| Level | What's Logged |
|---|---|
| ERROR | Delivery failures with error details |
| INFO | Successful deliveries with message ID |
| DEBUG | Pre-delivery details |
Control verbosity with RUST_LOG:
# Only errors
RUST_LOG=missive=error cargo run
# Info and above (recommended)
RUST_LOG=missive=info cargo run
# Full debug output
RUST_LOG=missive=debug cargo run
# Combined with other crates
RUST_LOG=info,missive=debug cargo runFor production log aggregation (ELK, Loki, etc.):
[dependencies]
tracing-subscriber = { version = "0.3", features = ["json"] }fn main() {
tracing_subscriber::fmt()
.json()
.init();
}Output:
{"timestamp":"2024-01-15T10:30:00Z","level":"INFO","target":"missive","span":{"name":"missive.deliver","provider":"resend","to":["user@example.com"],"subject":"Welcome!"},"message":"Email delivered","message_id":"abc123"}Export traces to Jaeger, Zipkin, or other backends:
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-opentelemetry = "0.22"
opentelemetry = "0.21"
opentelemetry-jaeger = "0.20"use opentelemetry::global;
use tracing_subscriber::prelude::*;
fn main() {
// Initialize Jaeger exporter
let tracer = opentelemetry_jaeger::new_agent_pipeline()
.with_service_name("my-app")
.install_simple()
.expect("Failed to install Jaeger exporter");
// Create OpenTelemetry layer
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
// Combine with fmt layer
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(telemetry)
.init();
// Email deliveries now appear in Jaeger
}Wrap email operations in your own spans for additional context:
use tracing::{instrument, info_span};
#[instrument(skip(user), fields(user_id = %user.id))]
async fn send_welcome_email(user: &User) -> Result<(), missive::MailError> {
let email = Email::new()
.to(&user.email)
.subject("Welcome!");
missive::deliver(&email).await
}Result:
send_welcome_email { user_id=42 }
└── missive.deliver { provider="resend", to=["user@example.com"], subject="Welcome!" }
Missive can emit Prometheus-style metrics for dashboards and alerting.
Enable the metrics feature:
[dependencies]
missive = { version = "0.7.0", features = ["resend", "metrics"] }| Metric | Type | Labels | Description |
|---|---|---|---|
missive_emails_total |
Counter | provider, status |
Total emails sent |
missive_delivery_duration_seconds |
Histogram | provider |
Time to deliver email |
missive_batch_total |
Counter | provider, status |
Batch operations count |
missive_batch_size |
Histogram | provider |
Emails per batch |
Labels:
provider: The email provider used (resend,sendgrid,smtp, etc.)status: Eithersuccessorerror
Install a Prometheus recorder:
[dependencies]
metrics-exporter-prometheus = "0.16"use metrics_exporter_prometheus::PrometheusBuilder;
fn main() {
PrometheusBuilder::new()
.install()
.expect("failed to install Prometheus recorder");
}With Axum:
use axum::{Router, routing::get};
use metrics_exporter_prometheus::PrometheusBuilder;
#[tokio::main]
async fn main() {
let recorder = PrometheusBuilder::new()
.install_recorder()
.expect("failed to install Prometheus recorder");
let app = Router::new()
.route("/metrics", get(move || {
std::future::ready(recorder.render())
}));
// Start server...
}Example output:
# HELP missive_emails_total Total emails sent
# TYPE missive_emails_total counter
missive_emails_total{provider="resend",status="success"} 142
missive_emails_total{provider="resend",status="error"} 3
# HELP missive_delivery_duration_seconds Time to deliver email
# TYPE missive_delivery_duration_seconds histogram
missive_delivery_duration_seconds_bucket{provider="resend",le="0.1"} 95
missive_delivery_duration_seconds_bucket{provider="resend",le="0.5"} 140
missive_delivery_duration_seconds_bucket{provider="resend",le="+Inf"} 142
[dependencies]
metrics-exporter-statsd = "0.7"use metrics_exporter_statsd::StatsdBuilder;
fn main() {
StatsdBuilder::from("127.0.0.1", 8125)
.install()
.expect("failed to install StatsD recorder");
}Example PromQL queries:
# Emails sent per minute
rate(missive_emails_total[1m])
# Success rate
sum(rate(missive_emails_total{status="success"}[5m])) /
sum(rate(missive_emails_total[5m]))
# Average delivery time
rate(missive_delivery_duration_seconds_sum[5m]) /
rate(missive_delivery_duration_seconds_count[5m])
# 95th percentile delivery time
histogram_quantile(0.95, rate(missive_delivery_duration_seconds_bucket[5m]))
# Errors by provider
sum by (provider) (rate(missive_emails_total{status="error"}[5m]))
Example Prometheus alert rules:
# Alert on high error rate
- alert: EmailDeliveryErrors
expr: |
sum(rate(missive_emails_total{status="error"}[5m])) /
sum(rate(missive_emails_total[5m])) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "Email delivery error rate above 5%"
# Alert on slow delivery
- alert: SlowEmailDelivery
expr: |
histogram_quantile(0.95, rate(missive_delivery_duration_seconds_bucket[5m])) > 5
for: 10m
labels:
severity: warning
annotations:
summary: "95th percentile email delivery time above 5 seconds"- If you don't enable the
metricsfeature, metric calls are not compiled into your binary - If you enable
metricsbut don't install a recorder, metric calls are no-ops with negligible overhead
Combine tracing and metrics for full observability:
[dependencies]
missive = { version = "0.7.0", features = ["resend", "metrics"] }
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
metrics-exporter-prometheus = "0.16"use metrics_exporter_prometheus::PrometheusBuilder;
use tracing_subscriber::EnvFilter;
fn setup_observability() {
// JSON logging for ELK/Loki
tracing_subscriber::fmt()
.json()
.with_env_filter(EnvFilter::new("info,missive=info"))
.init();
// Prometheus metrics for Grafana
PrometheusBuilder::new()
.install()
.expect("failed to install metrics");
}Recommendations:
- Use INFO level - DEBUG is verbose, ERROR misses successful deliveries
- Enable JSON logging - Easier to parse in log aggregators
- Add OpenTelemetry - For distributed tracing across services
- Use both tracing and metrics - Tracing for debugging, metrics for alerting
For microservices, propagate trace context so email deliveries appear in the same trace as the originating request:
use tracing_opentelemetry::OpenTelemetrySpanExt;
use opentelemetry::propagation::Extractor;
async fn handle_request(headers: HeaderMap) -> Response {
// Extract trace context from incoming request
let parent_cx = global::get_text_map_propagator(|propagator| {
propagator.extract(&HeaderExtractor(&headers))
});
// Create span with parent context
let span = info_span!("handle_request");
span.set_parent(parent_cx);
// Email delivery inherits the trace context
async move {
missive::deliver(&email).await
}
.instrument(span)
.await
}Now your email deliveries appear in the same trace as the HTTP request that triggered them, providing end-to-end visibility across your system.