Beego
Learn how to integrate OpenTelemetry with Beego Go applications and send telemetry data to Last9
This guide shows you how to instrument your Beego v2 Go application with OpenTelemetry and send traces, metrics, and logs to Last9.
Prerequisites
- Go 1.19 or later
- Beego v2 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 Beego v2
go get -u github.com/beego/beego/v2/server/web -
Install database instrumentation (optional)
# For PostgreSQLgo get -u go.nhat.io/otelsqlgo get -u github.com/lib/pq# For ORM integrationgo get -u github.com/beego/beego/v2/client/orm -
Install Redis instrumentation (optional)
# For go-redis v9go get -u github.com/redis/go-redis/v9go get -u github.com/redis/go-redis/extra/redisotel/v9
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="beego-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 ( "net/http" "regexp" "strconv" "strings" "time"
beego "github.com/beego/beego/v2/server/web" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" "go.opentelemetry.io/otel/trace")
var ( // Regular expressions for path normalization numericIDPattern = regexp.MustCompile(`/\d+`) uuidPattern = regexp.MustCompile(`/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`))
type BeegoMiddleware struct { serviceName string tracer trace.Tracer requestCounter metric.Int64Counter requestDuration metric.Float64Histogram activeConnections metric.Int64UpDownCounter}
func NewBeegoMiddleware(serviceName string) (*BeegoMiddleware, 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 &BeegoMiddleware{ serviceName: serviceName, tracer: tracer, requestCounter: requestCounter, requestDuration: requestDuration, activeConnections: activeConnections, }, nil}
func (m *BeegoMiddleware) normalizePath(path string) string { // Replace numeric IDs and UUIDs with placeholders for better span grouping path = numericIDPattern.ReplaceAllString(path, "/:id") path = uuidPattern.ReplaceAllString(path, "/:uuid") return path}
func (m *BeegoMiddleware) httpStatusCodeToSpanStatus(code int) codes.Code { if code < 100 || code >= 600 { return codes.Error } if code >= 400 { return codes.Error } return codes.Ok}
// WrapBeegoHandler wraps a Beego controller method with OpenTelemetry tracingfunc (m *BeegoMiddleware) WrapBeegoHandler(handler func(ctx *beego.Controller)) func(ctx *beego.Controller) { return func(ctx *beego.Controller) { start := time.Now()
// Extract trace context from headers propagator := otel.GetTextMapPropagator() carrier := propagation.HeaderCarrier(ctx.Ctx.Request.Header) parentCtx := propagator.Extract(ctx.Ctx.Request.Context(), carrier)
// Create span spanName := m.normalizePath(ctx.Ctx.Request.URL.Path) attrs := []attribute.KeyValue{ semconv.ServiceNameKey.String(m.serviceName), semconv.HTTPMethodKey.String(ctx.Ctx.Request.Method), semconv.HTTPRouteKey.String(ctx.Ctx.Request.URL.Path), semconv.HTTPURLKey.String(ctx.Ctx.Request.URL.String()), semconv.HTTPSchemeKey.String(ctx.Ctx.Request.URL.Scheme), }
if ua := ctx.Ctx.Request.UserAgent(); ua != "" { attrs = append(attrs, semconv.UserAgentOriginalKey.String(ua)) } if host := ctx.Ctx.Request.Host; host != "" { attrs = append(attrs, semconv.ServerAddressKey.String(host)) }
spanCtx, span := m.tracer.Start(parentCtx, spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindServer), ) defer func() { // Record metrics and span attributes after request duration := time.Since(start).Seconds() statusCode := ctx.Ctx.ResponseWriter.Status
attributes := []attribute.KeyValue{ semconv.HTTPMethodKey.String(ctx.Ctx.Request.Method), semconv.HTTPRouteKey.String(m.normalizePath(ctx.Ctx.Request.URL.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), attribute.String("otel.debug", "beego-controller"), )
// Set span status based on HTTP status code span.SetStatus(m.httpStatusCodeToSpanStatus(statusCode), http.StatusText(statusCode)) span.End() }()
// Track active connections m.activeConnections.Add(spanCtx, 1) defer m.activeConnections.Add(spanCtx, -1)
// Inject the span context and update request context propagator.Inject(spanCtx, carrier) ctx.Ctx.Request = ctx.Ctx.Request.WithContext(spanCtx)
// Call the actual handler handler(ctx) }}
// BeegoFilter is a Beego filter that can be registered globallyfunc (m *BeegoMiddleware) BeegoFilter(ctx *beego.Context) { start := time.Now()
// Extract trace context propagator := otel.GetTextMapPropagator() carrier := propagation.HeaderCarrier(ctx.Request.Header) parentCtx := propagator.Extract(ctx.Request.Context(), carrier)
// Create span spanName := m.normalizePath(ctx.Request.URL.Path) attrs := []attribute.KeyValue{ semconv.ServiceNameKey.String(m.serviceName), semconv.HTTPMethodKey.String(ctx.Request.Method), semconv.HTTPRouteKey.String(ctx.Request.URL.Path), semconv.HTTPURLKey.String(ctx.Request.URL.String()), }
spanCtx, span := m.tracer.Start(parentCtx, spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindServer), )
// Track active connections m.activeConnections.Add(spanCtx, 1) defer func() { m.activeConnections.Add(spanCtx, -1)
// Record metrics after request duration := time.Since(start).Seconds() statusCode := ctx.ResponseWriter.Status
attributes := []attribute.KeyValue{ semconv.HTTPMethodKey.String(ctx.Request.Method), semconv.HTTPRouteKey.String(m.normalizePath(ctx.Request.URL.Path)), semconv.HTTPStatusCodeKey.Int(statusCode), }
m.requestCounter.Add(spanCtx, 1, metric.WithAttributes(attributes...)) m.requestDuration.Record(spanCtx, duration, metric.WithAttributes(attributes...))
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(statusCode)) span.SetStatus(m.httpStatusCodeToSpanStatus(statusCode), http.StatusText(statusCode)) span.End() }()
// Update request context ctx.Request = ctx.Request.WithContext(spanCtx)}Main Application Setup
package main
import ( "context" "log" "os" "os/signal" "syscall" "time"
beego "github.com/beego/beego/v2/server/web"
"your-app/controllers" "your-app/instrumentation")
func main() { // Initialize instrumentation instr, err := instrumentation.NewInstrumentation("beego-api") if err != nil { log.Fatalf("Failed to initialize instrumentation: %v", err) }
// Initialize middleware middleware, err := instrumentation.NewBeegoMiddleware("beego-api") if err != nil { log.Fatalf("Failed to create middleware: %v", err) }
// Setup 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) } }()
// Register global filter for all requests (optional approach) beego.InsertFilter("/*", beego.BeforeRouter, middleware.BeegoFilter)
// Setup routes setupRoutes(instr, middleware)
// Configure Beego beego.BConfig.WebConfig.DirectoryIndex = true beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
// Start server log.Println("Starting Beego server...") beego.Run()}
func setupRoutes(instr *instrumentation.Instrumentation, middleware *instrumentation.BeegoMiddleware) { // Initialize controllers userController := controllers.NewUserController(instr) productController := controllers.NewProductController(instr)
// Register routes with controllers // Method 1: Using wrapped handlers beego.Router("/api/v1/users", &controllers.UserController{}, "get:ListUsers") beego.Router("/api/v1/users", &controllers.UserController{}, "post:CreateUser") beego.Router("/api/v1/users/:id", &controllers.UserController{}, "get:GetUser") beego.Router("/api/v1/users/:id", &controllers.UserController{}, "put:UpdateUser") beego.Router("/api/v1/users/:id", &controllers.UserController{}, "delete:DeleteUser")
// Product routes beego.Router("/api/v1/products", &controllers.ProductController{}, "get:ListProducts") beego.Router("/api/v1/products", &controllers.ProductController{}, "post:CreateProduct") beego.Router("/api/v1/products/:id", &controllers.ProductController{}, "get:GetProduct")
// Health check beego.Router("/health", &controllers.HealthController{}, "get:Check")}package controllers
import ( "context"
beego "github.com/beego/beego/v2/server/web" "go.opentelemetry.io/otel/trace"
"your-app/instrumentation")
// BaseController provides common functionality for all controllerstype BaseController struct { beego.Controller instrumentation *instrumentation.Instrumentation}
// NewBaseController creates a new base controllerfunc NewBaseController(instr *instrumentation.Instrumentation) *BaseController { return &BaseController{ instrumentation: instr, }}
// GetSpanContext returns the span context from the requestfunc (c *BaseController) GetSpanContext() context.Context { return c.Ctx.Request.Context()}
// GetSpan returns the current spanfunc (c *BaseController) GetSpan() trace.Span { return trace.SpanFromContext(c.GetSpanContext())}
// SetJSONResponse sets a JSON responsefunc (c *BaseController) SetJSONResponse(statusCode int, data interface{}) { c.Ctx.Output.SetStatus(statusCode) c.Data["json"] = data c.ServeJSON()}
// SetErrorResponse sets an error responsefunc (c *BaseController) SetErrorResponse(statusCode int, message string) { c.Ctx.Output.SetStatus(statusCode) c.Data["json"] = map[string]string{"error": message} c.ServeJSON()}package controllers
import ( "fmt" "strconv" "time"
beego "github.com/beego/beego/v2/server/web" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace"
"your-app/instrumentation")
type UserController struct { BaseController}
func NewUserController(instr *instrumentation.Instrumentation) *UserController { return &UserController{ BaseController: *NewBaseController(instr), }}
// ListUsers handles GET /api/v1/usersfunc (c *UserController) ListUsers() { span := c.GetSpan() span.SetAttributes(attribute.String("operation", "list_users"))
// Simulate business logic with child span ctx := c.GetSpanContext() _, childSpan := c.instrumentation.Tracer.Start(ctx, "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"}, }
c.SetJSONResponse(200, map[string]interface{}{ "users": users, "count": len(users), })}
// GetUser handles GET /api/v1/users/:idfunc (c *UserController) GetUser() { span := c.GetSpan()
userIDStr := c.Ctx.Input.Param(":id") userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { span.RecordError(err) span.SetAttributes(attribute.String("error", "invalid_user_id")) c.SetErrorResponse(400, "Invalid user ID") return }
span.SetAttributes( attribute.String("operation", "get_user"), attribute.Int64("user.id", userID), )
// Simulate database lookup ctx := c.GetSpanContext() _, childSpan := c.instrumentation.Tracer.Start(ctx, "fetch_user_by_id") childSpan.SetAttributes( attribute.String("db.operation", "SELECT"), attribute.String("db.table", "users"), attribute.Int64("db.user.id", userID), )
time.Sleep(30 * time.Millisecond)
if userID > 100 { childSpan.SetAttributes(attribute.String("result", "not_found")) childSpan.End() c.SetErrorResponse(404, "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), }
c.SetJSONResponse(200, user)}
// CreateUser handles POST /api/v1/usersfunc (c *UserController) CreateUser() { span := c.GetSpan() span.SetAttributes(attribute.String("operation", "create_user"))
var userData map[string]interface{} if err := c.Ctx.BindJSON(&userData); err != nil { span.RecordError(err) span.SetAttributes(attribute.String("error", "invalid_json")) c.SetErrorResponse(400, "Invalid JSON payload") return }
// Simulate user creation ctx := c.GetSpanContext() _, childSpan := c.instrumentation.Tracer.Start(ctx, "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"], }
c.SetJSONResponse(201, newUser)}
// UpdateUser handles PUT /api/v1/users/:idfunc (c *UserController) UpdateUser() { span := c.GetSpan()
userIDStr := c.Ctx.Input.Param(":id") userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { span.RecordError(err) c.SetErrorResponse(400, "Invalid user ID") return }
span.SetAttributes( attribute.String("operation", "update_user"), attribute.Int64("user.id", userID), )
var userData map[string]interface{} if err := c.Ctx.BindJSON(&userData); err != nil { span.RecordError(err) c.SetErrorResponse(400, "Invalid JSON payload") return }
// Simulate user update ctx := c.GetSpanContext() _, childSpan := c.instrumentation.Tracer.Start(ctx, "update_user_by_id") childSpan.SetAttributes( attribute.String("db.operation", "UPDATE"), attribute.String("db.table", "users"), attribute.Int64("db.user.id", userID), )
time.Sleep(75 * time.Millisecond) childSpan.End()
updatedUser := map[string]interface{}{ "id": userID, "name": userData["name"], "email": userData["email"], }
c.SetJSONResponse(200, updatedUser)}
// DeleteUser handles DELETE /api/v1/users/:idfunc (c *UserController) DeleteUser() { span := c.GetSpan()
userIDStr := c.Ctx.Input.Param(":id") userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { span.RecordError(err) c.SetErrorResponse(400, "Invalid user ID") return }
span.SetAttributes( attribute.String("operation", "delete_user"), attribute.Int64("user.id", userID), )
// Simulate user deletion ctx := c.GetSpanContext() _, childSpan := c.instrumentation.Tracer.Start(ctx, "delete_user_by_id") childSpan.SetAttributes( attribute.String("db.operation", "DELETE"), attribute.String("db.table", "users"), attribute.Int64("db.user.id", userID), )
time.Sleep(40 * time.Millisecond) childSpan.End()
c.Ctx.Output.SetStatus(204)}package controllers
import ( "time"
beego "github.com/beego/beego/v2/server/web")
type HealthController struct { beego.Controller}
// Check handles GET /healthfunc (c *HealthController) Check() { c.Ctx.Output.SetStatus(200) c.Data["json"] = map[string]interface{}{ "status": "healthy", "timestamp": time.Now().Unix(), } c.ServeJSON()}Database Integration
PostgreSQL with ORM
package models
import ( "database/sql" "fmt"
"github.com/beego/beego/v2/client/orm" "go.nhat.io/otelsql" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" _ "github.com/lib/pq")
// User modeltype User struct { ID int64 `orm:"auto" json:"id"` Name string `orm:"size(100)" json:"name"` Email string `orm:"size(100);unique" json:"email"`}
func InitDatabase() error { // Register the instrumented PostgreSQL driver driverName, err := otelsql.Register("postgres", otelsql.AllowRoot(), otelsql.TraceQueryWithoutArgs(), otelsql.TraceRowsClose(), otelsql.TraceRowsAffected(), otelsql.WithDatabaseName("beego_db"), otelsql.WithSystem(semconv.DBSystemPostgreSQL), ) if err != nil { return fmt.Errorf("failed to register driver: %w", err) }
// Configure Beego ORM to use the instrumented driver dataSource := "user=dbuser dbname=beego_db password=dbpass host=localhost port=5432 sslmode=disable"
// Register database with instrumented driver err = orm.RegisterDataBase("default", driverName, dataSource) if err != nil { return fmt.Errorf("failed to register database: %w", err) }
// Register models orm.RegisterModel(new(User))
// Create tables if they don't exist err = orm.RunSyncdb("default", false, true) if err != nil { return fmt.Errorf("failed to sync database: %w", err) }
return nil}
// GetDB returns the underlying sql.DB for metrics recordingfunc GetDB() (*sql.DB, error) { return orm.GetDB("default")}package models
import ( "context" "fmt"
"github.com/beego/beego/v2/client/orm" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute")
type UserRepository struct { orm orm.Ormer}
func NewUserRepository() *UserRepository { return &UserRepository{ orm: orm.NewOrm(), }}
func (r *UserRepository) GetAll(ctx context.Context) ([]User, error) { tracer := otel.Tracer("beego-api") _, span := tracer.Start(ctx, "user_repository.get_all") defer span.End()
span.SetAttributes( attribute.String("db.operation", "SELECT"), attribute.String("db.table", "users"), )
var users []User _, err := r.orm.QueryTable("user").All(&users) if err != nil { span.RecordError(err) return nil, fmt.Errorf("failed to get all users: %w", err) }
span.SetAttributes(attribute.Int("result.count", len(users))) return users, nil}
func (r *UserRepository) GetByID(ctx context.Context, id int64) (*User, error) { tracer := otel.Tracer("beego-api") _, span := tracer.Start(ctx, "user_repository.get_by_id") defer span.End()
span.SetAttributes( attribute.String("db.operation", "SELECT"), attribute.String("db.table", "users"), attribute.Int64("user.id", id), )
user := &User{ID: id} err := r.orm.Read(user) if err != nil { if err == orm.ErrNoRows { span.SetAttributes(attribute.String("result", "not_found")) return nil, fmt.Errorf("user not found") } span.RecordError(err) return nil, fmt.Errorf("failed to get user: %w", err) }
span.SetAttributes(attribute.String("result", "found")) return user, nil}
func (r *UserRepository) Create(ctx context.Context, user *User) error { tracer := otel.Tracer("beego-api") _, span := tracer.Start(ctx, "user_repository.create") defer span.End()
span.SetAttributes( attribute.String("db.operation", "INSERT"), attribute.String("db.table", "users"), attribute.String("user.name", user.Name), attribute.String("user.email", user.Email), )
_, err := r.orm.Insert(user) if err != nil { span.RecordError(err) return fmt.Errorf("failed to create user: %w", err) }
span.SetAttributes(attribute.Int64("user.created_id", user.ID)) return nil}
func (r *UserRepository) Update(ctx context.Context, user *User) error { tracer := otel.Tracer("beego-api") _, span := tracer.Start(ctx, "user_repository.update") defer span.End()
span.SetAttributes( attribute.String("db.operation", "UPDATE"), attribute.String("db.table", "users"), attribute.Int64("user.id", user.ID), )
_, err := r.orm.Update(user) if err != nil { span.RecordError(err) return fmt.Errorf("failed to update user: %w", err) }
return nil}
func (r *UserRepository) Delete(ctx context.Context, id int64) error { tracer := otel.Tracer("beego-api") _, span := tracer.Start(ctx, "user_repository.delete") defer span.End()
span.SetAttributes( attribute.String("db.operation", "DELETE"), attribute.String("db.table", "users"), attribute.Int64("user.id", id), )
user := &User{ID: id} _, err := r.orm.Delete(user) if err != nil { span.RecordError(err) return fmt.Errorf("failed to delete user: %w", err) }
return nil}Redis Integration
package cache
import ( "context" "log"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/extra/redisotel/v9")
type RedisClient struct { client *redis.Client}
func NewRedisClient() *RedisClient { 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 &RedisClient{client: rdb}}
func (r *RedisClient) Get(ctx context.Context, key string) (string, error) { val, err := r.client.Get(ctx, key).Result() if err == redis.Nil { return "", nil // Key does not exist } else if err != nil { return "", err } return val, nil}
func (r *RedisClient) Set(ctx context.Context, key, value string, expiration time.Duration) error { return r.client.Set(ctx, key, value, expiration).Err()}
func (r *RedisClient) Delete(ctx context.Context, key string) error { return r.client.Del(ctx, key).Err()}
func (r *RedisClient) Close() error { return r.client.Close()}HTTP Client Instrumentation
Outgoing HTTP Requests
package controllers
import ( "encoding/json" "io" "net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute")
// ExternalAPIController handles external API callstype ExternalAPIController struct { BaseController httpClient *http.Client}
func NewExternalAPIController(instr *instrumentation.Instrumentation) *ExternalAPIController { return &ExternalAPIController{ BaseController: *NewBaseController(instr), httpClient: &http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), }, }}
// GetRandomJoke handles GET /api/v1/jokes/randomfunc (c *ExternalAPIController) GetRandomJoke() { span := c.GetSpan() span.SetAttributes(attribute.String("operation", "get_random_joke"))
// Make request with instrumentation ctx := c.GetSpanContext() req, err := http.NewRequestWithContext(ctx, "GET", "https://official-joke-api.appspot.com/random_joke", nil) if err != nil { span.RecordError(err) c.SetErrorResponse(500, "Failed to create request") return }
resp, err := c.httpClient.Do(req) if err != nil { span.RecordError(err) c.SetErrorResponse(500, "Failed to fetch joke") return } defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) if err != nil { span.RecordError(err) c.SetErrorResponse(500, "Failed to read response") return }
var joke map[string]interface{} if err := json.Unmarshal(body, &joke); err != nil { span.RecordError(err) c.SetErrorResponse(500, "Failed to parse response") return }
c.SetJSONResponse(200, joke)}// GetQuoteWithHttplib demonstrates manual instrumentation with Beego's httplibfunc (c *ExternalAPIController) GetQuoteWithHttplib() { span := c.GetSpan() span.SetAttributes(attribute.String("operation", "get_quote_httplib"))
// Create manual span for external call ctx := c.GetSpanContext() _, httpSpan := c.instrumentation.Tracer.Start(ctx, "http_client.get_quote") httpSpan.SetAttributes( attribute.String("http.method", "GET"), attribute.String("http.url", "https://api.quotegarden.io/api/v3/quotes/random"), ) defer httpSpan.End()
req := httplib.Get("https://api.quotegarden.io/api/v3/quotes/random")
// Manual header injection for context propagation carrier := make(map[string]string) otel.GetTextMapPropagator().Inject(ctx, propagation.MapCarrier(carrier)) for k, v := range carrier { req.Header(k, v) }
resp, err := req.Response() if err != nil { httpSpan.RecordError(err) span.RecordError(err) c.SetErrorResponse(500, "Failed to fetch quote") return } defer resp.Body.Close()
httpSpan.SetAttributes(attribute.Int("http.status_code", resp.StatusCode))
body, err := io.ReadAll(resp.Body) if err != nil { httpSpan.RecordError(err) span.RecordError(err) c.SetErrorResponse(500, "Failed to read response") return }
var quote map[string]interface{} if err := json.Unmarshal(body, "e); err != nil { span.RecordError(err) c.SetErrorResponse(500, "Failed to parse response") return }
c.SetJSONResponse(200, quote)}Production Deployment
Docker
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Install build dependenciesRUN apk add --no-cache git
# Copy go mod filesCOPY go.mod go.sum ./RUN go mod download
# Copy source codeCOPY . .
# Build the applicationRUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o beego-app .
FROM alpine:latest
# Install runtime dependenciesRUN apk --no-cache add ca-certificates curl
WORKDIR /root/
# Copy the binaryCOPY --from=builder /app/beego-app .
# Copy configuration files if anyCOPY --from=builder /app/conf ./conf
# Expose portEXPOSE 8080
# Health checkHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1
CMD ["./beego-app"]version: "3.8"
services: beego-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=beego-api - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0 - DATABASE_URL=postgres://user:pass@postgres:5432/beego_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: beego_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: beego-api labels: app: beego-apispec: replicas: 3 selector: matchLabels: app: beego-api template: metadata: labels: app: beego-api spec: containers: - name: beego-api image: your-registry/beego-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: "beego-api" - name: OTEL_RESOURCE_ATTRIBUTES value: "deployment.environment=production,service.version=1.0.0,k8s.cluster.name=production" - name: DATABASE_URL valueFrom: secretKeyRef: name: beego-secrets key: database-url - name: REDIS_URL valueFrom: configMapKeyRef: name: beego-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: beego-api-servicespec: selector: app: beego-api ports: - protocol: TCP port: 80 targetPort: 8080 type: LoadBalancerTesting the Integration
Test your Beego application:
-
Start the application
go run main.go -
Make test requests
# Health checkcurl http://localhost:8080/health# 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"}' -
View telemetry in Last9
Check your Last9 dashboard for:
- HTTP request traces with proper context propagation
- Database operation spans with query details
- Redis operation traces
- HTTP client spans for external API calls
- Custom business logic spans