Meningkatkan aplikasi ke observability penuh: tracing dengan structured logs dan spans, TraceLayer untuk request tracing, metrics Prometheus, serta integrasi OpenTelemetry untuk ekosistem monitoring modern.

Aplikasi kalian berjalan — tapi apa yang terjadi di dalamnya saat produksi? Episode ini menjawab dengan observability: kemampuan memahami sistem dari luarnya lewat tiga pilar — logs (kejadian), metrics (angka), dan traces (perjalanan request). Di Rust, semua bermuara pada ekosistem tracing.
Mengapa tracing dan bukan println!? Karena println! adalah output, bukan observability. tracing memberi struktur — spans yang membungkus durasi request, fields yang bisa difilter, dan subscriber yang bisa menulis ke console, file, atau OpenTelemetry sekaligus. Ini upgrade terbesar yang bisa kalian lakukan untuk kemampuan debugging aplikasi Axum.
Dari episode 3 kita sudah memakai tracing_subscriber::fmt(). Mari naikkan level: log yang terstruktur (bisa di-parse mesin) dan diffilter per-module:
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
fn init_tracing() {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,axum_api=debug,tower_http=debug"));
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer().json())
.init();
}Dua keputusan penting:
EnvFilter — level log dikendalikan RUST_LOG saat runtime (sudah disiapkan di episode 16). Bisa menyetel debug untuk module tertentu tanpa restart..json() — output menjadi JSON lines; mudah diingest oleh log aggregator (Loki, CloudWatch, etc.). Untuk development, layer non-JSON lebih enak dibaca — pilih berdasarkan environment.Output contoh:
{"timestamp":"2026-08-16T10:00:00Z","level":"INFO","fields":{"message":"server berjalan di 127.0.0.1:3000"}}tracing memberi dua primitif: span (periode waktu, bisa nested) dan event (kejadian instan):
use tracing::{error, info, info_span};
pub async fn create_post(State(state): State<AppState>, Json(body): Json<NewPost>) -> AppResult<(StatusCode, Json<Post>)> {
let span = info_span!("create_post", title = %body.title, user_id = ?tracing::field::Empty);
let _guard = span.enter();
info!("validasi body dimulai");
body.validate().map_err(...)?;
let user_id = uuid::Uuid::new_v4();
span.record("user_id", &user_id.to_string());
match sqlx::query_as::<_, Post>(...).bind(&body.title).fetch_one(&state.db).await {
Ok(post) => {
info!(post_id = %post.id, "post dibuat");
Ok((StatusCode::CREATED, Json(post)))
}
Err(err) => {
error!(error = %err, "gagal membuat post");
Err(err.into())
}
}
}Poin penting span:
field::Empty → record), berguna untuk ID yang baru lahir di tengah request.%body.title menyimpan string; ?x menyimpan debug representation.Tip
Aturan praktis log: log pada transisi penting (mulai request, validasi selesai, query selesai, error), bukan setiap baris. Log yang terlalu banyak membuat noise dan menaikkan biaya storage — structured logs membantu, tetapi tetap disiplin dalam frekuensi.
Tidak perlu span manual untuk seluruh request — TraceLayer dari episode 8 sudah melakukannya. Kita tinggal menyesuaikan level agar tidak terlalu berisik:
use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
Router::new()
.route("/posts", get(list_posts).post(create_post))
.layer(
TraceLayer::new_for_http()
.make_span_with(
DefaultMakeSpan::new()
.include_headers(false)
.level(tracing::Level::INFO),
)
.on_response(
DefaultOnResponse::new()
.level(tracing::Level::INFO)
.latency_unit(tower_http::trace::LatencyUnit::Millis),
),
)Hasilnya tiap request menghasilkan span berisi method, uri, status, dan latency — dasar untuk menganalisis endpoint mana yang lambat.
Angka — bukan hanya kata-kata. metrics crate + metrics-exporter-prometheus mengekspos counter, gauge, dan histogram dalam format Prometheus:
cargo add metrics
cargo add metrics-exporter-prometheususe metrics_exporter_prometheus::PrometheusBuilder;
fn init_metrics() {
let builder = PrometheusBuilder::new();
builder.install().expect("gagal install metrics exporter");
}
fn register_metrics() {
metrics::describe_counter!("http_requests_total", "Total request masuk");
metrics::describe_histogram!("http_request_duration_seconds", "Durasi request");
}Middleware pencatat metrics — kombinasi from_fn dan tracing span latency:
use axum::{extract::Request, middleware::Next, response::Response};
use std::time::Instant;
async fn metrics_middleware(request: Request, next: Next) -> Response {
let start = Instant::now();
let method = request.method().clone();
let path = request.uri().path().to_string();
let response = next.run(request).await;
metrics::counter!("http_requests_total", "method" => method.to_string(), "status" => response.status().as_u16().to_string()).increment(1);
metrics::histogram!("http_request_duration_seconds").record(start.elapsed().as_secs_f64());
response
}Ekspos endpoint /metrics yang menghasilkan format Prometheus:
use axum::http::HeaderValue;
use metrics_exporter_prometheus::PrometheusHandle;
#[derive(Clone)]
struct MetricsHandle(PrometheusHandle);
async fn metrics_endpoint(axum::extract::State(handle): axum::extract::State<MetricsHandle>) -> axum::response::Response {
let body = handle.render();
let mut response = axum::response::Response::new(axum::body::Body::from(body));
if let Ok(v) = HeaderValue::from_static("text/plain; version=0.0.4") {
response.headers_mut().insert("content-type", v);
}
response
}Cek hasil di browser http://localhost:3000/metrics — kalian akan melihat http_requests_total dan histogram yang siap discrape Prometheus.
Warning
Path yang berubah-ubah (/posts/:id) membuat label cardinality metrics membengkak jika disimpan mentah. Normalisasi path ke bentuk :id sebelum dicatat (seperti middleware di atas yang memakai request.uri().path()). Metrics dengan ribuan label unik akan menghancurkan performa Prometheus.
Untuk ekosistem monitoring modern (Jaeger, Tempo, Datadog, New Relic), kalian butuh OpenTelemetry — standar terbuka untuk telemetry. tracing punya dukungan native via tracing-opentelemetry:
cargo add tracing-opentelemetry
cargo add opentelemetry
cargo add opentelemetry-otlpuse opentelemetry::trace::TracerProvider;
use opentelemetry_otlp::WithExportConfig;
use tracing_opentelemetry::OpenTelemetryLayer;
fn init_otel() -> Result<(), Box<dyn std::error::Error>> {
let provider = opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint("http://collector:4317")
.build()
.expect("gagal build OTLP exporter")
.start();
let tracer = provider.tracer("axum-api");
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer().json())
.with(OpenTelemetryLayer::new(tracer))
.init();
Ok(())
}Setelah ini, semua spans tracing otomatis diekspor ke collector OpenTelemetry — tanpa mengubah kode handler sama sekali. Itulah kekuatan tracing: lapisan observability adalah keputusan deployment, bukan perubahan aplikasi.
| Kebutuhan | Alat | Dipakai Sejak |
|---|---|---|
| Log console development | tracing_subscriber::fmt | Episode 3 |
| Structured JSON logs | tracing_subscriber::fmt().json() | Episode ini |
| Request tracing | TraceLayer | Episode 8 |
| Angka/metrik | metrics + Prometheus | Episode ini |
| Ekosistem monitoring terstandar | OpenTelemetry | Episode ini |
Prinsipnya: mulai dari tracing, tambah yang lain sesuai kebutuhan. Jangan pasang lima lapisan observability di hari pertama — bangun dari log, naik ke metrics saat butuh angka, dan OTel saat masuk ekosistem monitoring perusahaan.
Pada episode 17 ini aplikasi kalian "terlihat" dari luar:
EnvFilter berbasis RUST_LOG.tracing untuk korelasi log dalam satu request.TraceLayer mengukur method, uri, status, latency per request.http_requests_total, histogram durasi) di /metrics.Di episode 18 selanjutnya kita keraskan keamanan: security best practices — sanitasi input, perlindungan SQL injection, XSS/CSRF, security headers, dan hardening endpoint yang sudah kita bangun. Sampai jumpa di episode 18!