Compare commits
No commits in common. "main" and "v0.2.12" have entirely different histories.
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@ -1 +0,0 @@
|
|||||||
../CLAUDE.md
|
|
163
CLAUDE.md
163
CLAUDE.md
@ -1,163 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- Run all tests: `go test ./...`
|
|
||||||
- Run tests with verbose output: `go test -v ./...`
|
|
||||||
- Run tests for specific package: `go test ./config`
|
|
||||||
- Run specific test: `go test -run TestConfigBool ./config`
|
|
||||||
|
|
||||||
### Building
|
|
||||||
- Build all packages: `go build ./...`
|
|
||||||
- Check module dependencies: `go mod tidy`
|
|
||||||
- Verify dependencies: `go mod verify`
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
- Format code: `go fmt ./...`
|
|
||||||
- Vet code: `go vet ./...`
|
|
||||||
- Run static analysis: `staticcheck ./...` (if available)
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
This is a common library (`go.ntppool.org/common`) providing shared infrastructure for the NTP Pool project. The codebase emphasizes observability, security, and modern Go practices.
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
**Web Service Foundation:**
|
|
||||||
- `ekko/` - Enhanced Echo web framework with pre-configured middleware (OpenTelemetry, Prometheus, logging, security headers)
|
|
||||||
- `health/` - Standalone health check HTTP server with `/__health` endpoint
|
|
||||||
- `metricsserver/` - Prometheus metrics exposure via `/metrics` endpoint
|
|
||||||
|
|
||||||
**Observability Stack:**
|
|
||||||
- `logger/` - Structured logging with OpenTelemetry trace integration and multiple output formats
|
|
||||||
- `tracing/` - OpenTelemetry distributed tracing with OTLP export support
|
|
||||||
- `metricsserver/` - Prometheus metrics with custom registry
|
|
||||||
|
|
||||||
**Configuration & Environment:**
|
|
||||||
- `config/` - Environment-based configuration with code-generated accessors (`config_accessor.go`)
|
|
||||||
- `version/` - Build metadata and version information with Cobra CLI integration
|
|
||||||
|
|
||||||
**Security & Communication:**
|
|
||||||
- `apitls/` - TLS certificate management with automatic renewal via certman
|
|
||||||
- `kafka/` - Kafka client wrapper with TLS support for log streaming
|
|
||||||
- `xff/fastlyxff/` - Fastly CDN IP range management for trusted proxy handling
|
|
||||||
|
|
||||||
**Utilities:**
|
|
||||||
- `ulid/` - Thread-safe ULID generation with monotonic ordering
|
|
||||||
- `timeutil/` - JSON-serializable duration types
|
|
||||||
- `types/` - Shared data structures (LogScoreAttributes for NTP server scoring)
|
|
||||||
|
|
||||||
### Key Patterns
|
|
||||||
|
|
||||||
**Functional Options:** Used extensively in `ekko/` for flexible service configuration
|
|
||||||
**Interface-Based Design:** `CertificateProvider` in `apitls/` for pluggable certificate management
|
|
||||||
**Context Propagation:** Throughout the codebase for cancellation and tracing
|
|
||||||
**Graceful Shutdown:** Implemented in web servers and background services
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
The codebase heavily uses:
|
|
||||||
- Echo web framework with custom middleware stack
|
|
||||||
- OpenTelemetry for observability (traces, metrics, logs)
|
|
||||||
- Prometheus for metrics collection
|
|
||||||
- Kafka for message streaming
|
|
||||||
- Cobra for CLI applications
|
|
||||||
|
|
||||||
### Code Generation
|
|
||||||
|
|
||||||
`config/config_accessor.go` is generated - modify `config.go` and regenerate accessors when adding new configuration options.
|
|
||||||
|
|
||||||
## Package Overview
|
|
||||||
|
|
||||||
### `apitls/`
|
|
||||||
TLS certificate management with automatic renewal support via certman. Provides a CA pool for trusted certificates and interfaces for pluggable certificate providers. Used for secure inter-service communication.
|
|
||||||
|
|
||||||
### `config/`
|
|
||||||
Environment-based configuration system with code-generated accessor methods. Handles deployment mode, hostname configuration, and TLS settings. Provides URL building utilities for web and management interfaces.
|
|
||||||
|
|
||||||
### `ekko/`
|
|
||||||
Enhanced Echo web framework wrapper with pre-configured middleware stack including OpenTelemetry tracing, Prometheus metrics, structured logging, gzip compression, and security headers. Supports HTTP/2 with graceful shutdown.
|
|
||||||
|
|
||||||
### `health/`
|
|
||||||
Standalone HTTP health check server that runs independently from the main application. Exposes `/__health` endpoint with configurable health handlers, timeouts, and graceful shutdown capabilities.
|
|
||||||
|
|
||||||
### `kafka/`
|
|
||||||
Kafka client wrapper with TLS support for secure log streaming. Provides connection management, broker discovery, and reader/writer factories with compression and batching optimizations.
|
|
||||||
|
|
||||||
### `logger/`
|
|
||||||
Structured logging system with OpenTelemetry trace integration. Supports multiple output formats (text, OTLP) with configurable log levels, systemd compatibility, and context-aware logging.
|
|
||||||
|
|
||||||
### `metricsserver/`
|
|
||||||
Dedicated Prometheus metrics HTTP server with custom registry isolation. Exposes `/metrics` endpoint with OpenMetrics support and graceful shutdown handling.
|
|
||||||
|
|
||||||
### `timeutil/`
|
|
||||||
JSON-serializable duration types that support both string parsing ("30s", "5m") and numeric nanosecond values. Compatible with configuration files and REST APIs.
|
|
||||||
|
|
||||||
### `tracing/`
|
|
||||||
OpenTelemetry distributed tracing setup with support for OTLP export via gRPC or HTTP. Handles resource detection, propagation, and automatic instrumentation with configurable TLS.
|
|
||||||
|
|
||||||
### `types/`
|
|
||||||
Shared data structures for the NTP Pool project. Currently contains `LogScoreAttributes` for NTP server scoring with JSON and SQL database compatibility.
|
|
||||||
|
|
||||||
### `ulid/`
|
|
||||||
Thread-safe ULID (Universally Unique Lexicographically Sortable Identifier) generation using cryptographically secure randomness. Optimized for simplicity and performance in high-concurrency environments.
|
|
||||||
|
|
||||||
### `version/`
|
|
||||||
Build metadata and version information system with Git integration. Provides CLI commands for Cobra and Kong frameworks, Prometheus build info metrics, and semantic version validation.
|
|
||||||
|
|
||||||
### `xff/fastlyxff/`
|
|
||||||
Fastly CDN IP range management for trusted proxy handling. Parses Fastly's IP ranges JSON file and generates Echo framework trust options for proper client IP extraction.
|
|
||||||
|
|
||||||
## Go Development Best Practices
|
|
||||||
|
|
||||||
### Code Style
|
|
||||||
- Follow standard Go formatting (`go fmt ./...`)
|
|
||||||
- Use `go vet ./...` for static analysis
|
|
||||||
- Run `staticcheck ./...` when available
|
|
||||||
- Prefer short, descriptive variable names
|
|
||||||
- Use interfaces for testability and flexibility
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Always handle errors explicitly
|
|
||||||
- Use `errors.Join()` for combining multiple errors
|
|
||||||
- Wrap errors with context using `fmt.Errorf("context: %w", err)`
|
|
||||||
- Return early on errors to reduce nesting
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- Write table-driven tests when testing multiple scenarios
|
|
||||||
- Use `t.Helper()` in test helper functions
|
|
||||||
- Test error conditions, not just happy paths
|
|
||||||
- Use `testing.Short()` for integration tests that can be skipped
|
|
||||||
|
|
||||||
### Concurrency
|
|
||||||
- Use contexts for cancellation and timeouts
|
|
||||||
- Prefer channels for communication over shared memory
|
|
||||||
- Use `sync.Once` for one-time initialization
|
|
||||||
- Always call `defer cancel()` after `context.WithCancel()`
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- Use `sync.Pool` for frequently allocated objects
|
|
||||||
- Prefer slices over arrays for better performance
|
|
||||||
- Use `strings.Builder` for string concatenation in loops
|
|
||||||
- Profile before optimizing with `go tool pprof`
|
|
||||||
|
|
||||||
### Observability
|
|
||||||
- Use structured logging with key-value pairs
|
|
||||||
- Add OpenTelemetry spans for external calls
|
|
||||||
- Include trace IDs in error messages
|
|
||||||
- Use metrics for monitoring application health
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- Keep dependencies minimal and well-maintained
|
|
||||||
- Use `go mod tidy` to clean up unused dependencies
|
|
||||||
- Pin major versions to avoid breaking changes
|
|
||||||
- Prefer standard library when possible
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- Never log sensitive information (passwords, tokens)
|
|
||||||
- Use `crypto/rand` for cryptographic randomness
|
|
||||||
- Validate all inputs at API boundaries
|
|
||||||
- Use TLS for all network communication
|
|
20
README.md
20
README.md
@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
Common library for the NTP Pool project with shared infrastructure components.
|
|
||||||
|
|
||||||
## Packages
|
|
||||||
|
|
||||||
- **apitls** - TLS setup for NTP Pool internal services with embedded CA
|
|
||||||
- **config** - NTP Pool project configuration with environment variables
|
|
||||||
- **ekko** - Enhanced Echo web framework with observability middleware
|
|
||||||
- **health** - Standalone health check HTTP server
|
|
||||||
- **kafka** - Kafka client wrapper with TLS support
|
|
||||||
- **logger** - Structured logging with OpenTelemetry integration
|
|
||||||
- **metricsserver** - Prometheus metrics HTTP server
|
|
||||||
- **timeutil** - JSON-serializable duration types
|
|
||||||
- **tracing** - OpenTelemetry distributed tracing setup
|
|
||||||
- **types** - Shared data structures for NTP Pool
|
|
||||||
- **ulid** - Thread-safe ULID generation
|
|
||||||
- **version** - Build metadata and version information
|
|
||||||
- **xff/fastlyxff** - Fastly CDN IP range management
|
|
||||||
|
|
||||||
[](https://pkg.go.dev/go.ntppool.org/common)
|
|
@ -30,6 +30,7 @@ func CAPool() (*x509.CertPool, error) {
|
|||||||
// GetCertman sets up certman for the specified cert / key pair. It is
|
// GetCertman sets up certman for the specified cert / key pair. It is
|
||||||
// used in the monitor-api and (for now) in the client
|
// used in the monitor-api and (for now) in the client
|
||||||
func GetCertman(certFile, keyFile string) (*certman.CertMan, error) {
|
func GetCertman(certFile, keyFile string) (*certman.CertMan, error) {
|
||||||
|
|
||||||
cm, err := certman.New(certFile, keyFile)
|
cm, err := certman.New(certFile, keyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"go.ntppool.org/common/logger"
|
"go.ntppool.org/common/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go tool github.com/masaushi/accessory -type Config
|
//go:generate accessory -type Config
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
deploymentMode string `accessor:"getter"`
|
deploymentMode string `accessor:"getter"`
|
||||||
@ -50,10 +50,6 @@ func (c *Config) WebURL(path string, query *url.Values) string {
|
|||||||
return baseURL(c.webHostname, c.webTLS, path, query)
|
return baseURL(c.webHostname, c.webTLS, path, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) ManageURL(path string, query *url.Values) string {
|
|
||||||
return baseURL(c.manageHostname, c.webTLS, path, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
func baseURL(host string, tls bool, path string, query *url.Values) string {
|
func baseURL(host string, tls bool, path string, query *url.Values) string {
|
||||||
uri := url.URL{}
|
uri := url.URL{}
|
||||||
uri.Host = host
|
uri.Host = host
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBaseURL(t *testing.T) {
|
func TestBaseURL(t *testing.T) {
|
||||||
|
|
||||||
os.Setenv("web_hostname", "www.ntp.dev, web.ntppool.dev")
|
os.Setenv("web_hostname", "www.ntp.dev, web.ntppool.dev")
|
||||||
os.Setenv("web_tls", "yes")
|
os.Setenv("web_tls", "yes")
|
||||||
|
|
||||||
@ -21,4 +22,5 @@ func TestBaseURL(t *testing.T) {
|
|||||||
if u != "https://www.ntp.dev/foo?foo=bar" {
|
if u != "https://www.ntp.dev/foo?foo=bar" {
|
||||||
t.Fatalf("unexpected WebURL: %s", u)
|
t.Fatalf("unexpected WebURL: %s", u)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package depenv
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type contextKey struct{}
|
|
||||||
|
|
||||||
// NewContext adds the deployment environment to the context
|
|
||||||
func NewContext(ctx context.Context, d DeploymentEnvironment) context.Context {
|
|
||||||
return context.WithValue(ctx, contextKey{}, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromContext retrieves the deployment environment from the context
|
|
||||||
func FromContext(ctx context.Context) DeploymentEnvironment {
|
|
||||||
if d, ok := ctx.Value(contextKey{}).(DeploymentEnvironment); ok {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
return DeployUndefined
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
package depenv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var manageServers = map[DeploymentEnvironment]string{
|
|
||||||
DeployDevel: "https://manage.askdev.grundclock.com",
|
|
||||||
DeployTest: "https://manage.beta.grundclock.com",
|
|
||||||
DeployProd: "https://manage.ntppool.org",
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiServers = map[DeploymentEnvironment]string{
|
|
||||||
DeployDevel: "https://dev-api.ntppool.dev",
|
|
||||||
DeployTest: "https://beta-api.ntppool.dev",
|
|
||||||
DeployProd: "https://api.ntppool.dev",
|
|
||||||
}
|
|
||||||
|
|
||||||
// var validationServers = map[DeploymentEnvironment]string{
|
|
||||||
// DeployDevel: "https://v.ntp.dev/d/",
|
|
||||||
// DeployTest: "https://v.ntp.dev/b/",
|
|
||||||
// DeployProd: "https://v.ntp.dev/p/",
|
|
||||||
// }
|
|
||||||
|
|
||||||
const (
|
|
||||||
DeployUndefined DeploymentEnvironment = iota
|
|
||||||
DeployDevel
|
|
||||||
DeployTest
|
|
||||||
DeployProd
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeploymentEnvironment uint8
|
|
||||||
|
|
||||||
func DeploymentEnvironmentFromString(s string) DeploymentEnvironment {
|
|
||||||
switch s {
|
|
||||||
case "devel", "dev":
|
|
||||||
return DeployDevel
|
|
||||||
case "test", "beta":
|
|
||||||
return DeployTest
|
|
||||||
case "prod":
|
|
||||||
return DeployProd
|
|
||||||
default:
|
|
||||||
return DeployUndefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DeploymentEnvironment) String() string {
|
|
||||||
switch d {
|
|
||||||
case DeployProd:
|
|
||||||
return "prod"
|
|
||||||
case DeployTest:
|
|
||||||
return "test"
|
|
||||||
case DeployDevel:
|
|
||||||
return "devel"
|
|
||||||
default:
|
|
||||||
panic("invalid DeploymentEnvironment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DeploymentEnvironment) APIHost() string {
|
|
||||||
if apiHost := os.Getenv("API_HOST"); apiHost != "" {
|
|
||||||
return apiHost
|
|
||||||
}
|
|
||||||
return apiServers[d]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DeploymentEnvironment) ManageURL(path string) string {
|
|
||||||
return manageServers[d] + path
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DeploymentEnvironment) MonitorDomain() string {
|
|
||||||
return d.String() + ".mon.ntppool.dev"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DeploymentEnvironment) UnmarshalText(text []byte) error {
|
|
||||||
s := string(text)
|
|
||||||
if s == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
env := DeploymentEnvironmentFromString(s)
|
|
||||||
if env == DeployUndefined {
|
|
||||||
return fmt.Errorf("invalid deployment environment: %s", s)
|
|
||||||
}
|
|
||||||
*d = env
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package depenv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var monitorApiServers = map[DeploymentEnvironment]string{
|
|
||||||
DeployDevel: "https://api.devel.mon.ntppool.dev",
|
|
||||||
DeployTest: "https://api.test.mon.ntppool.dev",
|
|
||||||
DeployProd: "https://api.mon.ntppool.dev",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DeploymentEnvironment) MonitorAPIHost() string {
|
|
||||||
return monitorApiServers[d]
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDeploymentEnvironmentFromName(clientName string) (DeploymentEnvironment, error) {
|
|
||||||
clientName = strings.ToLower(clientName)
|
|
||||||
|
|
||||||
if !strings.HasSuffix(clientName, ".mon.ntppool.dev") {
|
|
||||||
return DeployUndefined, fmt.Errorf("invalid client name %s", clientName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if clientName == "api.mon.ntppool.dev" {
|
|
||||||
return DeployProd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := clientName[:strings.Index(clientName, ".mon.ntppool.dev")]
|
|
||||||
parts := strings.Split(prefix, ".")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return DeployUndefined, fmt.Errorf("invalid client name %s", clientName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if d := DeploymentEnvironmentFromString(parts[1]); d != DeployUndefined {
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return DeployUndefined, fmt.Errorf("invalid client name %s (unknown environment %s)", clientName, parts[1])
|
|
||||||
}
|
|
170
ekko/ekko.go
170
ekko/ekko.go
@ -1,170 +0,0 @@
|
|||||||
package ekko
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo-contrib/echoprometheus"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/labstack/echo/v4/middleware"
|
|
||||||
slogecho "github.com/samber/slog-echo"
|
|
||||||
"go.ntppool.org/common/logger"
|
|
||||||
"go.ntppool.org/common/version"
|
|
||||||
"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(name string, options ...func(*Ekko)) (*Ekko, error) {
|
|
||||||
ek := &Ekko{
|
|
||||||
writeTimeout: 60 * time.Second,
|
|
||||||
readHeaderTimeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, o := range options {
|
|
||||||
o(ek)
|
|
||||||
}
|
|
||||||
return ek, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup Echo; only intended for testing
|
|
||||||
func (ek *Ekko) SetupEcho(ctx context.Context) (*echo.Echo, error) {
|
|
||||||
return ek.setup(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup Echo and start the server. Will return if the http server
|
|
||||||
// returns or the context is done.
|
|
||||||
func (ek *Ekko) Start(ctx context.Context) error {
|
|
||||||
log := logger.Setup()
|
|
||||||
|
|
||||||
e, err := ek.setup(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
g, ctx := errgroup.WithContext(ctx)
|
|
||||||
g.Go(func() error {
|
|
||||||
e.Server.Addr = fmt.Sprintf(":%d", ek.port)
|
|
||||||
log.Info("server starting", "port", ek.port)
|
|
||||||
// err := e.Server.ListenAndServe()
|
|
||||||
err := e.StartH2CServer(e.Server.Addr, &http2.Server{})
|
|
||||||
if err == http.ErrServerClosed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Go(func() error {
|
|
||||||
<-ctx.Done()
|
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
return e.Shutdown(shutdownCtx)
|
|
||||||
})
|
|
||||||
|
|
||||||
return g.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ek *Ekko) setup(ctx context.Context) (*echo.Echo, error) {
|
|
||||||
log := logger.Setup()
|
|
||||||
|
|
||||||
e := echo.New()
|
|
||||||
|
|
||||||
e.Server.ReadHeaderTimeout = ek.readHeaderTimeout
|
|
||||||
e.Server.WriteTimeout = ek.writeTimeout
|
|
||||||
|
|
||||||
e.Server.BaseContext = func(_ net.Listener) context.Context {
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
trustOptions := []echo.TrustOption{
|
|
||||||
echo.TrustLoopback(true),
|
|
||||||
echo.TrustLinkLocal(false),
|
|
||||||
echo.TrustPrivateNet(true),
|
|
||||||
}
|
|
||||||
e.IPExtractor = echo.ExtractIPFromXFFHeader(trustOptions...)
|
|
||||||
|
|
||||||
if ek.otelmiddleware == nil {
|
|
||||||
e.Use(otelecho.Middleware(ek.name))
|
|
||||||
} else {
|
|
||||||
e.Use(ek.otelmiddleware)
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
|
|
||||||
LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
|
|
||||||
log.ErrorContext(c.Request().Context(), err.Error(), "stack", string(stack))
|
|
||||||
fmt.Println(string(stack))
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
e.Use(slogecho.NewWithConfig(log,
|
|
||||||
slogecho.Config{
|
|
||||||
WithTraceID: false, // done by logger already
|
|
||||||
Filters: ek.logFilters,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
if ek.prom != nil {
|
|
||||||
e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{
|
|
||||||
Subsystem: ek.name,
|
|
||||||
Registerer: ek.prom,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
if ek.gzipConfig != nil {
|
|
||||||
e.Use(middleware.GzipWithConfig(*ek.gzipConfig))
|
|
||||||
} else {
|
|
||||||
e.Use(middleware.Gzip())
|
|
||||||
}
|
|
||||||
|
|
||||||
secureConfig := middleware.DefaultSecureConfig
|
|
||||||
// secureConfig.ContentSecurityPolicy = "default-src *"
|
|
||||||
secureConfig.ContentSecurityPolicy = ""
|
|
||||||
secureConfig.HSTSMaxAge = int(time.Hour * 168 * 30 / time.Second)
|
|
||||||
secureConfig.HSTSPreloadEnabled = true
|
|
||||||
|
|
||||||
e.Use(middleware.SecureWithConfig(secureConfig))
|
|
||||||
|
|
||||||
e.Use(
|
|
||||||
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
request := c.Request()
|
|
||||||
|
|
||||||
span := trace.SpanFromContext(request.Context())
|
|
||||||
if span.IsRecording() {
|
|
||||||
|
|
||||||
span.SetAttributes(attribute.String("http.real_ip", c.RealIP()))
|
|
||||||
span.SetAttributes(attribute.String("url.path", c.Request().RequestURI))
|
|
||||||
if q := c.QueryString(); len(q) > 0 {
|
|
||||||
span.SetAttributes(attribute.String("url.query", q))
|
|
||||||
}
|
|
||||||
c.Response().Header().Set("Traceparent", span.SpanContext().TraceID().String())
|
|
||||||
}
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
vinfo := version.VersionInfo()
|
|
||||||
v := ek.name + "/" + vinfo.Version + "+" + vinfo.GitRevShort
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
c.Response().Header().Set(echo.HeaderServer, v)
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if ek.routeFn != nil {
|
|
||||||
err := ek.routeFn(e)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return e, nil
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
package ekko
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/labstack/echo/v4/middleware"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
slogecho "github.com/samber/slog-echo"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
writeTimeout time.Duration
|
|
||||||
readHeaderTimeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type RouteFn func(e *echo.Echo) error
|
|
||||||
|
|
||||||
func WithPort(port int) func(*Ekko) {
|
|
||||||
return func(ek *Ekko) {
|
|
||||||
ek.port = port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithPrometheus(reg prometheus.Registerer) func(*Ekko) {
|
|
||||||
return func(ek *Ekko) {
|
|
||||||
ek.prom = reg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithEchoSetup(rfn RouteFn) func(*Ekko) {
|
|
||||||
return func(ek *Ekko) {
|
|
||||||
ek.routeFn = rfn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithLogFilters(f []slogecho.Filter) func(*Ekko) {
|
|
||||||
return func(ek *Ekko) {
|
|
||||||
ek.logFilters = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithOtelMiddleware(mw echo.MiddlewareFunc) func(*Ekko) {
|
|
||||||
return func(ek *Ekko) {
|
|
||||||
ek.otelmiddleware = mw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithWriteTimeout(t time.Duration) func(*Ekko) {
|
|
||||||
return func(ek *Ekko) {
|
|
||||||
ek.writeTimeout = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithReadHeaderTimeout(t time.Duration) func(*Ekko) {
|
|
||||||
return func(ek *Ekko) {
|
|
||||||
ek.readHeaderTimeout = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithGzipConfig(gzipConfig *middleware.GzipConfig) func(*Ekko) {
|
|
||||||
return func(ek *Ekko) {
|
|
||||||
ek.gzipConfig = gzipConfig
|
|
||||||
}
|
|
||||||
}
|
|
75
go.mod
75
go.mod
@ -1,77 +1,56 @@
|
|||||||
module go.ntppool.org/common
|
module go.ntppool.org/common
|
||||||
|
|
||||||
go 1.23.5
|
go 1.22.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/abh/certman v0.4.0
|
github.com/abh/certman v0.4.0
|
||||||
github.com/labstack/echo-contrib v0.17.2
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
github.com/labstack/echo/v4 v4.13.3
|
|
||||||
github.com/oklog/ulid/v2 v2.1.0
|
github.com/oklog/ulid/v2 v2.1.0
|
||||||
github.com/prometheus/client_golang v1.20.5
|
github.com/prometheus/client_golang v1.19.1
|
||||||
github.com/remychantenay/slog-otel v1.3.2
|
github.com/remychantenay/slog-otel v1.3.1
|
||||||
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/segmentio/kafka-go v0.4.47
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.8.0
|
go.opentelemetry.io/otel v1.28.0
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.58.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.58.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0
|
||||||
go.opentelemetry.io/otel v1.33.0
|
go.opentelemetry.io/otel/sdk v1.28.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0
|
go.opentelemetry.io/otel/trace v1.28.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0
|
golang.org/x/mod v0.19.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0
|
golang.org/x/sync v0.7.0
|
||||||
go.opentelemetry.io/otel/log v0.9.0
|
|
||||||
go.opentelemetry.io/otel/sdk v1.33.0
|
|
||||||
go.opentelemetry.io/otel/sdk/log v0.9.0
|
|
||||||
go.opentelemetry.io/otel/trace v1.33.0
|
|
||||||
golang.org/x/mod v0.22.0
|
|
||||||
golang.org/x/net v0.33.0
|
|
||||||
golang.org/x/sync v0.10.0
|
|
||||||
google.golang.org/grpc v1.69.2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/uuid v1.6.0 // 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.20.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/labstack/gommon v0.4.2 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.61.0 // indirect
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/samber/lo v1.47.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.58.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 // indirect
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0 // indirect
|
golang.org/x/net v0.27.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 // indirect
|
golang.org/x/text v0.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.55.0 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 // indirect
|
google.golang.org/grpc v1.65.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect
|
|
||||||
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
|
||||||
golang.org/x/text v0.21.0 // indirect
|
|
||||||
golang.org/x/time v0.8.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
|
|
||||||
)
|
)
|
||||||
|
259
go.sum
259
go.sum
@ -2,17 +2,24 @@ github.com/abh/certman v0.4.0 h1:XHoDtb0YyRQPclaHMrBDlKTVZpNjTK6vhB0S3Bd/Sbs=
|
|||||||
github.com/abh/certman v0.4.0/go.mod h1:x8QhpKVZifmV1Hdiwdg9gLo2GMPAxezz1s3zrVnPs+I=
|
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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
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/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||||
|
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
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.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
@ -23,19 +30,21 @@ 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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/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.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
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.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w=
|
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||||
github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E=
|
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
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/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
@ -49,31 +58,43 @@ 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.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
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.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.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||||
|
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||||
|
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||||
|
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
|
||||||
|
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
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/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.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
|
||||||
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
|
||||||
|
github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck=
|
||||||
|
github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
|
||||||
|
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
|
||||||
|
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
|
||||||
|
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||||
|
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||||
|
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||||
|
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
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/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.2.4 h1:Z/IwIgFPzzGqLTI460KbTuJZPm5U830dgu0gPiEpufA=
|
||||||
github.com/remychantenay/slog-otel v1.3.2/go.mod h1:gKW4tQ8cGOKoA+bi7wtYba/tcJ6Tc9XyQ/EW8gHA/2E=
|
github.com/remychantenay/slog-otel v1.2.4/go.mod h1:Ar2ZBcRfIPyoKV/3Xq4oHmNgKc69juGB0QMUzo1vJOc=
|
||||||
|
github.com/remychantenay/slog-otel v1.3.0 h1:mppL97agkmwR416lKzltRQ9QRhrPdxwVidt0AnI3Ts4=
|
||||||
|
github.com/remychantenay/slog-otel v1.3.0/go.mod h1:L2VAe6WOMAk/kRzzuv2B/rWe/IDXAhUNae0919b4kHU=
|
||||||
|
github.com/remychantenay/slog-otel v1.3.1 h1:A+VjqHaUka/sg3meWKVZw9NuMCgYUu7tPLI87pvBHxs=
|
||||||
|
github.com/remychantenay/slog-otel v1.3.1/go.mod h1:smosUkTPRlTot5TDJ88qcmGz6tnBq6MJ1bb2ndO66uE=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
|
||||||
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
|
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
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/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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
@ -82,8 +103,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.7.1/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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
@ -95,80 +116,88 @@ github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3k
|
|||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
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/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.8.0 h1:G3sKsNueSdxuACINFxKrQeimAIst0A5ytA2YJH+3e1c=
|
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.8.0/go.mod h1:ptJm3wizguEPurZgarDAwOeX7O0iMR7l+QvIVenhYdE=
|
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.58.0 h1:gQFwWiqm4JUvOjpdmyU0di+2pVQ8QNpk1Ak/54Y6NcY=
|
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.58.0/go.mod h1:CNyFi9PuvHtEJNmMFHaXZMuA4XmgRXIqpFcHdqzLvVU=
|
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.58.0 h1:qVsDVgZd/bC6ZKDOHSjILpm0T/BWvASC9cQU3GYga78=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.58.0/go.mod h1:bAv7mY+5qTsFPFaRpr75vDOocX09I36QH4Rg0slEG/U=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.58.0 h1:DBk8Zh+Yn3WtWCdGSx1pbEV9/naLtjG16c1zwQA2MBI=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
|
||||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.58.0/go.mod h1:DFx32LPclW1MNdSKIMrjjetsk0tJtYhAvuGjDIG2SKE=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.33.0 h1:ig/IsHyyoQ1F1d6FUDIIW5oYpsuTVtN16AyGOgdjAHQ=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.33.0/go.mod h1:EsVYoNy+Eol5znb6wwN3XQTILyjl040gUpEnUSNZfsk=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 h1:gA2gh+3B3NDvRFP30Ufh7CC3TtJRbUSf2TTD0LbCagw=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0/go.mod h1:smRTR+02OtrVGjvWE1sQxhuazozKc/BXvvqqnmOxy+s=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0 h1:Za0Z/j9Gf3Z9DKQ1choU9xI2noCxlkcyFFP2Ob3miEQ=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0/go.mod h1:jMRB8N75meTNjDFQyJBA/2Z9en21CsxwMctn08NHY6c=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA=
|
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE=
|
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 h1:bSjzTvsXZbLSWU8hnZXcKmEVaJjjnandxD0PxThhVU8=
|
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0/go.mod h1:aj2rilHL8WjXY1I5V+ra+z8FELtk681deydgYT8ikxU=
|
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
|
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
|
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
|
go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.55.0 h1:sSPw658Lk2NWAv74lkD3B/RSDb+xRFx46GjkrL3VUZo=
|
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.55.0/go.mod h1:nC00vyCmQixoeaxF6KNyP42II/RHa9UdruK02qBmHvI=
|
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0 h1:iI15wfQb5ZtAVTdS5WROxpYmw6Kjez3hT9SuzXhrgGQ=
|
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0/go.mod h1:yepwlNzVVxHWR5ugHIrll+euPQPq4pvysHTDr/daV9o=
|
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs=
|
go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4=
|
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0=
|
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk=
|
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||||
go.opentelemetry.io/otel/log v0.9.0 h1:0OiWRefqJ2QszpCiqwGO0u9ajMPe17q6IscQvvp3czY=
|
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
||||||
go.opentelemetry.io/otel/log v0.9.0/go.mod h1:WPP4OJ+RBkQ416jrFCQFuFKtXKD6mOoYCQm6ykK8VaU=
|
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
|
||||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
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.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-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.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.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||||
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||||
|
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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-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.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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@ -179,8 +208,14 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||||
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@ -193,23 +228,43 @@ 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.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.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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c=
|
||||||
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 h1:QW9+G6Fir4VcRXVH8x3LilNAb6cxBGLa6+GM4hRwexE=
|
||||||
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3/go.mod h1:kdrSS/OiLkPrNUpzD4aHgCq2rVuC/YRxok32HXZ4vRE=
|
||||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3 h1:9Xyg6I9IWQZhRVfCWjKK+l6kI0jHcPesVlMnT//aHNo=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||||
|
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||||
|
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||||
|
google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8=
|
||||||
|
google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||||
|
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||||
|
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||||
|
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||||
|
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||||
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
// Package health provides a standalone HTTP server for health checks.
|
|
||||||
//
|
|
||||||
// This package implements a simple health check server that can be used
|
|
||||||
// to expose health status endpoints for monitoring and load balancing.
|
|
||||||
// It supports custom health check handlers and provides structured logging
|
|
||||||
// with graceful shutdown capabilities.
|
|
||||||
package health
|
package health
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -17,19 +11,11 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is a standalone HTTP server dedicated to health checks.
|
|
||||||
// It runs separately from the main application server to ensure health
|
|
||||||
// checks remain available even if the main server is experiencing issues.
|
|
||||||
//
|
|
||||||
// The server includes built-in timeouts, graceful shutdown, and structured
|
|
||||||
// logging for monitoring and debugging health check behavior.
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
healthFn http.HandlerFunc
|
healthFn http.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new health check server with the specified health handler.
|
|
||||||
// If healthFn is nil, a default handler that returns HTTP 200 "ok" is used.
|
|
||||||
func NewServer(healthFn http.HandlerFunc) *Server {
|
func NewServer(healthFn http.HandlerFunc) *Server {
|
||||||
if healthFn == nil {
|
if healthFn == nil {
|
||||||
healthFn = basicHealth
|
healthFn = basicHealth
|
||||||
@ -41,15 +27,12 @@ func NewServer(healthFn http.HandlerFunc) *Server {
|
|||||||
return srv
|
return srv
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLogger replaces the default logger with a custom one.
|
|
||||||
func (srv *Server) SetLogger(log *slog.Logger) {
|
func (srv *Server) SetLogger(log *slog.Logger) {
|
||||||
srv.log = log
|
srv.log = log
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen starts the health server on the specified port and blocks until ctx is cancelled.
|
|
||||||
// The server exposes the health handler at "/__health" with graceful shutdown support.
|
|
||||||
func (srv *Server) Listen(ctx context.Context, port int) error {
|
func (srv *Server) Listen(ctx context.Context, port int) error {
|
||||||
srv.log.Info("starting health listener", "port", port)
|
srv.log.Info("Starting health listener", "port", port)
|
||||||
|
|
||||||
serveMux := http.NewServeMux()
|
serveMux := http.NewServeMux()
|
||||||
|
|
||||||
@ -76,10 +59,11 @@ func (srv *Server) Listen(ctx context.Context, port int) error {
|
|||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
||||||
g.Go(func() error {
|
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||||
shCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := hsrv.Shutdown(shCtx); err != nil {
|
|
||||||
|
g.Go(func() error {
|
||||||
|
if err := hsrv.Shutdown(ctx); err != nil {
|
||||||
srv.log.Error("health check server shutdown failed", "err", err)
|
srv.log.Error("health check server shutdown failed", "err", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -89,7 +73,8 @@ func (srv *Server) Listen(ctx context.Context, port int) error {
|
|||||||
return g.Wait()
|
return g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthCheckListener runs a simple HTTP server on the specified port for health check probes.
|
// HealthCheckListener runs simple http server on the specified port for
|
||||||
|
// health check probes
|
||||||
func HealthCheckListener(ctx context.Context, port int, log *slog.Logger) error {
|
func HealthCheckListener(ctx context.Context, port int, log *slog.Logger) error {
|
||||||
srv := NewServer(nil)
|
srv := NewServer(nil)
|
||||||
srv.SetLogger(log)
|
srv.SetLogger(log)
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestHealthHandler(t *testing.T) {
|
func TestHealthHandler(t *testing.T) {
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/__health", nil)
|
req := httptest.NewRequest(http.MethodGet, "/__health", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
@ -42,9 +42,11 @@ type Kafka struct {
|
|||||||
l *log.Logger
|
l *log.Logger
|
||||||
|
|
||||||
// wr *kafka.Writer
|
// wr *kafka.Writer
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *Kafka) tlsConfig() (*tls.Config, error) {
|
func (k *Kafka) tlsConfig() (*tls.Config, error) {
|
||||||
|
|
||||||
cm, err := certman.New(k.tls.Cert, k.tls.Key)
|
cm, err := certman.New(k.tls.Cert, k.tls.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -187,6 +189,7 @@ func (k *Kafka) brokerAddrs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *Kafka) NewWriter(topic string) (*kafka.Writer, error) {
|
func (k *Kafka) NewWriter(topic string) (*kafka.Writer, error) {
|
||||||
|
|
||||||
// https://pkg.go.dev/github.com/segmentio/kafka-go#Writer
|
// https://pkg.go.dev/github.com/segmentio/kafka-go#Writer
|
||||||
w := &kafka.Writer{
|
w := &kafka.Writer{
|
||||||
Addr: kafka.TCP(k.brokerAddrs()...),
|
Addr: kafka.TCP(k.brokerAddrs()...),
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"log/slog"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type logfmt struct {
|
|
||||||
buf *bytes.Buffer
|
|
||||||
txt slog.Handler
|
|
||||||
next slog.Handler
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLogFmtHandler(next slog.Handler) slog.Handler {
|
|
||||||
buf := bytes.NewBuffer([]byte{})
|
|
||||||
|
|
||||||
h := &logfmt{
|
|
||||||
buf: buf,
|
|
||||||
next: next,
|
|
||||||
txt: slog.NewTextHandler(buf, &slog.HandlerOptions{
|
|
||||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
|
||||||
if a.Key == slog.TimeKey && len(groups) == 0 {
|
|
||||||
return slog.Attr{}
|
|
||||||
}
|
|
||||||
if a.Key == slog.LevelKey && len(groups) == 0 {
|
|
||||||
return slog.Attr{}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *logfmt) Enabled(ctx context.Context, lvl slog.Level) bool {
|
|
||||||
return h.next.Enabled(ctx, lvl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *logfmt) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
||||||
return &logfmt{
|
|
||||||
buf: bytes.NewBuffer([]byte{}),
|
|
||||||
next: h.next.WithAttrs(slices.Clone(attrs)),
|
|
||||||
txt: h.txt.WithAttrs(slices.Clone(attrs)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *logfmt) WithGroup(g string) slog.Handler {
|
|
||||||
if g == "" {
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
return &logfmt{
|
|
||||||
buf: bytes.NewBuffer([]byte{}),
|
|
||||||
next: h.next.WithGroup(g),
|
|
||||||
txt: h.txt.WithGroup(g),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *logfmt) Handle(ctx context.Context, r slog.Record) error {
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
|
|
||||||
if h.buf.Len() > 0 {
|
|
||||||
panic("buffer wasn't empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
h.txt.Handle(ctx, r)
|
|
||||||
r.Message = h.buf.String()
|
|
||||||
r.Message = strings.TrimSuffix(r.Message, "\n")
|
|
||||||
h.buf.Reset()
|
|
||||||
|
|
||||||
return h.next.Handle(ctx, r)
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLogFmt(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
jsonh := slog.NewJSONHandler(&buf, nil)
|
|
||||||
h := newLogFmtHandler(jsonh)
|
|
||||||
|
|
||||||
log := slog.New(h)
|
|
||||||
log.Info("test message", "id", 1010)
|
|
||||||
t.Logf("buf: %s", buf.String())
|
|
||||||
|
|
||||||
msg := map[string]any{}
|
|
||||||
|
|
||||||
err := json.Unmarshal(buf.Bytes(), &msg)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("couldn't unmarshal json log: %s", err)
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
if msgTxt, ok := msg["msg"].(string); ok {
|
|
||||||
if !strings.Contains(msgTxt, "id=1010") {
|
|
||||||
t.Log("didn't find id in msg value")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if strings.Contains(msgTxt, "level=") {
|
|
||||||
t.Log("msg value contains level=")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.Log("didn't find message in output")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
120
logger/logger.go
120
logger/logger.go
@ -8,28 +8,19 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
slogtraceid "github.com/remychantenay/slog-otel"
|
slogotel "github.com/remychantenay/slog-otel"
|
||||||
slogmulti "github.com/samber/slog-multi"
|
|
||||||
"go.opentelemetry.io/contrib/bridges/otelslog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ConfigPrefix = ""
|
var ConfigPrefix = ""
|
||||||
|
|
||||||
var (
|
var rootLogger *slog.Logger
|
||||||
textLogger *slog.Logger
|
var setup sync.Once
|
||||||
otlpLogger *slog.Logger
|
|
||||||
multiLogger *slog.Logger
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
func Setup() *slog.Logger {
|
||||||
setupText sync.Once // this sets the default
|
|
||||||
setupOtlp sync.Once // this never sets the default
|
|
||||||
setupMulti sync.Once // this sets the default, and will always run after the others
|
|
||||||
mu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupStdErrHandler() slog.Handler {
|
setup.Do(func() {
|
||||||
programLevel := new(slog.LevelVar) // Info by default
|
|
||||||
|
var programLevel = new(slog.LevelVar) // Info by default
|
||||||
|
|
||||||
envVar := "DEBUG"
|
envVar := "DEBUG"
|
||||||
if len(ConfigPrefix) > 0 {
|
if len(ConfigPrefix) > 0 {
|
||||||
@ -48,107 +39,46 @@ func setupStdErrHandler() slog.Handler {
|
|||||||
// don't add timestamps when running under systemd
|
// don't add timestamps when running under systemd
|
||||||
log.Default().SetFlags(0)
|
log.Default().SetFlags(0)
|
||||||
|
|
||||||
logOptions.ReplaceAttr = logRemoveTime
|
logReplace := func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
// Remove time
|
||||||
|
if a.Key == slog.TimeKey && len(groups) == 0 {
|
||||||
|
return slog.Attr{}
|
||||||
|
}
|
||||||
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
logHandler := slogtraceid.OtelHandler{
|
logOptions.ReplaceAttr = logReplace
|
||||||
|
}
|
||||||
|
|
||||||
|
logHandler := slogotel.OtelHandler{
|
||||||
Next: slog.NewTextHandler(os.Stderr, logOptions),
|
Next: slog.NewTextHandler(os.Stderr, logOptions),
|
||||||
}
|
}
|
||||||
|
|
||||||
return logHandler
|
// https://github.com/cyrusaf/ctxlog/pull/1
|
||||||
}
|
// log := slog.New(ctxlog.NewHandler(logHandler))
|
||||||
|
log := slog.New(logHandler)
|
||||||
|
|
||||||
func setupOtlpLogger() *slog.Logger {
|
slog.SetDefault(log)
|
||||||
setupOtlp.Do(func() {
|
|
||||||
otlpLogger = slog.New(
|
|
||||||
newLogFmtHandler(otelslog.NewHandler("common")),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return otlpLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupMultiLogger will setup and make default a logger that
|
rootLogger = log
|
||||||
// logs as described in Setup() as well as an OLTP logger.
|
|
||||||
// The "multi logger" is made the default the first time
|
|
||||||
// this function is called
|
|
||||||
func SetupMultiLogger() *slog.Logger {
|
|
||||||
setupMulti.Do(func() {
|
|
||||||
textHandler := Setup().Handler()
|
|
||||||
otlpHandler := setupOtlpLogger().Handler()
|
|
||||||
|
|
||||||
multiHandler := slogmulti.Fanout(
|
|
||||||
textHandler,
|
|
||||||
otlpHandler,
|
|
||||||
)
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
multiLogger = slog.New(multiHandler)
|
|
||||||
slog.SetDefault(multiLogger)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return multiLogger
|
return rootLogger
|
||||||
}
|
|
||||||
|
|
||||||
// SetupOLTP configures and returns a logger sending logs
|
|
||||||
// via OpenTelemetry (configured via the tracing package).
|
|
||||||
//
|
|
||||||
// This was made to work with Loki + Grafana that makes it
|
|
||||||
// hard to view the log attributes in the UI, so the log
|
|
||||||
// message is formatted similarly to the text logger. The
|
|
||||||
// attributes are duplicated as OLTP attributes in the
|
|
||||||
// log messages. https://github.com/grafana/loki/issues/14788
|
|
||||||
func SetupOLTP() *slog.Logger {
|
|
||||||
return setupOtlpLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup returns an slog.Logger configured for text formatting
|
|
||||||
// to stderr.
|
|
||||||
// OpenTelemetry trace_id and span_id's are logged as attributes
|
|
||||||
// when available.
|
|
||||||
// When the application is running under systemd timestamps are
|
|
||||||
// omitted. On first call the slog default logger is set to this
|
|
||||||
// logger as well.
|
|
||||||
//
|
|
||||||
// If SetupMultiLogger has been called Setup() will return
|
|
||||||
// the "multi logger"
|
|
||||||
func Setup() *slog.Logger {
|
|
||||||
setupText.Do(func() {
|
|
||||||
h := setupStdErrHandler()
|
|
||||||
textLogger = slog.New(h)
|
|
||||||
slog.SetDefault(textLogger)
|
|
||||||
})
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
if multiLogger != nil {
|
|
||||||
return multiLogger
|
|
||||||
}
|
|
||||||
return textLogger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type loggerKey struct{}
|
type loggerKey struct{}
|
||||||
|
|
||||||
// NewContext adds the logger to the context. Use this
|
// NewContext adds the logger to the context.
|
||||||
// to for example make a request specific logger available
|
|
||||||
// to other functions through the context
|
|
||||||
func NewContext(ctx context.Context, l *slog.Logger) context.Context {
|
func NewContext(ctx context.Context, l *slog.Logger) context.Context {
|
||||||
return context.WithValue(ctx, loggerKey{}, l)
|
return context.WithValue(ctx, loggerKey{}, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromContext retrieves a logger from the context. If there is none,
|
// FromContext retrieves a logger from the context. If there is none,
|
||||||
// it returns the default logger
|
// it returns the default logger.
|
||||||
func FromContext(ctx context.Context) *slog.Logger {
|
func FromContext(ctx context.Context) *slog.Logger {
|
||||||
if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
|
if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
return Setup()
|
return Setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
func logRemoveTime(groups []string, a slog.Attr) slog.Attr {
|
|
||||||
// Remove time
|
|
||||||
if a.Key == slog.TimeKey && len(groups) == 0 {
|
|
||||||
return slog.Attr{}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
@ -27,15 +27,10 @@ func NewStdLog(key string, debug bool, log *slog.Logger) *stdLoggerish {
|
|||||||
return sl
|
return sl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l stdLoggerish) Println(msg ...any) {
|
func (l stdLoggerish) Println(msg ...interface{}) {
|
||||||
l.f(l.key, "msg", msg)
|
l.f(l.key, "msg", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l stdLoggerish) Printf(msg string, args ...any) {
|
func (l stdLoggerish) Printf(msg string, args ...interface{}) {
|
||||||
l.f(l.key, "msg", fmt.Sprintf(msg, args...))
|
l.f(l.key, "msg", fmt.Sprintf(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l stdLoggerish) Fatalf(msg string, args ...any) {
|
|
||||||
l.log.Error(l.key, "msg", fmt.Sprintf(msg, args...))
|
|
||||||
panic("fatal error") // todo: does this make sense at all?
|
|
||||||
}
|
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
// Package metricsserver provides a standalone HTTP server for exposing Prometheus metrics.
|
|
||||||
//
|
|
||||||
// This package implements a dedicated metrics server that exposes application metrics
|
|
||||||
// via HTTP. It uses a custom Prometheus registry to avoid conflicts with other metric
|
|
||||||
// collectors and provides graceful shutdown capabilities.
|
|
||||||
package metricsserver
|
package metricsserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -18,13 +13,10 @@ import (
|
|||||||
"go.ntppool.org/common/logger"
|
"go.ntppool.org/common/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Metrics provides a custom Prometheus registry and HTTP handlers for metrics exposure.
|
|
||||||
// It isolates application metrics from the default global registry.
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
r *prometheus.Registry
|
r *prometheus.Registry
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Metrics instance with a custom Prometheus registry.
|
|
||||||
func New() *Metrics {
|
func New() *Metrics {
|
||||||
r := prometheus.NewRegistry()
|
r := prometheus.NewRegistry()
|
||||||
|
|
||||||
@ -35,14 +27,12 @@ func New() *Metrics {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry returns the custom Prometheus registry.
|
|
||||||
// Use this to register your application's metrics collectors.
|
|
||||||
func (m *Metrics) Registry() *prometheus.Registry {
|
func (m *Metrics) Registry() *prometheus.Registry {
|
||||||
return m.r
|
return m.r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler returns an HTTP handler for the /metrics endpoint with OpenMetrics support.
|
|
||||||
func (m *Metrics) Handler() http.Handler {
|
func (m *Metrics) Handler() http.Handler {
|
||||||
|
|
||||||
log := logger.NewStdLog("prom http", false, nil)
|
log := logger.NewStdLog("prom http", false, nil)
|
||||||
|
|
||||||
return promhttp.HandlerFor(m.r, promhttp.HandlerOpts{
|
return promhttp.HandlerFor(m.r, promhttp.HandlerOpts{
|
||||||
@ -52,9 +42,11 @@ func (m *Metrics) Handler() http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenAndServe starts a metrics server on the specified port and blocks until ctx is done.
|
// ListenAndServe starts a goroutine with a server running on
|
||||||
// The server exposes the metrics handler and shuts down gracefully when the context is cancelled.
|
// the specified port. The server will shutdown and return when
|
||||||
|
// the provided context is done
|
||||||
func (m *Metrics) ListenAndServe(ctx context.Context, port int) error {
|
func (m *Metrics) ListenAndServe(ctx context.Context, port int) error {
|
||||||
|
|
||||||
log := logger.Setup()
|
log := logger.Setup()
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
|
@ -1,242 +0,0 @@
|
|||||||
package metricsserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
metrics := New()
|
|
||||||
|
|
||||||
if metrics == nil {
|
|
||||||
t.Fatal("New returned nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if metrics.r == nil {
|
|
||||||
t.Error("metrics registry is nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegistry(t *testing.T) {
|
|
||||||
metrics := New()
|
|
||||||
registry := metrics.Registry()
|
|
||||||
|
|
||||||
if registry == nil {
|
|
||||||
t.Fatal("Registry() returned nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if registry != metrics.r {
|
|
||||||
t.Error("Registry() did not return the metrics registry")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that we can register a metric
|
|
||||||
counter := prometheus.NewCounter(prometheus.CounterOpts{
|
|
||||||
Name: "test_counter",
|
|
||||||
Help: "A test counter",
|
|
||||||
})
|
|
||||||
|
|
||||||
err := registry.Register(counter)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to register metric: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that the metric is registered
|
|
||||||
metricFamilies, err := registry.Gather()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to gather metrics: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, mf := range metricFamilies {
|
|
||||||
if mf.GetName() == "test_counter" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
t.Error("registered metric not found in registry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandler(t *testing.T) {
|
|
||||||
metrics := New()
|
|
||||||
|
|
||||||
// Register a test metric
|
|
||||||
counter := prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "test_requests_total",
|
|
||||||
Help: "Total number of test requests",
|
|
||||||
},
|
|
||||||
[]string{"method"},
|
|
||||||
)
|
|
||||||
metrics.Registry().MustRegister(counter)
|
|
||||||
counter.WithLabelValues("GET").Inc()
|
|
||||||
|
|
||||||
// Test the handler
|
|
||||||
handler := metrics.Handler()
|
|
||||||
if handler == nil {
|
|
||||||
t.Fatal("Handler() returned nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a test request
|
|
||||||
req := httptest.NewRequest("GET", "/metrics", nil)
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Call the handler
|
|
||||||
handler.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
// Check response
|
|
||||||
resp := recorder.Result()
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bodyStr := string(body)
|
|
||||||
|
|
||||||
// Check for our test metric
|
|
||||||
if !strings.Contains(bodyStr, "test_requests_total") {
|
|
||||||
t.Error("test metric not found in metrics output")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for OpenMetrics format indicators
|
|
||||||
if !strings.Contains(bodyStr, "# TYPE") {
|
|
||||||
t.Error("metrics output missing TYPE comments")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListenAndServe(t *testing.T) {
|
|
||||||
metrics := New()
|
|
||||||
|
|
||||||
// Register a test metric
|
|
||||||
counter := prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "test_requests_total",
|
|
||||||
Help: "Total number of test requests",
|
|
||||||
},
|
|
||||||
[]string{"method"},
|
|
||||||
)
|
|
||||||
metrics.Registry().MustRegister(counter)
|
|
||||||
counter.WithLabelValues("GET").Inc()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Start server in a goroutine
|
|
||||||
errCh := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
// Use a high port number to avoid conflicts
|
|
||||||
errCh <- metrics.ListenAndServe(ctx, 9999)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Give the server a moment to start
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Test metrics endpoint
|
|
||||||
resp, err := http.Get("http://localhost:9999/metrics")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to GET /metrics: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bodyStr := string(body)
|
|
||||||
|
|
||||||
// Check for our test metric
|
|
||||||
if !strings.Contains(bodyStr, "test_requests_total") {
|
|
||||||
t.Error("test metric not found in metrics output")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel context to stop server
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
// Wait for server to stop
|
|
||||||
select {
|
|
||||||
case err := <-errCh:
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("server returned error: %v", err)
|
|
||||||
}
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Error("server did not stop within timeout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListenAndServeContextCancellation(t *testing.T) {
|
|
||||||
metrics := New()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
errCh := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
errCh <- metrics.ListenAndServe(ctx, 9998)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Give server time to start
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Cancel context
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
// Server should stop gracefully
|
|
||||||
select {
|
|
||||||
case err := <-errCh:
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("server returned error on graceful shutdown: %v", err)
|
|
||||||
}
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Error("server did not stop within timeout after context cancellation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark the metrics handler response time
|
|
||||||
func BenchmarkMetricsHandler(b *testing.B) {
|
|
||||||
metrics := New()
|
|
||||||
|
|
||||||
// Register some test metrics
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
counter := prometheus.NewCounter(prometheus.CounterOpts{
|
|
||||||
Name: fmt.Sprintf("bench_counter_%d", i),
|
|
||||||
Help: "A benchmark counter",
|
|
||||||
})
|
|
||||||
metrics.Registry().MustRegister(counter)
|
|
||||||
counter.Add(float64(i * 100))
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := metrics.Handler()
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
req := httptest.NewRequest("GET", "/metrics", nil)
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
handler.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
if recorder.Code != http.StatusOK {
|
|
||||||
b.Fatalf("unexpected status code: %d", recorder.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
go install github.com/goreleaser/goreleaser/v2@v2.8.2
|
go install github.com/goreleaser/goreleaser@v1.26.2
|
||||||
|
|
||||||
if [ ! -z "${harbor_username:-}" ]; then
|
if [ ! -z "${harbor_username:-}" ]; then
|
||||||
DOCKER_FILE=~/.docker/config.json
|
DOCKER_FILE=~/.docker/config.json
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// Package timeutil provides JSON-serializable time utilities.
|
|
||||||
package timeutil
|
package timeutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -7,39 +6,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Duration is a wrapper around time.Duration that supports JSON marshaling/unmarshaling.
|
|
||||||
//
|
|
||||||
// When marshaling to JSON, it outputs the duration as a string using time.Duration.String().
|
|
||||||
// When unmarshaling from JSON, it accepts both:
|
|
||||||
// - String values that can be parsed by time.ParseDuration (e.g., "30s", "5m", "1h30m")
|
|
||||||
// - Numeric values that represent nanoseconds as a float64
|
|
||||||
//
|
|
||||||
// This makes it compatible with configuration files and APIs that need to represent
|
|
||||||
// durations in a human-readable format.
|
|
||||||
//
|
|
||||||
// Example usage:
|
|
||||||
//
|
|
||||||
// type Config struct {
|
|
||||||
// Timeout timeutil.Duration `json:"timeout"`
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // JSON: {"timeout": "30s"}
|
|
||||||
// // or: {"timeout": 30000000000}
|
|
||||||
type Duration struct {
|
type Duration struct {
|
||||||
time.Duration
|
time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements json.Marshaler.
|
|
||||||
// It marshals the duration as a string using time.Duration.String().
|
|
||||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(time.Duration(d.Duration).String())
|
return json.Marshal(time.Duration(d.Duration).String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON implements json.Unmarshaler.
|
|
||||||
// It accepts both string values (parsed via time.ParseDuration) and
|
|
||||||
// numeric values (interpreted as nanoseconds).
|
|
||||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||||
var v any
|
var v interface{}
|
||||||
if err := json.Unmarshal(b, &v); err != nil {
|
if err := json.Unmarshal(b, &v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -18,4 +18,5 @@ func TestDuration(t *testing.T) {
|
|||||||
if foo.Foo.Seconds() != 30 {
|
if foo.Foo.Seconds() != 30 {
|
||||||
t.Fatalf("parsed time.Duration wasn't 30 seconds: %s", foo.Foo)
|
t.Fatalf("parsed time.Duration wasn't 30 seconds: %s", foo.Foo)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,40 +7,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.ntppool.org/common/logger"
|
"go.ntppool.org/common/logger"
|
||||||
"go.ntppool.org/common/version"
|
"go.ntppool.org/common/version"
|
||||||
"google.golang.org/grpc/credentials"
|
|
||||||
|
|
||||||
"go.opentelemetry.io/contrib/exporters/autoexport"
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||||
logglobal "go.opentelemetry.io/otel/log/global"
|
|
||||||
"go.opentelemetry.io/otel/propagation"
|
"go.opentelemetry.io/otel/propagation"
|
||||||
sdklog "go.opentelemetry.io/otel/sdk/log"
|
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
otelsdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// svcNameKey is the environment variable name that Service Name information will be read from.
|
|
||||||
svcNameKey = "OTEL_SERVICE_NAME"
|
|
||||||
|
|
||||||
otelExporterOTLPProtoEnvKey = "OTEL_EXPORTER_OTLP_PROTOCOL"
|
|
||||||
otelExporterOTLPTracesProtoEnvKey = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errInvalidOTLPProtocol = errors.New("invalid OTLP protocol - should be one of ['grpc', 'http/protobuf']")
|
|
||||||
|
|
||||||
// https://github.com/open-telemetry/opentelemetry-go/blob/main/exporters/otlp/otlptrace/otlptracehttp/example_test.go
|
// https://github.com/open-telemetry/opentelemetry-go/blob/main/exporters/otlp/otlptrace/otlptracehttp/example_test.go
|
||||||
|
|
||||||
type TpShutdownFunc func(ctx context.Context) error
|
type TpShutdownFunc func(ctx context.Context) error
|
||||||
@ -66,196 +48,80 @@ type TracerConfig struct {
|
|||||||
RootCAs *x509.CertPool
|
RootCAs *x509.CertPool
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitTracer(ctx context.Context, cfg *TracerConfig) (TpShutdownFunc, error) {
|
var emptyTpShutdownFunc = func(_ context.Context) error {
|
||||||
// todo: setup environment from cfg
|
return nil
|
||||||
return SetupSDK(ctx, cfg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupSDK(ctx context.Context, cfg *TracerConfig) (shutdown TpShutdownFunc, err error) {
|
func InitTracer(ctx context.Context, cfg *TracerConfig) (TpShutdownFunc, error) {
|
||||||
if cfg == nil {
|
|
||||||
cfg = &TracerConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
log := logger.Setup()
|
log := logger.Setup()
|
||||||
|
|
||||||
if serviceName := os.Getenv(svcNameKey); len(serviceName) == 0 {
|
// exporter, err := srv.newStdoutExporter(os.Stdout)
|
||||||
if len(cfg.ServiceName) > 0 {
|
|
||||||
os.Setenv(svcNameKey, cfg.ServiceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resources := []resource.Option{
|
|
||||||
resource.WithFromEnv(), // Discover and provide attributes from OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables.
|
|
||||||
resource.WithTelemetrySDK(), // Discover and provide information about the OpenTelemetry SDK used.
|
|
||||||
resource.WithProcess(), // Discover and provide process information.
|
|
||||||
resource.WithOS(), // Discover and provide OS information.
|
|
||||||
resource.WithContainer(), // Discover and provide container information.
|
|
||||||
resource.WithHost(), // Discover and provide host information.
|
|
||||||
|
|
||||||
// set above via os.Setenv() for WithFromEnv to find
|
|
||||||
// resource.WithAttributes(semconv.ServiceNameKey.String(cfg.ServiceName)),
|
|
||||||
|
|
||||||
resource.WithAttributes(semconv.ServiceVersionKey.String(version.Version())),
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.Environment) > 0 {
|
|
||||||
resources = append(resources,
|
|
||||||
resource.WithAttributes(attribute.String("environment", cfg.Environment)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := resource.New(
|
|
||||||
context.Background(),
|
|
||||||
resources...,
|
|
||||||
)
|
|
||||||
if errors.Is(err, resource.ErrPartialResource) || errors.Is(err, resource.ErrSchemaURLConflict) {
|
|
||||||
log.Warn("otel resource setup", "err", err) // Log non-fatal issues.
|
|
||||||
} else if err != nil {
|
|
||||||
log.Error("otel resource setup", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var shutdownFuncs []func(context.Context) error
|
|
||||||
shutdown = func(ctx context.Context) error {
|
|
||||||
var err error
|
var err error
|
||||||
// need to shutdown the providers first,
|
var exporter otelsdktrace.SpanExporter
|
||||||
// exporters after which is the opposite
|
|
||||||
// order they are setup.
|
if otlpEndPoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"); len(otlpEndPoint) > 0 || len(cfg.Endpoint) > 0 {
|
||||||
slices.Reverse(shutdownFuncs)
|
exporter, err = newOLTPExporter(ctx, cfg)
|
||||||
for _, fn := range shutdownFuncs {
|
|
||||||
// log.Warn("shutting down", "fn", fn)
|
|
||||||
err = errors.Join(err, fn(ctx))
|
|
||||||
}
|
}
|
||||||
shutdownFuncs = nil
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("shutdown returned errors", "err", err)
|
return emptyTpShutdownFunc, err
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleErr calls shutdown for cleanup and makes sure that all errors are returned.
|
if exporter == nil {
|
||||||
handleErr := func(inErr error) {
|
log.Warn("tracing not configured")
|
||||||
err = errors.Join(inErr, shutdown(ctx))
|
return emptyTpShutdownFunc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
prop := newPropagator()
|
resource, err := newResource(cfg)
|
||||||
otel.SetTextMapPropagator(prop)
|
|
||||||
|
|
||||||
var spanExporter sdktrace.SpanExporter
|
|
||||||
|
|
||||||
switch os.Getenv("OTEL_TRACES_EXPORTER") {
|
|
||||||
case "":
|
|
||||||
spanExporter, err = newOLTPExporter(ctx, cfg)
|
|
||||||
case "otlp":
|
|
||||||
spanExporter, err = newOLTPExporter(ctx, cfg)
|
|
||||||
default:
|
|
||||||
// log.Debug("OTEL_TRACES_EXPORTER", "fallback", os.Getenv("OTEL_TRACES_EXPORTER"))
|
|
||||||
spanExporter, err = autoexport.NewSpanExporter(ctx)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleErr(err)
|
return nil, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
shutdownFuncs = append(shutdownFuncs, spanExporter.Shutdown)
|
|
||||||
|
|
||||||
logExporter, err := autoexport.NewLogExporter(ctx)
|
tp := otelsdktrace.NewTracerProvider(
|
||||||
if err != nil {
|
otelsdktrace.WithSampler(otelsdktrace.AlwaysSample()),
|
||||||
handleErr(err)
|
otelsdktrace.WithBatcher(exporter),
|
||||||
return
|
otelsdktrace.WithResource(resource),
|
||||||
}
|
)
|
||||||
shutdownFuncs = append(shutdownFuncs, logExporter.Shutdown)
|
|
||||||
|
|
||||||
// Set up trace provider.
|
otel.SetTracerProvider(tp)
|
||||||
tracerProvider, err := newTraceProvider(spanExporter, res)
|
|
||||||
if err != nil {
|
|
||||||
handleErr(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
|
|
||||||
otel.SetTracerProvider(tracerProvider)
|
|
||||||
|
|
||||||
logProvider := sdklog.NewLoggerProvider(sdklog.WithResource(res),
|
otel.SetTextMapPropagator(
|
||||||
sdklog.WithProcessor(
|
propagation.NewCompositeTextMapPropagator(
|
||||||
sdklog.NewBatchProcessor(logExporter, sdklog.WithExportBufferSize(10)),
|
propagation.TraceContext{}, // W3C Trace Context format; https://www.w3.org/TR/trace-context/
|
||||||
|
propagation.Baggage{},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
logglobal.SetLoggerProvider(logProvider)
|
return tp.Shutdown, nil
|
||||||
shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) error {
|
|
||||||
logProvider.ForceFlush(ctx)
|
|
||||||
return logProvider.Shutdown(ctx)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
handleErr(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOLTPExporter(ctx context.Context, cfg *TracerConfig) (sdktrace.SpanExporter, error) {
|
func newOLTPExporter(ctx context.Context, cfg *TracerConfig) (otelsdktrace.SpanExporter, error) {
|
||||||
|
|
||||||
log := logger.Setup()
|
log := logger.Setup()
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
|
||||||
|
|
||||||
if cfg.CertificateProvider != nil {
|
|
||||||
tlsConfig = &tls.Config{
|
|
||||||
GetClientCertificate: cfg.CertificateProvider,
|
|
||||||
RootCAs: cfg.RootCAs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proto := os.Getenv(otelExporterOTLPTracesProtoEnvKey)
|
|
||||||
if proto == "" {
|
|
||||||
proto = os.Getenv(otelExporterOTLPProtoEnvKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to default, http/protobuf.
|
|
||||||
if proto == "" {
|
|
||||||
proto = "http/protobuf"
|
|
||||||
}
|
|
||||||
|
|
||||||
var client otlptrace.Client
|
|
||||||
|
|
||||||
switch proto {
|
|
||||||
case "grpc":
|
|
||||||
opts := []otlptracegrpc.Option{
|
|
||||||
otlptracegrpc.WithCompressor("gzip"),
|
|
||||||
}
|
|
||||||
if tlsConfig != nil {
|
|
||||||
opts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
|
|
||||||
}
|
|
||||||
if len(cfg.Endpoint) > 0 {
|
|
||||||
log.Info("adding option", "Endpoint", cfg.Endpoint)
|
|
||||||
opts = append(opts, otlptracegrpc.WithEndpoint(cfg.Endpoint))
|
|
||||||
}
|
|
||||||
if len(cfg.EndpointURL) > 0 {
|
|
||||||
log.Info("adding option", "EndpointURL", cfg.EndpointURL)
|
|
||||||
opts = append(opts, otlptracegrpc.WithEndpointURL(cfg.EndpointURL))
|
|
||||||
}
|
|
||||||
|
|
||||||
client = otlptracegrpc.NewClient(opts...)
|
|
||||||
case "http/protobuf", "http/json":
|
|
||||||
opts := []otlptracehttp.Option{
|
opts := []otlptracehttp.Option{
|
||||||
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
|
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
|
||||||
}
|
}
|
||||||
if tlsConfig != nil {
|
|
||||||
opts = append(opts, otlptracehttp.WithTLSClientConfig(tlsConfig))
|
if cfg.CertificateProvider != nil {
|
||||||
|
log.InfoContext(ctx, "setting up cert provider")
|
||||||
|
opts = append(opts, otlptracehttp.WithTLSClientConfig(&tls.Config{
|
||||||
|
GetClientCertificate: cfg.CertificateProvider,
|
||||||
|
RootCAs: cfg.RootCAs,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.Endpoint) > 0 {
|
if len(cfg.Endpoint) > 0 {
|
||||||
opts = append(opts, otlptracehttp.WithEndpoint(cfg.Endpoint))
|
opts = append(opts, otlptracehttp.WithEndpoint(cfg.Endpoint))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.EndpointURL) > 0 {
|
if len(cfg.EndpointURL) > 0 {
|
||||||
opts = append(opts, otlptracehttp.WithEndpointURL(cfg.EndpointURL))
|
opts = append(opts, otlptracehttp.WithEndpointURL(cfg.EndpointURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
client = otlptracehttp.NewClient(opts...)
|
client := otlptracehttp.NewClient(opts...)
|
||||||
default:
|
|
||||||
return nil, errInvalidOTLPProtocol
|
|
||||||
}
|
|
||||||
|
|
||||||
exporter, err := otlptrace.New(ctx, client)
|
exporter, err := otlptrace.New(ctx, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(ctx, "creating OTLP trace exporter", "err", err)
|
log.ErrorContext(ctx, "creating OTLP trace exporter", "err", err)
|
||||||
@ -263,19 +129,44 @@ func newOLTPExporter(ctx context.Context, cfg *TracerConfig) (sdktrace.SpanExpor
|
|||||||
return exporter, err
|
return exporter, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTraceProvider(traceExporter sdktrace.SpanExporter, res *resource.Resource) (*sdktrace.TracerProvider, error) {
|
// func (srv *Server) newStdoutExporter(w io.Writer) (sdktrace.SpanExporter, error) {
|
||||||
traceProvider := sdktrace.NewTracerProvider(
|
// return stdouttrace.New(
|
||||||
sdktrace.WithResource(res),
|
// stdouttrace.WithWriter(w),
|
||||||
sdktrace.WithBatcher(traceExporter,
|
// // Use human-readable output.
|
||||||
sdktrace.WithBatchTimeout(time.Second*3),
|
// stdouttrace.WithPrettyPrint(),
|
||||||
),
|
// // Do not print timestamps for the demo.
|
||||||
)
|
// stdouttrace.WithoutTimestamps(),
|
||||||
return traceProvider, nil
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
func newPropagator() propagation.TextMapPropagator {
|
// newResource returns a resource describing this application.
|
||||||
return propagation.NewCompositeTextMapPropagator(
|
func newResource(cfg *TracerConfig) (*resource.Resource, error) {
|
||||||
propagation.TraceContext{},
|
|
||||||
propagation.Baggage{},
|
log := logger.Setup()
|
||||||
|
|
||||||
|
defaultResource := resource.Default()
|
||||||
|
log.Debug("default semconv", "url", defaultResource.SchemaURL())
|
||||||
|
|
||||||
|
newResource := resource.NewWithAttributes(
|
||||||
|
semconv.SchemaURL,
|
||||||
|
semconv.ServiceNameKey.String(cfg.ServiceName),
|
||||||
|
semconv.ServiceVersionKey.String(version.Version()),
|
||||||
|
attribute.String("environment", cfg.Environment),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log.Debug("new resource semconv", "url", newResource.SchemaURL())
|
||||||
|
|
||||||
|
r, err := resource.Merge(
|
||||||
|
defaultResource,
|
||||||
|
newResource,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("could not setup otel resource",
|
||||||
|
"err", err,
|
||||||
|
"default", defaultResource.SchemaURL(),
|
||||||
|
"local", newResource.SchemaURL(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestInit(t *testing.T) {
|
func TestInit(t *testing.T) {
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -17,4 +18,5 @@ func TestInit(t *testing.T) {
|
|||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
defer shutdownFn(ctx)
|
defer shutdownFn(ctx)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func (lsa *LogScoreAttributes) Value() (driver.Value, error) {
|
|||||||
return json.Marshal(lsa)
|
return json.Marshal(lsa)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lsa *LogScoreAttributes) Scan(value any) error {
|
func (lsa *LogScoreAttributes) Scan(value interface{}) error {
|
||||||
var source []byte
|
var source []byte
|
||||||
_t := LogScoreAttributes{}
|
_t := LogScoreAttributes{}
|
||||||
|
|
||||||
|
66
ulid/ulid.go
66
ulid/ulid.go
@ -1,44 +1,48 @@
|
|||||||
// Package ulid provides thread-safe ULID (Universally Unique Lexicographically Sortable Identifier) generation.
|
|
||||||
//
|
|
||||||
// ULIDs are 128-bit identifiers that are lexicographically sortable and contain
|
|
||||||
// a timestamp component. This package uses cryptographically secure random
|
|
||||||
// generation optimized for simplicity and performance in concurrent environments.
|
|
||||||
package ulid
|
package ulid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
cryptorand "crypto/rand"
|
cryptorand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
mathrand "math/rand"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
oklid "github.com/oklog/ulid/v2"
|
oklid "github.com/oklog/ulid/v2"
|
||||||
|
"go.ntppool.org/common/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MakeULID generates a new ULID with the specified timestamp using cryptographically secure randomness.
|
var monotonicPool = sync.Pool{
|
||||||
// The function is thread-safe and optimized for high-concurrency environments.
|
New: func() interface{} {
|
||||||
//
|
|
||||||
// This implementation prioritizes simplicity and performance over strict monotonicity within
|
log := logger.Setup()
|
||||||
// the same millisecond. Each ULID is guaranteed to be unique and lexicographically sortable
|
|
||||||
// across different timestamps.
|
var seed int64
|
||||||
//
|
err := binary.Read(cryptorand.Reader, binary.BigEndian, &seed)
|
||||||
// Returns a pointer to the generated ULID or an error if generation fails.
|
if err != nil {
|
||||||
// Generation should only fail under extreme circumstances (entropy exhaustion).
|
log.Error("crypto/rand error", "err", err)
|
||||||
|
os.Exit(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
rand := mathrand.New(mathrand.NewSource(seed))
|
||||||
|
|
||||||
|
inc := uint64(mathrand.Int63())
|
||||||
|
|
||||||
|
// log.Printf("seed: %d", seed)
|
||||||
|
// log.Printf("inc: %d", inc)
|
||||||
|
|
||||||
|
// inc = inc & ^uint64(1<<63) // only want 63 bits
|
||||||
|
mono := oklid.Monotonic(rand, inc)
|
||||||
|
return mono
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func MakeULID(t time.Time) (*oklid.ULID, error) {
|
func MakeULID(t time.Time) (*oklid.ULID, error) {
|
||||||
id, err := oklid.New(oklid.Timestamp(t), cryptorand.Reader)
|
|
||||||
if err != nil {
|
mono := monotonicPool.Get().(io.Reader)
|
||||||
return nil, err
|
|
||||||
}
|
id, err := oklid.New(oklid.Timestamp(t), mono)
|
||||||
|
|
||||||
return &id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make generates a new ULID with the current timestamp using cryptographically secure randomness.
|
|
||||||
// This is a convenience function equivalent to MakeULID(time.Now()).
|
|
||||||
//
|
|
||||||
// The function is thread-safe and optimized for high-concurrency environments.
|
|
||||||
//
|
|
||||||
// Returns a pointer to the generated ULID or an error if generation fails.
|
|
||||||
// Generation should only fail under extreme circumstances (entropy exhaustion).
|
|
||||||
func Make() (*oklid.ULID, error) {
|
|
||||||
id, err := oklid.New(oklid.Now(), cryptorand.Reader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1,336 +1,25 @@
|
|||||||
package ulid
|
package ulid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
cryptorand "crypto/rand"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
oklid "github.com/oklog/ulid/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMakeULID(t *testing.T) {
|
func TestULID(t *testing.T) {
|
||||||
tm := time.Now()
|
tm := time.Now()
|
||||||
ul1, err := MakeULID(tm)
|
ul1, err := MakeULID(tm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MakeULID failed: %s", err)
|
t.Logf("makeULID failed: %s", err)
|
||||||
|
t.Fail()
|
||||||
}
|
}
|
||||||
ul2, err := MakeULID(tm)
|
ul2, err := MakeULID(tm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MakeULID failed: %s", err)
|
t.Logf("MakeULID failed: %s", err)
|
||||||
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
if ul1 == nil || ul2 == nil {
|
|
||||||
t.Fatal("MakeULID returned nil ULID")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ul1.String() == ul2.String() {
|
if ul1.String() == ul2.String() {
|
||||||
t.Errorf("ul1 and ul2 should be different: %s", ul1.String())
|
t.Logf("ul1 and ul2 got the same string: %s", ul1.String())
|
||||||
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify they have the same timestamp
|
|
||||||
if ul1.Time() != ul2.Time() {
|
|
||||||
t.Errorf("ULIDs with same input time should have same timestamp: %d != %d", ul1.Time(), ul2.Time())
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("ulid string 1 and 2: %s | %s", ul1.String(), ul2.String())
|
t.Logf("ulid string 1 and 2: %s | %s", ul1.String(), ul2.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMake(t *testing.T) {
|
|
||||||
// Test Make() function (uses current time)
|
|
||||||
ul1, err := Make()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Make failed: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ul1 == nil {
|
|
||||||
t.Fatal("Make returned nil ULID")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep a bit and generate another
|
|
||||||
time.Sleep(2 * time.Millisecond)
|
|
||||||
|
|
||||||
ul2, err := Make()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Make failed: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be different ULIDs
|
|
||||||
if ul1.String() == ul2.String() {
|
|
||||||
t.Errorf("ULIDs from Make() should be different: %s", ul1.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second should be later (or at least not earlier)
|
|
||||||
if ul1.Time() > ul2.Time() {
|
|
||||||
t.Errorf("second ULID should not have earlier timestamp: %d > %d", ul1.Time(), ul2.Time())
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Make() ULIDs: %s | %s", ul1.String(), ul2.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeULIDUniqueness(t *testing.T) {
|
|
||||||
tm := time.Now()
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
ul, err := MakeULID(tm)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MakeULID failed on iteration %d: %s", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
str := ul.String()
|
|
||||||
if seen[str] {
|
|
||||||
t.Errorf("duplicate ULID generated: %s", str)
|
|
||||||
}
|
|
||||||
seen[str] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeUniqueness(t *testing.T) {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
ul, err := Make()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Make failed on iteration %d: %s", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
str := ul.String()
|
|
||||||
if seen[str] {
|
|
||||||
t.Errorf("duplicate ULID generated: %s", str)
|
|
||||||
}
|
|
||||||
seen[str] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeULIDTimestampProgression(t *testing.T) {
|
|
||||||
t1 := time.Now()
|
|
||||||
ul1, err := MakeULID(t1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MakeULID failed: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait to ensure different timestamp
|
|
||||||
time.Sleep(2 * time.Millisecond)
|
|
||||||
|
|
||||||
t2 := time.Now()
|
|
||||||
ul2, err := MakeULID(t2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MakeULID failed: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ul1.Time() >= ul2.Time() {
|
|
||||||
t.Errorf("second ULID should have later timestamp: %d >= %d", ul1.Time(), ul2.Time())
|
|
||||||
}
|
|
||||||
|
|
||||||
if ul1.Compare(*ul2) >= 0 {
|
|
||||||
t.Errorf("second ULID should be greater: %s >= %s", ul1.String(), ul2.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeULIDConcurrency(t *testing.T) {
|
|
||||||
const numGoroutines = 10
|
|
||||||
const numULIDsPerGoroutine = 100
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
ulidChan := make(chan *oklid.ULID, numGoroutines*numULIDsPerGoroutine)
|
|
||||||
tm := time.Now()
|
|
||||||
|
|
||||||
// Start multiple goroutines generating ULIDs concurrently
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < numULIDsPerGoroutine; j++ {
|
|
||||||
ul, err := MakeULID(tm)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("MakeULID failed: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ulidChan <- ul
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(ulidChan)
|
|
||||||
|
|
||||||
// Collect all ULIDs and check uniqueness
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
count := 0
|
|
||||||
|
|
||||||
for ul := range ulidChan {
|
|
||||||
str := ul.String()
|
|
||||||
if seen[str] {
|
|
||||||
t.Errorf("duplicate ULID generated in concurrent test: %s", str)
|
|
||||||
}
|
|
||||||
seen[str] = true
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
|
|
||||||
if count != numGoroutines*numULIDsPerGoroutine {
|
|
||||||
t.Errorf("expected %d ULIDs, got %d", numGoroutines*numULIDsPerGoroutine, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeConcurrency(t *testing.T) {
|
|
||||||
const numGoroutines = 10
|
|
||||||
const numULIDsPerGoroutine = 100
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
ulidChan := make(chan *oklid.ULID, numGoroutines*numULIDsPerGoroutine)
|
|
||||||
|
|
||||||
// Start multiple goroutines generating ULIDs concurrently
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < numULIDsPerGoroutine; j++ {
|
|
||||||
ul, err := Make()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Make failed: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ulidChan <- ul
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(ulidChan)
|
|
||||||
|
|
||||||
// Collect all ULIDs and check uniqueness
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
count := 0
|
|
||||||
|
|
||||||
for ul := range ulidChan {
|
|
||||||
str := ul.String()
|
|
||||||
if seen[str] {
|
|
||||||
t.Errorf("duplicate ULID generated in concurrent test: %s", str)
|
|
||||||
}
|
|
||||||
seen[str] = true
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
|
|
||||||
if count != numGoroutines*numULIDsPerGoroutine {
|
|
||||||
t.Errorf("expected %d ULIDs, got %d", numGoroutines*numULIDsPerGoroutine, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeULIDErrorHandling(t *testing.T) {
|
|
||||||
// Test with various timestamps
|
|
||||||
timestamps := []time.Time{
|
|
||||||
time.Unix(0, 0), // Unix epoch
|
|
||||||
time.Now(), // Current time
|
|
||||||
time.Now().Add(time.Hour), // Future time
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tm := range timestamps {
|
|
||||||
ul, err := MakeULID(tm)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("MakeULID failed with timestamp %d: %s", i, err)
|
|
||||||
}
|
|
||||||
if ul == nil {
|
|
||||||
t.Errorf("MakeULID returned nil ULID with timestamp %d", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeULIDLexicographicOrdering(t *testing.T) {
|
|
||||||
var ulids []*oklid.ULID
|
|
||||||
var timestamps []time.Time
|
|
||||||
|
|
||||||
// Generate ULIDs with increasing timestamps
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
tm := time.Now().Add(time.Duration(i) * time.Millisecond)
|
|
||||||
timestamps = append(timestamps, tm)
|
|
||||||
|
|
||||||
ul, err := MakeULID(tm)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MakeULID failed: %s", err)
|
|
||||||
}
|
|
||||||
ulids = append(ulids, ul)
|
|
||||||
|
|
||||||
// Small delay to ensure different timestamps
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort ULID strings lexicographically
|
|
||||||
ulidStrings := make([]string, len(ulids))
|
|
||||||
for i, ul := range ulids {
|
|
||||||
ulidStrings[i] = ul.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
originalOrder := make([]string, len(ulidStrings))
|
|
||||||
copy(originalOrder, ulidStrings)
|
|
||||||
|
|
||||||
sort.Strings(ulidStrings)
|
|
||||||
|
|
||||||
// Verify lexicographic order matches chronological order
|
|
||||||
for i := 0; i < len(originalOrder); i++ {
|
|
||||||
if originalOrder[i] != ulidStrings[i] {
|
|
||||||
t.Errorf("lexicographic order doesn't match chronological order at index %d: %s != %s",
|
|
||||||
i, originalOrder[i], ulidStrings[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark ULID generation performance
|
|
||||||
func BenchmarkMakeULID(b *testing.B) {
|
|
||||||
tm := time.Now()
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, err := MakeULID(tm)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("MakeULID failed: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark Make function
|
|
||||||
func BenchmarkMake(b *testing.B) {
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, err := Make()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Make failed: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark concurrent ULID generation
|
|
||||||
func BenchmarkMakeULIDConcurrent(b *testing.B) {
|
|
||||||
tm := time.Now()
|
|
||||||
|
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
|
||||||
for pb.Next() {
|
|
||||||
_, err := MakeULID(tm)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("MakeULID failed: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark concurrent Make function
|
|
||||||
func BenchmarkMakeConcurrent(b *testing.B) {
|
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
|
||||||
for pb.Next() {
|
|
||||||
_, err := Make()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Make failed: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark random number generation
|
|
||||||
func BenchmarkCryptoRand(b *testing.B) {
|
|
||||||
buf := make([]byte, 10) // ULID entropy size
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
cryptorand.Read(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,53 +1,34 @@
|
|||||||
// Package version provides build metadata and version information management.
|
|
||||||
//
|
|
||||||
// This package manages application version information including semantic version,
|
|
||||||
// Git revision, build time, and provides integration with CLI frameworks (Cobra, Kong)
|
|
||||||
// and Prometheus metrics for operational visibility.
|
|
||||||
//
|
|
||||||
// Version information can be injected at build time using ldflags:
|
|
||||||
//
|
|
||||||
// go build -ldflags "-X go.ntppool.org/common/version.VERSION=v1.0.0 \
|
|
||||||
// -X go.ntppool.org/common/version.buildTime=2023-01-01T00:00:00Z \
|
|
||||||
// -X go.ntppool.org/common/version.gitVersion=abc123"
|
|
||||||
//
|
|
||||||
// The package also automatically extracts build information from Go's debug.BuildInfo
|
|
||||||
// when available, providing fallback values for VCS time and revision.
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"go.ntppool.org/common/logger"
|
||||||
"golang.org/x/mod/semver"
|
"golang.org/x/mod/semver"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VERSION contains the current software version (typically set during the build process via ldflags).
|
// VERSION has the current software version (set in the build process)
|
||||||
// If not set, defaults to "dev-snapshot". The version should follow semantic versioning.
|
var VERSION string
|
||||||
var (
|
var buildTime string
|
||||||
VERSION string // Semantic version (e.g., "1.0.0" or "v1.0.0")
|
var gitVersion string
|
||||||
buildTime string // Build timestamp (RFC3339 format)
|
var gitModified bool
|
||||||
gitVersion string // Git commit hash
|
|
||||||
gitModified bool // Whether the working tree was modified during build
|
|
||||||
)
|
|
||||||
|
|
||||||
// info holds the consolidated version information extracted from build variables and debug.BuildInfo.
|
|
||||||
var info Info
|
var info Info
|
||||||
|
|
||||||
// Info represents structured version and build information.
|
|
||||||
// This struct is used for JSON serialization and programmatic access to build metadata.
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Version string `json:",omitempty"` // Semantic version with "v" prefix
|
Version string `json:",omitempty"`
|
||||||
GitRev string `json:",omitempty"` // Full Git commit hash
|
GitRev string `json:",omitempty"`
|
||||||
GitRevShort string `json:",omitempty"` // Shortened Git commit hash (7 characters)
|
GitRevShort string `json:",omitempty"`
|
||||||
BuildTime string `json:",omitempty"` // Build timestamp
|
BuildTime string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
||||||
info.BuildTime = buildTime
|
info.BuildTime = buildTime
|
||||||
info.GitRev = gitVersion
|
info.GitRev = gitVersion
|
||||||
|
|
||||||
@ -58,7 +39,7 @@ func init() {
|
|||||||
VERSION = "v" + VERSION
|
VERSION = "v" + VERSION
|
||||||
}
|
}
|
||||||
if !semver.IsValid(VERSION) {
|
if !semver.IsValid(VERSION) {
|
||||||
slog.Default().Warn("invalid version number", "version", VERSION)
|
logger.Setup().Warn("invalid version number", "version", VERSION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bi, ok := debug.ReadBuildInfo(); ok {
|
if bi, ok := debug.ReadBuildInfo(); ok {
|
||||||
@ -97,16 +78,10 @@ func init() {
|
|||||||
Version()
|
Version()
|
||||||
}
|
}
|
||||||
|
|
||||||
// VersionCmd creates a Cobra command for displaying version information.
|
|
||||||
// The name parameter is used as a prefix in the output (e.g., "myapp v1.0.0").
|
|
||||||
// Returns a configured cobra.Command that can be added to any CLI application.
|
|
||||||
func VersionCmd(name string) *cobra.Command {
|
func VersionCmd(name string) *cobra.Command {
|
||||||
versionCmd := &cobra.Command{
|
versionCmd := &cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Print version and build information",
|
Short: "Print version and build information",
|
||||||
Long: `Print detailed version information including semantic version,
|
|
||||||
Git revision, build time, and Go version. Build information is automatically
|
|
||||||
extracted from Go's debug.BuildInfo when available.`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
ver := Version()
|
ver := Version()
|
||||||
fmt.Printf("%s %s\n", name, ver)
|
fmt.Printf("%s %s\n", name, ver)
|
||||||
@ -115,23 +90,6 @@ extracted from Go's debug.BuildInfo when available.`,
|
|||||||
return versionCmd
|
return versionCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// KongVersionCmd provides a Kong CLI framework compatible version command.
|
|
||||||
// The Name field should be set to the application name for proper output formatting.
|
|
||||||
type KongVersionCmd struct {
|
|
||||||
Name string `kong:"-"` // Application name, excluded from Kong parsing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run executes the version command for Kong CLI framework.
|
|
||||||
// Prints the application name and version information to stdout.
|
|
||||||
func (cmd *KongVersionCmd) Run() error {
|
|
||||||
fmt.Printf("%s %s\n", cmd.Name, Version())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterMetric registers a Prometheus gauge metric with build information.
|
|
||||||
// If name is provided, it creates a metric named "{name}_build_info", otherwise "build_info".
|
|
||||||
// The metric includes labels for version, build time, Git time, and Git revision.
|
|
||||||
// This is useful for exposing build information in monitoring systems.
|
|
||||||
func RegisterMetric(name string, registry prometheus.Registerer) {
|
func RegisterMetric(name string, registry prometheus.Registerer) {
|
||||||
if len(name) > 0 {
|
if len(name) > 0 {
|
||||||
name = strings.ReplaceAll(name, "-", "_")
|
name = strings.ReplaceAll(name, "-", "_")
|
||||||
@ -142,13 +100,13 @@ func RegisterMetric(name string, registry prometheus.Registerer) {
|
|||||||
buildInfo := prometheus.NewGaugeVec(
|
buildInfo := prometheus.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: name,
|
Name: name,
|
||||||
Help: "Build information including version, build time, and git revision",
|
Help: "Build information",
|
||||||
},
|
},
|
||||||
[]string{
|
[]string{
|
||||||
"version", // Combined version/git format (e.g., "v1.0.0/abc123")
|
"version",
|
||||||
"buildtime", // Build timestamp from ldflags
|
"buildtime",
|
||||||
"gittime", // Git commit timestamp from VCS info
|
"gittime",
|
||||||
"git", // Full Git commit hash
|
"git",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
registry.MustRegister(buildInfo)
|
registry.MustRegister(buildInfo)
|
||||||
@ -163,20 +121,12 @@ func RegisterMetric(name string, registry prometheus.Registerer) {
|
|||||||
).Set(1)
|
).Set(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// v caches the formatted version string to avoid repeated computation.
|
|
||||||
var v string
|
var v string
|
||||||
|
|
||||||
// VersionInfo returns the structured version information.
|
|
||||||
// This provides programmatic access to version details for JSON serialization
|
|
||||||
// or other structured uses.
|
|
||||||
func VersionInfo() Info {
|
func VersionInfo() Info {
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version returns a human-readable version string suitable for display.
|
|
||||||
// The format includes semantic version, Git revision, build time, and Go version.
|
|
||||||
// Example: "v1.0.0/abc123f-M (2023-01-01T00:00:00Z, go1.21.0)"
|
|
||||||
// The "-M" suffix indicates the working tree was modified during build.
|
|
||||||
func Version() string {
|
func Version() string {
|
||||||
if len(v) > 0 {
|
if len(v) > 0 {
|
||||||
return v
|
return v
|
||||||
@ -204,23 +154,10 @@ func Version() string {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckVersion compares a version against a minimum required version.
|
|
||||||
// Returns true if the version meets or exceeds the minimum requirement.
|
|
||||||
//
|
|
||||||
// Special handling:
|
|
||||||
// - "dev-snapshot" is always considered valid (returns true)
|
|
||||||
// - Git hash suffixes (e.g., "v1.0.0/abc123") are stripped before comparison
|
|
||||||
// - Uses semantic version comparison rules
|
|
||||||
//
|
|
||||||
// Both version and minimumVersion should follow semantic versioning with "v" prefix.
|
|
||||||
func CheckVersion(version, minimumVersion string) bool {
|
func CheckVersion(version, minimumVersion string) bool {
|
||||||
if version == "dev-snapshot" {
|
if version == "dev-snapshot" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Strip Git hash suffix if present (e.g., "v1.0.0/abc123" -> "v1.0.0")
|
|
||||||
if idx := strings.Index(version, "/"); idx >= 0 {
|
|
||||||
version = version[0:idx]
|
|
||||||
}
|
|
||||||
if semver.Compare(version, minimumVersion) < 0 {
|
if semver.Compare(version, minimumVersion) < 0 {
|
||||||
// log.Debug("version too old", "v", cl.Version.Version)
|
// log.Debug("version too old", "v", cl.Version.Version)
|
||||||
return false
|
return false
|
||||||
|
@ -1,311 +0,0 @@
|
|||||||
package version
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCheckVersion(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
In string
|
|
||||||
Min string
|
|
||||||
Expected bool
|
|
||||||
}{
|
|
||||||
// Basic version comparisons
|
|
||||||
{"v3.8.4", "v3.8.5", false},
|
|
||||||
{"v3.9.3", "v3.8.5", true},
|
|
||||||
{"v3.8.5", "v3.8.5", true},
|
|
||||||
// Dev snapshot should always pass
|
|
||||||
{"dev-snapshot", "v3.8.5", true},
|
|
||||||
{"dev-snapshot", "v99.99.99", true},
|
|
||||||
// Versions with Git hashes should be stripped
|
|
||||||
{"v3.8.5/abc123", "v3.8.5", true},
|
|
||||||
{"v3.8.4/abc123", "v3.8.5", false},
|
|
||||||
{"v3.9.0/def456", "v3.8.5", true},
|
|
||||||
// Pre-release versions
|
|
||||||
{"v3.8.5-alpha", "v3.8.5", false},
|
|
||||||
{"v3.8.5", "v3.8.5-alpha", true},
|
|
||||||
{"v3.8.5-beta", "v3.8.5-alpha", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range tests {
|
|
||||||
r := CheckVersion(d.In, d.Min)
|
|
||||||
if r != d.Expected {
|
|
||||||
t.Errorf("CheckVersion(%q, %q) = %t, expected %t", d.In, d.Min, r, d.Expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersionInfo(t *testing.T) {
|
|
||||||
info := VersionInfo()
|
|
||||||
|
|
||||||
// Check that we get a valid Info struct
|
|
||||||
if info.Version == "" {
|
|
||||||
t.Error("VersionInfo().Version should not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version should start with "v" or be "dev-snapshot"
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GitRevShort should be <= 7 characters if set
|
|
||||||
if info.GitRevShort != "" && len(info.GitRevShort) > 7 {
|
|
||||||
t.Errorf("GitRevShort should be <= 7 characters, got: %s", info.GitRevShort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GitRevShort should be prefix of GitRev if both are set
|
|
||||||
if info.GitRev != "" && info.GitRevShort != "" {
|
|
||||||
if !strings.HasPrefix(info.GitRev, info.GitRevShort) {
|
|
||||||
t.Errorf("GitRevShort should be prefix of GitRev: %s not prefix of %s",
|
|
||||||
info.GitRevShort, info.GitRev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersion(t *testing.T) {
|
|
||||||
version := Version()
|
|
||||||
|
|
||||||
if version == "" {
|
|
||||||
t.Error("Version() should not return empty string")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should contain Go version
|
|
||||||
if !strings.Contains(version, runtime.Version()) {
|
|
||||||
t.Errorf("Version should contain Go version %s, got: %s", runtime.Version(), version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should contain the VERSION variable (or dev-snapshot)
|
|
||||||
info := VersionInfo()
|
|
||||||
if !strings.Contains(version, info.Version) {
|
|
||||||
t.Errorf("Version should contain %s, got: %s", info.Version, version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be in expected format: "version (extras)"
|
|
||||||
if !strings.Contains(version, "(") || !strings.Contains(version, ")") {
|
|
||||||
t.Errorf("Version should be in format 'version (extras)', got: %s", version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersionCmd(t *testing.T) {
|
|
||||||
appName := "testapp"
|
|
||||||
cmd := VersionCmd(appName)
|
|
||||||
|
|
||||||
// Test basic command properties
|
|
||||||
if cmd.Use != "version" {
|
|
||||||
t.Errorf("Expected command use to be 'version', got: %s", cmd.Use)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Short == "" {
|
|
||||||
t.Error("Command should have a short description")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Long == "" {
|
|
||||||
t.Error("Command should have a long description")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Run == nil {
|
|
||||||
t.Error("Command should have a Run function")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that the command can be executed without error
|
|
||||||
cmd.SetArgs([]string{})
|
|
||||||
err := cmd.Execute()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("VersionCmd execution should not return error, got: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKongVersionCmd(t *testing.T) {
|
|
||||||
cmd := &KongVersionCmd{Name: "testapp"}
|
|
||||||
|
|
||||||
// Test that Run() doesn't return an error
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("KongVersionCmd.Run() should not return error, got: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegisterMetric(t *testing.T) {
|
|
||||||
// Create a test registry
|
|
||||||
registry := prometheus.NewRegistry()
|
|
||||||
|
|
||||||
// Test registering metric without name
|
|
||||||
RegisterMetric("", registry)
|
|
||||||
|
|
||||||
// Gather metrics
|
|
||||||
metricFamilies, err := registry.Gather()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to gather metrics: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the build_info metric
|
|
||||||
var buildInfoFamily *dto.MetricFamily
|
|
||||||
for _, family := range metricFamilies {
|
|
||||||
if family.GetName() == "build_info" {
|
|
||||||
buildInfoFamily = family
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if buildInfoFamily == nil {
|
|
||||||
t.Fatal("build_info metric not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if buildInfoFamily.GetHelp() == "" {
|
|
||||||
t.Error("build_info metric should have help text")
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics := buildInfoFamily.GetMetric()
|
|
||||||
if len(metrics) == 0 {
|
|
||||||
t.Fatal("build_info metric should have at least one sample")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the metric has the expected labels
|
|
||||||
metric := metrics[0]
|
|
||||||
labels := metric.GetLabel()
|
|
||||||
|
|
||||||
expectedLabels := []string{"version", "buildtime", "gittime", "git"}
|
|
||||||
labelMap := make(map[string]string)
|
|
||||||
|
|
||||||
for _, label := range labels {
|
|
||||||
labelMap[label.GetName()] = label.GetValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, expectedLabel := range expectedLabels {
|
|
||||||
if _, exists := labelMap[expectedLabel]; !exists {
|
|
||||||
t.Errorf("Expected label %s not found in metric", expectedLabel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the metric value is 1
|
|
||||||
if metric.GetGauge().GetValue() != 1 {
|
|
||||||
t.Errorf("Expected build_info metric value to be 1, got %f", metric.GetGauge().GetValue())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegisterMetricWithName(t *testing.T) {
|
|
||||||
// Create a test registry
|
|
||||||
registry := prometheus.NewRegistry()
|
|
||||||
|
|
||||||
// Test registering metric with custom name
|
|
||||||
appName := "my-test-app"
|
|
||||||
RegisterMetric(appName, registry)
|
|
||||||
|
|
||||||
// Gather metrics
|
|
||||||
metricFamilies, err := registry.Gather()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to gather metrics: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the my_test_app_build_info metric
|
|
||||||
expectedName := "my_test_app_build_info"
|
|
||||||
var buildInfoFamily *dto.MetricFamily
|
|
||||||
for _, family := range metricFamilies {
|
|
||||||
if family.GetName() == expectedName {
|
|
||||||
buildInfoFamily = family
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if buildInfoFamily == nil {
|
|
||||||
t.Fatalf("%s metric not found", expectedName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersionConsistency(t *testing.T) {
|
|
||||||
// Call Version() multiple times and ensure it returns the same result
|
|
||||||
v1 := Version()
|
|
||||||
v2 := Version()
|
|
||||||
|
|
||||||
if v1 != v2 {
|
|
||||||
t.Errorf("Version() should return consistent results: %s != %s", v1, v2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersionInfoConsistency(t *testing.T) {
|
|
||||||
// Ensure VersionInfo() is consistent with Version()
|
|
||||||
info := VersionInfo()
|
|
||||||
version := Version()
|
|
||||||
|
|
||||||
// Version string should contain the semantic version
|
|
||||||
if !strings.Contains(version, info.Version) {
|
|
||||||
t.Errorf("Version() should contain VersionInfo().Version: %s not in %s",
|
|
||||||
info.Version, version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If GitRevShort is set, version should contain it
|
|
||||||
if info.GitRevShort != "" {
|
|
||||||
if !strings.Contains(version, info.GitRevShort) {
|
|
||||||
t.Errorf("Version() should contain GitRevShort: %s not in %s",
|
|
||||||
info.GitRevShort, version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test edge cases
|
|
||||||
func TestCheckVersionEdgeCases(t *testing.T) {
|
|
||||||
// Test with empty strings
|
|
||||||
if CheckVersion("", "v1.0.0") {
|
|
||||||
t.Error("Empty version should not be >= v1.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with malformed versions (should be handled gracefully)
|
|
||||||
// Note: semver.Compare might panic or return unexpected results for invalid versions
|
|
||||||
// but our function should handle the common cases
|
|
||||||
tests := []struct {
|
|
||||||
version string
|
|
||||||
minimum string
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{"v1.0.0/", "v1.0.0", "version with trailing slash"},
|
|
||||||
{"v1.0.0/abc/def", "v1.0.0", "version with multiple slashes"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
// This should not panic
|
|
||||||
result := CheckVersion(test.version, test.minimum)
|
|
||||||
t.Logf("%s: CheckVersion(%q, %q) = %t", test.desc, test.version, test.minimum, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark version operations
|
|
||||||
func BenchmarkVersion(b *testing.B) {
|
|
||||||
// Reset the cached version to test actual computation
|
|
||||||
v = ""
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = Version()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkVersionInfo(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = VersionInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkCheckVersion(b *testing.B) {
|
|
||||||
version := "v1.2.3/abc123"
|
|
||||||
minimum := "v1.2.0"
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = CheckVersion(version, minimum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkCheckVersionDevSnapshot(b *testing.B) {
|
|
||||||
version := "dev-snapshot"
|
|
||||||
minimum := "v1.2.0"
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = CheckVersion(version, minimum)
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,12 +3,14 @@ package fastlyxff
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestFastlyIPRanges(t *testing.T) {
|
func TestFastlyIPRanges(t *testing.T) {
|
||||||
|
|
||||||
fastlyxff, err := New("fastly.json")
|
fastlyxff, err := New("fastly.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not load test data: %s", err)
|
t.Fatalf("could not load test data: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := fastlyxff.EchoTrustOption()
|
data, err := fastlyxff.EchoTrustOption()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not parse test data: %s", err)
|
t.Fatalf("could not parse test data: %s", err)
|
||||||
}
|
}
|
||||||
@ -17,4 +19,5 @@ func TestFastlyIPRanges(t *testing.T) {
|
|||||||
t.Logf("only got %d prefixes, expected more", len(data))
|
t.Logf("only got %d prefixes, expected more", len(data))
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user