Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,19 @@ POSTGRES_SSL_MODE="disable"
# CORS
## Allowed Frontend Origins
ALLOWED_ORIGINS=http://localhost:3000,http://example.com

# OTEL Configuration
## Example for New Relic
OTEL_ENABLED=false

## Application name which will be shown in OTEL Frontend (e.g NewRelic, Dynatrace)
OTEL_SERVICE_NAME=my-service
OTEL_SERVICE_VERSION=1.0.0

## OTEL Exporter Configuration
OTEL_EXPORTER_OTLP_ENDPOINT=otlp.eu01.nr-data.net:4317
OTEL_EXPORTER_OTLP_HEADERS="api-key=XXXX"
OTEL_EXPORTER_OTLP_COMPRESSION=gzip
OTEL_EXPORTER_OTLP_QUEUE_SIZE=4094
OTEL_EXPORTER_OTLP_MAX_EXPORT_BATCH_SIZE=1024
OTEL_INSECURE=false
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This is a template for a Go API project. It includes the following features:
- API Security
- API Logging
- API Testing
- Platform agnostic OpenTelemetry

## Requirements

Expand Down
72 changes: 72 additions & 0 deletions bin/load-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

export const options = {
stages: [
{ duration: '30s', target: 5 }, // Ramp up to 5 users
{ duration: '1m', target: 5 }, // Stay at 5 users
{ duration: '20s', target: 10 }, // Ramp up to 10 users
{ duration: '1m', target: 10 }, // Stay at 10 users
{ duration: '20s', target: 0 }, // Scale down to 0 users
],
thresholds: {
http_req_duration: ['p(95)<2000'], // 95% of requests must complete below 2s
http_req_failed: ['rate<0.1'], // Less than 10% can fail
},
};

const BASE_URL = 'http://localhost:8080/api/v1';

// Generates a random UUID v4
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}

export default function () {
const userId = uuidv4();
const authToken = `Bearer ${randomString(32)}`;

// Add trace context headers
const headers = {
'Authorization': authToken,
'traceparent': `00-${randomString(32)}-${randomString(16)}-01`,
'Content-Type': 'application/json',
};

// Test user endpoint with trace context
const userResponse = http.get(`${BASE_URL}/users/${userId}`, {
headers: headers,
tags: { name: 'GetUserByID' },
});

// Check response and add custom span attributes via tags
check(userResponse, {
'status is 401 or 422': (r) => r.status === 401 || r.status === 422,
'response time OK': (r) => r.timings.duration < 2000,
});

// Add some variation in the test pattern
if (Math.random() < 0.3) {
// Simulate slow requests occasionally
sleep(2);
} else {
sleep(1);
}

// Test invalid UUID to generate error traces
if (Math.random() < 0.1) {
const invalidResponse = http.get(`${BASE_URL}/users/invalid-uuid`, {
headers: headers,
tags: { name: 'GetUserByIDInvalid' },
});

check(invalidResponse, {
'invalid uuid returns 400': (r) => r.status === 400,
});
}
}
2 changes: 2 additions & 0 deletions configuration/config.go → configuration/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Env struct {
PostgresPassword string
PostgresSSLMode string
AllowedOrigins []string
Telemetry *Telemetry
}

func Load() (*Env, error) {
Expand Down Expand Up @@ -51,5 +52,6 @@ func Load() (*Env, error) {
PostgresPassword: viper.GetString("POSTGRES_PASSWORD"),
PostgresSSLMode: viper.GetString("POSTGRES_SSL_MODE"),
AllowedOrigins: origins,
Telemetry: TelemetryNew(),
}, nil
}
56 changes: 56 additions & 0 deletions configuration/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package configuration

import (
"strings"

"github.com/spf13/viper"
)

type Telemetry struct {
Enabled bool
Environment string
Endpoint string
Headers map[string]string
Insecure bool
QueueSize int
MaxExportBatchSize int
Compression string
ServiceName string
ServiceVersion string
}

