Observability
Log, metric, আর trace — এই তিন স্তম্ভ আপনাকে বুঝতে দেয় production-এ আপনার Go service আসলে কী করছে।
গল্পে বুঝি
শহরের এক ডেলিভারি কোম্পানির কন্ট্রোল রুমে আল-খোয়ারিজমি আর ফাতিমা আল-ফিহরি সারাদিন একদেয়ালজোড়া স্ক্রিনের দিকে তাকিয়ে বসে থাকে। বাঁ দিকের স্ক্রিনে একটা লম্বা ইভেন্ট লগ গড়িয়ে গড়িয়ে নামছে — “৯টা ১২, ভ্যান-৭ পার্সেল তুলল ধানমন্ডি থেকে”, “৯টা ৩১, ভ্যান-৭ ড্রপ করল মিরপুর হাবে”, প্রতিটা লাইনে সময়, ভ্যান নম্বর, জায়গা — সব নির্দিষ্ট করে লেখা। মাঝের স্ক্রিনে কয়েকটা বড় গেজ ঘুরছে — এই মুহূর্তে রাস্তায় কত ভ্যান, গড় ডেলিভারি টাইম কত মিনিট, বহরের তেল কতটুকু বাকি। কোনো লাইন ঘেঁটে দেখতে হয় না, একনজরে গোটা বহরের হালচাল বোঝা যায়।
দুপুরে এক কাস্টমার ফোন করে বলল, “আমার পার্সেলটা কই? ট্র্যাকিং নম্বর TRK-8842।” ফাতিমা আল-ফিহরি ওই নম্বরটা টাইপ করতেই ডান দিকের স্ক্রিনে একটা GPS ব্রেডক্রাম্ব ট্রেইল ফুটে উঠল — ওই একটা পার্সেল ধানমন্ডি থেকে উঠে ভ্যান-৭-এ মিরপুর হাব, সেখানে ভ্যান-১২-এ হাতবদল হয়ে উত্তরার পথে — প্রতিটা হাতবদলে ঠিক কত সময় লাগল, কোথায় আধঘণ্টা আটকে ছিল, পুরোটা এক সুতোয় গাঁথা। এক ভ্যান থেকে আরেক ভ্যানে গেলেও ট্র্যাকিং নম্বরটা এক থাকে বলেই গোটা যাত্রাটা জোড়া দেওয়া যায়।
এই কন্ট্রোল রুমটাই আসলে observability-র তিন স্তম্ভ। গড়িয়ে নামা ইভেন্ট লগ হলো structured logging — কী ঘটল তার সময়-জায়গা-সহ নির্দিষ্ট রেকর্ড। গেজগুলো হলো metrics — গোটা সিস্টেম কেমন চলছে তার একনজরের সংখ্যা (rate, latency, saturation)। GPS ব্রেডক্রাম্ব ট্রেইল হলো distributed tracing — একটা request কোন কোন service ঘুরে গেল, প্রতিটা ধাপে কত সময় লাগল। আর ট্র্যাকিং নম্বরটাই correlation ID — যা দিয়ে ভিন্ন ভ্যানের (service-এর) বিচ্ছিন্ন log আর span-গুলো এক request-এ জোড়া লাগে। বাস্তবে Go service-এ ঠিক এই কাজটাই করে — metrics তোলে Prometheus, আর trace ছড়িয়ে দেয় OpenTelemetry, request ঢোকার সময় বসানো একটা correlation ID পুরো পথ ধরে বয়ে নিয়ে যায়।
তিন স্তম্ভ
Production service-এর তিন ধরনের observability data দরকার:
| স্তম্ভ | যে প্রশ্নের উত্তর দেয় | উদাহরণ |
|---|---|---|
| Logs | কী ঘটেছিল? | “User 42 login failed: invalid password” |
| Metrics | System কেমন perform করছে? | “p99 latency = 250ms, error rate = 0.5%” |
| Traces | একটা request কীভাবে flow করে? | “Request → API Gateway → User Service → Database (total: 180ms)” |
বাস্তব উদাহরণ
Log = সিকিউরিটি ক্যামেরার ফুটেজ। কিছু গড়বড় হলে আপনি সেগুলো দেখে বোঝেন। Metric = ড্যাশবোর্ডের গেজ (গতি, তেল, তাপমাত্রা)। সব ঠিক আছে কিনা জানতে আপনি একনজরে দেখেন। Trace = GPS ট্র্যাকিং। একটা প্যাকেজ ওয়্যারহাউস থেকে দরজা পর্যন্ত ঠিক কোন পথে গেল, প্রতিটা স্টপে কতটা সময় লাগল — সব দেখেন।
Structured Logging (slog)
// Production logging setup
func setupLogger(env string) *slog.Logger {
var handler slog.Handler
switch env {
case "production":
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
default:
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
}
return slog.New(handler)
}
// Request-scoped logger
func requestLogger(logger *slog.Logger, r *http.Request) *slog.Logger {
return logger.With(
"request_id", getRequestID(r.Context()),
"method", r.Method,
"path", r.URL.Path,
"remote_addr", r.RemoteAddr,
)
}
// Usage in handlers
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
log := requestLogger(h.logger, r)
log.Info("fetching user", "user_id", id)
user, err := h.service.GetByID(r.Context(), id)
if err != nil {
log.Error("failed to get user", "error", err, "user_id", id)
// ...
}
log.Info("user fetched successfully", "user_id", id)
} Prometheus Metrics
Go service-এর metric-এর জন্য Prometheus হলো standard:
import "github.com/prometheus/client_golang/prometheus"
import "github.com/prometheus/client_golang/prometheus/promhttp"
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5},
},
[]string{"method", "path"},
)
activeConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "active_connections",
Help: "Number of active connections",
},
)
dbQueryDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "db_query_duration_seconds",
Help: "Database query duration",
Buckets: prometheus.DefBuckets,
},
[]string{"query"},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
prometheus.MustRegister(activeConnections)
prometheus.MustRegister(dbQueryDuration)
} Metrics Middleware
func MetricsMiddleware() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(wrapped, r)
duration := time.Since(start).Seconds()
status := strconv.Itoa(wrapped.status)
httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, status).Inc()
httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
})
}
}
// Expose metrics endpoint
mux.Handle("GET /metrics", promhttp.Handler()) Custom Business Metric
var (
ordersPlaced = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "orders_placed_total",
Help: "Total orders placed",
},
[]string{"payment_method", "status"},
)
orderAmount = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "order_amount_dollars",
Help: "Order amounts in dollars",
Buckets: []float64{10, 25, 50, 100, 250, 500, 1000},
},
)
)
func (s *OrderService) PlaceOrder(ctx context.Context, order Order) error {
err := s.processOrder(ctx, order)
if err != nil {
ordersPlaced.WithLabelValues(order.PaymentMethod, "failed").Inc()
return err
}
ordersPlaced.WithLabelValues(order.PaymentMethod, "success").Inc()
orderAmount.Observe(order.Total)
return nil
} OpenTelemetry দিয়ে Distributed Tracing
Trace একটা request-কে একাধিক service জুড়ে follow করে:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func setupTracing(ctx context.Context, serviceName string) (func(), error) {
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("localhost:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("creating exporter: %w", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
)),
)
otel.SetTracerProvider(tp)
return func() { tp.Shutdown(ctx) }, nil
} আপনার Code Instrument করা
var tracer = otel.Tracer("bookstore")
func (s *BookService) GetByID(ctx context.Context, id int) (*Book, error) {
ctx, span := tracer.Start(ctx, "BookService.GetByID",
trace.WithAttributes(
attribute.Int("book.id", id),
),
)
defer span.End()
// Check cache
ctx, cacheSpan := tracer.Start(ctx, "cache.get")
book, err := s.cache.Get(ctx, fmt.Sprintf("book:%d", id))
cacheSpan.End()
if err == nil && book != nil {
span.SetAttributes(attribute.Bool("cache.hit", true))
return book, nil
}
// Query database
ctx, dbSpan := tracer.Start(ctx, "db.query",
trace.WithAttributes(
attribute.String("db.statement", "SELECT * FROM books WHERE id = $1"),
),
)
book, err = s.repo.GetByID(ctx, id)
dbSpan.End()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
return book, nil
} Health Check Dashboard
তিন স্তম্ভকে একসাথে করে একটা health overview বানান:
type HealthStatus struct {
Status string `json:"status"`
Version string `json:"version"`
Uptime string `json:"uptime"`
Checks map[string]Check `json:"checks"`
}
type Check struct {
Status string `json:"status"`
Duration string `json:"duration"`
Error string `json:"error,omitempty"`
}
func (h *HealthHandler) DetailedHealth(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
checks := make(map[string]Check)
// Database check
dbStart := time.Now()
if err := h.db.PingContext(ctx); err != nil {
checks["database"] = Check{Status: "unhealthy", Duration: time.Since(dbStart).String(), Error: err.Error()}
} else {
checks["database"] = Check{Status: "healthy", Duration: time.Since(dbStart).String()}
}
// Redis check
redisStart := time.Now()
if err := h.redis.Ping(ctx).Err(); err != nil {
checks["redis"] = Check{Status: "unhealthy", Duration: time.Since(redisStart).String(), Error: err.Error()}
} else {
checks["redis"] = Check{Status: "healthy", Duration: time.Since(redisStart).String()}
}
overall := "healthy"
for _, c := range checks {
if c.Status != "healthy" {
overall = "degraded"
break
}
}
status := http.StatusOK
if overall != "healthy" {
status = http.StatusServiceUnavailable
}
writeJSON(w, status, HealthStatus{
Status: overall,
Version: version,
Uptime: time.Since(startTime).String(),
Checks: checks,
})
} মূল কথা
slogদিয়ে structured logging — production-এ JSON, development-এ text- চার ধরনের metric: counter (যা শুধু বাড়ে), gauge (যা বাড়ে-কমে), histogram (distribution), summary (quantile)
- service-এর জন্য RED method: Rate (requests/sec), Errors (error rate), Duration (latency)
- resource-এর জন্য USE method: Utilization, Saturation, Errors (CPU, memory, connection-এর জন্য)
- Trace একটা request-কে service জুড়ে follow করে — প্রতিটা span হলো timing সহ একটা কাজের একক
- Prometheus-এর জন্য
/metricsexpose করুন, load balancer-এর জন্য/health, profiling-এর জন্য/debug/pprof - অতিরিক্ত instrument করবেন না — RED metric দিয়ে শুরু করুন, সমস্যা খুঁজতে গেলে নির্দিষ্ট metric যোগ করুন