LogTide
Language
Medium

Go and Gin Framework Logging Integration

Send structured logs from Go applications to LogTide using slog, zerolog, or zap with Gin middleware support.

slog integration (Go 1.21+) Gin middleware Zero-allocation logging Context propagation

Go’s standard library now includes slog for structured logging. This guide shows you how to integrate Go applications with LogTide using slog, zerolog, or zap, with Gin middleware for HTTP request logging.

Why use LogTide with Go?

  • Zero-allocation: LogTide’s Go SDK uses zero-allocation patterns for high performance
  • slog native: First-class support for Go 1.21+ slog
  • Context propagation: Trace requests across goroutines
  • Async batching: Non-blocking log shipping
  • Type safety: Strongly typed log fields

Prerequisites

  • Go 1.21+ (for slog) or Go 1.18+ (for zerolog/zap)
  • LogTide instance with API key

Installation

go get github.com/logtide/logtide-go

Quick Start with slog

Basic Setup

package main

import (
    "context"
    "log/slog"
    "os"

    "github.com/logtide/logtide-go"
)

func main() {
    // Create LogTide handler
    handler, err := logtide.NewHandler(logtide.Config{
        APIURL: os.Getenv("LOGTIDE_API_URL"),
        APIKey: os.Getenv("LOGTIDE_API_KEY"),
        Service: "my-service",
        Environment: os.Getenv("ENVIRONMENT"),
    })
    if err != nil {
        panic(err)
    }
    defer handler.Close()

    // Set as default logger
    logger := slog.New(handler)
    slog.SetDefault(logger)

    // Log with structured data
    slog.Info("Application started",
        slog.String("version", "1.0.0"),
        slog.Int("port", 8080),
    )

    slog.Error("Failed to connect",
        slog.String("host", "db.example.com"),
        slog.Any("error", err),
    )
}

Configuration Options

handler, err := logtide.NewHandler(logtide.Config{
    // Required
    APIURL: os.Getenv("LOGTIDE_API_URL"),
    APIKey: os.Getenv("LOGTIDE_API_KEY"),

    // Service identification
    Service:     "api-server",
    Environment: "production",
    Version:     "1.2.3",

    // Batching
    BatchSize:     100,           // Flush after N logs
    FlushInterval: 5 * time.Second,

    // Reliability
    MaxRetries:   3,
    RetryBackoff: time.Second,

    // Performance
    AsyncMode:  true,             // Non-blocking (recommended)
    BufferSize: 10000,            // Channel buffer size

    // Filtering
    MinLevel: slog.LevelInfo,     // Minimum log level
})

Gin Middleware

Setup

package main

import (
    "log/slog"
    "net/http"
    "os"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/logtide/logtide-go"
    "github.com/logtide/logtide-go/ginlog"
)

func main() {
    // Initialize LogTide
    handler, _ := logtide.NewHandler(logtide.Config{
        APIURL:  os.Getenv("LOGTIDE_API_URL"),
        APIKey:  os.Getenv("LOGTIDE_API_KEY"),
        Service: "api",
    })
    defer handler.Close()

    logger := slog.New(handler)
    slog.SetDefault(logger)

    // Create Gin router
    r := gin.New()

    // Add LogTide middleware
    r.Use(ginlog.Middleware(ginlog.Config{
        Logger:          logger,
        SkipPaths:       []string{"/health", "/ready", "/metrics"},
        LogRequestBody:  false,
        LogResponseBody: false,
    }))

    // Recovery middleware with logging
    r.Use(ginlog.Recovery(logger))

    // Routes
    r.GET("/users/:id", getUser)
    r.POST("/orders", createOrder)

    r.Run(":8080")
}

Request Logging Output

{
  "time": "2025-01-31T10:00:00.000Z",
  "level": "INFO",
  "msg": "HTTP GET /users/123",
  "service": "api",
  "method": "GET",
  "path": "/users/123",
  "status": 200,
  "latency_ms": 12,
  "client_ip": "192.168.1.1",
  "user_agent": "Mozilla/5.0...",
  "request_id": "abc123",
  "bytes_out": 256
}

Request Context

Access the logger with request context:

