Skip to main content

Send Traces to Beakpoint Insights using the OpenTelemetry Golang SDK

Prerequisites

Before you can send OpenTelemetry traces to Beakpoint Insights, ensure that you have:

  • A Beakpoint Insights account.
  • A Beakpoint Insights API key.

These are required to authenticate and send telemetry data successfully.

Install Required Packages

info

This guide assumes you're running go1.22.2.

First, add the necessary Go packages to your project. You can do this by using go get or by adding them to your go.mod file:

  go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/trace
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp

Configure the TracerProvider

The TracerProvider is the core component that manages how your application generates and exports traces. Here's an example of how to configure it:

main.go
func initTracerProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
// Create OTLP client for Beakpoint
client := otlptracehttp.NewClient(
otlptracehttp.WithEndpoint("otel.beakpointinsights.com"),
otlptracehttp.WithURLPath("/api/traces"),
otlptracehttp.WithHeaders(map[string]string{
"x-bkpt-key": "<YOUR_API_KEY>",
}),
)

// Create OTLP exporter
beakpointExporter, err := otlptrace.New(ctx, client)
if err != nil {
return nil, fmt.Errorf("failed to create OTLP exporter: %v", err)
}

// Create a resource with service information
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceNameKey.String("YourServiceName"),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %v", err)
}

// Create TracerProvider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(beakpointExporter),
sdktrace.WithResource(res),
)

// Set the global TracerProvider
otel.SetTracerProvider(tp)

return tp, nil
}

Providing Cost Calculation Tags

Understanding the cost of your infrastructure isn't possible without understanding what type of infrastructure component generated a given span. In distributed systems, different computing resources generate different types of spans, and each has its own cost attribution model. For example, a span from a Lambda function has different cost implications than a span from a continuously running EC2 instance.

View the cost attribution tags for our supported cloud services.

To accurately calculate costs, Beakpoint Insights needs specific metadata about the source of each span. The required attributes vary depending on the type of resource generating the telemetry data. For example, AWS Lambda functions require the following tags:

  • aws.lambda.architecture
  • aws.lambda.memory_size
  • aws.lambda.region
main.go
// attributeSpanProcessor is a custom span processor that adds AWS Lambda attributes to every span
type attributeSpanProcessor struct {
sdktrace.SpanProcessor
}

// OnStart implements SpanProcessor.OnStart and adds AWS Lambda attributes to every span
func (p *attributeSpanProcessor) OnStart(parent context.Context, s sdktrace.ReadWriteSpan) {
s.SetAttributes(
attribute.String("aws.lambda.architecture", getArchitecture()),
attribute.String("aws.lambda.memory_size", os.Getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")),
attribute.String("aws.region", os.Getenv("AWS_REGION")),

attribute.String("aws.lambda.function_name", os.Getenv("AWS_LAMBDA_FUNCTION_NAME")),
attribute.String("aws.lambda.request_id", os.Getenv("AWS_LAMBDA_REQUEST_ID")),
)
}

// Required interface implementations
func (p *attributeSpanProcessor) OnEnd(s sdktrace.ReadOnlySpan) {}
func (p *attributeSpanProcessor) Shutdown(context.Context) error { return nil }
func (p *attributeSpanProcessor) ForceFlush(context.Context) error { return nil }

// Helper function to determine architecture
func getArchitecture() string {
if strings.Contains(os.Getenv("AWS_EXECUTION_ENV"), "arm64") {
return "arm64"
}
return "x86_64"
}

Full Example

Here's a full example of how to set up OpenTelemetry in a .NET application running in AWS Lambda.

main.go
package main

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
)

var (
tracer trace.Tracer
httpClient *http.Client
)

// attributeSpanProcessor is a custom span processor that adds AWS Lambda attributes to every span
type attributeSpanProcessor struct {
sdktrace.SpanProcessor
}

