Iris
Learn how to integrate OpenTelemetry with Iris Go applications and send telemetry data to Last9
This guide shows you how to instrument your Iris Go application with OpenTelemetry and send traces, metrics, and logs to Last9.
Prerequisites
- Go 1.19 or later
- An Iris Go application
- Last9 account with OTLP endpoint configured
Installation
-
Install OpenTelemetry packages
go get -u go.opentelemetry.io/otelgo get -u go.opentelemetry.io/otel/sdkgo get -u go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpcgo get -u go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpcgo get -u go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpcgo get -u go.opentelemetry.io/otel/sdk/metricgo get -u go.opentelemetry.io/otel/sdk/loggo get -u go.opentelemetry.io/otel/log/globalgo get -u go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp -
Install database instrumentation (optional)
For PostgreSQL with standard database/sql:
go get -u go.nhat.io/otelsqlgo get -u github.com/lib/pqFor PostgreSQL with pgx:
go get -u github.com/jackc/pgx/v5/pgxpoolgo get -u github.com/exaring/otelpgx -
Install Redis instrumentation (optional)
Check your go-redis version first:
go list -m github.com/redis/go-redis/v9# or for older versionsgo list -m github.com/go-redis/redis/v8For go-redis v9:
go get -u github.com/redis/go-redis/extra/redisotel/v9For go-redis v8:
go get -u github.com/go-redis/redis/extra/redisotel/v8
Configuration
Set up your environment variables:
export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"export OTEL_SERVICE_NAME="iris-api"export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"export OTEL_TRACES_SAMPLER="always_on"export OTEL_LOG_LEVEL="info"Instrumentation Setup
Core Instrumentation
Create a comprehensive instrumentation setup:
package instrumentation
import ( "context" "fmt" "log" "time"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/log/global" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/log" sdkmetric "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.TracerProvider MeterProvider *sdkmetric.MeterProvider LoggerProvider *log.LoggerProvider Tracer trace.Tracer Meter metric.Meter Logger *log.Logger}
func NewInstrumentation(serviceName string) (*Instrumentation, error) { // Create resource with service information res, err := resource.New(context.Background(), resource.WithFromEnv(), resource.WithTelemetrySDK(), resource.WithAttributes( semconv.ServiceNameKey.String(serviceName), ), ) if err != nil { return nil, fmt.Errorf("failed to create resource: %w", err) }
// Initialize trace provider traceProvider, err := initTracerProvider(res) if err != nil { return nil, fmt.Errorf("failed to initialize tracer provider: %w", err) }
// Initialize metrics provider metricsProvider, err := initMeterProvider(res) if err != nil { return nil, fmt.Errorf("failed to initialize meter provider: %w", err) }
// Initialize log provider logProvider, err := initLogProvider(res) if err != nil { return nil, fmt.Errorf("failed to initialize log provider: %w", err) }
// Set global providers otel.SetTracerProvider(traceProvider) otel.SetMeterProvider(metricsProvider) global.SetLoggerProvider(logProvider) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, ))
tracer := traceProvider.Tracer(serviceName) meter := metricsProvider.Meter(serviceName) logger := logProvider.Logger(serviceName)
return &Instrumentation{ TracerProvider: traceProvider, MeterProvider: metricsProvider, LoggerProvider: logProvider, Tracer: tracer, Meter: meter, Logger: &logger, }, nil}
func initTracerProvider(res *resource.Resource) (*sdktrace.TracerProvider, error) { ctx := context.Background()
exporter, err := otlptracegrpc.New(ctx) if err != nil { return nil, fmt.Errorf("failed to create trace exporter: %w", err) }
provider := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(res), sdktrace.WithSampler(sdktrace.AlwaysSample()), )
return provider, nil}
func initMeterProvider(res *resource.Resource) (*sdkmetric.MeterProvider, error) { ctx := context.Background()
exporter, err := otlpmetricgrpc.New(ctx) if err != nil { return nil, fmt.Errorf("failed to create metric exporter: %w", err) }
provider := sdkmetric.NewMeterProvider( sdkmetric.WithReader( sdkmetric.NewPeriodicReader(exporter, sdkmetric.WithInterval(30*time.Second)), ), sdkmetric.WithResource(res), )
return provider, nil}
func initLogProvider(res *resource.Resource) (*log.LoggerProvider, error) { ctx := context.Background()
exporter, err := otlploggrpc.New(ctx) if err != nil { return nil, fmt.Errorf("failed to create log exporter: %w", err) }
provider := log.NewLoggerProvider( log.WithProcessor(log.NewBatchProcessor(exporter)), log.WithResource(res), )
return provider, nil}
func (i *Instrumentation) Shutdown(ctx context.Context) error { var errs []error
if err := i.TracerProvider.Shutdown(ctx); err != nil { errs = append(errs, fmt.Errorf("failed to shutdown tracer provider: %w", err)) }
if err := i.MeterProvider.Shutdown(ctx); err != nil { errs = append(errs, fmt.Errorf("failed to shutdown meter provider: %w", err)) }
if err := i.LoggerProvider.Shutdown(ctx); err != nil { errs = append(errs, fmt.Errorf("failed to shutdown log provider: %w", err)) }
if len(errs) > 0 { return fmt.Errorf("shutdown errors: %v", errs) }
return nil}package instrumentation
import ( "strconv" "time"
"github.com/kataras/iris/v12" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" "go.opentelemetry.io/otel/trace")
type Middleware struct { tracer trace.Tracer requestCounter metric.Int64Counter requestDuration metric.Float64Histogram activeConnections metric.Int64UpDownCounter}
func NewMiddleware(serviceName string) (*Middleware, error) { tracer := otel.Tracer(serviceName) meter := otel.Meter(serviceName)
requestCounter, err := meter.Int64Counter( "http_requests_total", metric.WithDescription("Total number of HTTP requests"), ) if err != nil { return nil, err }
requestDuration, err := meter.Float64Histogram( "http_request_duration_seconds", metric.WithDescription("HTTP request duration in seconds"), metric.WithUnit("s"), ) if err != nil { return nil, err }
activeConnections, err := meter.Int64UpDownCounter( "http_active_connections", metric.WithDescription("Number of active HTTP connections"), ) if err != nil { return nil, err }
return &Middleware{ tracer: tracer, requestCounter: requestCounter, requestDuration: requestDuration, activeConnections: activeConnections, }, nil}
func (m *Middleware) Handler() iris.Handler { return func(ctx iris.Context) { start := time.Now()
// Extract trace context from headers parentCtx := otel.GetTextMapPropagator().Extract( ctx.Request().Context(), propagation.HeaderCarrier(ctx.Request().Header), )
// Create span spanCtx, span := m.tracer.Start(parentCtx, ctx.Method()+" "+ctx.Path(), trace.WithAttributes( semconv.HTTPMethodKey.String(ctx.Method()), semconv.HTTPURLKey.String(ctx.Request().URL.String()), semconv.HTTPRouteKey.String(ctx.Path()), semconv.UserAgentOriginalKey.String(ctx.Request().UserAgent()), semconv.HTTPRequestContentLengthKey.Int64(ctx.Request().ContentLength), ), ) defer span.End()
// Set span context in Iris context ctx.Values().Set("otel_span_context", spanCtx)
// Track active connections m.activeConnections.Add(spanCtx, 1) defer m.activeConnections.Add(spanCtx, -1)
// Continue to next handler ctx.Next()
// Record metrics and span attributes after request duration := time.Since(start).Seconds() statusCode := ctx.GetStatusCode()
attributes := []attribute.KeyValue{ semconv.HTTPMethodKey.String(ctx.Method()), semconv.HTTPRouteKey.String(ctx.Path()), semconv.HTTPStatusCodeKey.Int(statusCode), }
// Record metrics m.requestCounter.Add(spanCtx, 1, metric.WithAttributes(attributes...)) m.requestDuration.Record(spanCtx, duration, metric.WithAttributes(attributes...))
// Set span attributes span.SetAttributes( semconv.HTTPStatusCodeKey.Int(statusCode), semconv.HTTPResponseContentLengthKey.Int64(int64(ctx.ResponseWriter().Header().Get("Content-Length") != "")), )
// Set span status based on HTTP status code if statusCode >= 400 { span.RecordError(nil) } }}Main Application Setup
package main
import ( "context" "fmt" "log" "os" "os/signal" "syscall" "time"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/logger" "github.com/kataras/iris/v12/middleware/recover"
"your-app/instrumentation")
func main() { // Initialize instrumentation instr, err := instrumentation.NewInstrumentation("iris-api") if err != nil { log.Fatalf("Failed to initialize instrumentation: %v", err) }
// Create Iris app app := iris.New() app.Use(recover.New()) app.Use(logger.New())
// Add OpenTelemetry middleware otelMiddleware, err := instrumentation.NewMiddleware("iris-api") if err != nil { log.Fatalf("Failed to create middleware: %v", err) } app.Use(otelMiddleware.Handler())
// Setup routes setupRoutes(app, instr)
// Graceful shutdown go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) <-sigCh
log.Println("Shutting down server...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel()
if err := instr.Shutdown(shutdownCtx); err != nil { log.Printf("Error shutting down instrumentation: %v", err) }
app.Shutdown(shutdownCtx) }()
// Start server port := os.Getenv("PORT") if port == "" { port = "8080" }
log.Printf("Starting server on port %s", port) app.Listen(":" + port)}
func setupRoutes(app *iris.Application, instr *instrumentation.Instrumentation) { // Health check endpoint app.Get("/health", func(ctx iris.Context) { ctx.JSON(iris.Map{"status": "healthy", "timestamp": time.Now().Unix()}) })
// API routes api := app.Party("/api/v1") { // Users routes users := api.Party("/users") { users.Get("/", listUsers(instr)) users.Post("/", createUser(instr)) users.Get("/{id:uint64}", getUser(instr)) users.Put("/{id:uint64}", updateUser(instr)) users.Delete("/{id:uint64}", deleteUser(instr)) }
// Products routes products := api.Party("/products") { products.Get("/", listProducts(instr)) products.Post("/", createProduct(instr)) products.Get("/{id:uint64}", getProduct(instr)) } }}package main
import ( "context" "fmt" "strconv" "time"
"github.com/kataras/iris/v12" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace"
"your-app/instrumentation")
func getSpanFromContext(ctx iris.Context) trace.Span { if spanCtx, ok := ctx.Values().Get("otel_span_context").(context.Context); ok { return trace.SpanFromContext(spanCtx) } return nil}
func listUsers(instr *instrumentation.Instrumentation) iris.Handler { return func(ctx iris.Context) { span := getSpanFromContext(ctx) if span != nil { span.SetAttributes(attribute.String("operation", "list_users")) }
// Simulate business logic _, childSpan := instr.Tracer.Start(ctx.Request().Context(), "fetch_users_from_db") childSpan.SetAttributes( attribute.String("db.operation", "SELECT"), attribute.String("db.table", "users"), )
// Simulate DB query time time.Sleep(50 * time.Millisecond) childSpan.End()
users := []map[string]interface{}{ {"id": 1, "name": "John Doe", "email": "john@example.com"}, {"id": 2, "name": "Jane Smith", "email": "jane@example.com"}, }
ctx.JSON(iris.Map{ "users": users, "count": len(users), }) }}
func getUser(instr *instrumentation.Instrumentation) iris.Handler { return func(ctx iris.Context) { span := getSpanFromContext(ctx)
userID, err := ctx.Params().GetUint64("id") if err != nil { if span != nil { span.RecordError(err) span.SetAttributes(attribute.String("error", "invalid_user_id")) } ctx.StatusCode(iris.StatusBadRequest) ctx.JSON(iris.Map{"error": "Invalid user ID"}) return }
if span != nil { span.SetAttributes( attribute.String("operation", "get_user"), attribute.Int64("user.id", int64(userID)), ) }
// Simulate database lookup _, childSpan := instr.Tracer.Start(ctx.Request().Context(), "fetch_user_by_id") childSpan.SetAttributes( attribute.String("db.operation", "SELECT"), attribute.String("db.table", "users"), attribute.Int64("db.user.id", int64(userID)), )
time.Sleep(30 * time.Millisecond)
if userID > 100 { childSpan.SetAttributes(attribute.String("result", "not_found")) childSpan.End()
ctx.StatusCode(iris.StatusNotFound) ctx.JSON(iris.Map{"error": "User not found"}) return }
childSpan.SetAttributes(attribute.String("result", "found")) childSpan.End()
user := map[string]interface{}{ "id": userID, "name": fmt.Sprintf("User %d", userID), "email": fmt.Sprintf("user%d@example.com", userID), }
ctx.JSON(user) }}
func createUser(instr *instrumentation.Instrumentation) iris.Handler { return func(ctx iris.Context) { span := getSpanFromContext(ctx) if span != nil { span.SetAttributes(attribute.String("operation", "create_user")) }
var userData map[string]interface{} if err := ctx.ReadJSON(&userData); err != nil { if span != nil { span.RecordError(err) span.SetAttributes(attribute.String("error", "invalid_json")) } ctx.StatusCode(iris.StatusBadRequest) ctx.JSON(iris.Map{"error": "Invalid JSON payload"}) return }
// Simulate user creation _, childSpan := instr.Tracer.Start(ctx.Request().Context(), "insert_user") childSpan.SetAttributes( attribute.String("db.operation", "INSERT"), attribute.String("db.table", "users"), )
time.Sleep(100 * time.Millisecond) childSpan.End()
newUser := map[string]interface{}{ "id": time.Now().Unix(), "name": userData["name"], "email": userData["email"], }
ctx.StatusCode(iris.StatusCreated) ctx.JSON(newUser) }}
func updateUser(instr *instrumentation.Instrumentation) iris.Handler { return func(ctx iris.Context) { span := getSpanFromContext(ctx)
userID, err := ctx.Params().GetUint64("id") if err != nil { if span != nil { span.RecordError(err) } ctx.StatusCode(iris.StatusBadRequest) ctx.JSON(iris.Map{"error": "Invalid user ID"}) return }
if span != nil { span.SetAttributes( attribute.String("operation", "update_user"), attribute.Int64("user.id", int64(userID)), ) }
var userData map[string]interface{} if err := ctx.ReadJSON(&userData); err != nil { if span != nil { span.RecordError(err) } ctx.StatusCode(iris.StatusBadRequest) ctx.JSON(iris.Map{"error": "Invalid JSON payload"}) return }
// Simulate user update _, childSpan := instr.Tracer.Start(ctx.Request().Context(), "update_user_by_id") childSpan.SetAttributes( attribute.String("db.operation", "UPDATE"), attribute.String("db.table", "users"), attribute.Int64("db.user.id", int64(userID)), )
time.Sleep(75 * time.Millisecond) childSpan.End()
updatedUser := map[string]interface{}{ "id": userID, "name": userData["name"], "email": userData["email"], }
ctx.JSON(updatedUser) }}
func deleteUser(instr *instrumentation.Instrumentation) iris.Handler { return func(ctx iris.Context) { span := getSpanFromContext(ctx)
userID, err := ctx.Params().GetUint64("id") if err != nil { if span != nil { span.RecordError(err) } ctx.StatusCode(iris.StatusBadRequest) ctx.JSON(iris.Map{"error": "Invalid user ID"}) return }
if span != nil { span.SetAttributes( attribute.String("operation", "delete_user"), attribute.Int64("user.id", int64(userID)), ) }
// Simulate user deletion _, childSpan := instr.Tracer.Start(ctx.Request().Context(), "delete_user_by_id") childSpan.SetAttributes( attribute.String("db.operation", "DELETE"), attribute.String("db.table", "users"), attribute.Int64("db.user.id", int64(userID)), )
time.Sleep(40 * time.Millisecond) childSpan.End()
ctx.StatusCode(iris.StatusNoContent) }}
func listProducts(instr *instrumentation.Instrumentation) iris.Handler { return func(ctx iris.Context) { span := getSpanFromContext(ctx) if span != nil { span.SetAttributes(attribute.String("operation", "list_products")) }
// Simulate fetching products with caching _, cacheSpan := instr.Tracer.Start(ctx.Request().Context(), "check_product_cache") cacheSpan.SetAttributes(attribute.String("cache.operation", "GET")) time.Sleep(10 * time.Millisecond) cacheSpan.End()
_, dbSpan := instr.Tracer.Start(ctx.Request().Context(), "fetch_products_from_db") dbSpan.SetAttributes( attribute.String("db.operation", "SELECT"), attribute.String("db.table", "products"), ) time.Sleep(80 * time.Millisecond) dbSpan.End()
products := []map[string]interface{}{ {"id": 1, "name": "Product A", "price": 19.99}, {"id": 2, "name": "Product B", "price": 29.99}, }
ctx.JSON(iris.Map{ "products": products, "count": len(products), }) }}
func getProduct(instr *instrumentation.Instrumentation) iris.Handler { return func(ctx iris.Context) { span := getSpanFromContext(ctx)
productID, err := ctx.Params().GetUint64("id") if err != nil { if span != nil { span.RecordError(err) } ctx.StatusCode(iris.StatusBadRequest) ctx.JSON(iris.Map{"error": "Invalid product ID"}) return }
if span != nil { span.SetAttributes( attribute.String("operation", "get_product"), attribute.Int64("product.id", int64(productID)), ) }
// Simulate product lookup _, childSpan := instr.Tracer.Start(ctx.Request().Context(), "fetch_product_by_id") childSpan.SetAttributes( attribute.String("db.operation", "SELECT"), attribute.String("db.table", "products"), attribute.Int64("db.product.id", int64(productID)), ) time.Sleep(25 * time.Millisecond) childSpan.End()
product := map[string]interface{}{ "id": productID, "name": fmt.Sprintf("Product %d", productID), "price": float64(productID) * 9.99, }
ctx.JSON(product) }}
func createProduct(instr *instrumentation.Instrumentation) iris.Handler { return func(ctx iris.Context) { span := getSpanFromContext(ctx) if span != nil { span.SetAttributes(attribute.String("operation", "create_product")) }
var productData map[string]interface{} if err := ctx.ReadJSON(&productData); err != nil { if span != nil { span.RecordError(err) } ctx.StatusCode(iris.StatusBadRequest) ctx.JSON(iris.Map{"error": "Invalid JSON payload"}) return }
// Simulate product creation _, childSpan := instr.Tracer.Start(ctx.Request().Context(), "insert_product") childSpan.SetAttributes( attribute.String("db.operation", "INSERT"), attribute.String("db.table", "products"), ) time.Sleep(120 * time.Millisecond) childSpan.End()
newProduct := map[string]interface{}{ "id": time.Now().Unix(), "name": productData["name"], "price": productData["price"], }
ctx.StatusCode(iris.StatusCreated) ctx.JSON(newProduct) }}Database Integration
PostgreSQL with database/sql
package database
import ( "database/sql" "fmt" "context"
"go.nhat.io/otelsql" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" _ "github.com/lib/pq")
func InitPostgresDB(dsnName string) (*sql.DB, error) { // Register the instrumented driver driverName, err := otelsql.Register("postgres", otelsql.AllowRoot(), otelsql.TraceQueryWithoutArgs(), otelsql.TraceRowsClose(), otelsql.TraceRowsAffected(), otelsql.WithDatabaseName("iris_db"), otelsql.WithSystem(semconv.DBSystemPostgreSQL), ) if err != nil { return nil, fmt.Errorf("failed to register driver: %w", err) }
db, err := sql.Open(driverName, dsnName) if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) }
// Configure connection pool db.SetMaxOpenConns(25) db.SetMaxIdleConns(5)
// Record stats for metrics if err := otelsql.RecordStats(db); err != nil { return nil, fmt.Errorf("failed to record stats: %w", err) }
return db, nil}
// Example usage in handlersfunc GetUserFromDB(ctx context.Context, db *sql.DB, userID int64) (*User, error) { query := "SELECT id, name, email FROM users WHERE id = $1" row := db.QueryRowContext(ctx, query, userID)
var user User err := row.Scan(&user.ID, &user.Name, &user.Email) if err != nil { return nil, fmt.Errorf("failed to scan user: %w", err) }
return &user, nil}package database
import ( "context" "fmt" "os"
"github.com/jackc/pgx/v5/pgxpool" "github.com/exaring/otelpgx")
func InitPgxPool() (*pgxpool.Pool, error) { connString := os.Getenv("DATABASE_URL") if connString == "" { return nil, fmt.Errorf("DATABASE_URL environment variable is required") }
config, err := pgxpool.ParseConfig(connString) if err != nil { return nil, fmt.Errorf("failed to parse database config: %w", err) }
// Add OpenTelemetry tracer config.ConnConfig.Tracer = otelpgx.NewTracer()
pool, err := pgxpool.NewWithConfig(context.Background(), config) if err != nil { return nil, fmt.Errorf("failed to create connection pool: %w", err) }
return pool, nil}
// Example usagefunc GetUserFromPgx(ctx context.Context, pool *pgxpool.Pool, userID int64) (*User, error) { query := "SELECT id, name, email FROM users WHERE id = $1" row := pool.QueryRow(ctx, query, userID)
var user User err := row.Scan(&user.ID, &user.Name, &user.Email) if err != nil { return nil, fmt.Errorf("failed to scan user: %w", err) }
return &user, nil}Redis Integration
package cache
import ( "context" "log"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/extra/redisotel/v9")
func InitRedisV9() *redis.Client { rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password set DB: 0, // use default DB })
// Enable tracing instrumentation if err := redisotel.InstrumentTracing(rdb); err != nil { log.Fatalf("Failed to instrument Redis tracing: %v", err) }
// Enable metrics instrumentation if err := redisotel.InstrumentMetrics(rdb); err != nil { log.Fatalf("Failed to instrument Redis metrics: %v", err) }
return rdb}
// Example usage in handlersfunc GetFromCache(ctx context.Context, rdb *redis.Client, key string) (string, error) { val, err := rdb.Get(ctx, key).Result() if err == redis.Nil { return "", nil // Key does not exist } else if err != nil { return "", err } return val, nil}
func SetInCache(ctx context.Context, rdb *redis.Client, key, value string) error { return rdb.Set(ctx, key, value, 0).Err()}package cache
import ( "context"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/extra/redisotel/v8")
func InitRedisV8() *redis.Client { rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password set DB: 0, // use default DB })
// Add tracing hook rdb.AddHook(redisotel.NewTracingHook())
return rdb}Production Deployment
Docker
FROM golang:1.21-alpine AS builder
WORKDIR /appCOPY go.mod go.sum ./RUN go mod download
COPY . .RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
FROM alpine:latestRUN apk --no-cache add ca-certificatesWORKDIR /root/
COPY --from=builder /app/main .
# Install curl for health checksRUN apk add --no-cache curl
EXPOSE 8080
CMD ["./main"]version: "3.8"
services: iris-app: build: . ports: - "8080:8080" environment: - OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint - OTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_header - OTEL_SERVICE_NAME=iris-api - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0 - DATABASE_URL=postgres://user:pass@postgres:5432/iris_db?sslmode=disable - REDIS_URL=redis:6379 depends_on: - postgres - redis healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3
postgres: image: postgres:15-alpine environment: POSTGRES_DB: iris_db POSTGRES_USER: user POSTGRES_PASSWORD: pass ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data
redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis_data:/data
volumes: postgres_data: redis_data:Kubernetes
apiVersion: apps/v1kind: Deploymentmetadata: name: iris-api labels: app: iris-apispec: replicas: 3 selector: matchLabels: app: iris-api template: metadata: labels: app: iris-api spec: containers: - name: iris-api image: your-registry/iris-api:latest ports: - containerPort: 8080 env: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "$last9_otlp_endpoint" - name: OTEL_EXPORTER_OTLP_HEADERS value: "Authorization=$last9_otlp_auth_header" - name: OTEL_SERVICE_NAME value: "iris-api" - name: OTEL_RESOURCE_ATTRIBUTES value: "deployment.environment=production,service.version=1.0.0,k8s.cluster.name=production,k8s.namespace.name=default" - name: DATABASE_URL valueFrom: secretKeyRef: name: iris-secrets key: database-url - name: REDIS_URL valueFrom: configMapKeyRef: name: iris-config key: redis-url livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m"---apiVersion: v1kind: Servicemetadata: name: iris-api-servicespec: selector: app: iris-api ports: - protocol: TCP port: 80 targetPort: 8080 type: LoadBalancer---apiVersion: v1kind: ConfigMapmetadata: name: iris-configdata: redis-url: "redis-service:6379"---apiVersion: v1kind: Secretmetadata: name: iris-secretstype: Opaquedata: database-url: <base64-encoded-database-url>Testing the Integration
Run your Iris application and test the instrumentation:
-
Start the application
go run main.go -
Make test requests
# Test health endpointcurl http://localhost:8080/health# Test user endpointscurl http://localhost:8080/api/v1/userscurl http://localhost:8080/api/v1/users/1# Create a usercurl -X POST http://localhost:8080/api/v1/users \-H "Content-Type: application/json" \-d '{"name": "Test User", "email": "test@example.com"}' -
Check telemetry in Last9
Visit your Last9 dashboard to see:
- Traces: HTTP request traces with database and cache spans
- Metrics: Request count, duration, and active connections
- Logs: Application logs correlated with traces