func getUser(c *gin.Context) {
    // Get logger with request context
    logger := ginlog.Logger(c)

    userID := c.Param("id")

    logger.Info("Fetching user",
        slog.String("user_id", userID),
    )

    user, err := db.GetUser(userID)
    if err != nil {
        logger.Error("Failed to fetch user",
            slog.String("user_id", userID),
            slog.Any("error", err),
        )
        c.JSON(500, gin.H{"error": "internal error"})
        return
    }

    logger.Info("User fetched successfully",
        slog.String("user_id", userID),
        slog.String("email", user.Email),
    )

    c.JSON(200, user)
}

Zerolog Integration

For projects using zerolog:

package main

import (
    "os"
    "time"

    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
    "github.com/logtide/logtide-go/zerolog"
)

func main() {
    // Create LogTide writer
    writer, err := logtidezerolog.NewWriter(logtidezerolog.Config{
        APIURL:  os.Getenv("LOGTIDE_API_URL"),
        APIKey:  os.Getenv("LOGTIDE_API_KEY"),
        Service: "my-service",
    })
    if err != nil {
        panic(err)
    }
    defer writer.Close()

    // Configure zerolog
    log.Logger = zerolog.New(writer).
        With().
        Timestamp().
        Str("environment", os.Getenv("ENVIRONMENT")).
        Logger()

    // Use zerolog as normal
    log.Info().
        Str("user_id", "123").
        Msg("User logged in")

    log.Error().
        Err(err).
        Str("order_id", "456").
        Msg("Payment failed")
}

Zap Integration

For projects using zap:

package main

import (
    "os"

    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "github.com/logtide/logtide-go/zaplog"
)

func main() {
    // Create LogTide core
    core, err := zaplog.NewCore(zaplog.Config{
        APIURL:  os.Getenv("LOGTIDE_API_URL"),
        APIKey:  os.Getenv("LOGTIDE_API_KEY"),
        Service: "my-service",
    })
    if err != nil {
        panic(err)
    }

    // Create logger with LogTide core
    logger := zap.New(core,
        zap.AddCaller(),
        zap.AddStacktrace(zapcore.ErrorLevel),
    )
    defer logger.Sync()

    // Use zap as normal
    logger.Info("Application started",
        zap.String("version", "1.0.0"),
        zap.Int("port", 8080),
    )

    logger.Error("Database connection failed",
        zap.Error(err),
        zap.String("host", "db.example.com"),
    )
}

Context Propagation

Adding Context to Requests

package main

import (
    "context"
    "log/slog"
)

// Context key for request-scoped logger
type loggerKey struct{}

// WithLogger adds a logger to the context
func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
    return context.WithValue(ctx, loggerKey{}, logger)
}

// LoggerFromContext retrieves the logger from context
func LoggerFromContext(ctx context.Context) *slog.Logger {
    if logger, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
        return logger
    }
    return slog.Default()
}

// Middleware that adds request context
func RequestContextMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        requestID := c.GetHeader("X-Request-ID")
        if requestID == "" {
            requestID = uuid.New().String()
        }

        // Create logger with request context
        logger := slog.Default().With(
            slog.String("request_id", requestID),
            slog.String("path", c.Request.URL.Path),
            slog.String("method", c.Request.Method),
        )

        // Add to context
        ctx := WithLogger(c.Request.Context(), logger)
        c.Request = c.Request.WithContext(ctx)

        // Set response header
        c.Header("X-Request-ID", requestID)

        c.Next()
    }
}

Using in Services

type UserService struct{}

func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
    logger := LoggerFromContext(ctx)

    logger.Info("Fetching user from database", slog.String("id", id))

    user, err := db.GetUser(ctx, id)
    if err != nil {
        logger.Error("Database query failed",
            slog.String("id", id),
            slog.Any("error", err),
        )
        return nil, err
    }

    logger.Info("User fetched", slog.String("id", id))
    return user, nil
}

Error Handling

Structured Error Logging

