tutorial

  1. gRPC Logging Terstruktur dengan Zap

79. gRPC Logging Terstruktur dengan Zap

Logging merupakan aspek esensial dalam membangun microservices berbasis gRPC. Tanpa logging terstruktur, proses troubleshooting, monitoring, hingga alerting akan menjadi hal yang sangat menantang. Dalam praktik modern, engineer profesional beralih dari plain logging (misalnya dengan fmt.Println) menuju logging terstruktur menggunakan libraries seperti Uber Zap: sebuah high-performance, structured, leveled logging library untuk Golang.

Pada artikel ini saya akan membahas praktik terbaik implementasi structured logging dengan Zap pada layanan gRPC, dilengkapi contoh kode, diagram, hingga simulasi output log yang relevan untuk kebutuhan observabilitas produksi.


Mengapa Structured Logging?

Sebelum masuk ke implementasi, mari kita pahami kenapa structured logging itu krusial:

  • Pencarian Lebih Mudah: Data log dalam format JSON atau key-value mudah diproses oleh tools seperti ElasticSearch, Grafana Loki, atau Datadog.
  • Konteks Lebih Kaya: Kita bisa menambahkan metadata kontekstual seperti traceID, userID, status code; sangat membantu untuk tracing dan audit trail.
  • Query Log yang Powerful: Karakterisme structured log membuka jalan untuk filter-dan-aggregate berdasarkan fields tertentu tanpa harus parsing secara manual.

Konteks gRPC & Logging

Layanan gRPC, berbeda dengan HTTP konvensional, menangani request dalam bentuk protokol biner dan biasanya memanfaatkan context propagation secara lebih intensif. Maka error handling dan logging context sangat krusial.

Sayangnya, log yang tidak terstruktur tidak dapat menangkap detail penting seperti latency, request metadata, maupun status secara konsisten.


Memulai Zap pada Layanan gRPC

Mari kita beralih ke contoh implementasi. Kita akan membangun interceptor gRPC yang menggabungkan Zap Logger secara terstruktur ke setiap lifecycle request. Interceptor di gRPC mirip dengan middleware pada HTTP, mereka mengeksekusi fungsi sebelum dan sesudah request handler dijalankan.

Instalasi

go get go.uber.org/zap
go get google.golang.org/grpc

Setup Zap Logger

package main

import (
    "go.uber.org/zap"
)

func NewZapLogger() *zap.Logger {
    logger, err := zap.NewProduction() // Output: JSON, cocok untuk produksi
    if err != nil {
        panic(err)
    }
    return logger
}

Zap mendukung dua mode utama:

ModeKeterangan
ProductionOutput JSON, cocok untuk deployment
DevelopmentOutput friendly untuk local development

Zap Interceptor untuk gRPC

Berikut contoh implementasi unary interceptor yang melakukan structured logging pada setiap request:

package main

import (
    "context"
    "time"

    "go.uber.org/zap"
    "google.golang.org/grpc"
    "google.golang.org/grpc/status"
)

func UnaryZapLogger(logger *zap.Logger) grpc.UnaryServerInterceptor {
    return func(
        ctx context.Context,
        req interface{},
        info *grpc.UnaryServerInfo,
        handler grpc.UnaryHandler,
    ) (resp interface{}, err error) {

        start := time.Now()
        resp, err = handler(ctx, req)
        duration := time.Since(start)

        grpcStatus, _ := status.FromError(err)
        logger.Info("gRPC Request",
            zap.String("method", info.FullMethod),
            zap.Any("request", req),
            zap.Duration("latency_ms", duration),
            zap.String("code", grpcStatus.Code().String()),
            zap.Error(err),
        )

        return resp, err
    }
}

Penjelasan:

  • info.FullMethod: Nama method yang diakses (misal: /helloworld.Greeter/SayHello)
  • zap.Any("request", req): Melog request dalam bentuk serializable (hati-hati sensitive info)
  • zap.Duration("latency_ms", duration): Latensi proses
  • zap.String("code", grpcStatus.Code().String()): Status code dari gRPC

Integrasi Interceptor ke Server gRPC

func main() {
    logger := NewZapLogger()
    grpcServer := grpc.NewServer(
        grpc.UnaryInterceptor(UnaryZapLogger(logger)),
    )
    // Register service & listen...
}

Simulasi Output Log

Dengan setup di atas, log Anda akan terekam dengan output JSON rapi seperti ini:

{
  "level": "info",
  "ts": 1719491811.966,
  "caller": "main.go:24",
  "msg": "gRPC Request",
  "method": "/helloworld.Greeter/SayHello",
  "request": {"name": "Bob"},
  "latency_ms": 7.424,
  "code": "OK",
  "error": ""
}

Struktur log seperti tersebut sangat mudah dicari dan dianalisis di backend observabilitas.


Menambah Context Metadata (TraceID dsb)

gRPC Context dapat membawa metadata seperti traceID, userID, dsb yang dapat diteruskan ke log Zap.

Berikut contoh mengakses traceID dari context dan inject ke log:

func UnaryZapLogger(logger *zap.Logger) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{},
        info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

        // Simulasikan extract traceID dari context/metadata
        traceID, _ := ctx.Value("trace_id").(string)

        start := time.Now()
        resp, err := handler(ctx, req)
        duration := time.Since(start)
        grpcStatus, _ := status.FromError(err)
        logger.Info("gRPC Request",
            zap.String("trace_id", traceID),
            zap.String("method", info.FullMethod),
            zap.Duration("latency_ms", duration),
            zap.String("code", grpcStatus.Code().String()),
            zap.Error(err),
        )
        return resp, err
    }
}

Tips: Untuk otomatisasi, gunakan grpc-metadata atau library tracing seperti OpenTelemetry untuk inject context metadata.


Diagram Alur Logging dengan Interceptor (Mermaid)

sequenceDiagram
    participant Client as gRPC Client
    participant Server as gRPC Server
    participant Logger as Zap Logger

    Client->>Server: Send gRPC Request
    Server->>Logger: Log context, request, dsb
    Server->>Server: Handler process
    Server->>Logger: Log status, duration, error
    Server-->>Client: Return gRPC Response

Good Practices

  1. Jangan Log Sensitive Data
    Hindari melog data rahasia seperti password.

  2. Level Logging
    Gunakan logger.Error untuk error, logger.Info untuk event normal.

  3. Field Baku
    Biasakan log dengan field tetap: method, userID, code, duration.
    Berikut tabel contoh fields esensial yang disarankan:

    FieldFungsi
    methodNama API/method
    codeStatus code gRPC
    latency_msDurasi eksekusi (ms)
    trace_idTrace untuk tracing
    errorError detail (jika ada)
    user_idUser (jika user-facing)
  4. Integrasikan Logger di Context
    Untuk handler yang kompleks, pattern menyimpan logger pada context bisa diaplikasikan, sehingga tiap sub-fungsi bisa akses logger yang sama dengan kontekstual info.


Kesimpulan

Structured logging dengan Zap pada layanan gRPC memberikan kemudahan dalam hal monitoring, troubleshooting, dan post-mortem. Ketersediaan context-rich log akan memangkas waktu MTTR (Mean Time To Recovery) serta meningkatkan observabilitas secara signifikan.

Jika Anda serius membangun produk berbasis gRPC, jangan abaikan logging terstruktur—jadikan Zap dan interceptor sebagai default template proyek Anda.

Selamat bereksperimen dan jangan lupa refactor log Anda ke arah yang lebih terstruktur!


Referensi:

comments powered by Disqus