Go Gin OpenTelemetry Integration
This guide demonstrates how to integrate APItoolkit with your Go Chi application for distributed tracing using OpenTelemetry. We'll walk through the code and explain each part of the integration process.
To get started, you'll need to install OpenTelemetry Go packages and some basic configuration.
Prerequisites
Ensure you have already completed the first three steps of the onboarding guide.
Installation
Unlike NodeJs which has Auto Instrumentation, the corresponding Go OpenTelemetry initiative is still a work in progress. As a result, it will be a bit technical but not difficulty. So follow closely.
We will be using this Opensource simple rate limiter as our Starter project.
It has two branches;
- main: Without OpenTelemetry
- withotel_: With OpenTelemetry Integrated
This tutorial will guide you on how to move from are Uninstrumented service to an Instrumented one.
1. Add OpenTelemetry Instrumentation
Kindly run the command below to install the required packages and dependencies.
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
go get go.opentelemetry.io/otel/sdk/resource
go get go.opentelemetry.io/otel/sdk/trace
Ensure they are installed correctly
2. Initialize the OpenTelemetry SDK
This is required for any application that exports telemetry data.
Create an otel.go
file with OpenTelemetry SDK bootstrapping code for good separation of concern and for ease of continuous improvement. The content of this file would look like this:
package main
import (
"context"
"errors"
"os"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/propagation"
sdklog "go.opentelemetry.io/otel/sdk/log"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
var (
otlpEndpoint = os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
)
// setupOTelSDK bootstraps the OpenTelemetry pipeline.
func setupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
var shutdownFuncs []func(context.Context) error
// Set up propagator
prop := propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
otel.SetTextMapPropagator(prop)
// Set up trace provider
traceExporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(otlpEndpoint),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(traceExporter, sdktrace.WithBatchTimeout(5*time.Second)),
)
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
// Set up meter provider
metricExporter, err := stdoutmetric.New()
if err != nil {
return nil, err
}
meterProvider := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter, sdkmetric.WithInterval(3*time.Second))),
)
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
otel.SetMeterProvider(meterProvider)
// Set up logger provider
logExporter, err := stdoutlog.New()
if err != nil {
return nil, err
}
loggerProvider := sdklog.NewLoggerProvider(
sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)),
)
shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown)
global.SetLoggerProvider(loggerProvider)
// Return shutdown function
return func(ctx context.Context) error {
var err error
for _, fn := range shutdownFuncs {
err = errors.Join(err, fn(ctx))
}
return err
}, nil
}
What's happening above:
- This function sets up the OpenTelemetry tracer provider with an OTLP gRPC exporter.
- It configures the service name and ensures all spans are sampled.
3. Instrument your application
Now that we have the OpenTelemetry SDK initializer set up, we can instrument our HTTP server.
Modify your main.go
file to include code that initializes OpenTelemetry SDK and instruments the HTTP server using the otelhttp instrumentation library:
package main
import (
"context"
"flag"
"log"
"github.com/fatih/color"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"go.uber.org/ratelimit"
)
var (
limit ratelimit.Limiter
rps = flag.Int("rps", 100, "request per second")
)
func init() {
log.SetFlags(0)
log.SetPrefix("[GIN] ")
log.SetOutput(gin.DefaultWriter)
}
func ginRun(rps int) {
limit = ratelimit.New(rps)
app := gin.Default()
app.Use(otelgin.Middleware("gin test project"))
app.Use(leakBucket())
app.GET("/rate", handleRate)
log.Printf(color.CyanString("Current Rate Limit: %v requests/s", rps))
app.Run(":8081")
}
func main() {
flag.Parse()
// Set up OTel
ctx := context.Background()
shutdown, err := setupOTelSDK(ctx)
if err != nil {
log.Fatalf("Error setting up OTel SDK: %v", err)
}
defer func() {
if err := shutdown(ctx); err != nil {
log.Fatalf("Error shutting down OTel SDK: %v", err)
}
}()
ginRun(*rps)
}
4. Add Custom Instrumentation
Instrumentation libraries capture telemetry at the edges of your systems, such as inbound and outbound HTTP requests, but they don’t capture what’s going on in your application. For that you’ll need to write some custom manual instrumentation.
Go to the rate_limiter.go
and modify the handleRate
function to include custom instrumentation using OpenTelemetry API:
package main
import (
"fmt"
"log"
"time"
"github.com/fatih/color"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
func leakBucket() gin.HandlerFunc {
prev := time.Now()
return func(ctx *gin.Context) {
now := limit.Take()
log.Print(color.CyanString("%v", now.Sub(prev)))
prev = now
}
}
func handleRate(ctx *gin.Context) {
// Initialize OpenTelemetry tracer
tracer := otel.Tracer("rate-limiter-service")
_, span := tracer.Start(ctx, "rate-limiting")
defer span.End()
// Extract request data
method := ctx.Request.Method
scheme := "http"
statusCode := 200
host := ctx.Request.Host
port := ctx.Request.URL.Port()
if port == "" {
port = "8081"
}
startTime := time.Now()
// Set span status
span.SetStatus(codes.Ok, "")
// Add semantic conventions for HTTP request attributes
span.SetAttributes(
semconv.HTTPMethodKey.String(method),
semconv.HTTPSchemeKey.String(scheme),
semconv.HTTPStatusCodeKey.Int(statusCode),
semconv.HTTPTargetKey.String(ctx.Request.URL.Path),
semconv.HTTPURLKey.String(ctx.Request.URL.String()),
semconv.HTTPHostKey.String(host),
semconv.NetHostPortKey.String(port),
semconv.HTTPUserAgentKey.String(ctx.Request.UserAgent()),
semconv.HTTPRequestContentLengthKey.Int64(ctx.Request.ContentLength),
semconv.NetPeerIPKey.String(ctx.ClientIP()),
)
// Custom attributes for OpenTelemetry span
span.SetAttributes(
attribute.String("created_at", startTime.Format(time.RFC3339Nano)),
attribute.Float64("duration_ns", float64(time.Since(startTime).Nanoseconds())),
attribute.String("referer", ctx.Request.Referer()),
attribute.String("request_type", "Incoming"),
attribute.String("sdk_type", "go-gin"),
attribute.String("service_version", "1.0.0"), // Add your service version here
attribute.StringSlice("tags", []string{}),
)
// Additional request-specific attributes (path params, query params, headers, body)
span.SetAttributes(
attribute.String("query_params", ctx.Request.URL.RawQuery),
attribute.String("request_body", "{}"), // Assuming empty body for demonstration
attribute.String("request_headers", fmt.Sprintf("%v", ctx.Request.Header)),
attribute.String("response_body", "{}"),
attribute.String("response_headers", "{}"),
)
// Respond with JSON
ctx.JSON(statusCode, "rate limiting test")
}
Set Your Environment Variables
Before running your application, set the following environment variables:
export OTEL_TRACES_EXPORTER="otlp"
export OTEL_EXPORTER_OTLP_ENDPOINT="otelcol.apitoolkit.io:4317"
export OTEL_NODE_RESOURCE_DETECTORS="env,host,os"
export OTEL_SERVICE_NAME="my go gin project test"
export OTEL_RESOURCE_ATTRIBUTES=at-project-key="z6BJfZVEOSozztMfhqZsGTpG9DiXT9Weurvk1bpe9mwF8orB"
export OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
export OTEL_PROPAGATORS="baggage,tracecontext"
Quick overview of the configuration parameters
Attribute | Description |
---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | Specifies the endpoint URL for the OpenTelemetry collector. In this case, it's set to "otelcol.apitoolkit.io:4317". |
OTEL_NODE_RESOURCE_DETECTORS | Defines which resource detectors to use. Here, it's set to detect environment variables, host information, and operating system details. |
OTEL_SERVICE_NAME | Sets the name of your service. You should replace "your-service-name" with the actual name of your service. |
OTEL_RESOURCE_ATTRIBUTES | Specifies additional resource attributes. In this case, it's setting an API Toolkit project key. |
OTEL_EXPORTER_OTLP_PROTOCOL | Defines the protocol used for exporting telemetry data. It's set to "grpc" (gRPC protocol). |
OTEL_PROPAGATORS | Specifies which context propagators to use. Here, it's set to use both "baggage" and "tracecontext". |
5. Run the application to Ensure Everything works
Build and run the application with the following command:
go mod tidy
go run .
The server will start on port 8081. You can test it by sending a request:
curl http://localhost:8081/rate
This should return: "rate limiting test"
With this setup, your application will send traces to APItoolkit for visualization and analysis.
Tips
-
Remember to keep your APIToolkit project key (
at-project-key
) secure and not expose it in public repositories or logs. -
Ensure you don't make the mistake of adding the http scheme
http
orhttps
to your OTLP Endpoint