func processOrder(ctx context.Context, orderID string) error {
    logger := LoggerFromContext(ctx)

    order, err := db.GetOrder(ctx, orderID)
    if err != nil {
        logger.Error("Failed to fetch order",
            slog.String("order_id", orderID),
            slog.Any("error", err),
        )
        return fmt.Errorf("fetch order: %w", err)
    }

    if err := paymentService.Charge(ctx, order); err != nil {
        // Log with full error context
        var paymentErr *PaymentError
        if errors.As(err, &paymentErr) {
            logger.Error("Payment failed",
                slog.String("order_id", orderID),
                slog.String("error_code", paymentErr.Code),
                slog.String("error_message", paymentErr.Message),
                slog.Float64("amount", order.Total),
            )
        }
        return fmt.Errorf("charge order: %w", err)
    }

    logger.Info("Order processed successfully",
        slog.String("order_id", orderID),
        slog.Float64("amount", order.Total),
    )
    return nil
}

Panic Recovery with Logging

func RecoveryMiddleware(logger *slog.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if r := recover(); r != nil {
                // Get stack trace
                stack := debug.Stack()

                logger.Error("Panic recovered",
                    slog.Any("panic", r),
                    slog.String("stack", string(stack)),
                    slog.String("path", c.Request.URL.Path),
                    slog.String("method", c.Request.Method),
                )

                c.AbortWithStatusJSON(500, gin.H{
                    "error": "internal server error",
                })
            }
        }()

        c.Next()
    }
}

Docker Deployment

Dockerfile (Multi-stage)

# Build stage
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Download dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy source
COPY . .

# Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server

# Runtime stage
FROM alpine:3.19

RUN apk --no-cache add ca-certificates

WORKDIR /app

COPY --from=builder /app/server .

# Environment variables
ENV LOGTIDE_API_URL=""
ENV LOGTIDE_API_KEY=""

EXPOSE 8080

CMD ["./server"]

docker-compose.yml

version: "3.8"

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - LOGTIDE_API_URL=https://api.logtide.dev
      - LOGTIDE_API_KEY=${LOGTIDE_API_KEY}
      - ENVIRONMENT=production
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Graceful Shutdown

func main() {
    handler, _ := logtide.NewHandler(logtide.Config{...})

    // Create server
    srv := &http.Server{
        Addr:    ":8080",
        Handler: router,
    }

    // Start server in goroutine
    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            slog.Error("Server error", slog.Any("error", err))
        }
    }()

    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    slog.Info("Shutting down server...")

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        slog.Error("Server forced to shutdown", slog.Any("error", err))
    }

    // Close LogTide handler (flushes remaining logs)
    if err := handler.Close(); err != nil {
        slog.Error("Failed to close LogTide handler", slog.Any("error", err))
    }

    slog.Info("Server stopped")
}

Performance

Metricslogzerologzap
Allocations per log0-200-1
Latency (batched)<100ns<50ns<80ns
Memory overhead~5MB~3MB~4MB
Throughput500k logs/sec1M logs/sec800k logs/sec

Best Practices

1. Use Structured Fields

// Bad: String interpolation
slog.Info(fmt.Sprintf("User %s logged in from %s", userID, ip))

// Good: Structured fields
slog.Info("User logged in",
    slog.String("user_id", userID),
    slog.String("ip", ip),
)

2. Consistent Field Names

// Define constants for field names
const (
    FieldUserID    = "user_id"
    FieldOrderID   = "order_id"
    FieldRequestID = "request_id"
    FieldError     = "error"
)

slog.Info("Order created",
    slog.String(FieldUserID, userID),
    slog.String(FieldOrderID, orderID),
)

3. Log Levels

LevelUse Case
DebugDevelopment debugging, verbose output
InfoNormal operations, request logs
WarnRecoverable issues, deprecation notices
ErrorFailures requiring attention

Troubleshooting

Logs not appearing

  1. Check configuration:

    fmt.Printf("API URL: %s\n", os.Getenv("LOGTIDE_API_URL"))
    fmt.Printf("API Key set: %v\n", os.Getenv("LOGTIDE_API_KEY") != "")
    
  2. Ensure handler is closed on shutdown:

    defer handler.Close()
    

High memory usage

Reduce buffer size:

handler, _ := logtide.NewHandler(logtide.Config{
    BufferSize: 1000, // Reduce from default 10000
})

Next Steps