Gin
Instrument Gin web applications with OpenTelemetry for comprehensive HTTP request tracing, database monitoring, and distributed system observability
Use OpenTelemetry to instrument your Go Gin web application and send telemetry data to Last9. This integration provides automatic instrumentation for HTTP requests, database operations, Redis commands, and external API calls in your Gin applications.
Gin is a popular HTTP web framework for Go that provides excellent performance and developer experience. With OpenTelemetry instrumentation, you can monitor request performance, trace distributed operations, and track system health across your Gin-based microservices.
Prerequisites
Before setting up Gin monitoring, ensure you have:
- Go Development Environment: Go 1.19 or higher installed
- Gin Application: Existing Gin web application to instrument
- Last9 Account: With OpenTelemetry integration credentials
- Module Support: Go modules enabled (
go mod initif not already done)
-
Install OpenTelemetry Packages
Install the required OpenTelemetry packages for comprehensive Gin instrumentation:
# Core OpenTelemetry packagesgo get go.opentelemetry.io/otel@latestgo get go.opentelemetry.io/otel/sdk@latestgo get go.opentelemetry.io/otel/trace@latestgo get go.opentelemetry.io/otel/sdk/metric@latest# OTLP exporters for Last9go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@latestgo get go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc@latest# Gin-specific instrumentationgo get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin@latest# Additional instrumentation packagesgo get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp@latestgo get github.com/redis/go-redis/extra/redisotel/v9@latestgo get go.nhat.io/otelsql@latestThese packages provide:
- Core OTEL: Base OpenTelemetry functionality and SDK
- OTLP Exporters: Direct export to Last9 for traces and metrics
- Gin Instrumentation: Automatic HTTP request/response tracing
- Database/Redis: SQL database and Redis operation tracing
- HTTP Client: External API call instrumentation
-
Set Environment Variables
Configure OpenTelemetry environment variables for your Gin application:
export OTEL_SERVICE_NAME="gin-api-server"export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"export OTEL_TRACES_SAMPLER="always_on"export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"export OTEL_LOG_LEVEL="error"Replace
gin-api-serverwith your actual service name. -
Create Instrumentation Setup
Create a comprehensive instrumentation configuration file
instrumentation.go:package mainimport ("context""fmt""log""os""time""go.opentelemetry.io/otel""go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc""go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp""go.opentelemetry.io/otel/propagation""go.opentelemetry.io/otel/sdk/metric""go.opentelemetry.io/otel/sdk/resource"sdktrace "go.opentelemetry.io/otel/sdk/trace"semconv "go.opentelemetry.io/otel/semconv/v1.26.0""go.opentelemetry.io/otel/trace")type Instrumentation struct {TracerProvider *sdktrace.TracerProviderMeterProvider *metric.MeterProviderTracer trace.TracerResource *resource.Resource}func NewInstrumentation() (*Instrumentation, error) {// Create resource with service informationres, err := createResource()if err != nil {return nil, fmt.Errorf("failed to create resource: %w", err)}// Initialize tracer providertp, err := initTracerProvider(res)if err != nil {return nil, fmt.Errorf("failed to create tracer provider: %w", err)}// Initialize meter providermp, err := initMeterProvider(res)if err != nil {return nil, fmt.Errorf("failed to create meter provider: %w", err)}// Set global providersotel.SetTracerProvider(tp)otel.SetMeterProvider(mp)otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{},propagation.Baggage{},))return &Instrumentation{TracerProvider: tp,MeterProvider: mp,Tracer: tp.Tracer(getServiceName()),Resource: res,}, nil}func createResource() (*resource.Resource, error) {return resource.New(context.Background(),resource.WithFromEnv(),resource.WithTelemetrySDK(),resource.WithProcess(),resource.WithOS(),resource.WithContainer(),resource.WithHost(),resource.WithAttributes(semconv.ServiceNameKey.String(getServiceName()),semconv.ServiceVersionKey.String(getServiceVersion()),semconv.DeploymentEnvironmentKey.String(getEnvironment()),),)}func initTracerProvider(res *resource.Resource) (*sdktrace.TracerProvider, error) {// Create OTLP trace exporterexporter, err := otlptracehttp.New(context.Background())if err != nil {return nil, fmt.Errorf("failed to create trace exporter: %w", err)}// Create tracer provider with batch processortp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter,sdktrace.WithBatchTimeout(5*time.Second),sdktrace.WithMaxExportBatchSize(512),),sdktrace.WithResource(res),sdktrace.WithSampler(sdktrace.AlwaysSample()),)return tp, nil}func initMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {// Create OTLP metric exporterexporter, err := otlpmetricgrpc.New(context.Background())if err != nil {return nil, fmt.Errorf("failed to create metric exporter: %w", err)}// Create meter provider with periodic readermp := metric.NewMeterProvider(metric.WithResource(res),metric.WithReader(metric.NewPeriodicReader(exporter,metric.WithInterval(30*time.Second),)),)return mp, nil}func (i *Instrumentation) Shutdown() error {ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()// Shutdown tracer providerif err := i.TracerProvider.Shutdown(ctx); err != nil {log.Printf("Error shutting down tracer provider: %v", err)}// Shutdown meter providerif err := i.MeterProvider.Shutdown(ctx); err != nil {log.Printf("Error shutting down meter provider: %v", err)}return nil}// Helper functions to get configuration from environmentfunc getServiceName() string {if name := os.Getenv("OTEL_SERVICE_NAME"); name != "" {return name}return "gin-application"}func getServiceVersion() string {if version := os.Getenv("SERVICE_VERSION"); version != "" {return version}return "1.0.0"}func getEnvironment() string {if env := os.Getenv("DEPLOYMENT_ENV"); env != "" {return env}return "production"} -
Instrument Your Gin Application
Update your main application file to include OpenTelemetry instrumentation:
package mainimport ("context""log""net/http""os""os/signal""syscall""time""github.com/gin-gonic/gin""go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin")func main() {// Initialize OpenTelemetry instrumentationinstr, err := NewInstrumentation()if err != nil {log.Fatalf("Failed to initialize instrumentation: %v", err)}// Ensure proper shutdowndefer func() {if err := instr.Shutdown(); err != nil {log.Printf("Error during shutdown: %v", err)}}()// Create Gin router with OpenTelemetry middlewarer := gin.Default()// Add OpenTelemetry middlewarer.Use(otelgin.Middleware(getServiceName()))// Define routesr.GET("/health", healthHandler)r.GET("/", indexHandler)r.GET("/users/:id", getUserHandler)r.POST("/users", createUserHandler)// Start HTTP serversrv := &http.Server{Addr: ":8080",Handler: r,}// Graceful shutdowngo func() {if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalf("Server failed to start: %v", err)}}()log.Println("Server started on :8080")// Wait for interrupt signalquit := make(chan os.Signal, 1)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quitlog.Println("Shutting down server...")// Shutdown serverctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := srv.Shutdown(ctx); err != nil {log.Fatal("Server forced to shutdown:", err)}log.Println("Server exited")}func healthHandler(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"status": "healthy"})}func indexHandler(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"message": "Welcome to Gin API"})}func getUserHandler(c *gin.Context) {userID := c.Param("id")// Add custom span attributesspan := trace.SpanFromContext(c.Request.Context())span.SetAttributes(attribute.String("user.id", userID),attribute.String("operation", "get_user"),)// Simulate user lookupuser := gin.H{"id": userID,"name": "John Doe","email": "john@example.com",}c.JSON(http.StatusOK, user)}func createUserHandler(c *gin.Context) {// Add custom span for user creationspan := trace.SpanFromContext(c.Request.Context())span.SetAttributes(attribute.String("operation", "create_user"),)c.JSON(http.StatusCreated, gin.H{"message": "User created"})}package mainimport ("database/sql""log""net/http""github.com/gin-gonic/gin""go.nhat.io/otelsql""go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/trace"_ "github.com/lib/pq" // PostgreSQL driver)var db *sql.DBfunc main() {// Initialize OpenTelemetryinstr, err := NewInstrumentation()if err != nil {log.Fatalf("Failed to initialize instrumentation: %v", err)}defer instr.Shutdown()// Initialize database with OpenTelemetry instrumentationinitDatabase()// Create Gin routerr := gin.Default()r.Use(otelgin.Middleware(getServiceName()))// API routesr.GET("/users", listUsersHandler)r.GET("/users/:id", getUserHandler)r.POST("/users", createUserHandler)log.Println("Server starting on :8080")r.Run(":8080")}func initDatabase() {var err error// Wrap database driver with OpenTelemetrydriverName, err := otelsql.Register("postgres",otelsql.AllowRoot(),otelsql.TraceQueryWithoutArgs(),otelsql.TraceRowsClose(),otelsql.TraceRowsAffected(),)if err != nil {log.Fatal("Failed to register otelsql driver:", err)}// Open database connectiondb, err = sql.Open(driverName, "postgres://user:password@localhost/dbname?sslmode=disable")if err != nil {log.Fatal("Failed to connect to database:", err)}if err = db.Ping(); err != nil {log.Fatal("Failed to ping database:", err)}log.Println("Database connected successfully")}func listUsersHandler(c *gin.Context) {span := trace.SpanFromContext(c.Request.Context())span.SetAttributes(attribute.String("operation", "list_users"))// Database query with automatic tracingrows, err := db.QueryContext(c.Request.Context(), "SELECT id, name, email FROM users")if err != nil {span.SetAttributes(attribute.String("error", err.Error()))c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})return}defer rows.Close()var users []gin.Hfor rows.Next() {var id intvar name, email stringif err := rows.Scan(&id, &name, &email); err != nil {continue}users = append(users, gin.H{"id": id,"name": name,"email": email,})}span.SetAttributes(attribute.Int("users.count", len(users)))c.JSON(http.StatusOK, gin.H{"users": users})}func createUserHandler(c *gin.Context) {span := trace.SpanFromContext(c.Request.Context())span.SetAttributes(attribute.String("operation", "create_user"))var user struct {Name string `json:"name"`Email string `json:"email"`}if err := c.ShouldBindJSON(&user); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// Database insert with automatic tracingvar userID interr := db.QueryRowContext(c.Request.Context(),"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",user.Name, user.Email).Scan(&userID)if err != nil {span.SetAttributes(attribute.String("error", err.Error()))c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})return}span.SetAttributes(attribute.Int("user.created_id", userID))c.JSON(http.StatusCreated, gin.H{"id": userID,"name": user.Name,"email": user.Email,})}package mainimport ("context""log""net/http""github.com/gin-gonic/gin""github.com/redis/go-redis/extra/redisotel/v9""github.com/redis/go-redis/v9""go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/trace")var rdb *redis.Clientfunc main() {// Initialize OpenTelemetryinstr, err := NewInstrumentation()if err != nil {log.Fatalf("Failed to initialize instrumentation: %v", err)}defer instr.Shutdown()// Initialize Redis with OpenTelemetry instrumentationinitRedis()// Create Gin routerr := gin.Default()r.Use(otelgin.Middleware(getServiceName()))// Cache routesr.GET("/cache/:key", getCacheHandler)r.POST("/cache/:key", setCacheHandler)r.DELETE("/cache/:key", deleteCacheHandler)log.Println("Server starting on :8080")r.Run(":8080")}func initRedis() {rdb = redis.NewClient(&redis.Options{Addr: "localhost:6379",DB: 0,})// Enable OpenTelemetry instrumentation for Redisif err := redisotel.InstrumentTracing(rdb); err != nil {log.Fatal("Failed to instrument Redis tracing:", err)}if err := redisotel.InstrumentMetrics(rdb); err != nil {log.Fatal("Failed to instrument Redis metrics:", err)}// Test connectionctx := context.Background()if err := rdb.Ping(ctx).Err(); err != nil {log.Fatal("Failed to connect to Redis:", err)}log.Println("Redis connected successfully")}func getCacheHandler(c *gin.Context) {key := c.Param("key")span := trace.SpanFromContext(c.Request.Context())span.SetAttributes(attribute.String("operation", "cache_get"),attribute.String("cache.key", key),)// Redis GET with automatic tracingval, err := rdb.Get(c.Request.Context(), key).Result()if err == redis.Nil {c.JSON(http.StatusNotFound, gin.H{"error": "Key not found"})return} else if err != nil {span.SetAttributes(attribute.String("error", err.Error()))c.JSON(http.StatusInternalServerError, gin.H{"error": "Cache error"})return}span.SetAttributes(attribute.String("cache.hit", "true"))c.JSON(http.StatusOK, gin.H{"key": key, "value": val})}func setCacheHandler(c *gin.Context) {key := c.Param("key")span := trace.SpanFromContext(c.Request.Context())span.SetAttributes(attribute.String("operation", "cache_set"),attribute.String("cache.key", key),)var req struct {Value string `json:"value"`TTL int `json:"ttl,omitempty"`}if err := c.ShouldBindJSON(&req); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// Redis SET with automatic tracingttl := time.Duration(req.TTL) * time.Secondif req.TTL == 0 {ttl = 0 // No expiration}err := rdb.Set(c.Request.Context(), key, req.Value, ttl).Err()if err != nil {span.SetAttributes(attribute.String("error", err.Error()))c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to set cache"})return}span.SetAttributes(attribute.String("cache.operation", "set"),attribute.Int("cache.ttl", req.TTL),)c.JSON(http.StatusOK, gin.H{"message": "Cache set successfully"})} -
Add External API Call Instrumentation
Instrument outbound HTTP calls to external services:
package mainimport ("context""encoding/json""net/http""github.com/gin-gonic/gin""go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/trace")// Create instrumented HTTP clientvar httpClient = &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport),}func externalAPIHandler(c *gin.Context) {span := trace.SpanFromContext(c.Request.Context())span.SetAttributes(attribute.String("operation", "external_api_call"))// Make external API call with tracingreq, err := http.NewRequestWithContext(c.Request.Context(),"GET","https://api.external-service.com/users",nil,)if err != nil {span.SetAttributes(attribute.String("error", err.Error()))c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"})return}// Add custom headersreq.Header.Set("User-Agent", "gin-service/1.0")resp, err := httpClient.Do(req)if err != nil {span.SetAttributes(attribute.String("error", err.Error()))c.JSON(http.StatusInternalServerError, gin.H{"error": "External API error"})return}defer resp.Body.Close()span.SetAttributes(attribute.Int("http.status_code", resp.StatusCode),attribute.String("http.url", req.URL.String()),)if resp.StatusCode != http.StatusOK {c.JSON(resp.StatusCode, gin.H{"error": "External API returned error"})return}var result interface{}if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {span.SetAttributes(attribute.String("error", err.Error()))c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode response"})return}c.JSON(http.StatusOK, result)} -
Run Your Instrumented Application
Build and run your Gin application with OpenTelemetry instrumentation:
# Build the applicationgo build -o gin-app# Run with environment variables./gin-app# Or run directly with gogo run *.go
Understanding Gin Instrumentation
The OpenTelemetry Gin integration provides comprehensive observability:
HTTP Request Tracing
- Request/Response Cycles: Complete HTTP request lifecycle tracing
- Route Information: Gin route patterns and handler identification
- Request Metadata: HTTP method, status codes, response times
- Error Tracking: Automatic error detection and span status updates
Middleware Integration
- Automatic Instrumentation: Zero-code instrumentation via Gin middleware
- Context Propagation: Seamless trace context passing through request pipeline
- Custom Attributes: Add business-specific metadata to spans
- Performance Metrics: Request duration, throughput, and error rates
Database and External Services
- SQL Database Tracing: Query execution, connection pooling, transaction tracking
- Redis Operations: Cache operations with hit/miss ratios and performance metrics
- HTTP Client Calls: External API calls with request/response details
- Error Handling: Comprehensive error tracking across all integrations
Advanced Configuration
Custom Middleware and Spans
Add custom spans for business logic:
func businessLogicMiddleware() gin.HandlerFunc { return func(c *gin.Context) { tracer := otel.Tracer("business-logic") ctx, span := tracer.Start(c.Request.Context(), "business.operation") defer span.End()
// Add custom attributes span.SetAttributes( attribute.String("business.operation", "user_validation"), attribute.String("tenant.id", getTenantID(c)), )
// Update context c.Request = c.Request.WithContext(ctx) c.Next() }}Custom Metrics
Create application-specific metrics:
import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/metric")
var ( requestCounter metric.Int64Counter requestDuration metric.Float64Histogram)
func initCustomMetrics() { meter := otel.Meter("gin-application")
var err error requestCounter, err = meter.Int64Counter( "http_requests_total", metric.WithDescription("Total HTTP requests"), ) if err != nil { log.Fatal("Failed to create counter:", err) }
requestDuration, err = meter.Float64Histogram( "http_request_duration_seconds", metric.WithDescription("HTTP request duration"), metric.WithUnit("s"), ) if err != nil { log.Fatal("Failed to create histogram:", err) }}
func metricsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now()
c.Next()
duration := time.Since(start).Seconds()
// Record metrics requestCounter.Add(c.Request.Context(), 1, metric.WithAttributes( attribute.String("method", c.Request.Method), attribute.String("route", c.FullPath()), attribute.Int("status", c.Writer.Status()), ), )
requestDuration.Record(c.Request.Context(), duration, metric.WithAttributes( attribute.String("method", c.Request.Method), attribute.String("route", c.FullPath()), ), ) }}Error Handling and Span Status
Proper error handling with OpenTelemetry:
import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace")
func errorHandlingExample(c *gin.Context) { span := trace.SpanFromContext(c.Request.Context())
// Simulate business logic if err := performBusinessOperation(); err != nil { // Record error in span span.RecordError(err) span.SetStatus(codes.Error, err.Error()) span.SetAttributes( attribute.String("error.type", "business_logic_error"), attribute.String("error.detail", err.Error()), )
c.JSON(http.StatusInternalServerError, gin.H{ "error": "Internal server error", "trace_id": span.SpanContext().TraceID().String(), }) return }
// Success case span.SetStatus(codes.Ok, "Operation completed successfully") c.JSON(http.StatusOK, gin.H{"status": "success"})}Verification
-
Check Application Startup
Verify OpenTelemetry initializes correctly:
# Look for initialization logsgo run *.go# Should see logs like:# "OpenTelemetry instrumentation initialized"# "Server started on :8080" -
Generate Test Traffic
Make requests to your instrumented endpoints:
# Health checkcurl http://localhost:8080/health# API endpointscurl http://localhost:8080/users/123curl -X POST http://localhost:8080/users \-H "Content-Type: application/json" \-d '{"name":"John","email":"john@example.com"}'# Cache operations (if implemented)curl -X POST http://localhost:8080/cache/test-key \-H "Content-Type: application/json" \-d '{"value":"test-value","ttl":3600}'curl http://localhost:8080/cache/test-key -
Test Database Operations
If using database integration, perform database operations to generate database spans.
-
Verify Traces in Last9
Log into your Last9 account and check that traces are being received in the Traces dashboard.
Look for:
- HTTP request traces with Gin route information
- Database query spans (if database integration is enabled)
- Redis operation spans (if Redis integration is enabled)
- External API call traces (if external service calls are made)
- Custom business logic spans
Best Practices
Development
- Environment Variables: Use environment variables for configuration
- Service Naming: Use descriptive, consistent service names across environments
- Resource Attribution: Include version, environment, and deployment metadata
- Error Handling: Always set span status for errors and include relevant error details
Production
- Sampling: Configure appropriate sampling rates for production workloads
- Resource Limits: Set memory and CPU limits for telemetry processing
- Graceful Shutdown: Ensure proper cleanup of OpenTelemetry resources
- Health Checks: Include telemetry health in your application health checks
Monitoring Strategy
- Custom Spans: Add spans for critical business operations
- Metrics: Combine tracing with custom metrics for comprehensive observability
- Alerting: Set up alerts on trace data for error rates and latency
- Context Propagation: Ensure trace context flows through all service boundaries
Security
- Credential Management: Store sensitive configuration in environment variables
- Data Privacy: Be mindful of sensitive data in trace attributes
- Network Security: Ensure secure communication to Last9 endpoints
Supported Gin Features
The OpenTelemetry Gin integration works seamlessly with:
- Middleware Chain: Integrates with existing Gin middleware
- Route Groups: Supports nested route groups and common middleware
- Custom Recovery: Works with custom recovery middleware
- Static File Serving: Traces static file requests
- WebSocket Connections: Basic WebSocket request tracing
- File Uploads: Traces multipart file upload requests
Need Help?
If you encounter any issues or have questions:
- Join our Discord community for real-time support
- Contact our support team at support@last9.io