// OnStart implements SpanProcessor.OnStart and adds AWS Lambda attributes to every span
func (p *attributeSpanProcessor) OnStart(parent context.Context, s sdktrace.ReadWriteSpan) {
s.SetAttributes(
attribute.String("aws.region", os.Getenv("AWS_REGION")),
attribute.String("aws.lambda.architecture", getArchitecture()),
attribute.String("aws.lambda.function_name", os.Getenv("AWS_LAMBDA_FUNCTION_NAME")),
attribute.String("aws.lambda.memory_size", os.Getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")),
attribute.String("aws.lambda.request_id", os.Getenv("AWS_LAMBDA_REQUEST_ID")),
)
}

// OnEnd implements SpanProcessor.OnEnd
func (p *attributeSpanProcessor) OnEnd(s sdktrace.ReadOnlySpan) {}
func (p *attributeSpanProcessor) Shutdown(context.Context) error { return nil }
func (p *attributeSpanProcessor) ForceFlush(context.Context) error { return nil }

// Response represents the structure of a response with message, metadata, and debug information.
type Response struct {
Message string `json:"message"`
Location string `json:"location"`
Memory string `json:"memory"`
Tracer string `json:"tracer"`
DebugLogs []string `json:"debug_logs"`
}

// init initializes the httpClient with an OpenTelemetry-enabled transport layer for HTTP requests.
func init() {
httpClient = &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
}

// initTracerProvider initializes and configures an OpenTelemetry tracer provider.
// It sets up a resource, OTLP exporter, and console exporter with service-specific attributes.
// Returns the initialized TracerProvider instance or an error if any configuration fails.
func initTracerProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
client := otlptracehttp.NewClient(
otlptracehttp.WithEndpoint("otel.beakpointinsights.com"),
otlptracehttp.WithURLPath("/api/traces"),
otlptracehttp.WithHeaders(map[string]string{
"x-bkpt-key": "<YOUR_API_KEY>",
}),
)

beakpointExporter, err := otlptrace.New(ctx, client)
if err != nil {
return nil, fmt.Errorf("failed to create OTLP exporter: %v", err)
}

consoleExporter, err := stdouttrace.New()
if err != nil {
return nil, fmt.Errorf("failed to create console exporter: %v", err)
}

res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceNameKey.String("LambdaTraceGenerator"),
attribute.String("aws.lambda.architecture", getArchitecture()),
attribute.String("aws.region", os.Getenv("AWS_REGION")),
),
resource.WithFromEnv(), // This will automatically read resource attributes from environment variables
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %v", err)
}

tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(beakpointExporter, sdktrace.WithMaxExportBatchSize(1)),
sdktrace.WithBatcher(consoleExporter, sdktrace.WithMaxExportBatchSize(1)),
sdktrace.WithResource(res),
sdktrace.WithSpanProcessor(&attributeSpanProcessor{}), // Add our custom processor
)

otel.SetTracerProvider(tp)
tracer = tp.Tracer("LambdaTraceGenerator")

return tp, nil
}

// getArchitecture determines the processor architecture based on the AWS_EXECUTION_ENV environment variable.
// Returns "arm64" if the environment variable contains "arm64", otherwise defaults to "x86_64".
func getArchitecture() string {
if strings.Contains(os.Getenv("AWS_EXECUTION_ENV"), "arm64") {
return "arm64"
}
return "x86_64"
}

// getCallingIP retrieves the public IP address of the caller by making an HTTP request to an external service.
// It utilizes AWS's `checkip.amazonaws.com` endpoint and returns the IP address as a string.
// The function supports tracing by starting and ending a span, and records any errors encountered during execution.
// Context propagation is used to ensure proper tracing and cancellation of requests if needed.
func getCallingIP(ctx context.Context) (string, error) {
ctx, span := tracer.Start(ctx, "getCallingIP")
defer span.End()

req, err := http.NewRequestWithContext(ctx, "GET", "http://checkip.amazonaws.com/", nil)
if err != nil {
if span != nil {
span.RecordError(err)
}
return "", err
}

req.Header.Set("User-Agent", "AWS Lambda Go Client")

resp, err := httpClient.Do(req)
if err != nil {
if span != nil {
span.RecordError(err)
}
return "", err
}
defer func() {
if cerr := resp.Body.Close(); cerr != nil {
if span != nil {
span.RecordError(cerr)
}
// If we don't already have an error, use this one
if err == nil {
err = cerr
}
}
}()

ip, err := io.ReadAll(resp.Body)
if err != nil {
if span != nil {
span.RecordError(err)
}
return "", err
}

callerIP := strings.TrimSpace(string(ip))
span.SetAttributes(attribute.String("caller.ip", callerIP))
return callerIP, nil
}

// handleRequest processes an API Gateway proxy request, initializes tracing, retrieves caller metadata, and forms a response.
func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
tp, err := initTracerProvider(ctx)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: fmt.Sprintf("Error initializing tracer: %v", err),
}, err
}
defer func() {
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
if err := tp.Shutdown(ctx); err != nil {
fmt.Printf("Error shutting down tracer provider: %v\n", err)
}
}()

ctx, span := tracer.Start(ctx, "handleRequest")
defer span.End()

location, err := getCallingIP(ctx)
if err != nil {
if span != nil {
span.RecordError(err)
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: fmt.Sprintf("Error getting IP: %v", err),
}, err
}

response := Response{
Message: "hello world",
Location: location,
Memory: os.Getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE"),
Tracer: fmt.Sprintf("%T", tp),
DebugLogs: []string{},
}

body, err := json.Marshal(response)
if err != nil {
if span != nil {
span.RecordError(err)
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: fmt.Sprintf("Error marshaling response: %v", err),
}, err
}

return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Body: string(body),
Headers: map[string]string{
"Content-Type": "application/json",
},
}, nil
}

func main() {
lambda.Start(otellambda.InstrumentHandler(handleRequest))
}