Observability ও Service Mesh
OpenTelemetry দিয়ে distributed tracing, Prometheus metrics এবং correlation ID সহ structured logging বানান।
গল্পে বুঝি
শহরের বড় হাসপাতালে ইবনে সিনা ভর্তি হয়েছে জরুরি বিভাগে। তার হাতে একটা ব্যান্ড, তাতে লেখা একটাই patient ID। এই ID দিয়েই তার সব কিছু বাঁধা। ওয়ার্ডের নার্স ফাতিমা আল-ফিহরি প্রতিবার কিছু হলে খাতায় লিখে রাখে — কখন ওষুধ দেওয়া হলো, কখন ইবনে সিনা বমি করল, কখন ডাক্তার এসে দেখে গেল। এগুলো আলাদা আলাদা ঘটনা, প্রতিটার সাথে সময় আর সেই patient ID লেখা। এদিকে ইবনে সিনার বিছানার পাশে একটা মনিটর সারাক্ষণ pulse, temperature, oxygen-এর সংখ্যা দেখিয়ে যাচ্ছে — প্রতি সেকেন্ডে সংখ্যাগুলো বদলাচ্ছে, আর কোনোটা বিপদসীমা পার হলেই অ্যালার্ম বেজে ওঠে।
ইবনে সিনার অবস্থা খারাপ হওয়ায় তাকে জরুরি বিভাগ থেকে ICU, তারপর অপারেশন থিয়েটার, শেষে কেবিনে সরানো হলো। প্রতিটা ওয়ার্ডে ঢোকা-বের হওয়ার সময়, কে দেখল, কতক্ষণ থাকল — সব একটা case-file-এ গোছানো, যেটা দেখলে ইবনে সিনার পুরো যাত্রাটা এক নজরে বোঝা যায়, কোন ধাপে বেশি সময় গেল তাও ধরা যায়। আর এই সব বিভাগের মধ্যে খবর নিরাপদে পৌঁছে দেয় হাসপাতালের ইন্টারকম আর ওয়ার্ড-বয় ব্যবস্থা — রিপোর্ট এক তলা থেকে আরেক তলায় যায়, লাইন ব্যস্ত থাকলে আবার চেষ্টা করে, আর কে কখন কী পাঠাল তার হিসাবও রাখে; ডাক্তারদের নিজেদের এসব নিয়ে ভাবতে হয় না।
এই গল্পটাই আসলে observability। নার্সের খাতার আলাদা আলাদা নোট হলো logs, মনিটরের চলমান সংখ্যাগুলো হলো metrics, ইবনে সিনার ওয়ার্ড-টু-ওয়ার্ড case-file হলো traces, আর সবকিছু একসূত্রে বাঁধা এই একটা patient ID-ই হলো correlation ID। বিভাগে-বিভাগে নির্ভরযোগ্যভাবে খবর পৌঁছে দেওয়া ইন্টারকম ব্যবস্থাটা হলো service mesh। বাস্তবে ঠিক এভাবেই Prometheus metrics জমায়, OpenTelemetry logs-traces এক correlation ID-তে বাঁধে, আর Istio-র মতো service mesh সার্ভিসগুলোর মধ্যে যোগাযোগ, retry আর telemetry সামলায়।
Observability-র তিনটি স্তম্ভ
সার্ভারে SSH করে log file পড়ে আপনি একটি distributed system debug করতে পারবেন না। আপনার দরকার তিনটি পরিপূরক signal:
- Logs — কী ঘটেছে (correlation ID সহ structured JSON)
- Metrics — কতটুকু (request count, latency percentile, error rate)
- Traces — কতক্ষণ আর কোথায় (service-জুড়ে end-to-end request flow)
বাস্তব জীবনের উদাহরণ
একটি হাসপাতালের monitoring সিস্টেমের মতো — heart rate monitor vitals track করে (metrics), নার্সরা observation লিখে রাখে (logs), আর patient chart care journey-টা trace করে (traces)। Vitals নেমে গেলে alarm বেজে ওঠে।
Production Observability সেটআপ
import http from 'node:http';
// --- Structured Logger ---
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogEntry {
level: LogLevel;
msg: string;
timestamp: string;
service: string;
traceId?: string;
spanId?: string;
requestId?: string;
[key: string]: unknown;
}
class Logger {
constructor(private service: string) {}
private log(level: LogLevel, msg: string, fields: Record<string, unknown> = {}): void {
const entry: LogEntry = {
level,
msg,
timestamp: new Date().toISOString(),
service: this.service,
...fields
};
// In production: write to stdout, collected by Fluentd/Vector
console.log(JSON.stringify(entry));
}
debug(msg: string, fields?: Record<string, unknown>) {
this.log('debug', msg, fields);
}
info(msg: string, fields?: Record<string, unknown>) {
this.log('info', msg, fields);
}
warn(msg: string, fields?: Record<string, unknown>) {
this.log('warn', msg, fields);
}
error(msg: string, fields?: Record<string, unknown>) {
this.log('error', msg, fields);
}
// Create child logger with additional context
child(fields: Record<string, unknown>): ChildLogger {
return new ChildLogger(this, fields);
}
}
class ChildLogger {
constructor(
private parent: Logger,
private fields: Record<string, unknown>
) {}
info(msg: string, extra?: Record<string, unknown>) {
this.parent.info(msg, { ...this.fields, ...extra });
}
error(msg: string, extra?: Record<string, unknown>) {
this.parent.error(msg, { ...this.fields, ...extra });
}
warn(msg: string, extra?: Record<string, unknown>) {
this.parent.warn(msg, { ...this.fields, ...extra });
}
}
// --- Metrics (Prometheus-compatible) ---
class Counter {
private values = new Map<string, number>();
constructor(
private name: string,
private help: string
) {}
inc(labels: Record<string, string> = {}, value = 1): void {
const key = this.labelKey(labels);
this.values.set(key, (this.values.get(key) || 0) + value);
}
private labelKey(labels: Record<string, string>): string {
return Object.entries(labels)
.sort()
.map(([k, v]) => `${k}="${v}"`)
.join(',');
}
serialize(): string {
let out = `# HELP ${this.name} ${this.help}\n# TYPE ${this.name} counter\n`;
for (const [labels, value] of this.values) {
const labelStr = labels ? `{${labels}}` : '';
out += `${this.name}${labelStr} ${value}\n`;
}
return out;
}
}
class Histogram {
private observations = new Map<string, number[]>();
private buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
constructor(
private name: string,
private help: string
) {}
observe(labels: Record<string, string>, value: number): void {
const key = this.labelKey(labels);
const obs = this.observations.get(key) || [];
obs.push(value);
this.observations.set(key, obs);
}
private labelKey(labels: Record<string, string>): string {
return Object.entries(labels)
.sort()
.map(([k, v]) => `${k}="${v}"`)
.join(',');
}
serialize(): string {
let out = `# HELP ${this.name} ${this.help}\n# TYPE ${this.name} histogram\n`;
for (const [labels, values] of this.observations) {
const sorted = [...values].sort((a, b) => a - b);
const sum = values.reduce((a, b) => a + b, 0);
for (const bucket of this.buckets) {
const count = sorted.filter((v) => v <= bucket).length;
const labelStr = labels ? `${labels},` : '';
out += `${this.name}_bucket{${labelStr}le="${bucket}"} ${count}\n`;
}
const labelStr = labels ? `${labels},` : '';
out += `${this.name}_bucket{${labelStr}le="+Inf"} ${values.length}\n`;
out += `${this.name}_sum{${labels}} ${sum}\n`;
out += `${this.name}_count{${labels}} ${values.length}\n`;
}
return out;
}
}
// --- Distributed Tracing ---
interface Span {
traceId: string;
spanId: string;
parentSpanId?: string;
operationName: string;
serviceName: string;
startTime: number;
duration?: number;
status: 'ok' | 'error';
attributes: Record<string, string | number>;
events: { name: string; timestamp: number; attributes?: Record<string, string> }[];
}
function generateId(): string {
return crypto.randomUUID().replace(/-/g, '').slice(0, 16);
}
class Tracer {
private spans: Span[] = [];
constructor(private serviceName: string) {}
startSpan(name: string, parentSpan?: Span): Span {
const span: Span = {
traceId: parentSpan?.traceId || generateId() + generateId(),
spanId: generateId(),
parentSpanId: parentSpan?.spanId,
operationName: name,
serviceName: this.serviceName,
startTime: performance.now(),
status: 'ok',
attributes: {},
events: []
};
return span;
}
endSpan(span: Span): void {
span.duration = performance.now() - span.startTime;
this.spans.push(span);
// In production: export to Jaeger/Tempo via OTLP
console.log(
JSON.stringify({
type: 'span',
traceId: span.traceId,
spanId: span.spanId,
parent: span.parentSpanId,
operation: span.operationName,
duration_ms: span.duration.toFixed(2),
status: span.status,
attributes: span.attributes
})
);
}
}
// --- Metrics Registry ---
const httpRequestsTotal = new Counter('http_requests_total', 'Total number of HTTP requests');
const httpRequestDuration = new Histogram(
'http_request_duration_seconds',
'HTTP request duration in seconds'
);
const httpRequestsInFlight = { value: 0 };
// --- Middleware ---
const logger = new Logger('api-gateway');
const tracer = new Tracer('api-gateway');
function metricsEndpoint(_req: http.IncomingMessage, res: http.ServerResponse): void {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(httpRequestsTotal.serialize() + httpRequestDuration.serialize());
}
async function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
// Extract or create trace context
const incomingTraceId = req.headers['x-trace-id'] as string | undefined;
const requestId = (req.headers['x-request-id'] as string) || crypto.randomUUID();
const span = tracer.startSpan(`${req.method} ${req.url}`);
if (incomingTraceId) {
(span as any).traceId = incomingTraceId;
}
span.attributes['http.method'] = req.method || 'GET';
span.attributes['http.url'] = req.url || '/';
span.attributes['http.request_id'] = requestId;
const reqLogger = logger.child({
traceId: span.traceId,
spanId: span.spanId,
requestId,
method: req.method,
path: req.url
});
const startTime = performance.now();
httpRequestsInFlight.value++;
reqLogger.info('request_started');
try {
// Simulate downstream service call
const dbSpan = tracer.startSpan('db.query', span);
dbSpan.attributes['db.system'] = 'postgresql';
dbSpan.attributes['db.statement'] = 'SELECT * FROM users WHERE id = $1';
await new Promise((r) => setTimeout(r, Math.random() * 50));
tracer.endSpan(dbSpan);
// Response
const statusCode = 200;
res.writeHead(statusCode, {
'Content-Type': 'application/json',
'X-Trace-Id': span.traceId,
'X-Request-Id': requestId
});
res.end(JSON.stringify({ status: 'ok', traceId: span.traceId }));
span.attributes['http.status_code'] = statusCode;
span.status = 'ok';
const duration = (performance.now() - startTime) / 1000;
httpRequestsTotal.inc({
method: req.method || 'GET',
status: String(statusCode),
path: req.url || '/'
});
httpRequestDuration.observe({ method: req.method || 'GET', path: req.url || '/' }, duration);
reqLogger.info('request_completed', { statusCode, duration_ms: (duration * 1000).toFixed(2) });
} catch (err) {
span.status = 'error';
span.events.push({ name: 'exception', timestamp: performance.now() });
const statusCode = 500;
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal error' }));
httpRequestsTotal.inc({ method: req.method || 'GET', status: '500', path: req.url || '/' });
reqLogger.error('request_failed', { error: String(err) });
} finally {
httpRequestsInFlight.value--;
tracer.endSpan(span);
}
}
// --- Server ---
const server = http.createServer((req, res) => {
if (req.url === '/metrics') return metricsEndpoint(req, res);
if (req.url === '/health') {
res.writeHead(200);
res.end('ok');
return;
}
handleRequest(req, res);
});
server.listen(3000, () => logger.info('server_started', { port: 3000 }));package main
import (
"context"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
)
// --- Structured Logger ---
type Logger struct {
service string
}
type LogEntry struct {
Level string `json:"level"`
Msg string `json:"msg"`
Timestamp string `json:"timestamp"`
Service string `json:"service"`
Fields map[string]interface{} `json:"fields,omitempty"`
}
func NewLogger(service string) *Logger {
return &Logger{service: service}
}
func (l *Logger) log(level, msg string, fields map[string]interface{}) {
entry := LogEntry{
Level: level,
Msg: msg,
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
Service: l.service,
Fields: fields,
}
data, _ := json.Marshal(entry)
fmt.Fprintln(os.Stdout, string(data))
}
func (l *Logger) Info(msg string, fields map[string]interface{}) { l.log("info", msg, fields) }
func (l *Logger) Error(msg string, fields map[string]interface{}) { l.log("error", msg, fields) }
func (l *Logger) Warn(msg string, fields map[string]interface{}) { l.log("warn", msg, fields) }
func (l *Logger) With(fields map[string]interface{}) *ContextLogger {
return &ContextLogger{parent: l, fields: fields}
}
type ContextLogger struct {
parent *Logger
fields map[string]interface{}
}
func (cl *ContextLogger) Info(msg string, extra map[string]interface{}) {
merged := make(map[string]interface{})
for k, v := range cl.fields {
merged[k] = v
}
for k, v := range extra {
merged[k] = v
}
cl.parent.Info(msg, merged)
}
func (cl *ContextLogger) Error(msg string, extra map[string]interface{}) {
merged := make(map[string]interface{})
for k, v := range cl.fields {
merged[k] = v
}
for k, v := range extra {
merged[k] = v
}
cl.parent.Error(msg, merged)
}
// --- Metrics ---
type Counter struct {
name string
help string
mu sync.Mutex
values map[string]int64
}
func NewCounter(name, help string) *Counter {
return &Counter{name: name, help: help, values: make(map[string]int64)}
}
func (c *Counter) Inc(labels map[string]string) {
c.mu.Lock()
defer c.mu.Unlock()
key := labelKey(labels)
c.values[key]++
}
func (c *Counter) Serialize() string {
c.mu.Lock()
defer c.mu.Unlock()
var sb strings.Builder
fmt.Fprintf(&sb, "# HELP %s %s\n# TYPE %s counter\n", c.name, c.help, c.name)
for labels, value := range c.values {
labelStr := ""
if labels != "" {
labelStr = "{" + labels + "}"
}
fmt.Fprintf(&sb, "%s%s %d\n", c.name, labelStr, value)
}
return sb.String()
}
type Histogram struct {
name string
help string
mu sync.Mutex
observations map[string][]float64
buckets []float64
}
func NewHistogram(name, help string) *Histogram {
return &Histogram{
name: name, help: help,
observations: make(map[string][]float64),
buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
}
}
func (h *Histogram) Observe(labels map[string]string, value float64) {
h.mu.Lock()
defer h.mu.Unlock()
key := labelKey(labels)
h.observations[key] = append(h.observations[key], value)
}
func (h *Histogram) Serialize() string {
h.mu.Lock()
defer h.mu.Unlock()
var sb strings.Builder
fmt.Fprintf(&sb, "# HELP %s %s\n# TYPE %s histogram\n", h.name, h.help, h.name)
for labels, values := range h.observations {
sorted := make([]float64, len(values))
copy(sorted, values)
sort.Float64s(sorted)
var sum float64
for _, v := range values {
sum += v
}
for _, bucket := range h.buckets {
count := 0
for _, v := range sorted {
if v <= bucket {
count++
}
}
fmt.Fprintf(&sb, "%s_bucket{%sle=\"%g\"} %d\n", h.name, labels+",", bucket, count)
}
fmt.Fprintf(&sb, "%s_bucket{%sle=\"+Inf\"} %d\n", h.name, labels+",", len(values))
fmt.Fprintf(&sb, "%s_sum{%s} %g\n", h.name, labels, sum)
fmt.Fprintf(&sb, "%s_count{%s} %d\n", h.name, labels, len(values))
}
return sb.String()
}
func labelKey(labels map[string]string) string {
keys := make([]string, 0, len(labels))
for k := range labels {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, fmt.Sprintf(`%s="%s"`, k, labels[k]))
}
return strings.Join(parts, ",")
}
// --- Tracing ---
type Span struct {
TraceID string `json:"traceId"`
SpanID string `json:"spanId"`
ParentSpanID string `json:"parentSpanId,omitempty"`
OperationName string `json:"operationName"`
ServiceName string `json:"serviceName"`
StartTime time.Time `json:"startTime"`
Duration time.Duration `json:"duration,omitempty"`
Status string `json:"status"`
Attributes map[string]string `json:"attributes"`
}
type Tracer struct {
serviceName string
}
func NewTracer(service string) *Tracer {
return &Tracer{serviceName: service}
}
func (t *Tracer) StartSpan(name string, parent *Span) *Span {
traceID := uuid.New().String()[:16] + uuid.New().String()[:16]
if parent != nil {
traceID = parent.TraceID
}
parentID := ""
if parent != nil {
parentID = parent.SpanID
}
return &Span{
TraceID: traceID, SpanID: uuid.New().String()[:16],
ParentSpanID: parentID, OperationName: name,
ServiceName: t.serviceName, StartTime: time.Now(),
Status: "ok", Attributes: make(map[string]string),
}
}
func (t *Tracer) EndSpan(span *Span) {
span.Duration = time.Since(span.StartTime)
data, _ := json.Marshal(map[string]interface{}{
"type": "span", "traceId": span.TraceID, "spanId": span.SpanID,
"operation": span.OperationName, "duration_ms": span.Duration.Milliseconds(),
"status": span.Status, "attributes": span.Attributes,
})
fmt.Fprintln(os.Stdout, string(data))
}
// --- Middleware ---
var (
logger = NewLogger("api-gateway")
tracer = NewTracer("api-gateway")
httpRequestsTotal = NewCounter("http_requests_total", "Total HTTP requests")
httpRequestDuration = NewHistogram("http_request_duration_seconds", "Request duration")
httpRequestsInFlight atomic.Int64
)
type contextKey string
func observabilityMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-Id")
if requestID == "" {
requestID = uuid.New().String()
}
span := tracer.StartSpan(fmt.Sprintf("%s %s", r.Method, r.URL.Path), nil)
if traceID := r.Header.Get("X-Trace-Id"); traceID != "" {
span.TraceID = traceID
}
span.Attributes["http.method"] = r.Method
span.Attributes["http.url"] = r.URL.Path
reqLogger := logger.With(map[string]interface{}{
"traceId": span.TraceID, "spanId": span.SpanID,
"requestId": requestID, "method": r.Method, "path": r.URL.Path,
})
start := time.Now()
httpRequestsInFlight.Add(1)
reqLogger.Info("request_started", nil)
// Wrap response writer to capture status code
rw := &responseWriter{ResponseWriter: w, statusCode: 200}
ctx := context.WithValue(r.Context(), contextKey("span"), span)
ctx = context.WithValue(ctx, contextKey("requestId"), requestID)
w.Header().Set("X-Trace-Id", span.TraceID)
w.Header().Set("X-Request-Id", requestID)
next.ServeHTTP(rw, r.WithContext(ctx))
duration := time.Since(start).Seconds()
httpRequestsInFlight.Add(-1)
status := fmt.Sprintf("%d", rw.statusCode)
httpRequestsTotal.Inc(map[string]string{"method": r.Method, "status": status, "path": r.URL.Path})
httpRequestDuration.Observe(map[string]string{"method": r.Method, "path": r.URL.Path}, duration)
span.Attributes["http.status_code"] = status
if rw.statusCode >= 500 {
span.Status = "error"
}
tracer.EndSpan(span)
reqLogger.Info("request_completed", map[string]interface{}{
"statusCode": rw.statusCode, "duration_ms": duration * 1000,
})
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/metrics", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprint(w, httpRequestsTotal.Serialize()+httpRequestDuration.Serialize())
})
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("ok"))
})
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
// Simulate DB query with child span
parentSpan := r.Context().Value(contextKey("span")).(*Span)
dbSpan := tracer.StartSpan("db.query", parentSpan)
dbSpan.Attributes["db.system"] = "postgresql"
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
tracer.EndSpan(dbSpan)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
handler := observabilityMiddleware(mux)
log.Println("Server with observability on :3000")
log.Fatal(http.ListenAndServe(":3000", handler))
}মূল কথা
- সবসময় structured JSON logging ব্যবহার করুন — grep-friendly text log স্কেল করে না
- Correlation ID (trace ID, request ID) দিয়ে আপনি সব service-জুড়ে একটি request follow করতে পারেন
- Prometheus format-এ একটি
/metricsendpoint expose করুন — এটাই industry standard - Downstream call-এর (DB query, HTTP call) জন্য child span বানান, যাতে দেখা যায় সময় কোথায় খরচ হচ্ছে
- Observability-তে fail open করুন — tracing/metrics ব্যর্থ হলে request block করবেন না
বাস্তব ব্যবহার
- Uber ৪,০০০+ microservice-জুড়ে distributed tracing-এর জন্য Jaeger (এখন CNCF) বানিয়েছে
- Netflix তাদের service mesh-জুড়ে latency debug করতে distributed tracing ব্যবহার করে
- Datadog, Grafana, New Relic সবাই OpenTelemetry data নেয় — OTEL দিয়ে instrument করলে আপনি vendor-neutral থাকেন
- প্রথম দিন থেকেই observability যোগ করুন। বিদ্যমান সিস্টেমে পরে tracing বসানো ১০ গুণ কঠিন।