Skip to content
Last9 named a Gartner Cool Vendor in AI for SRE Observability for 2025! Read more →
Last9

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

  1. Install OpenTelemetry packages

    go get -u go.opentelemetry.io/otel
    go get -u go.opentelemetry.io/otel/sdk
    go get -u go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
    go get -u go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
    go get -u go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc
    go get -u go.opentelemetry.io/otel/sdk/metric
    go get -u go.opentelemetry.io/otel/sdk/log
    go get -u go.opentelemetry.io/otel/log/global
    go get -u go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
  2. Install database instrumentation (optional)

    For PostgreSQL with standard database/sql:

    go get -u go.nhat.io/otelsql
    go get -u github.com/lib/pq

    For PostgreSQL with pgx:

    go get -u github.com/jackc/pgx/v5/pgxpool
    go get -u github.com/exaring/otelpgx
  3. Install Redis instrumentation (optional)

    Check your go-redis version first:

    go list -m github.com/redis/go-redis/v9
    # or for older versions
    go list -m github.com/go-redis/redis/v8

    For go-redis v9:

    go get -u github.com/redis/go-redis/extra/redisotel/v9

    For 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
}

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))
}
}
}

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 handlers
func 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
}

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 handlers
func 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()
}

Production Deployment

Docker

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
# Install curl for health checks
RUN apk add --no-cache curl
EXPOSE 8080
CMD ["./main"]

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
name: iris-api
labels:
app: iris-api
spec:
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: v1
kind: Service
metadata:
name: iris-api-service
spec:
selector:
app: iris-api
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
---
apiVersion: v1
kind: ConfigMap
metadata:
name: iris-config
data:
redis-url: "redis-service:6379"
---
apiVersion: v1
kind: Secret
metadata:
name: iris-secrets
type: Opaque
data:
database-url: <base64-encoded-database-url>

Testing the Integration

Run your Iris application and test the instrumentation:

  1. Start the application

    go run main.go
  2. Make test requests

    # Test health endpoint
    curl http://localhost:8080/health
    # Test user endpoints
    curl http://localhost:8080/api/v1/users
    curl http://localhost:8080/api/v1/users/1
    # Create a user
    curl -X POST http://localhost:8080/api/v1/users \
    -H "Content-Type: application/json" \
    -d '{"name": "Test User", "email": "test@example.com"}'
  3. 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