Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7b8a92a7c | |||
| 1b9e566892 | |||
| 82de580879 | |||
| 92b202037a | |||
| af7683da9a | |||
| 3c801842e4 | |||
| d56e33b171 | |||
| 614cbf8097 | |||
| 1df4b0d4b4 | |||
| d43ff0f2a9 |
@@ -75,13 +75,17 @@ sslmode: prefer
|
||||
|
||||
### PoolOptions
|
||||
|
||||
Defaults match [pgxpool defaults](https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Config):
|
||||
|
||||
- `ConfigFiles` - List of config file paths to search (default: `database.yaml`, `/vault/secrets/database.yaml`)
|
||||
- `MinConns` - Minimum connections (default: 0)
|
||||
- `MaxConns` - Maximum connections (default: 25)
|
||||
- `MaxConns` - Maximum connections (default: 4)
|
||||
- `MaxConnLifetime` - Connection lifetime (default: 1 hour)
|
||||
- `MaxConnIdleTime` - Idle timeout (default: 30 minutes)
|
||||
- `HealthCheckPeriod` - Health check interval (default: 1 minute)
|
||||
|
||||
For higher connection limits, set via `PoolOptions` or URI query parameter `?pool_max_conns=25`.
|
||||
|
||||
### PostgreSQL Config Fields
|
||||
|
||||
- `host` - Database host (required)
|
||||
@@ -93,8 +97,9 @@ sslmode: prefer
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `DATABASE_URI` - PostgreSQL connection URI (takes precedence over config files)
|
||||
- `DATABASE_URI` - PostgreSQL connection URI (takes precedence over all other methods)
|
||||
- `DATABASE_CONFIG_FILE` - Override config file location
|
||||
- Standard `PG*` variables (`PGHOST`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGPORT`, `PGSSLMODE`, etc.) - used as fallback when no config file is found; parsed natively by pgx
|
||||
|
||||
### URI Format
|
||||
|
||||
@@ -108,7 +113,7 @@ Pool settings can be included in the URI query string:
|
||||
- `pool_max_conn_lifetime`, `pool_max_conn_idle_time`
|
||||
- `pool_health_check_period`
|
||||
|
||||
When using `DATABASE_URI`, `PoolOptions` are ignored and all settings come from the URI.
|
||||
When using `DATABASE_URI`, pool settings come from the URI. Since `PoolOptions` defaults match pgxpool defaults, behavior is consistent whether using URI or config files.
|
||||
|
||||
Example with CloudNativePG:
|
||||
```yaml
|
||||
|
||||
@@ -9,42 +9,50 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"go.ntppool.org/common/database"
|
||||
"go.ntppool.org/common/logger"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// PoolOptions configures pgxpool connection behavior
|
||||
// PoolOptions configures pgxpool connection behavior.
|
||||
//
|
||||
// Default values match pgxpool defaults from github.com/jackc/pgx/v5/pgxpool.
|
||||
// See: https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Config
|
||||
//
|
||||
// To customize pool settings, either:
|
||||
// - Modify PoolOptions before calling OpenPool (for config file mode)
|
||||
// - Use URI query parameters like ?pool_max_conns=25 (for DATABASE_URI mode)
|
||||
type PoolOptions struct {
|
||||
// ConfigFiles is a list of config file paths to search for database configuration
|
||||
ConfigFiles []string
|
||||
|
||||
// MinConns is the minimum number of connections in the pool
|
||||
// Default: 0 (no minimum)
|
||||
// MinConns is the minimum number of connections in the pool.
|
||||
// Default: 0 (matches pgxpool default)
|
||||
MinConns int32
|
||||
|
||||
// MaxConns is the maximum number of connections in the pool
|
||||
// Default: 25
|
||||
// MaxConns is the maximum number of connections in the pool.
|
||||
// Default: 4 (matches pgxpool default)
|
||||
// For higher concurrency, increase via PoolOptions or URI ?pool_max_conns=N
|
||||
MaxConns int32
|
||||
|
||||
// MaxConnLifetime is the maximum lifetime of a connection
|
||||
// Default: 1 hour
|
||||
// MaxConnLifetime is the maximum lifetime of a connection.
|
||||
// Default: 1 hour (matches pgxpool default)
|
||||
MaxConnLifetime time.Duration
|
||||
|
||||
// MaxConnIdleTime is the maximum idle time of a connection
|
||||
// Default: 30 minutes
|
||||
// MaxConnIdleTime is the maximum idle time of a connection.
|
||||
// Default: 30 minutes (matches pgxpool default)
|
||||
MaxConnIdleTime time.Duration
|
||||
|
||||
// HealthCheckPeriod is how often to check connection health
|
||||
// Default: 1 minute
|
||||
// HealthCheckPeriod is how often to check connection health.
|
||||
// Default: 1 minute (matches pgxpool default)
|
||||
HealthCheckPeriod time.Duration
|
||||
}
|
||||
|
||||
// DefaultPoolOptions returns sensible defaults for pgxpool
|
||||
// DefaultPoolOptions returns defaults matching pgxpool.
|
||||
// See https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Config for pgxpool defaults.
|
||||
func DefaultPoolOptions() PoolOptions {
|
||||
return PoolOptions{
|
||||
ConfigFiles: GetConfigFiles(),
|
||||
MinConns: 0,
|
||||
MaxConns: 25,
|
||||
MaxConns: 4,
|
||||
MaxConnLifetime: time.Hour,
|
||||
MaxConnIdleTime: 30 * time.Minute,
|
||||
HealthCheckPeriod: time.Minute,
|
||||
@@ -58,13 +66,12 @@ func DefaultPoolOptions() PoolOptions {
|
||||
// 1. DATABASE_URI environment variable (pool settings can be included in URI)
|
||||
// 2. DATABASE_CONFIG_FILE environment variable (YAML)
|
||||
// 3. Default config files (database.yaml, /vault/secrets/database.yaml)
|
||||
// 4. Standard PG* environment variables (PGHOST, PGUSER, PGPASSWORD, PGDATABASE, etc.)
|
||||
//
|
||||
// When using DATABASE_URI, pool settings (pool_max_conns, pool_min_conns, etc.)
|
||||
// can be specified in the URI query string and PoolOptions are ignored.
|
||||
// When using config files, PoolOptions are applied.
|
||||
func OpenPool(ctx context.Context, options PoolOptions) (*pgxpool.Pool, error) {
|
||||
log := logger.FromContext(ctx)
|
||||
|
||||
// Validate PoolOptions
|
||||
if options.MaxConns <= 0 {
|
||||
return nil, fmt.Errorf("pgdb: MaxConns must be positive, got: %d", options.MaxConns)
|
||||
@@ -80,33 +87,33 @@ func OpenPool(ctx context.Context, options PoolOptions) (*pgxpool.Pool, error) {
|
||||
var err error
|
||||
|
||||
// Check DATABASE_URI environment variable first (highest priority)
|
||||
// When using DATABASE_URI, pool settings come from URI query parameters
|
||||
// (e.g., ?pool_max_conns=25). PoolOptions are not applied since our defaults
|
||||
// match pgxpool defaults.
|
||||
if uri := os.Getenv("DATABASE_URI"); uri != "" {
|
||||
poolConfig, err = pgxpool.ParseConfig(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse DATABASE_URI: %w", err)
|
||||
}
|
||||
|
||||
// Log when PoolOptions differ from defaults (they will be ignored)
|
||||
defaults := DefaultPoolOptions()
|
||||
if options.MaxConns != defaults.MaxConns || options.MinConns != defaults.MinConns {
|
||||
log.WarnContext(ctx, "DATABASE_URI is set; PoolOptions are ignored (use URI query parameters for pool settings)",
|
||||
"ignored_max_conns", options.MaxConns,
|
||||
"ignored_min_conns", options.MinConns,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Fall back to config file approach
|
||||
pgCfg, _, err := FindConfig(options.ConfigFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
pgCfg, _, configErr := FindConfig(options.ConfigFiles)
|
||||
if configErr != nil {
|
||||
// No config file found; try standard PG* environment variables
|
||||
// (PGHOST, PGUSER, PGPASSWORD, PGDATABASE, etc.)
|
||||
// pgxpool.ParseConfig("") reads these natively.
|
||||
poolConfig, err = pgxpool.ParseConfig("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w (also tried PG* environment variables: %w)", configErr, err)
|
||||
}
|
||||
} else {
|
||||
poolConfig, err = CreatePoolConfig(pgCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
poolConfig, err = CreatePoolConfig(pgCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply pool-specific settings from PoolOptions (config files don't support these)
|
||||
// Apply pool-specific settings from PoolOptions (config files and PG* vars don't support these)
|
||||
poolConfig.MinConns = options.MinConns
|
||||
poolConfig.MaxConns = options.MaxConns
|
||||
poolConfig.MaxConnLifetime = options.MaxConnLifetime
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"go.ntppool.org/common/database"
|
||||
)
|
||||
|
||||
@@ -115,8 +116,8 @@ func TestDefaultPoolOptions(t *testing.T) {
|
||||
if opts.MinConns != 0 {
|
||||
t.Errorf("Expected MinConns=0, got %d", opts.MinConns)
|
||||
}
|
||||
if opts.MaxConns != 25 {
|
||||
t.Errorf("Expected MaxConns=25, got %d", opts.MaxConns)
|
||||
if opts.MaxConns != 4 {
|
||||
t.Errorf("Expected MaxConns=4 (pgxpool default), got %d", opts.MaxConns)
|
||||
}
|
||||
if opts.MaxConnLifetime != time.Hour {
|
||||
t.Errorf("Expected MaxConnLifetime=1h, got %v", opts.MaxConnLifetime)
|
||||
@@ -184,6 +185,86 @@ func TestOpenPoolWithDatabaseURI(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPGEnvVarsFallback(t *testing.T) {
|
||||
// Verify that pgxpool.ParseConfig("") picks up PG* env vars
|
||||
t.Setenv("PGHOST", "pg.example.com")
|
||||
t.Setenv("PGUSER", "pguser")
|
||||
t.Setenv("PGDATABASE", "pgdb")
|
||||
t.Setenv("PGPORT", "5433")
|
||||
t.Setenv("PGSSLMODE", "require")
|
||||
|
||||
cfg, err := pgxpool.ParseConfig("")
|
||||
if err != nil {
|
||||
t.Fatalf("pgxpool.ParseConfig(\"\") failed: %v", err)
|
||||
}
|
||||
if cfg.ConnConfig.Host != "pg.example.com" {
|
||||
t.Errorf("Expected Host=pg.example.com, got %s", cfg.ConnConfig.Host)
|
||||
}
|
||||
if cfg.ConnConfig.User != "pguser" {
|
||||
t.Errorf("Expected User=pguser, got %s", cfg.ConnConfig.User)
|
||||
}
|
||||
if cfg.ConnConfig.Database != "pgdb" {
|
||||
t.Errorf("Expected Database=pgdb, got %s", cfg.ConnConfig.Database)
|
||||
}
|
||||
if cfg.ConnConfig.Port != 5433 {
|
||||
t.Errorf("Expected Port=5433, got %d", cfg.ConnConfig.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenPoolPGEnvVarsFallback(t *testing.T) {
|
||||
// Unset DATABASE_URI to ensure we don't take that path
|
||||
t.Setenv("DATABASE_URI", "")
|
||||
|
||||
// Set PG* vars pointing to a non-listening port
|
||||
t.Setenv("PGHOST", "127.0.0.1")
|
||||
t.Setenv("PGPORT", "59998")
|
||||
t.Setenv("PGUSER", "testuser")
|
||||
t.Setenv("PGDATABASE", "testdb")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opts := DefaultPoolOptions()
|
||||
opts.ConfigFiles = []string{"/nonexistent/path/database.yaml"}
|
||||
|
||||
_, err := OpenPool(ctx, opts)
|
||||
|
||||
// Should fail with connection error (not config file error),
|
||||
// proving the PG* fallback path was taken
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "no valid config file") {
|
||||
t.Errorf("expected connection error from PG* fallback, got config file error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPGEnvVarsFallbackAppliesPoolOptions(t *testing.T) {
|
||||
// Verify that PoolOptions are applied when using the PG* fallback path.
|
||||
// We can't call OpenPool (it would try to connect), so we verify the
|
||||
// behavior by confirming pgxpool.ParseConfig("") succeeds and that the
|
||||
// code path applies PoolOptions. We test this indirectly via
|
||||
// TestOpenPoolPGEnvVarsFallback (connection error proves PG* path was
|
||||
// taken with pool options applied).
|
||||
|
||||
// pgxpool.ParseConfig("") always succeeds (falls back to libpq defaults),
|
||||
// so when no config files exist the PG* path is always attempted.
|
||||
t.Setenv("DATABASE_URI", "")
|
||||
t.Setenv("PGHOST", "192.0.2.1") // RFC 5737 TEST-NET, won't connect
|
||||
t.Setenv("PGPORT", "5432")
|
||||
t.Setenv("PGUSER", "testuser")
|
||||
t.Setenv("PGDATABASE", "testdb")
|
||||
|
||||
cfg, err := pgxpool.ParseConfig("")
|
||||
if err != nil {
|
||||
t.Fatalf("pgxpool.ParseConfig(\"\") failed: %v", err)
|
||||
}
|
||||
if cfg.ConnConfig.Host != "192.0.2.1" {
|
||||
t.Errorf("Expected Host=192.0.2.1, got %s", cfg.ConnConfig.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseURIPrecedence(t *testing.T) {
|
||||
// Test that DATABASE_URI takes precedence over config files
|
||||
// We use localhost with a port that's unlikely to have postgres running
|
||||
|
||||
28
ekko/ekko.go
28
ekko/ekko.go
@@ -34,6 +34,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo-contrib/echoprometheus"
|
||||
@@ -70,6 +72,7 @@ import (
|
||||
// - WithGzipConfig(): Custom gzip compression settings
|
||||
func New(name string, options ...func(*Ekko)) (*Ekko, error) {
|
||||
ek := &Ekko{
|
||||
name: name,
|
||||
writeTimeout: 60 * time.Second,
|
||||
readHeaderTimeout: 30 * time.Second,
|
||||
}
|
||||
@@ -77,6 +80,20 @@ func New(name string, options ...func(*Ekko)) (*Ekko, error) {
|
||||
for _, o := range options {
|
||||
o(ek)
|
||||
}
|
||||
|
||||
if ek.name == "" {
|
||||
if bi, ok := debug.ReadBuildInfo(); ok && bi.Main.Path != "" {
|
||||
if idx := strings.LastIndex(bi.Main.Path, "/"); idx >= 0 {
|
||||
ek.name = bi.Main.Path[idx+1:]
|
||||
} else {
|
||||
ek.name = bi.Main.Path
|
||||
}
|
||||
}
|
||||
if ek.name == "" {
|
||||
ek.name = "ekko-app"
|
||||
}
|
||||
}
|
||||
|
||||
return ek, nil
|
||||
}
|
||||
|
||||
@@ -146,6 +163,7 @@ func (ek *Ekko) setup(ctx context.Context) (*echo.Echo, error) {
|
||||
echo.TrustLinkLocal(false),
|
||||
echo.TrustPrivateNet(true),
|
||||
}
|
||||
trustOptions = append(trustOptions, ek.extraTrustOptions...)
|
||||
e.IPExtractor = echo.ExtractIPFromXFFHeader(trustOptions...)
|
||||
|
||||
if ek.otelmiddleware == nil {
|
||||
@@ -162,12 +180,10 @@ func (ek *Ekko) setup(ctx context.Context) (*echo.Echo, error) {
|
||||
},
|
||||
}))
|
||||
|
||||
e.Use(slogecho.NewWithConfig(log,
|
||||
slogecho.Config{
|
||||
WithTraceID: false, // done by logger already
|
||||
Filters: ek.logFilters,
|
||||
},
|
||||
))
|
||||
logConfig := slogecho.DefaultConfig()
|
||||
logConfig.WithTraceID = false // done by logger already
|
||||
logConfig.Filters = ek.logFilters
|
||||
e.Use(slogecho.NewWithConfig(log, logConfig))
|
||||
|
||||
if ek.prom != nil {
|
||||
e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{
|
||||
|
||||
@@ -13,13 +13,14 @@ import (
|
||||
// It encapsulates server configuration, middleware options, and lifecycle management
|
||||
// for NTP Pool web services. Use New() with functional options to configure.
|
||||
type Ekko struct {
|
||||
name string
|
||||
prom prometheus.Registerer
|
||||
port int
|
||||
routeFn func(e *echo.Echo) error
|
||||
logFilters []slogecho.Filter
|
||||
otelmiddleware echo.MiddlewareFunc
|
||||
gzipConfig *middleware.GzipConfig
|
||||
name string
|
||||
prom prometheus.Registerer
|
||||
port int
|
||||
routeFn func(e *echo.Echo) error
|
||||
logFilters []slogecho.Filter
|
||||
otelmiddleware echo.MiddlewareFunc
|
||||
gzipConfig *middleware.GzipConfig
|
||||
extraTrustOptions []echo.TrustOption
|
||||
|
||||
writeTimeout time.Duration
|
||||
readHeaderTimeout time.Duration
|
||||
@@ -92,6 +93,16 @@ func WithReadHeaderTimeout(t time.Duration) func(*Ekko) {
|
||||
}
|
||||
}
|
||||
|
||||
// WithTrustOptions appends additional trust options to the default IP extraction
|
||||
// configuration. These options are applied after the built-in trust settings
|
||||
// (loopback trusted, link-local untrusted, private networks trusted) when
|
||||
// extracting client IPs from the X-Forwarded-For header.
|
||||
func WithTrustOptions(opts ...echo.TrustOption) func(*Ekko) {
|
||||
return func(ek *Ekko) {
|
||||
ek.extraTrustOptions = append(ek.extraTrustOptions, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithGzipConfig provides custom gzip compression configuration.
|
||||
// By default, gzip compression is enabled with standard settings.
|
||||
// Use this option to customize compression level, skip patterns, or disable compression.
|
||||
|
||||
123
go.mod
123
go.mod
@@ -1,91 +1,94 @@
|
||||
module go.ntppool.org/common
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/abh/certman v0.4.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/labstack/echo-contrib v0.17.2
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/prometheus/client_model v0.6.1
|
||||
github.com/remychantenay/slog-otel v1.3.2
|
||||
github.com/samber/slog-echo v1.14.8
|
||||
github.com/samber/slog-multi v1.2.4
|
||||
github.com/segmentio/kafka-go v0.4.47
|
||||
github.com/spf13/cobra v1.8.1
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.8.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.58.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.58.0
|
||||
go.opentelemetry.io/otel v1.33.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0
|
||||
go.opentelemetry.io/otel/log v0.9.0
|
||||
go.opentelemetry.io/otel/metric v1.33.0
|
||||
go.opentelemetry.io/otel/sdk v1.33.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.9.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0
|
||||
go.opentelemetry.io/otel/trace v1.33.0
|
||||
golang.org/x/mod v0.28.0
|
||||
golang.org/x/net v0.44.0
|
||||
golang.org/x/sync v0.17.0
|
||||
google.golang.org/grpc v1.69.2
|
||||
github.com/jackc/pgx/v5 v5.9.0
|
||||
github.com/labstack/echo-contrib v0.50.1
|
||||
github.com/labstack/echo/v4 v4.15.1
|
||||
github.com/oklog/ulid/v2 v2.1.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/remychantenay/slog-otel v1.3.5
|
||||
github.com/samber/slog-echo v1.21.0
|
||||
github.com/samber/slog-multi v1.7.1
|
||||
github.com/segmentio/kafka-go v0.4.50
|
||||
github.com/spf13/cobra v1.10.2
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.67.0
|
||||
go.opentelemetry.io/otel v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
|
||||
go.opentelemetry.io/otel/log v0.18.0
|
||||
go.opentelemetry.io/otel/metric v1.42.0
|
||||
go.opentelemetry.io/otel/sdk v1.42.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.18.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0
|
||||
go.opentelemetry.io/otel/trace v1.42.0
|
||||
golang.org/x/mod v0.34.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/grpc v1.79.3
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dmarkham/enumer v1.6.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/masaushi/accessory v0.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pascaldekloe/name v1.0.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/common v0.61.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/samber/lo v1.47.0 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/samber/lo v1.53.0 // indirect
|
||||
github.com/samber/slog-common v0.20.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.55.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
|
||||
313
go.sum
313
go.sum
@@ -1,119 +1,118 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/abh/certman v0.4.0 h1:XHoDtb0YyRQPclaHMrBDlKTVZpNjTK6vhB0S3Bd/Sbs=
|
||||
github.com/abh/certman v0.4.0/go.mod h1:x8QhpKVZifmV1Hdiwdg9gLo2GMPAxezz1s3zrVnPs+I=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dmarkham/enumer v1.6.1 h1:aSc9awYtZL07TUueWs40QcHtxTvHTAwG0EqrNsK45w4=
|
||||
github.com/dmarkham/enumer v1.6.1/go.mod h1:yixql+kDDQRYqcuBM2n9Vlt7NoT9ixgXhaXry8vmRg8=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/pgx/v5 v5.9.0 h1:T/dI+2TvmI2H8s/KH1/lXIbz1CUFk3gn5oTjr0/mBsE=
|
||||
github.com/jackc/pgx/v5 v5.9.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w=
|
||||
github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/echo-contrib v0.50.1 h1:W9cZZ9viA4TDdFtm8cuA+XGFwOcnfbjJpl7VgfsRLHE=
|
||||
github.com/labstack/echo-contrib v0.50.1/go.mod h1:8r/++U/Fw/QniApFnzunLanKaviPfBX7fX7/2QX0qOk=
|
||||
github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs=
|
||||
github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/masaushi/accessory v0.6.0 h1:HYAzxkuhfvlbaQwinxXTxsSPbFabAnwHt8K6I/DvNBU=
|
||||
github.com/masaushi/accessory v0.6.0/go.mod h1:8GZMgq3wcIapVZWt7VVQCh5+onPc/8gJeHb8WRXezvQ=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U=
|
||||
github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
|
||||
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/remychantenay/slog-otel v1.3.2 h1:ZBx8qnwfLJ6e18Vba4e9Xp9B7khTmpIwFsU1sAmActw=
|
||||
github.com/remychantenay/slog-otel v1.3.2/go.mod h1:gKW4tQ8cGOKoA+bi7wtYba/tcJ6Tc9XyQ/EW8gHA/2E=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/remychantenay/slog-otel v1.3.5 h1:VBxvLh6wJ+ioY9Lup66Bin8UWzRYdVYCDhr8cpBneM4=
|
||||
github.com/remychantenay/slog-otel v1.3.5/go.mod h1:ZkazuFMICKGDrO0r1njxKRdjTt/YcXKn6v2+0q/b0+U=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/samber/slog-echo v1.14.8 h1:R7RF2LWEepsKtC7i6A6o9peS3Rz5HO8+H8OD+8mPD1I=
|
||||
github.com/samber/slog-echo v1.14.8/go.mod h1:K21nbusPmai/MYm8PFactmZoFctkMmkeaTdXXyvhY1c=
|
||||
github.com/samber/slog-multi v1.2.4 h1:k9x3JAWKJFPKffx+oXZ8TasaNuorIW4tG+TXxkt6Ry4=
|
||||
github.com/samber/slog-multi v1.2.4/go.mod h1:ACuZ5B6heK57TfMVkVknN2UZHoFfjCwRxR0Q2OXKHlo=
|
||||
github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
|
||||
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
|
||||
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
|
||||
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
|
||||
github.com/samber/slog-echo v1.21.0 h1:7qyzNeYTpbCbBrlF7C3XY1gsG5LHdrDVd0Ci9mSWRl0=
|
||||
github.com/samber/slog-echo v1.21.0/go.mod h1:caG3zeXgrPRlGKaPVqyWG1MEc6nwrmtDjoLN/mc0PrM=
|
||||
github.com/samber/slog-multi v1.7.1 h1:aCLXHRxgU+2v0PVlEOh7phynzM7CRo89ZgFtOwaqVEE=
|
||||
github.com/samber/slog-multi v1.7.1/go.mod h1:A4KQC99deqfkCDJcL/cO3kX6McX7FffQAx/8QHink+c=
|
||||
github.com/segmentio/kafka-go v0.4.50 h1:mcyC3tT5WeyWzrFbd6O374t+hmcu1NKt2Pu1L3QaXmc=
|
||||
github.com/segmentio/kafka-go v0.4.50/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
@@ -124,124 +123,90 @@ github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.8.0 h1:G3sKsNueSdxuACINFxKrQeimAIst0A5ytA2YJH+3e1c=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.8.0/go.mod h1:ptJm3wizguEPurZgarDAwOeX7O0iMR7l+QvIVenhYdE=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.58.0 h1:gQFwWiqm4JUvOjpdmyU0di+2pVQ8QNpk1Ak/54Y6NcY=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.58.0/go.mod h1:CNyFi9PuvHtEJNmMFHaXZMuA4XmgRXIqpFcHdqzLvVU=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.58.0 h1:qVsDVgZd/bC6ZKDOHSjILpm0T/BWvASC9cQU3GYga78=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.58.0/go.mod h1:bAv7mY+5qTsFPFaRpr75vDOocX09I36QH4Rg0slEG/U=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.58.0 h1:DBk8Zh+Yn3WtWCdGSx1pbEV9/naLtjG16c1zwQA2MBI=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.58.0/go.mod h1:DFx32LPclW1MNdSKIMrjjetsk0tJtYhAvuGjDIG2SKE=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.33.0 h1:ig/IsHyyoQ1F1d6FUDIIW5oYpsuTVtN16AyGOgdjAHQ=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.33.0/go.mod h1:EsVYoNy+Eol5znb6wwN3XQTILyjl040gUpEnUSNZfsk=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 h1:gA2gh+3B3NDvRFP30Ufh7CC3TtJRbUSf2TTD0LbCagw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0/go.mod h1:smRTR+02OtrVGjvWE1sQxhuazozKc/BXvvqqnmOxy+s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0 h1:Za0Z/j9Gf3Z9DKQ1choU9xI2noCxlkcyFFP2Ob3miEQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0/go.mod h1:jMRB8N75meTNjDFQyJBA/2Z9en21CsxwMctn08NHY6c=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 h1:bSjzTvsXZbLSWU8hnZXcKmEVaJjjnandxD0PxThhVU8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0/go.mod h1:aj2rilHL8WjXY1I5V+ra+z8FELtk681deydgYT8ikxU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.55.0 h1:sSPw658Lk2NWAv74lkD3B/RSDb+xRFx46GjkrL3VUZo=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.55.0/go.mod h1:nC00vyCmQixoeaxF6KNyP42II/RHa9UdruK02qBmHvI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0 h1:iI15wfQb5ZtAVTdS5WROxpYmw6Kjez3hT9SuzXhrgGQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0/go.mod h1:yepwlNzVVxHWR5ugHIrll+euPQPq4pvysHTDr/daV9o=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk=
|
||||
go.opentelemetry.io/otel/log v0.9.0 h1:0OiWRefqJ2QszpCiqwGO0u9ajMPe17q6IscQvvp3czY=
|
||||
go.opentelemetry.io/otel/log v0.9.0/go.mod h1:WPP4OJ+RBkQ416jrFCQFuFKtXKD6mOoYCQm6ykK8VaU=
|
||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/sdk/log v0.9.0 h1:YPCi6W1Eg0vwT/XJWsv2/PaQ2nyAJYuF7UUjQSBe3bc=
|
||||
go.opentelemetry.io/otel/sdk/log v0.9.0/go.mod h1:y0HdrOz7OkXQBuc2yjiqnEHc+CRKeVhRE3hx4RwTmV4=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0 h1:NFIS6x7wyObQ7cR84x7bt1sr8nYBx89s3x3GwRjw40k=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0/go.mod h1:39SaByOyDMRMe872AE7uelMuQZidIw7LLFAnQi0FWTE=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.67.0 h1:0FKdyaoWXDmSCpQuv3m2UiJIRNxb1CK1mILy5QyKxc4=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.67.0/go.mod h1:IXtTS6zjKfM2yNRD9rWOS7SfIYGtuLGhL9ent5WX3Uk=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
|
||||
go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg=
|
||||
go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw=
|
||||
go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
||||
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
52
internal/tracerconfig/auth.go
Normal file
52
internal/tracerconfig/auth.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package tracerconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// bearerCredentials implements gRPC PerRPCCredentials for bearer token authentication.
|
||||
// It is safe for concurrent use as required by the gRPC PerRPCCredentials interface.
|
||||
type bearerCredentials struct {
|
||||
tokenFunc BearerTokenFunc
|
||||
}
|
||||
|
||||
// GetRequestMetadata returns authorization metadata for each RPC call.
|
||||
// It calls the token function to retrieve the current token.
|
||||
func (c *bearerCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
token, err := c.tokenFunc(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token == "" {
|
||||
return nil, nil // Omit header for empty token
|
||||
}
|
||||
return map[string]string{"authorization": "Bearer " + token}, nil
|
||||
}
|
||||
|
||||
// RequireTransportSecurity returns true because bearer tokens require TLS.
|
||||
func (c *bearerCredentials) RequireTransportSecurity() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// bearerRoundTripper wraps an http.RoundTripper to add bearer token authentication.
|
||||
// It is safe for concurrent use as required by the http.RoundTripper interface.
|
||||
type bearerRoundTripper struct {
|
||||
base http.RoundTripper
|
||||
tokenFunc BearerTokenFunc
|
||||
}
|
||||
|
||||
// RoundTrip adds the Authorization header with the bearer token.
|
||||
func (rt *bearerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
token, err := rt.tokenFunc(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token == "" {
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
// Clone only when adding a header to preserve the original request
|
||||
req = req.Clone(req.Context())
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
326
internal/tracerconfig/auth_test.go
Normal file
326
internal/tracerconfig/auth_test.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package tracerconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBearerCredentials_GetRequestMetadata(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tokenFunc BearerTokenFunc
|
||||
wantMeta map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid token",
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
return "test-token-123", nil
|
||||
},
|
||||
wantMeta: map[string]string{"authorization": "Bearer test-token-123"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty token returns nil map",
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
wantMeta: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "token function error",
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
return "", errors.New("token retrieval failed")
|
||||
},
|
||||
wantMeta: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &bearerCredentials{tokenFunc: tt.tokenFunc}
|
||||
meta, err := c.GetRequestMetadata(context.Background())
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetRequestMetadata() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantMeta == nil && meta != nil {
|
||||
t.Errorf("GetRequestMetadata() = %v, want nil", meta)
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantMeta != nil {
|
||||
if meta == nil {
|
||||
t.Errorf("GetRequestMetadata() = nil, want %v", tt.wantMeta)
|
||||
return
|
||||
}
|
||||
for k, v := range tt.wantMeta {
|
||||
if meta[k] != v {
|
||||
t.Errorf("GetRequestMetadata()[%q] = %q, want %q", k, meta[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerCredentials_GetRequestMetadata_ContextPassed(t *testing.T) {
|
||||
type ctxKey string
|
||||
key := ctxKey("test-key")
|
||||
expectedValue := "test-value"
|
||||
|
||||
var receivedCtx context.Context
|
||||
c := &bearerCredentials{
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
receivedCtx = ctx
|
||||
return "token", nil
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), key, expectedValue)
|
||||
_, err := c.GetRequestMetadata(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRequestMetadata() error = %v", err)
|
||||
}
|
||||
|
||||
if receivedCtx == nil {
|
||||
t.Fatal("context was not passed to tokenFunc")
|
||||
}
|
||||
|
||||
if receivedCtx.Value(key) != expectedValue {
|
||||
t.Errorf("context value = %v, want %v", receivedCtx.Value(key), expectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerCredentials_RequireTransportSecurity(t *testing.T) {
|
||||
c := &bearerCredentials{tokenFunc: func(ctx context.Context) (string, error) {
|
||||
return "", nil
|
||||
}}
|
||||
|
||||
if !c.RequireTransportSecurity() {
|
||||
t.Error("RequireTransportSecurity() = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerRoundTripper_RoundTrip(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tokenFunc BearerTokenFunc
|
||||
wantAuthHeader string
|
||||
wantErr bool
|
||||
serverShouldRun bool
|
||||
}{
|
||||
{
|
||||
name: "adds authorization header with valid token",
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
return "test-token-abc", nil
|
||||
},
|
||||
wantAuthHeader: "Bearer test-token-abc",
|
||||
wantErr: false,
|
||||
serverShouldRun: true,
|
||||
},
|
||||
{
|
||||
name: "omits header for empty token",
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
wantAuthHeader: "",
|
||||
wantErr: false,
|
||||
serverShouldRun: true,
|
||||
},
|
||||
{
|
||||
name: "propagates token function errors",
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
return "", errors.New("token error")
|
||||
},
|
||||
wantAuthHeader: "",
|
||||
wantErr: true,
|
||||
serverShouldRun: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var receivedAuthHeader string
|
||||
serverCalled := false
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
serverCalled = true
|
||||
receivedAuthHeader = r.Header.Get("Authorization")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
rt := &bearerRoundTripper{
|
||||
base: http.DefaultTransport,
|
||||
tokenFunc: tt.tokenFunc,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := rt.RoundTrip(req)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("RoundTrip() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if tt.serverShouldRun {
|
||||
if !serverCalled {
|
||||
t.Error("expected server to be called but it wasn't")
|
||||
return
|
||||
}
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if serverCalled && receivedAuthHeader != tt.wantAuthHeader {
|
||||
t.Errorf("Authorization header = %q, want %q", receivedAuthHeader, tt.wantAuthHeader)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerRoundTripper_PreservesOriginalRequest(t *testing.T) {
|
||||
originalHeader := "original-value"
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
rt := &bearerRoundTripper{
|
||||
base: http.DefaultTransport,
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
return "new-token", nil
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
req.Header.Set("X-Custom", originalHeader)
|
||||
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatalf("RoundTrip() error = %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Original request should not be modified
|
||||
if auth := req.Header.Get("Authorization"); auth != "" {
|
||||
t.Errorf("original request Authorization header was modified to %q", auth)
|
||||
}
|
||||
|
||||
if custom := req.Header.Get("X-Custom"); custom != originalHeader {
|
||||
t.Errorf("original request X-Custom header = %q, want %q", custom, originalHeader)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerRoundTripper_UsesRequestContext(t *testing.T) {
|
||||
type ctxKey string
|
||||
key := ctxKey("test-key")
|
||||
expectedValue := "context-value"
|
||||
|
||||
var receivedCtx context.Context
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
rt := &bearerRoundTripper{
|
||||
base: http.DefaultTransport,
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
receivedCtx = ctx
|
||||
return "token", nil
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), key, expectedValue)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatalf("RoundTrip() error = %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if receivedCtx == nil {
|
||||
t.Fatal("context was not passed to tokenFunc")
|
||||
}
|
||||
|
||||
if receivedCtx.Value(key) != expectedValue {
|
||||
t.Errorf("context value = %v, want %v", receivedCtx.Value(key), expectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerRoundTripper_PreservesRequestBody(t *testing.T) {
|
||||
expectedBody := "request body content"
|
||||
var receivedBody string
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
receivedBody = string(body)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
rt := &bearerRoundTripper{
|
||||
base: http.DefaultTransport,
|
||||
tokenFunc: func(ctx context.Context) (string, error) {
|
||||
return "token", nil
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), "POST", server.URL,
|
||||
io.NopCloser(newStringReader(expectedBody)))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
req.ContentLength = int64(len(expectedBody))
|
||||
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatalf("RoundTrip() error = %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if receivedBody != expectedBody {
|
||||
t.Errorf("body = %q, want %q", receivedBody, expectedBody)
|
||||
}
|
||||
}
|
||||
|
||||
// stringReader is an io.Reader that reads from a string exactly once
|
||||
type stringReader struct {
|
||||
s string
|
||||
done bool
|
||||
}
|
||||
|
||||
func newStringReader(s string) *stringReader {
|
||||
return &stringReader{s: s}
|
||||
}
|
||||
|
||||
func (r *stringReader) Read(p []byte) (n int, err error) {
|
||||
if r.done {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n = copy(p, r.s)
|
||||
r.done = true
|
||||
return n, io.EOF
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
@@ -109,6 +111,19 @@ func ValidateAndStore(ctx context.Context, cfg *Config, logFactory LogExporterFa
|
||||
// client certificate authentication.
|
||||
type GetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
|
||||
|
||||
// BearerTokenFunc retrieves a bearer token for OTLP authentication.
|
||||
// It is called for each export request (traces, logs, metrics).
|
||||
// The caller is responsible for caching and token renewal.
|
||||
// Returns the token string (without "Bearer " prefix) or an error.
|
||||
// An empty string with no error means skip the Authorization header.
|
||||
//
|
||||
// Thread safety: This function may be called concurrently from multiple
|
||||
// goroutines. Implementations must be safe for concurrent use.
|
||||
//
|
||||
// Protocol support: Bearer authentication is supported for both gRPC
|
||||
// (via PerRPCCredentials) and HTTP (via custom RoundTripper) exporters.
|
||||
type BearerTokenFunc func(ctx context.Context) (string, error)
|
||||
|
||||
// Config provides configuration options for OpenTelemetry tracing setup.
|
||||
// It supplements standard OpenTelemetry environment variables with additional
|
||||
// NTP Pool-specific configuration including TLS settings for secure OTLP export.
|
||||
@@ -119,6 +134,7 @@ type Config struct {
|
||||
EndpointURL string // Complete OTLP endpoint URL (e.g., "https://otlp.example.com:4317/v1/traces")
|
||||
CertificateProvider GetClientCertificate // Client certificate provider for mutual TLS
|
||||
RootCAs *x509.CertPool // CA certificate pool for server verification
|
||||
BearerTokenFunc BearerTokenFunc // Token provider for bearer authentication
|
||||
}
|
||||
|
||||
// LogExporterFactory creates an OTLP log exporter using the provided configuration.
|
||||
@@ -229,6 +245,27 @@ func getProtocol(signalSpecificEnv string) string {
|
||||
return proto
|
||||
}
|
||||
|
||||
// getHTTPClient builds an HTTP client with bearer auth and/or TLS if configured.
|
||||
// Returns nil if neither is configured, allowing the SDK to use its defaults.
|
||||
func getHTTPClient(cfg *Config, tlsConfig *tls.Config) *http.Client {
|
||||
if cfg.BearerTokenFunc == nil && tlsConfig == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var transport http.RoundTripper = http.DefaultTransport
|
||||
if tlsConfig != nil {
|
||||
transport = &http.Transport{TLSClientConfig: tlsConfig}
|
||||
}
|
||||
if cfg.BearerTokenFunc != nil {
|
||||
transport = &bearerRoundTripper{
|
||||
base: transport,
|
||||
tokenFunc: cfg.BearerTokenFunc,
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
// CreateOTLPLogExporter creates an OTLP log exporter using the provided configuration.
|
||||
func CreateOTLPLogExporter(ctx context.Context, cfg *Config) (sdklog.Exporter, error) {
|
||||
tlsConfig := getTLSConfig(cfg)
|
||||
@@ -242,6 +279,10 @@ func CreateOTLPLogExporter(ctx context.Context, cfg *Config) (sdklog.Exporter, e
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlploggrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
|
||||
}
|
||||
if cfg.BearerTokenFunc != nil {
|
||||
creds := &bearerCredentials{tokenFunc: cfg.BearerTokenFunc}
|
||||
opts = append(opts, otlploggrpc.WithDialOption(grpc.WithPerRPCCredentials(creds)))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
opts = append(opts, otlploggrpc.WithEndpoint(cfg.Endpoint))
|
||||
}
|
||||
@@ -254,8 +295,8 @@ func CreateOTLPLogExporter(ctx context.Context, cfg *Config) (sdklog.Exporter, e
|
||||
opts := []otlploghttp.Option{
|
||||
otlploghttp.WithCompression(otlploghttp.GzipCompression),
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlploghttp.WithTLSClientConfig(tlsConfig))
|
||||
if client := getHTTPClient(cfg, tlsConfig); client != nil {
|
||||
opts = append(opts, otlploghttp.WithHTTPClient(client))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
opts = append(opts, otlploghttp.WithEndpoint(cfg.Endpoint))
|
||||
@@ -290,6 +331,10 @@ func CreateOTLPMetricExporter(ctx context.Context, cfg *Config) (sdkmetric.Expor
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlpmetricgrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
|
||||
}
|
||||
if cfg.BearerTokenFunc != nil {
|
||||
creds := &bearerCredentials{tokenFunc: cfg.BearerTokenFunc}
|
||||
opts = append(opts, otlpmetricgrpc.WithDialOption(grpc.WithPerRPCCredentials(creds)))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
opts = append(opts, otlpmetricgrpc.WithEndpoint(cfg.Endpoint))
|
||||
}
|
||||
@@ -302,8 +347,8 @@ func CreateOTLPMetricExporter(ctx context.Context, cfg *Config) (sdkmetric.Expor
|
||||
opts := []otlpmetrichttp.Option{
|
||||
otlpmetrichttp.WithCompression(otlpmetrichttp.GzipCompression),
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlpmetrichttp.WithTLSClientConfig(tlsConfig))
|
||||
if client := getHTTPClient(cfg, tlsConfig); client != nil {
|
||||
opts = append(opts, otlpmetrichttp.WithHTTPClient(client))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
opts = append(opts, otlpmetrichttp.WithEndpoint(cfg.Endpoint))
|
||||
@@ -340,6 +385,10 @@ func CreateOTLPTraceExporter(ctx context.Context, cfg *Config) (sdktrace.SpanExp
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
|
||||
}
|
||||
if cfg.BearerTokenFunc != nil {
|
||||
creds := &bearerCredentials{tokenFunc: cfg.BearerTokenFunc}
|
||||
opts = append(opts, otlptracegrpc.WithDialOption(grpc.WithPerRPCCredentials(creds)))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
opts = append(opts, otlptracegrpc.WithEndpoint(cfg.Endpoint))
|
||||
}
|
||||
@@ -352,8 +401,8 @@ func CreateOTLPTraceExporter(ctx context.Context, cfg *Config) (sdktrace.SpanExp
|
||||
opts := []otlptracehttp.Option{
|
||||
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlptracehttp.WithTLSClientConfig(tlsConfig))
|
||||
if httpClient := getHTTPClient(cfg, tlsConfig); httpClient != nil {
|
||||
opts = append(opts, otlptracehttp.WithHTTPClient(httpClient))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
opts = append(opts, otlptracehttp.WithEndpoint(cfg.Endpoint))
|
||||
|
||||
@@ -23,6 +23,9 @@ type bufferingExporter struct {
|
||||
// Real exporter (created when tracing is configured)
|
||||
exporter otellog.Exporter
|
||||
|
||||
// Track whether buffer has been flushed (separate from exporter creation)
|
||||
bufferFlushed bool
|
||||
|
||||
// Thread-safe initialization state (managed only by checkReadiness)
|
||||
initErr error
|
||||
|
||||
@@ -71,22 +74,63 @@ func (e *bufferingExporter) initialize() error {
|
||||
initCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
exporter, err := factory(initCtx, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create OTLP exporter: %w", err)
|
||||
e.mu.RLock()
|
||||
hasExporter := e.exporter != nil
|
||||
e.mu.RUnlock()
|
||||
|
||||
// Create exporter if not already created
|
||||
if !hasExporter {
|
||||
exporter, err := factory(initCtx, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create OTLP exporter: %w", err)
|
||||
}
|
||||
e.mu.Lock()
|
||||
// Double-check: another goroutine may have created it while we were waiting
|
||||
if e.exporter == nil {
|
||||
e.exporter = exporter
|
||||
} else {
|
||||
// Another goroutine beat us, close the one we created
|
||||
_ = exporter.Shutdown(context.Background())
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Check if we can flush (token verification if configured)
|
||||
if !e.canFlush(initCtx, cfg, false) {
|
||||
return errors.New("waiting for token authentication")
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.exporter = exporter
|
||||
flushErr := e.flushBuffer(initCtx)
|
||||
if !e.bufferFlushed {
|
||||
flushErr := e.flushBuffer(initCtx)
|
||||
if flushErr != nil {
|
||||
e.mu.Unlock()
|
||||
// Log but don't fail initialization
|
||||
Setup().Warn("buffer flush failed during initialization", "error", flushErr)
|
||||
return nil
|
||||
}
|
||||
e.bufferFlushed = true
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
if flushErr != nil {
|
||||
// Log but don't fail initialization
|
||||
Setup().Warn("buffer flush failed during initialization", "error", flushErr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// canFlush checks if we're ready to flush buffered logs.
|
||||
// If BearerTokenFunc is configured, it must return without error.
|
||||
// If forceFlush is true (during shutdown with cancelled context), skip token check.
|
||||
func (e *bufferingExporter) canFlush(ctx context.Context, cfg *tracerconfig.Config, forceFlush bool) bool {
|
||||
if cfg.BearerTokenFunc == nil {
|
||||
return true // No token auth configured, can flush immediately
|
||||
}
|
||||
|
||||
return nil
|
||||
if forceFlush {
|
||||
return true // During shutdown, proceed with best-effort flush
|
||||
}
|
||||
|
||||
// Check if token is available (call returns without error)
|
||||
_, err := cfg.BearerTokenFunc(ctx)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// bufferRecords adds records to the buffer for later processing
|
||||
@@ -119,16 +163,16 @@ func (e *bufferingExporter) checkReadiness() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Check if we already have a working exporter
|
||||
// Check if we're fully ready (exporter created AND buffer flushed)
|
||||
e.mu.RLock()
|
||||
hasExporter := e.exporter != nil
|
||||
fullyReady := e.exporter != nil && e.bufferFlushed
|
||||
e.mu.RUnlock()
|
||||
|
||||
if hasExporter {
|
||||
return // Exporter ready, checker no longer needed
|
||||
if fullyReady {
|
||||
return // Fully initialized, checker no longer needed
|
||||
}
|
||||
|
||||
// Try to initialize
|
||||
// Try to initialize (creates exporter and flushes if token ready)
|
||||
err := e.initialize()
|
||||
e.mu.Lock()
|
||||
e.initErr = err
|
||||
@@ -182,18 +226,42 @@ func (e *bufferingExporter) Shutdown(ctx context.Context) error {
|
||||
// Wait for readiness checker goroutine to complete
|
||||
<-e.checkerDone
|
||||
|
||||
cfg, _, _ := tracerconfig.Get()
|
||||
|
||||
// Check if context is cancelled for best-effort flush
|
||||
forceFlush := ctx.Err() != nil
|
||||
|
||||
// Give one final chance for TLS/tracing to become ready for buffer flushing
|
||||
e.mu.RLock()
|
||||
hasExporter := e.exporter != nil
|
||||
bufferFlushed := e.bufferFlushed
|
||||
e.mu.RUnlock()
|
||||
|
||||
if !hasExporter {
|
||||
err := e.initialize()
|
||||
e.mu.Lock()
|
||||
e.initErr = err
|
||||
hasExporter = e.exporter != nil
|
||||
bufferFlushed = e.bufferFlushed
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// If exporter exists but buffer not flushed, try to flush now
|
||||
if hasExporter && !bufferFlushed {
|
||||
canFlushNow := cfg == nil || e.canFlush(ctx, cfg, forceFlush)
|
||||
if canFlushNow {
|
||||
e.mu.Lock()
|
||||
if !e.bufferFlushed {
|
||||
flushErr := e.flushBuffer(ctx)
|
||||
if flushErr != nil {
|
||||
Setup().Warn("buffer flush failed during shutdown", "error", flushErr)
|
||||
}
|
||||
e.bufferFlushed = true
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
go install github.com/goreleaser/goreleaser/v2@v2.12.3
|
||||
go install github.com/goreleaser/goreleaser/v2@v2.14.1
|
||||
|
||||
if [ ! -z "${harbor_username:-}" ]; then
|
||||
DOCKER_FILE=~/.docker/config.json
|
||||
@@ -13,11 +13,11 @@ if [ ! -z "${harbor_username:-}" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
DRONE_TAG=${DRONE_TAG-""}
|
||||
CI_TAG=${CI_COMMIT_TAG:-${DRONE_TAG:-""}}
|
||||
|
||||
is_snapshot=""
|
||||
|
||||
if [ -z "$DRONE_TAG" ]; then
|
||||
if [ -z "$CI_TAG" ]; then
|
||||
is_snapshot="--snapshot"
|
||||
fi
|
||||
|
||||
|
||||
@@ -112,6 +112,10 @@ func Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption)
|
||||
// This maintains backward compatibility for existing code.
|
||||
type GetClientCertificate = tracerconfig.GetClientCertificate
|
||||
|
||||
// BearerTokenFunc is an alias for the type defined in tracerconfig.
|
||||
// It retrieves a bearer token for OTLP authentication.
|
||||
type BearerTokenFunc = tracerconfig.BearerTokenFunc
|
||||
|
||||
// TracerConfig provides configuration options for OpenTelemetry tracing setup.
|
||||
// It supplements standard OpenTelemetry environment variables with additional
|
||||
// NTP Pool-specific configuration including TLS settings for secure OTLP export.
|
||||
@@ -123,6 +127,7 @@ type TracerConfig struct {
|
||||
|
||||
CertificateProvider GetClientCertificate // Client certificate provider for mutual TLS
|
||||
RootCAs *x509.CertPool // CA certificate pool for server verification
|
||||
BearerTokenFunc BearerTokenFunc // Token provider for bearer authentication
|
||||
}
|
||||
|
||||
// InitTracer initializes the OpenTelemetry SDK with the provided configuration.
|
||||
@@ -160,6 +165,7 @@ func SetupSDK(ctx context.Context, cfg *TracerConfig) (shutdown TpShutdownFunc,
|
||||
EndpointURL: cfg.EndpointURL,
|
||||
CertificateProvider: cfg.CertificateProvider,
|
||||
RootCAs: cfg.RootCAs,
|
||||
BearerTokenFunc: cfg.BearerTokenFunc,
|
||||
}
|
||||
tracerconfig.Store(ctx, bridgeConfig, createOTLPLogExporter, createOTLPMetricExporter, createOTLPTraceExporter)
|
||||
|
||||
|
||||
@@ -90,10 +90,13 @@ func init() {
|
||||
VERSION = "dev-snapshot"
|
||||
} else {
|
||||
if !strings.HasPrefix(VERSION, "v") {
|
||||
VERSION = "v" + VERSION
|
||||
vVersion := "v" + VERSION
|
||||
if semver.IsValid(vVersion) {
|
||||
VERSION = vVersion
|
||||
}
|
||||
}
|
||||
if !semver.IsValid(VERSION) {
|
||||
slog.Default().Warn("invalid version number", "version", VERSION)
|
||||
if !semver.IsValid(VERSION) && VERSION != "dev-snapshot" {
|
||||
slog.Default().Info("non-semver version", "version", VERSION)
|
||||
}
|
||||
}
|
||||
if bi, ok := debug.ReadBuildInfo(); ok {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
func TestCheckVersion(t *testing.T) {
|
||||
@@ -48,9 +49,12 @@ func TestVersionInfo(t *testing.T) {
|
||||
t.Error("VersionInfo().Version should not be empty")
|
||||
}
|
||||
|
||||
// Version should start with "v" or be "dev-snapshot"
|
||||
// Version should start with "v", be "dev-snapshot", or be a non-semver string (e.g. branch name)
|
||||
if !strings.HasPrefix(info.Version, "v") && info.Version != "dev-snapshot" {
|
||||
t.Errorf("Version should start with 'v' or be 'dev-snapshot', got: %s", info.Version)
|
||||
// Non-semver versions like branch names ("main") are acceptable
|
||||
if semver.IsValid("v" + info.Version) {
|
||||
t.Errorf("Semver-like version should start with 'v', got: %s", info.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// GitRevShort should be <= 7 characters if set
|
||||
@@ -398,10 +402,10 @@ func TestParseBuildTimeConsistency(t *testing.T) {
|
||||
|
||||
func BenchmarkParseBuildTime(b *testing.B) {
|
||||
inputs := []string{
|
||||
"1672531200", // Unix epoch
|
||||
"2023-01-01T00:00:00Z", // RFC3339
|
||||
"invalid-timestamp", // Invalid
|
||||
"", // Empty
|
||||
"1672531200", // Unix epoch
|
||||
"2023-01-01T00:00:00Z", // RFC3339
|
||||
"invalid-timestamp", // Invalid
|
||||
"", // Empty
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
|
||||
48
xff/echo/echo.go
Normal file
48
xff/echo/echo.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Package xffecho adapts [xff.TrustedProxies] for use with the Echo web
|
||||
// framework's X-Forwarded-For IP extraction.
|
||||
//
|
||||
// # Usage
|
||||
//
|
||||
// tp, err := fastlyxff.New("fastly.json")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// trustOpts := xffecho.TrustOptions(tp)
|
||||
// e.IPExtractor = echo.ExtractIPFromXFFHeader(trustOpts...)
|
||||
package xffecho
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"go.ntppool.org/common/xff"
|
||||
)
|
||||
|
||||
// TrustOptions converts a [xff.TrustedProxies] into Echo trust options
|
||||
// for use with [echo.ExtractIPFromXFFHeader].
|
||||
func TrustOptions(tp *xff.TrustedProxies) []echo.TrustOption {
|
||||
prefixes := tp.Prefixes()
|
||||
opts := make([]echo.TrustOption, 0, len(prefixes))
|
||||
for _, p := range prefixes {
|
||||
opts = append(opts, echo.TrustIPRange(prefixToIPNet(p)))
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// prefixToIPNet bridges netip.Prefix (used by xff) to net.IPNet (used by Echo).
|
||||
func prefixToIPNet(p netip.Prefix) *net.IPNet {
|
||||
addr := p.Masked().Addr()
|
||||
bits := p.Bits()
|
||||
|
||||
ipLen := 128
|
||||
if addr.Is4() {
|
||||
ipLen = 32
|
||||
}
|
||||
|
||||
return &net.IPNet{
|
||||
IP: net.IP(addr.AsSlice()),
|
||||
Mask: net.CIDRMask(bits, ipLen),
|
||||
}
|
||||
}
|
||||
31
xff/echo/echo_test.go
Normal file
31
xff/echo/echo_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package xffecho
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.ntppool.org/common/xff"
|
||||
)
|
||||
|
||||
func TestTrustOptions(t *testing.T) {
|
||||
tp, err := xff.NewFromCIDRs([]string{
|
||||
"192.0.2.0/24",
|
||||
"203.0.113.0/24",
|
||||
"2001:db8::/32",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
opts := TrustOptions(tp)
|
||||
if len(opts) != 3 {
|
||||
t.Errorf("expected 3 trust options, got %d", len(opts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustOptionsEmpty(t *testing.T) {
|
||||
tp := xff.New()
|
||||
opts := TrustOptions(tp)
|
||||
if len(opts) != 0 {
|
||||
t.Errorf("expected 0 trust options, got %d", len(opts))
|
||||
}
|
||||
}
|
||||
@@ -1,270 +1,53 @@
|
||||
// Package fastlyxff provides Fastly CDN IP range management for trusted proxy handling.
|
||||
// Package fastlyxff loads Fastly CDN IP ranges and returns a generic
|
||||
// [xff.TrustedProxies] for trusted proxy handling.
|
||||
//
|
||||
// This package parses Fastly's public IP ranges JSON file and provides middleware
|
||||
// for both Echo framework and standard net/http for proper client IP extraction
|
||||
// from X-Forwarded-For headers. It's designed specifically for services deployed
|
||||
// behind Fastly's CDN that need to identify real client IPs for logging, rate
|
||||
// limiting, and security purposes.
|
||||
//
|
||||
// Fastly publishes their edge server IP ranges in a JSON format that this package
|
||||
// consumes to automatically configure trusted proxy ranges. This ensures that
|
||||
// X-Forwarded-For headers are only trusted when they originate from legitimate
|
||||
// Fastly edge servers.
|
||||
//
|
||||
// Key features:
|
||||
// - Automatic parsing of Fastly's IP ranges JSON format
|
||||
// - Support for both IPv4 and IPv6 address ranges
|
||||
// - Echo framework integration via TrustOption generation
|
||||
// - Standard net/http middleware support
|
||||
// - CIDR notation parsing and validation
|
||||
//
|
||||
// # Echo Framework Usage
|
||||
//
|
||||
// fastlyRanges, err := fastlyxff.New("fastly.json")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// options, err := fastlyRanges.EchoTrustOption()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// e.IPExtractor = echo.ExtractIPFromXFFHeader(options...)
|
||||
//
|
||||
// # Net/HTTP Usage
|
||||
//
|
||||
// fastlyRanges, err := fastlyxff.New("fastly.json")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// middleware := fastlyRanges.HTTPMiddleware()
|
||||
//
|
||||
// handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// // Both methods work - middleware updates r.RemoteAddr (with port 0) and stores in context
|
||||
// realIP := fastlyxff.GetRealIP(r) // Preferred method
|
||||
// // OR: host, _, _ := net.SplitHostPort(r.RemoteAddr) // Direct access (port will be "0")
|
||||
// fmt.Fprintf(w, "Real IP: %s\n", realIP)
|
||||
// })
|
||||
//
|
||||
// http.ListenAndServe(":8080", middleware(handler))
|
||||
//
|
||||
// # Net/HTTP with Additional Trusted Ranges
|
||||
//
|
||||
// fastlyRanges, err := fastlyxff.New("fastly.json")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// // Add custom trusted CIDRs (e.g., internal load balancers)
|
||||
// // Note: For Echo framework, use the ekko package for additional ranges
|
||||
// err = fastlyRanges.AddTrustedCIDR("10.0.0.0/8")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// middleware := fastlyRanges.HTTPMiddleware()
|
||||
// handler := middleware(yourHandler)
|
||||
//
|
||||
// The JSON file typically contains IP ranges in this format:
|
||||
// Fastly publishes their edge server IP ranges in a JSON format:
|
||||
//
|
||||
// {
|
||||
// "addresses": ["23.235.32.0/20", "43.249.72.0/22", ...],
|
||||
// "ipv6_addresses": ["2a04:4e40::/32", "2a04:4e42::/32", ...]
|
||||
// }
|
||||
//
|
||||
// # Usage
|
||||
//
|
||||
// tp, err := fastlyxff.New("fastly.json")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // Use tp.HTTPMiddleware(), tp.ExtractRealIP(r), etc.
|
||||
// // For Echo framework, use the xff/echo package:
|
||||
// // opts, err := xffecho.TrustOptions(tp)
|
||||
package fastlyxff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.ntppool.org/common/xff"
|
||||
)
|
||||
|
||||
// FastlyXFF represents Fastly's published IP ranges for their CDN edge servers.
|
||||
// This structure matches the JSON format provided by Fastly for their public IP ranges.
|
||||
// It contains separate lists for IPv4 and IPv6 CIDR ranges, plus additional trusted CIDRs.
|
||||
type FastlyXFF struct {
|
||||
IPv4 []string `json:"addresses"` // IPv4 CIDR ranges (e.g., "23.235.32.0/20")
|
||||
IPv6 []string `json:"ipv6_addresses"` // IPv6 CIDR ranges (e.g., "2a04:4e40::/32")
|
||||
extraCIDRs []string // Additional trusted CIDRs added via AddTrustedCIDR
|
||||
// fastlyIPRanges matches the JSON format published by Fastly for their
|
||||
// edge server IP ranges.
|
||||
type fastlyIPRanges struct {
|
||||
IPv4 []string `json:"addresses"`
|
||||
IPv6 []string `json:"ipv6_addresses"`
|
||||
}
|
||||
|
||||
// TrustedNets holds parsed network prefixes for efficient IP range checking.
|
||||
type TrustedNets struct {
|
||||
prefixes []netip.Prefix // Parsed network prefixes for efficient lookups
|
||||
}
|
||||
|
||||
// contextKey is used for storing the real client IP in request context
|
||||
type contextKey string
|
||||
|
||||
const realIPKey contextKey = "fastly-real-ip"
|
||||
|
||||
// New loads and parses Fastly IP ranges from a JSON file.
|
||||
// The file should contain Fastly's published IP ranges in their standard JSON format.
|
||||
//
|
||||
// Parameters:
|
||||
// - fileName: Path to the Fastly IP ranges JSON file
|
||||
//
|
||||
// Returns the parsed FastlyXFF structure or an error if the file cannot be
|
||||
// read or the JSON format is invalid.
|
||||
func New(fileName string) (*FastlyXFF, error) {
|
||||
// New loads Fastly IP ranges from a JSON file and returns a [xff.TrustedProxies].
|
||||
func New(fileName string) (*xff.TrustedProxies, error) {
|
||||
b, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := FastlyXFF{}
|
||||
|
||||
err = json.Unmarshal(b, &d)
|
||||
if err != nil {
|
||||
var ranges fastlyIPRanges
|
||||
if err := json.Unmarshal(b, &ranges); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// EchoTrustOption converts Fastly IP ranges into Echo framework trust options.
|
||||
// This method generates trust configurations that tell Echo to accept X-Forwarded-For
|
||||
// headers only from Fastly's edge servers, ensuring accurate client IP extraction.
|
||||
//
|
||||
// The generated trust options should be used with Echo's IP extractor:
|
||||
//
|
||||
// options, err := fastlyRanges.EchoTrustOption()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// e.IPExtractor = echo.ExtractIPFromXFFHeader(options...)
|
||||
//
|
||||
// Returns a slice of Echo trust options or an error if any CIDR range cannot be parsed.
|
||||
func (xff *FastlyXFF) EchoTrustOption() ([]echo.TrustOption, error) {
|
||||
ranges := []echo.TrustOption{}
|
||||
|
||||
for _, s := range append(xff.IPv4, xff.IPv6...) {
|
||||
_, cidr, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trust := echo.TrustIPRange(cidr)
|
||||
ranges = append(ranges, trust)
|
||||
}
|
||||
|
||||
return ranges, nil
|
||||
}
|
||||
|
||||
// AddTrustedCIDR adds an additional CIDR to the list of trusted proxies.
|
||||
// This allows trusting proxies beyond Fastly's published ranges.
|
||||
// The cidr parameter must be a valid CIDR notation (e.g., "10.0.0.0/8", "192.168.1.0/24").
|
||||
// Returns an error if the CIDR format is invalid.
|
||||
func (xff *FastlyXFF) AddTrustedCIDR(cidr string) error {
|
||||
// Validate CIDR format
|
||||
_, _, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add to extra CIDRs
|
||||
xff.extraCIDRs = append(xff.extraCIDRs, cidr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// isTrustedProxy checks if the given IP address belongs to Fastly's trusted IP ranges
|
||||
// or any additional CIDRs added via AddTrustedCIDR.
|
||||
func (xff *FastlyXFF) isTrustedProxy(ip string) bool {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check all IPv4 and IPv6 ranges (Fastly + additional)
|
||||
allRanges := append(append(xff.IPv4, xff.IPv6...), xff.extraCIDRs...)
|
||||
for _, s := range allRanges {
|
||||
_, cidr, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if cidr.Contains(net.IP(addr.AsSlice())) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// extractRealIP extracts the real client IP from X-Forwarded-For header.
|
||||
// It returns the rightmost IP that is not from a trusted Fastly proxy.
|
||||
func (xff *FastlyXFF) extractRealIP(r *http.Request) string {
|
||||
// Get the immediate peer IP
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host = r.RemoteAddr
|
||||
}
|
||||
|
||||
// If the immediate peer is not a trusted Fastly proxy, return it
|
||||
if !xff.isTrustedProxy(host) {
|
||||
return host
|
||||
}
|
||||
|
||||
// Check X-Forwarded-For header
|
||||
xff_header := r.Header.Get("X-Forwarded-For")
|
||||
if xff_header == "" {
|
||||
return host
|
||||
}
|
||||
|
||||
// Parse comma-separated IP list
|
||||
ips := strings.Split(xff_header, ",")
|
||||
if len(ips) == 0 {
|
||||
return host
|
||||
}
|
||||
|
||||
// Find the leftmost IP that is not from a trusted proxy
|
||||
// This represents the original client IP
|
||||
for i := 0; i < len(ips); i++ {
|
||||
ip := strings.TrimSpace(ips[i])
|
||||
if ip != "" && !xff.isTrustedProxy(ip) {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the immediate peer
|
||||
return host
|
||||
}
|
||||
|
||||
// HTTPMiddleware returns a net/http middleware that extracts real client IP
|
||||
// from X-Forwarded-For headers when the request comes from trusted Fastly proxies.
|
||||
// The real IP is stored in the request context and also updates r.RemoteAddr
|
||||
// with port 0 (since the original port is from the proxy, not the real client).
|
||||
func (xff *FastlyXFF) HTTPMiddleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
realIP := xff.extractRealIP(r)
|
||||
|
||||
// Store in context for GetRealIP function
|
||||
ctx := context.WithValue(r.Context(), realIPKey, realIP)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// Update RemoteAddr to be consistent with extracted IP
|
||||
// Use port 0 since the original port is from the proxy, not the real client
|
||||
r.RemoteAddr = net.JoinHostPort(realIP, "0")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetRealIP retrieves the real client IP from the request context.
|
||||
// This should be used after the HTTPMiddleware has processed the request.
|
||||
// Returns the remote address if no real IP was extracted.
|
||||
func GetRealIP(r *http.Request) string {
|
||||
if ip, ok := r.Context().Value(realIPKey).(string); ok {
|
||||
return ip
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
cidrs := make([]string, 0, len(ranges.IPv4)+len(ranges.IPv6))
|
||||
cidrs = append(cidrs, ranges.IPv4...)
|
||||
cidrs = append(cidrs, ranges.IPv6...)
|
||||
|
||||
return xff.NewFromCIDRs(cidrs)
|
||||
}
|
||||
|
||||
@@ -1,356 +1,44 @@
|
||||
package fastlyxff
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFastlyIPRanges(t *testing.T) {
|
||||
fastlyxff, err := New("fastly.json")
|
||||
func TestNew(t *testing.T) {
|
||||
tp, err := New("fastly.json")
|
||||
if err != nil {
|
||||
t.Fatalf("could not load test data: %s", err)
|
||||
}
|
||||
|
||||
data, err := fastlyxff.EchoTrustOption()
|
||||
prefixes := tp.Prefixes()
|
||||
if len(prefixes) < 10 {
|
||||
t.Errorf("only got %d prefixes, expected more", len(prefixes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFileNotFound(t *testing.T) {
|
||||
_, err := New("nonexistent.json")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInvalidJSON(t *testing.T) {
|
||||
// Create a temp file with invalid JSON
|
||||
f, err := os.CreateTemp("", "fastlyxff-test-*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse test data: %s", err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
if len(data) < 10 {
|
||||
t.Logf("only got %d prefixes, expected more", len(data))
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPMiddleware(t *testing.T) {
|
||||
// Create a test FastlyXFF instance with known IP ranges
|
||||
xff := &FastlyXFF{
|
||||
IPv4: []string{"192.0.2.0/24", "203.0.113.0/24"},
|
||||
IPv6: []string{"2001:db8::/32"},
|
||||
}
|
||||
|
||||
middleware := xff.HTTPMiddleware()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteAddr string
|
||||
xForwardedFor string
|
||||
expectedRealIP string
|
||||
}{
|
||||
{
|
||||
name: "direct connection",
|
||||
remoteAddr: "198.51.100.1:12345",
|
||||
xForwardedFor: "",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "trusted proxy with XFF",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "trusted proxy with multiple XFF",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1, 203.0.113.1",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "untrusted proxy ignored",
|
||||
remoteAddr: "198.51.100.2:80",
|
||||
xForwardedFor: "10.0.0.1",
|
||||
expectedRealIP: "198.51.100.2",
|
||||
},
|
||||
{
|
||||
name: "IPv6 trusted proxy",
|
||||
remoteAddr: "[2001:db8::1]:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create test handler that captures both GetRealIP and r.RemoteAddr
|
||||
var capturedRealIP, capturedRemoteAddr string
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedRealIP = GetRealIP(r)
|
||||
capturedRemoteAddr = r.RemoteAddr
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Create request with middleware
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = tt.remoteAddr
|
||||
if tt.xForwardedFor != "" {
|
||||
req.Header.Set("X-Forwarded-For", tt.xForwardedFor)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
middleware(handler).ServeHTTP(rr, req)
|
||||
|
||||
// Test GetRealIP function
|
||||
if capturedRealIP != tt.expectedRealIP {
|
||||
t.Errorf("GetRealIP: expected %s, got %s", tt.expectedRealIP, capturedRealIP)
|
||||
}
|
||||
|
||||
// Test that r.RemoteAddr is updated with real IP and port 0
|
||||
// (since the original port is from the proxy, not the real client)
|
||||
expectedRemoteAddr := net.JoinHostPort(tt.expectedRealIP, "0")
|
||||
if capturedRemoteAddr != expectedRemoteAddr {
|
||||
t.Errorf("RemoteAddr: expected %s, got %s", expectedRemoteAddr, capturedRemoteAddr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTrustedProxy(t *testing.T) {
|
||||
xff := &FastlyXFF{
|
||||
IPv4: []string{"192.0.2.0/24", "203.0.113.0/24"},
|
||||
IPv6: []string{"2001:db8::/32"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
ip string
|
||||
expected bool
|
||||
}{
|
||||
{"192.0.2.1", true},
|
||||
{"192.0.2.255", true},
|
||||
{"203.0.113.1", true},
|
||||
{"192.0.3.1", false},
|
||||
{"198.51.100.1", false},
|
||||
{"2001:db8::1", true},
|
||||
{"2001:db8:ffff::1", true},
|
||||
{"2001:db9::1", false},
|
||||
{"invalid-ip", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.ip, func(t *testing.T) {
|
||||
result := xff.isTrustedProxy(tt.ip)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isTrustedProxy(%s) = %v, expected %v", tt.ip, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRealIP(t *testing.T) {
|
||||
xff := &FastlyXFF{
|
||||
IPv4: []string{"192.0.2.0/24"},
|
||||
IPv6: []string{"2001:db8::/32"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteAddr string
|
||||
xForwardedFor string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no XFF header",
|
||||
remoteAddr: "198.51.100.1:12345",
|
||||
xForwardedFor: "",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "trusted proxy with single IP",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "trusted proxy with multiple IPs",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1, 203.0.113.5",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "untrusted proxy",
|
||||
remoteAddr: "198.51.100.1:80",
|
||||
xForwardedFor: "10.0.0.1",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "empty XFF",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "",
|
||||
expected: "192.0.2.1",
|
||||
},
|
||||
{
|
||||
name: "malformed remote addr",
|
||||
remoteAddr: "192.0.2.1",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = tt.remoteAddr
|
||||
if tt.xForwardedFor != "" {
|
||||
req.Header.Set("X-Forwarded-For", tt.xForwardedFor)
|
||||
}
|
||||
|
||||
result := xff.extractRealIP(req)
|
||||
if result != tt.expected {
|
||||
t.Errorf("extractRealIP() = %s, expected %s", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRealIPWithoutMiddleware(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "198.51.100.1:12345"
|
||||
|
||||
realIP := GetRealIP(req)
|
||||
expected := "198.51.100.1"
|
||||
if realIP != expected {
|
||||
t.Errorf("GetRealIP() = %s, expected %s", realIP, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTrustedCIDR(t *testing.T) {
|
||||
xff := &FastlyXFF{
|
||||
IPv4: []string{"192.0.2.0/24"},
|
||||
IPv6: []string{"2001:db8::/32"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cidr string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid IPv4 range", "10.0.0.0/8", false},
|
||||
{"valid IPv6 range", "fc00::/7", false},
|
||||
{"valid single IP", "203.0.113.1/32", false},
|
||||
{"invalid CIDR", "not-a-cidr", true},
|
||||
{"invalid format", "10.0.0.0/99", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := xff.AddTrustedCIDR(tt.cidr)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AddTrustedCIDR(%s) error = %v, wantErr %v", tt.cidr, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomTrustedCIDRs(t *testing.T) {
|
||||
xff := &FastlyXFF{
|
||||
IPv4: []string{"192.0.2.0/24"},
|
||||
IPv6: []string{"2001:db8::/32"},
|
||||
}
|
||||
|
||||
// Add custom trusted CIDRs
|
||||
err := xff.AddTrustedCIDR("10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add trusted CIDR: %v", err)
|
||||
}
|
||||
|
||||
err = xff.AddTrustedCIDR("172.16.0.0/12")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add trusted CIDR: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
ip string
|
||||
expected bool
|
||||
}{
|
||||
// Original Fastly ranges
|
||||
{"192.0.2.1", true},
|
||||
{"2001:db8::1", true},
|
||||
// Custom CIDRs
|
||||
{"10.1.2.3", true},
|
||||
{"172.16.1.1", true},
|
||||
// Not trusted
|
||||
{"198.51.100.1", false},
|
||||
{"172.15.1.1", false},
|
||||
{"10.0.0.0", true}, // Network address should still match
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.ip, func(t *testing.T) {
|
||||
result := xff.isTrustedProxy(tt.ip)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isTrustedProxy(%s) = %v, expected %v", tt.ip, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPMiddlewareWithCustomCIDRs(t *testing.T) {
|
||||
xff := &FastlyXFF{
|
||||
IPv4: []string{"192.0.2.0/24"},
|
||||
IPv6: []string{"2001:db8::/32"},
|
||||
}
|
||||
|
||||
// Add custom trusted CIDR for internal proxies
|
||||
err := xff.AddTrustedCIDR("10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add trusted CIDR: %v", err)
|
||||
}
|
||||
|
||||
middleware := xff.HTTPMiddleware()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteAddr string
|
||||
xForwardedFor string
|
||||
expectedRealIP string
|
||||
}{
|
||||
{
|
||||
name: "custom trusted proxy with XFF",
|
||||
remoteAddr: "10.1.2.3:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "fastly proxy with XFF",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "untrusted proxy ignored",
|
||||
remoteAddr: "172.16.1.1:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expectedRealIP: "172.16.1.1",
|
||||
},
|
||||
{
|
||||
name: "chain through custom and fastly",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1, 10.1.2.3",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var capturedIP string
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedIP = GetRealIP(r)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = tt.remoteAddr
|
||||
if tt.xForwardedFor != "" {
|
||||
req.Header.Set("X-Forwarded-For", tt.xForwardedFor)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
middleware(handler).ServeHTTP(rr, req)
|
||||
|
||||
if capturedIP != tt.expectedRealIP {
|
||||
t.Errorf("expected real IP %s, got %s", tt.expectedRealIP, capturedIP)
|
||||
}
|
||||
})
|
||||
if _, err := f.WriteString("{invalid"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
_, err = New(f.Name())
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
163
xff/xff.go
Normal file
163
xff/xff.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// Package xff provides trusted proxy handling and real client IP extraction
|
||||
// from X-Forwarded-For headers.
|
||||
//
|
||||
// This package has no external dependencies — it uses only the Go standard library.
|
||||
//
|
||||
// The XFF extraction algorithm walks right-to-left through the X-Forwarded-For
|
||||
// header, skipping trusted proxy IPs, and returns the first untrusted IP as the
|
||||
// real client address. This follows the MDN-recommended approach for secure
|
||||
// client IP extraction.
|
||||
//
|
||||
// # Usage with net/http middleware
|
||||
//
|
||||
// tp, err := xff.NewFromCIDRs([]string{"10.0.0.0/8", "192.168.0.0/16"})
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// handler := tp.HTTPMiddleware()(yourHandler)
|
||||
//
|
||||
// # Direct extraction
|
||||
//
|
||||
// realIP := tp.ExtractRealIP(r)
|
||||
package xff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TrustedProxies holds a set of trusted proxy network prefixes and provides
|
||||
// methods for extracting the real client IP from X-Forwarded-For headers.
|
||||
type TrustedProxies struct {
|
||||
prefixes []netip.Prefix
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
|
||||
const realIPKey contextKey = "xff-real-ip"
|
||||
|
||||
// New creates a TrustedProxies from already-parsed prefixes.
|
||||
func New(prefixes ...netip.Prefix) *TrustedProxies {
|
||||
return &TrustedProxies{prefixes: prefixes}
|
||||
}
|
||||
|
||||
// NewFromCIDRs creates a TrustedProxies from CIDR strings (e.g., "10.0.0.0/8").
|
||||
func NewFromCIDRs(cidrs []string) (*TrustedProxies, error) {
|
||||
prefixes := make([]netip.Prefix, 0, len(cidrs))
|
||||
for _, s := range cidrs {
|
||||
p, err := netip.ParsePrefix(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prefixes = append(prefixes, p)
|
||||
}
|
||||
return &TrustedProxies{prefixes: prefixes}, nil
|
||||
}
|
||||
|
||||
// AddCIDR adds a CIDR string to the trusted proxy list.
|
||||
func (tp *TrustedProxies) AddCIDR(cidr string) error {
|
||||
p, err := netip.ParsePrefix(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tp.prefixes = append(tp.prefixes, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPrefix adds a parsed prefix to the trusted proxy list.
|
||||
func (tp *TrustedProxies) AddPrefix(prefix netip.Prefix) {
|
||||
tp.prefixes = append(tp.prefixes, prefix)
|
||||
}
|
||||
|
||||
// Prefixes returns a copy of the trusted proxy prefixes.
|
||||
func (tp *TrustedProxies) Prefixes() []netip.Prefix {
|
||||
out := make([]netip.Prefix, len(tp.prefixes))
|
||||
copy(out, tp.prefixes)
|
||||
return out
|
||||
}
|
||||
|
||||
// IsTrusted reports whether ip belongs to any of the trusted proxy ranges.
|
||||
func (tp *TrustedProxies) IsTrusted(ip string) bool {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return tp.isTrustedAddr(addr)
|
||||
}
|
||||
|
||||
func (tp *TrustedProxies) isTrustedAddr(addr netip.Addr) bool {
|
||||
for _, p := range tp.prefixes {
|
||||
if p.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExtractRealIP extracts the real client IP from a request by walking the
|
||||
// X-Forwarded-For header right-to-left, skipping trusted proxy IPs.
|
||||
// If the immediate peer is not a trusted proxy, its IP is returned.
|
||||
func (tp *TrustedProxies) ExtractRealIP(r *http.Request) string {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host = r.RemoteAddr
|
||||
}
|
||||
|
||||
hostAddr, err := netip.ParseAddr(host)
|
||||
if err != nil || !tp.isTrustedAddr(hostAddr) {
|
||||
return host
|
||||
}
|
||||
|
||||
xffHeader := r.Header.Get("X-Forwarded-For")
|
||||
if xffHeader == "" {
|
||||
return host
|
||||
}
|
||||
|
||||
ips := strings.Split(xffHeader, ",")
|
||||
|
||||
// Walk right-to-left: skip trusted proxies, return first untrusted IP.
|
||||
for i := len(ips) - 1; i >= 0; i-- {
|
||||
ip := strings.TrimSpace(ips[i])
|
||||
if ip == "" {
|
||||
continue
|
||||
}
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil || !tp.isTrustedAddr(addr) {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
// HTTPMiddleware returns a net/http middleware that extracts the real client IP
|
||||
// from X-Forwarded-For headers and stores it in the request context and
|
||||
// RemoteAddr. The port in RemoteAddr is set to 0 because the original port
|
||||
// belongs to the proxy connection, not the real client.
|
||||
func (tp *TrustedProxies) HTTPMiddleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
realIP := tp.ExtractRealIP(r)
|
||||
ctx := context.WithValue(r.Context(), realIPKey, realIP)
|
||||
r = r.WithContext(ctx)
|
||||
r.RemoteAddr = net.JoinHostPort(realIP, "0")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetRealIP retrieves the real client IP from the request context.
|
||||
// Returns the remote address host if no real IP was extracted by middleware.
|
||||
func GetRealIP(r *http.Request) string {
|
||||
if ip, ok := r.Context().Value(realIPKey).(string); ok {
|
||||
return ip
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
225
xff/xff_test.go
Normal file
225
xff/xff_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package xff
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testProxies(t *testing.T) *TrustedProxies {
|
||||
t.Helper()
|
||||
tp, err := NewFromCIDRs([]string{"192.0.2.0/24", "203.0.113.0/24", "2001:db8::/32"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tp
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
p := netip.MustParsePrefix("10.0.0.0/8")
|
||||
tp := New(p)
|
||||
if len(tp.Prefixes()) != 1 {
|
||||
t.Fatalf("expected 1 prefix, got %d", len(tp.Prefixes()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromCIDRs(t *testing.T) {
|
||||
_, err := NewFromCIDRs([]string{"not-a-cidr"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid CIDR")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTrusted(t *testing.T) {
|
||||
tp := testProxies(t)
|
||||
|
||||
tests := []struct {
|
||||
ip string
|
||||
expected bool
|
||||
}{
|
||||
{"192.0.2.1", true},
|
||||
{"192.0.2.255", true},
|
||||
{"203.0.113.1", true},
|
||||
{"192.0.3.1", false},
|
||||
{"198.51.100.1", false},
|
||||
{"2001:db8::1", true},
|
||||
{"2001:db8:ffff::1", true},
|
||||
{"2001:db9::1", false},
|
||||
{"invalid-ip", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.ip, func(t *testing.T) {
|
||||
if got := tp.IsTrusted(tt.ip); got != tt.expected {
|
||||
t.Errorf("IsTrusted(%s) = %v, want %v", tt.ip, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddCIDR(t *testing.T) {
|
||||
tp := testProxies(t)
|
||||
|
||||
if err := tp.AddCIDR("10.0.0.0/8"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !tp.IsTrusted("10.1.2.3") {
|
||||
t.Error("expected 10.1.2.3 to be trusted after AddCIDR")
|
||||
}
|
||||
|
||||
if err := tp.AddCIDR("bad"); err == nil {
|
||||
t.Error("expected error for invalid CIDR")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPrefix(t *testing.T) {
|
||||
tp := testProxies(t)
|
||||
tp.AddPrefix(netip.MustParsePrefix("172.16.0.0/12"))
|
||||
if !tp.IsTrusted("172.16.1.1") {
|
||||
t.Error("expected 172.16.1.1 to be trusted after AddPrefix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRealIP(t *testing.T) {
|
||||
tp := testProxies(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteAddr string
|
||||
xForwardedFor string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no XFF, untrusted peer",
|
||||
remoteAddr: "198.51.100.1:12345",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "trusted proxy, single XFF",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "trusted proxy, empty XFF",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "",
|
||||
expected: "192.0.2.1",
|
||||
},
|
||||
{
|
||||
name: "untrusted peer ignores XFF",
|
||||
remoteAddr: "198.51.100.1:80",
|
||||
xForwardedFor: "10.0.0.1",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "malformed remote addr",
|
||||
remoteAddr: "192.0.2.1",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
// Right-to-left: "client, proxy1(trusted)" -> skip proxy1, return client
|
||||
{
|
||||
name: "right-to-left skips trusted proxies in XFF",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1, 203.0.113.1",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
// Right-to-left: "spoofed, real-client, trusted-proxy"
|
||||
// should return real-client (first untrusted from right)
|
||||
{
|
||||
name: "right-to-left stops at first untrusted from right",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.50, 198.51.100.99, 203.0.113.1",
|
||||
expected: "198.51.100.99",
|
||||
},
|
||||
{
|
||||
name: "IPv6 trusted proxy",
|
||||
remoteAddr: "[2001:db8::1]:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = tt.remoteAddr
|
||||
if tt.xForwardedFor != "" {
|
||||
req.Header.Set("X-Forwarded-For", tt.xForwardedFor)
|
||||
}
|
||||
if got := tp.ExtractRealIP(req); got != tt.expected {
|
||||
t.Errorf("ExtractRealIP() = %s, want %s", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPMiddleware(t *testing.T) {
|
||||
tp := testProxies(t)
|
||||
mw := tp.HTTPMiddleware()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteAddr string
|
||||
xForwardedFor string
|
||||
expectedRealIP string
|
||||
}{
|
||||
{
|
||||
name: "direct connection",
|
||||
remoteAddr: "198.51.100.1:12345",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "trusted proxy with XFF",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "untrusted proxy ignored",
|
||||
remoteAddr: "198.51.100.2:80",
|
||||
xForwardedFor: "10.0.0.1",
|
||||
expectedRealIP: "198.51.100.2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var capturedRealIP, capturedRemoteAddr string
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedRealIP = GetRealIP(r)
|
||||
capturedRemoteAddr = r.RemoteAddr
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = tt.remoteAddr
|
||||
if tt.xForwardedFor != "" {
|
||||
req.Header.Set("X-Forwarded-For", tt.xForwardedFor)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
mw(handler).ServeHTTP(rr, req)
|
||||
|
||||
if capturedRealIP != tt.expectedRealIP {
|
||||
t.Errorf("GetRealIP: got %s, want %s", capturedRealIP, tt.expectedRealIP)
|
||||
}
|
||||
|
||||
expectedAddr := net.JoinHostPort(tt.expectedRealIP, "0")
|
||||
if capturedRemoteAddr != expectedAddr {
|
||||
t.Errorf("RemoteAddr: got %s, want %s", capturedRemoteAddr, expectedAddr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRealIPWithoutMiddleware(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "198.51.100.1:12345"
|
||||
|
||||
if got := GetRealIP(req); got != "198.51.100.1" {
|
||||
t.Errorf("GetRealIP() = %s, want 198.51.100.1", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user