// TelemetryNew creates a new Telemetry configuration from environment
func TelemetryNew() *Telemetry {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets call it NewTelemetry() or NewOpenTelemetry() even

viper.SetDefault("OTEL_ENABLED", false)
viper.SetDefault("OTEL_SERVICE_NAME", "go-api-template")
viper.SetDefault("OTEL_SERVICE_VERSION", "1.0.0")
viper.SetDefault("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317")
viper.SetDefault("OTEL_INSECURE", true)
viper.SetDefault("OTEL_EXPORTER_OTLP_HEADERS", "")
viper.SetDefault("OTEL_EXPORTER_OTLP_QUEUE_SIZE", 4096)
viper.SetDefault("OTEL_EXPORTER_OTLP_MAX_EXPORT_BATCH_SIZE", 512)
viper.SetDefault("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip")

headers := make(map[string]string)
if headerStr := viper.GetString("OTEL_EXPORTER_OTLP_HEADERS"); headerStr != "" {
for _, header := range strings.Split(headerStr, ",") {
parts := strings.SplitN(header, "=", 2)
if len(parts) == 2 {
headers[parts[0]] = parts[1]
}
}
}

return &Telemetry{
Enabled: viper.GetBool("OTEL_ENABLED"),
ServiceName: viper.GetString("OTEL_SERVICE_NAME"),
ServiceVersion: viper.GetString("OTEL_SERVICE_VERSION"),
Environment: viper.GetString("APP_ENV"),
Endpoint: viper.GetString("OTEL_EXPORTER_OTLP_ENDPOINT"),
Headers: headers,
Insecure: viper.GetBool("OTEL_INSECURE"),
QueueSize: viper.GetInt("OTEL_EXPORTER_OTLP_QUEUE_SIZE"),
MaxExportBatchSize: viper.GetInt("OTEL_EXPORTER_OTLP_MAX_EXPORT_BATCH_SIZE"),
Compression: viper.GetString("OTEL_EXPORTER_OTLP_COMPRESSION"),
}
}
12 changes: 10 additions & 2 deletions controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

"github.com/gin-gonic/gin"
"github.com/go-openapi/strfmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)

type IUser interface {
Expand All @@ -14,11 +16,13 @@ type IUser interface {

type user struct {
service service.IUser
tracer trace.Tracer
}

func NewUser(service service.IUser) IUser {
return &user{
service: service,
tracer: otel.Tracer("controller/user"),
}
}

Expand All @@ -36,15 +40,19 @@ func NewUser(service service.IUser) IUser {
// @Failure 500 {object} model.InternalErrorResponse
// @Router /users/{user_id} [get]
func (controller *user) UserByID(ctx *gin.Context) {
// Validate path params
spanCtx, span := controller.tracer.Start(ctx.Request.Context(), "UserByID")
Copy link
Owner

@Matrix278 Matrix278 Apr 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets just call the spanCtx as normal ctx, the way you did within other folders

defer span.End()

userID := ctx.Param("user_id")
if !strfmt.IsUUID4(userID) {
span.RecordError(commonerrors.ErrInvalidUserID)
StatusBadRequest(ctx, commonerrors.ErrInvalidUserID)
return
}

response, err := controller.service.UserByID(ctx, strfmt.UUID4(userID))
response, err := controller.service.UserByID(spanCtx, strfmt.UUID4(userID))
if err != nil {
span.RecordError(err)
HandleCommonErrors(ctx, err)
return
}
Expand Down
34 changes: 26 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module go-api-template
go 1.23.5

require (
github.com/XSAM/otelsql v0.37.0
github.com/gin-gonic/gin v1.10.0
github.com/go-openapi/strfmt v0.23.0
github.com/go-playground/validator/v10 v10.25.0
Expand All @@ -16,7 +17,15 @@ require (
github.com/spf13/viper v1.20.0
github.com/stretchr/testify v1.10.0
github.com/swaggo/swag v1.16.4
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/trace v1.35.0
go.uber.org/zap v1.27.0
google.golang.org/grpc v1.71.0
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0
gorm.io/driver/postgres v1.5.11
)
Expand All @@ -26,14 +35,16 @@ require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/bytedance/sonic v1.12.10 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
Expand All @@ -43,6 +54,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
Expand All @@ -51,7 +63,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand All @@ -71,15 +83,21 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/net v0.36.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.36.1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/gorm v1.25.10 // indirect
Expand Down
Loading