This episode makes the application observable: Prometheus metrics, distributed tracing with OpenTelemetry, structured logs with slog, health check endpoints, and dashboard and alerting integration for latency, error rate, and throughput.

An application running in production is an application that must be observed. Observability unites three pillars: metrics for numbers, logs for details, and traces for flows. This episode connects all three to Echo with industry-standard tooling.
Episode 18 covers Prometheus metrics, tracing with OpenTelemetry, structured logs with slog, health check endpoints, and dashboard and alerting integration.
Prometheus collects metrics by pulling an HTTP endpoint. Echo makes this easy through echo-contrib:
go get github.com/labstack/echo-contrib/prometheusimport "github.com/labstack/echo-contrib/prometheus"
p := prometheus.NewPrometheus("echo", nil)
p.Use(e)
e.GET("/metrics", prometheus.Handler())Metrics like request count, latency, and status codes are now available at /metrics. The p.Use(e) call installs the metrics middleware, while prometheus.Handler() serves the scrape endpoint — Prometheus just needs to be configured to pull it on each interval.
scrape_configs:
- job_name: belajar-echo
static_configs:
- targets: ["api:8080"]OpenTelemetry gives you distributed traces — the journey of one request through many services. Echo instrumentation is available via the OTel contribution:
import "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
tp, err := initTracer(ctx)
e.Use(otelecho.Middleware("belajar-echo"))Every request now produces a span that carries propagation context. Spans can be linked to the database and HTTP calls inside them:
_, span := otel.Tracer("service").Start(c.Request().Context(), "processOrder")
defer span.End()Traces are sent to a backend like Jaeger or Tempo, then linked to logs through the same trace_id — an important bridge for end-to-end debugging.
In episode 11 you started logging a request_id. Integrate it with the OTel trace_id so logs and traces can be linked:
slog.Info("order diproses",
"order_id", order.ID,
"trace_id", span.SpanContext().TraceID().String(),
)One trace_id links logs, metrics, and traces for a single request — the key to fast debugging.
Infrastructure like Kubernetes performs health checks periodically. Provide dedicated endpoints:
e.GET("/health/live", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "alive"})
})
e.GET("/health/ready", func(c echo.Context) error {
if err := db.Ping(c.Request().Context()); err != nil {
return echo.NewHTTPError(http.StatusServiceUnavailable, "db tidak siap")
}
return c.JSON(http.StatusOK, map[string]string{"status": "ready"})
})/health/live indicates the process is alive; /health/ready indicates the dependencies are ready. Keep them separate so restarts and traffic routing don't hit the wrong target.
Data from Prometheus is visualized in Grafana. The metrics always monitored:
Alerting triggers actions when thresholds are exceeded:
groups:
- name: api
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{code=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) > 0.05
for: 10m
labels:
severity: critical
annotations:
summary: "Error rate di atas 5 persen selama 10 menit"This rule sounds the alarm when more than 5 percent of requests fail for 10 minutes.
Episode 18 makes your application truly observable: Prometheus metrics at /metrics, distributed tracing with OpenTelemetry and otecho, structured logs unified with trace_id, health checks split between live and ready, and dashboards and alerting for latency, error rate, and throughput.
Key takeaways:
echo-contrib/prometheus provides scrape-ready metrics.trace_id across logs, metrics, and traces./health/live and /health/ready.In episode 19 next, we'll discuss performance & troubleshooting — reducing allocations and reuse, connection pooling, Redis caching, HTTP server tuning, and troubleshooting route conflicts, binding errors, deadlocks, goroutine leaks, and the v4 to v5 migration.