Compare commits
62 Commits
Author | SHA1 | Date | |
---|---|---|---|
09b52f92d7 | |||
785abdec8d | |||
ce203a4618 | |||
3c994a7343 | |||
f69c3e9c3c | |||
fac5b1f275 | |||
a37559b93e | |||
faac09ac0c | |||
62a7605869 | |||
0996167865 | |||
87344dd601 | |||
39e6611602 | |||
355d246010 | |||
e5836a8b97 | |||
f6d160a7f8 | |||
9e2d6fb74e | |||
0df1154bb5 | |||
b926a85737 | |||
68bd4d8904 | |||
152be9d956 | |||
ab94adb925 | |||
ddb56b3566 | |||
4367ef9c29 | |||
d6a77f4003 | |||
3f3fb29bc9 | |||
8e898d9c59 | |||
1ecd5684e6 | |||
59580b50ba | |||
9a86b2aaf5 | |||
bcf7232154 | |||
9934dc8e36 | |||
a458dcb226 | |||
4ed44c72a4 | |||
8a8ff93996 | |||
1e8785bd32 | |||
5aeaa97c6f | |||
df2285d355 | |||
232a6f98df | |||
4f6b09200f | |||
7085202154 | |||
5c7ae6ab8a | |||
608f05d395 | |||
b5420f9dbd | |||
537ee53384 | |||
0a92ad768e | |||
61d73f7be3 | |||
2bff6d8ef3 | |||
62e28b71f1 | |||
5b033a1f0b | |||
a4447c97f6 | |||
9d136b2502 | |||
7420ad12f4 | |||
cf33a99566 | |||
c2b303bec9 | |||
6fd0728668 | |||
09f963b267 | |||
ad63071f60 | |||
020966a4b3 | |||
053de4fd16 | |||
3f1f4436df | |||
be9b63f382 | |||
e9d0f7419a |
1
.github/copilot-instructions.md
vendored
Symbolic link
1
.github/copilot-instructions.md
vendored
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../CLAUDE.md
|
163
CLAUDE.md
Normal file
163
CLAUDE.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# 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
|
10
LICENSE
Normal file
10
LICENSE
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Copyright (c) 2012-2024 Develooper LLC
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of Develooper LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
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,7 +30,6 @@ 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
|
||||||
|
94
config/config.go
Normal file
94
config/config.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// Package config provides NTP Pool specific
|
||||||
|
// configuration tools.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.ntppool.org/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go tool github.com/masaushi/accessory -type Config
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
deploymentMode string `accessor:"getter"`
|
||||||
|
|
||||||
|
manageHostname string `accessor:"getter"`
|
||||||
|
manageTLS bool
|
||||||
|
|
||||||
|
webHostname string `accessor:"getter"`
|
||||||
|
webHostnames []string
|
||||||
|
webTLS bool
|
||||||
|
|
||||||
|
valid bool `accessor:"getter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Config {
|
||||||
|
c := Config{}
|
||||||
|
c.deploymentMode = os.Getenv("deployment_mode")
|
||||||
|
c.manageHostname = os.Getenv("manage_hostname")
|
||||||
|
|
||||||
|
c.webHostnames = strings.Split(os.Getenv("web_hostname"), ",")
|
||||||
|
for i, h := range c.webHostnames {
|
||||||
|
c.webHostnames[i] = strings.TrimSpace(h)
|
||||||
|
}
|
||||||
|
if len(c.webHostnames) > 0 {
|
||||||
|
c.webHostname = c.webHostnames[0]
|
||||||
|
c.valid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
c.manageTLS = parseBool(os.Getenv("manage_tls"))
|
||||||
|
c.webTLS = parseBool(os.Getenv("web_tls"))
|
||||||
|
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WebURL(path string, query *url.Values) string {
|
||||||
|
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 {
|
||||||
|
uri := url.URL{}
|
||||||
|
uri.Host = host
|
||||||
|
if tls {
|
||||||
|
uri.Scheme = "https"
|
||||||
|
} else {
|
||||||
|
uri.Scheme = "http"
|
||||||
|
}
|
||||||
|
uri.Path = path
|
||||||
|
if query != nil {
|
||||||
|
uri.RawQuery = query.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBool(s string) bool {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "yes":
|
||||||
|
return true
|
||||||
|
case "y":
|
||||||
|
return true
|
||||||
|
case "no":
|
||||||
|
return false
|
||||||
|
case "n":
|
||||||
|
return false
|
||||||
|
case "":
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := strconv.ParseBool(s)
|
||||||
|
if err != nil {
|
||||||
|
logger.Setup().Error("could not parse bool", "string", s, "err", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
31
config/config_accessor.go
Normal file
31
config/config_accessor.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Code generated by accessory; DO NOT EDIT.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
func (c *Config) DeploymentMode() string {
|
||||||
|
if c == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.deploymentMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ManageHostname() string {
|
||||||
|
if c == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.manageHostname
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WebHostname() string {
|
||||||
|
if c == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.webHostname
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Valid() bool {
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.valid
|
||||||
|
}
|
24
config/config_test.go
Normal file
24
config/config_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBaseURL(t *testing.T) {
|
||||||
|
os.Setenv("web_hostname", "www.ntp.dev, web.ntppool.dev")
|
||||||
|
os.Setenv("web_tls", "yes")
|
||||||
|
|
||||||
|
c := New()
|
||||||
|
if !c.Valid() {
|
||||||
|
t.Fatalf("config not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
q := url.Values{}
|
||||||
|
q.Set("foo", "bar")
|
||||||
|
u := c.WebURL("/foo", &q)
|
||||||
|
if u != "https://www.ntp.dev/foo?foo=bar" {
|
||||||
|
t.Fatalf("unexpected WebURL: %s", u)
|
||||||
|
}
|
||||||
|
}
|
18
config/depenv/context.go
Normal file
18
config/depenv/context.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
}
|
87
config/depenv/depenv.go
Normal file
87
config/depenv/depenv.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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
|
||||||
|
}
|
40
config/depenv/monitor_names.go
Normal file
40
config/depenv/monitor_names.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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
Normal file
170
ekko/ekko.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
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
|
||||||
|
}
|
73
ekko/options.go
Normal file
73
ekko/options.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
83
go.mod
83
go.mod
@ -1,34 +1,77 @@
|
|||||||
module go.ntppool.org/common
|
module go.ntppool.org/common
|
||||||
|
|
||||||
go 1.20
|
go 1.23.5
|
||||||
|
|
||||||
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.13.3
|
||||||
github.com/oklog/ulid/v2 v2.1.0
|
github.com/oklog/ulid/v2 v2.1.0
|
||||||
github.com/prometheus/client_golang v1.16.0
|
github.com/prometheus/client_golang v1.20.5
|
||||||
github.com/segmentio/kafka-go v0.4.42
|
github.com/remychantenay/slog-otel v1.3.2
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/samber/slog-echo v1.14.8
|
||||||
golang.org/x/exp v0.0.0-20230711023510-fffb14384f22
|
github.com/samber/slog-multi v1.2.4
|
||||||
golang.org/x/mod v0.12.0
|
github.com/segmentio/kafka-go v0.4.47
|
||||||
golang.org/x/sync v0.3.0
|
github.com/spf13/cobra v1.8.1
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelslog v0.8.0
|
||||||
|
go.opentelemetry.io/contrib/exporters/autoexport v0.58.0
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.58.0
|
||||||
|
go.opentelemetry.io/otel v1.33.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0
|
||||||
|
go.opentelemetry.io/otel/log v0.9.0
|
||||||
|
go.opentelemetry.io/otel/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/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.16.7 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_model v0.4.0 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.44.0 // indirect
|
github.com/prometheus/common v0.61.0 // indirect
|
||||||
github.com/prometheus/procfs v0.11.0 // 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/stretchr/testify v1.8.4 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
golang.org/x/net v0.11.0 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
go.opentelemetry.io/contrib/bridges/prometheus v0.58.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/prometheus v0.55.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 // indirect
|
||||||
|
go.opentelemetry.io/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
|
||||||
)
|
)
|
||||||
|
214
go.sum
214
go.sum
@ -2,65 +2,92 @@ 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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/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.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||||
github.com/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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
|
||||||
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.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
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/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
github.com/oklog/ulid/v2 v2.1.0 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.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.22/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.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
|
||||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
||||||
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/remychantenay/slog-otel v1.3.2 h1:ZBx8qnwfLJ6e18Vba4e9Xp9B7khTmpIwFsU1sAmActw=
|
||||||
|
github.com/remychantenay/slog-otel v1.3.2/go.mod h1:gKW4tQ8cGOKoA+bi7wtYba/tcJ6Tc9XyQ/EW8gHA/2E=
|
||||||
github.com/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/segmentio/kafka-go v0.4.42 h1:qffhBZCz4WcWyNuHEclHjIMLs2slp6mZO8px+5W5tfU=
|
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||||
github.com/segmentio/kafka-go v0.4.42/go.mod h1:d0g15xPMqoUookug0OU75DhGZxXwCFxSLeJ4uphwJzg=
|
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
github.com/samber/slog-echo v1.14.8 h1:R7RF2LWEepsKtC7i6A6o9peS3Rz5HO8+H8OD+8mPD1I=
|
||||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
github.com/samber/slog-echo v1.14.8/go.mod h1:K21nbusPmai/MYm8PFactmZoFctkMmkeaTdXXyvhY1c=
|
||||||
|
github.com/samber/slog-multi v1.2.4 h1:k9x3JAWKJFPKffx+oXZ8TasaNuorIW4tG+TXxkt6Ry4=
|
||||||
|
github.com/samber/slog-multi v1.2.4/go.mod h1:ACuZ5B6heK57TfMVkVknN2UZHoFfjCwRxR0Q2OXKHlo=
|
||||||
|
github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
|
||||||
|
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
|
||||||
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||||
@ -68,55 +95,122 @@ 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/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelslog v0.8.0 h1:G3sKsNueSdxuACINFxKrQeimAIst0A5ytA2YJH+3e1c=
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelslog v0.8.0/go.mod h1:ptJm3wizguEPurZgarDAwOeX7O0iMR7l+QvIVenhYdE=
|
||||||
|
go.opentelemetry.io/contrib/bridges/prometheus v0.58.0 h1:gQFwWiqm4JUvOjpdmyU0di+2pVQ8QNpk1Ak/54Y6NcY=
|
||||||
|
go.opentelemetry.io/contrib/bridges/prometheus v0.58.0/go.mod h1:CNyFi9PuvHtEJNmMFHaXZMuA4XmgRXIqpFcHdqzLvVU=
|
||||||
|
go.opentelemetry.io/contrib/exporters/autoexport v0.58.0 h1:qVsDVgZd/bC6ZKDOHSjILpm0T/BWvASC9cQU3GYga78=
|
||||||
|
go.opentelemetry.io/contrib/exporters/autoexport v0.58.0/go.mod h1:bAv7mY+5qTsFPFaRpr75vDOocX09I36QH4Rg0slEG/U=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.58.0 h1:DBk8Zh+Yn3WtWCdGSx1pbEV9/naLtjG16c1zwQA2MBI=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.58.0/go.mod h1:DFx32LPclW1MNdSKIMrjjetsk0tJtYhAvuGjDIG2SKE=
|
||||||
|
go.opentelemetry.io/contrib/propagators/b3 v1.33.0 h1:ig/IsHyyoQ1F1d6FUDIIW5oYpsuTVtN16AyGOgdjAHQ=
|
||||||
|
go.opentelemetry.io/contrib/propagators/b3 v1.33.0/go.mod h1:EsVYoNy+Eol5znb6wwN3XQTILyjl040gUpEnUSNZfsk=
|
||||||
|
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||||
|
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 h1:gA2gh+3B3NDvRFP30Ufh7CC3TtJRbUSf2TTD0LbCagw=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0/go.mod h1:smRTR+02OtrVGjvWE1sQxhuazozKc/BXvvqqnmOxy+s=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0 h1:Za0Z/j9Gf3Z9DKQ1choU9xI2noCxlkcyFFP2Ob3miEQ=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0/go.mod h1:jMRB8N75meTNjDFQyJBA/2Z9en21CsxwMctn08NHY6c=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 h1:bSjzTvsXZbLSWU8hnZXcKmEVaJjjnandxD0PxThhVU8=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0/go.mod h1:aj2rilHL8WjXY1I5V+ra+z8FELtk681deydgYT8ikxU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
||||||
|
go.opentelemetry.io/otel/exporters/prometheus v0.55.0 h1:sSPw658Lk2NWAv74lkD3B/RSDb+xRFx46GjkrL3VUZo=
|
||||||
|
go.opentelemetry.io/otel/exporters/prometheus v0.55.0/go.mod h1:nC00vyCmQixoeaxF6KNyP42II/RHa9UdruK02qBmHvI=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0 h1:iI15wfQb5ZtAVTdS5WROxpYmw6Kjez3hT9SuzXhrgGQ=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0/go.mod h1:yepwlNzVVxHWR5ugHIrll+euPQPq4pvysHTDr/daV9o=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk=
|
||||||
|
go.opentelemetry.io/otel/log v0.9.0 h1:0OiWRefqJ2QszpCiqwGO0u9ajMPe17q6IscQvvp3czY=
|
||||||
|
go.opentelemetry.io/otel/log v0.9.0/go.mod h1:WPP4OJ+RBkQ416jrFCQFuFKtXKD6mOoYCQm6ykK8VaU=
|
||||||
|
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||||
|
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.9.0 h1:YPCi6W1Eg0vwT/XJWsv2/PaQ2nyAJYuF7UUjQSBe3bc=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.9.0/go.mod h1:y0HdrOz7OkXQBuc2yjiqnEHc+CRKeVhRE3hx4RwTmV4=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
|
||||||
|
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||||
|
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||||
|
go.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/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/exp v0.0.0-20230711023510-fffb14384f22 h1:FqrVOBQxQ8r/UwwXibI0KMolVhvFiGobSfdE33deHJM=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/exp v0.0.0-20230711023510-fffb14384f22/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
|
||||||
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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||||
|
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
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.7.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.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.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=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.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=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
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.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-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/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=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||||
|
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||||
|
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||||
|
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
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=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@ -1,25 +1,61 @@
|
|||||||
|
// 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 (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
"go.ntppool.org/common/logger"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HealthCheckListener runs simple http server on the specified port for
|
// Server is a standalone HTTP server dedicated to health checks.
|
||||||
// health check probes
|
// It runs separately from the main application server to ensure health
|
||||||
func HealthCheckListener(ctx context.Context, port int, log *slog.Logger) error {
|
// checks remain available even if the main server is experiencing issues.
|
||||||
log.Info("Starting health listener", "port", port)
|
//
|
||||||
|
// The server includes built-in timeouts, graceful shutdown, and structured
|
||||||
|
// logging for monitoring and debugging health check behavior.
|
||||||
|
type Server struct {
|
||||||
|
log *slog.Logger
|
||||||
|
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 {
|
||||||
|
if healthFn == nil {
|
||||||
|
healthFn = basicHealth
|
||||||
|
}
|
||||||
|
srv := &Server{
|
||||||
|
log: logger.Setup(),
|
||||||
|
healthFn: healthFn,
|
||||||
|
}
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogger replaces the default logger with a custom one.
|
||||||
|
func (srv *Server) SetLogger(log *slog.Logger) {
|
||||||
|
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 {
|
||||||
|
srv.log.Info("starting health listener", "port", port)
|
||||||
|
|
||||||
serveMux := http.NewServeMux()
|
serveMux := http.NewServeMux()
|
||||||
|
|
||||||
serveMux.HandleFunc("/__health", basicHealth)
|
serveMux.HandleFunc("/__health", srv.healthFn)
|
||||||
|
|
||||||
srv := &http.Server{
|
hsrv := &http.Server{
|
||||||
Addr: ":" + strconv.Itoa(port),
|
Addr: ":" + strconv.Itoa(port),
|
||||||
ReadTimeout: 10 * time.Second,
|
ReadTimeout: 10 * time.Second,
|
||||||
WriteTimeout: 20 * time.Second,
|
WriteTimeout: 20 * time.Second,
|
||||||
@ -30,9 +66,9 @@ func HealthCheckListener(ctx context.Context, port int, log *slog.Logger) error
|
|||||||
g, ctx := errgroup.WithContext(ctx)
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
err := srv.ListenAndServe()
|
err := hsrv.ListenAndServe()
|
||||||
if err != http.ErrServerClosed {
|
if err != http.ErrServerClosed {
|
||||||
log.Warn("health check server done listening", "err", err)
|
srv.log.Warn("health check server done listening", "err", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -40,12 +76,11 @@ func HealthCheckListener(ctx context.Context, port int, log *slog.Logger) error
|
|||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
shCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
log.Error("health check server shutdown failed", "err", err)
|
defer cancel()
|
||||||
|
if err := hsrv.Shutdown(shCtx); err != nil {
|
||||||
|
srv.log.Error("health check server shutdown failed", "err", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -54,6 +89,13 @@ func HealthCheckListener(ctx context.Context, port int, log *slog.Logger) error
|
|||||||
return g.Wait()
|
return g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HealthCheckListener runs a simple HTTP server on the specified port for health check probes.
|
||||||
|
func HealthCheckListener(ctx context.Context, port int, log *slog.Logger) error {
|
||||||
|
srv := NewServer(nil)
|
||||||
|
srv.SetLogger(log)
|
||||||
|
return srv.Listen(ctx, port)
|
||||||
|
}
|
||||||
|
|
||||||
func basicHealth(w http.ResponseWriter, r *http.Request) {
|
func basicHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
|
@ -8,7 +8,6 @@ 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()
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/abh/certman"
|
"github.com/abh/certman"
|
||||||
"github.com/segmentio/kafka-go"
|
"github.com/segmentio/kafka-go"
|
||||||
|
"go.ntppool.org/common/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -41,11 +42,9 @@ 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
|
||||||
@ -188,7 +187,6 @@ 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()...),
|
||||||
@ -203,3 +201,17 @@ func (k *Kafka) NewWriter(topic string) (*kafka.Writer, error) {
|
|||||||
|
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (k *Kafka) CheckPartitions() error {
|
||||||
|
partitions, err := k.conn.ReadPartitions()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// should this result in an error?
|
||||||
|
if len(partitions) == 0 {
|
||||||
|
log := logger.Setup()
|
||||||
|
log.Info("kafka connection has no partitions")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
78
logger/logfmt.go
Normal file
78
logger/logfmt.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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)
|
||||||
|
}
|
41
logger/logfmt_test.go
Normal file
41
logger/logfmt_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
150
logger/logger.go
150
logger/logger.go
@ -3,80 +3,152 @@ package logger
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
slogtraceid "github.com/remychantenay/slog-otel"
|
||||||
|
slogmulti "github.com/samber/slog-multi"
|
||||||
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ConfigPrefix = ""
|
var ConfigPrefix = ""
|
||||||
|
|
||||||
var rootLogger *slog.Logger
|
var (
|
||||||
var setup sync.Once
|
textLogger *slog.Logger
|
||||||
|
otlpLogger *slog.Logger
|
||||||
|
multiLogger *slog.Logger
|
||||||
|
)
|
||||||
|
|
||||||
func Setup() *slog.Logger {
|
var (
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
setup.Do(func() {
|
func setupStdErrHandler() slog.Handler {
|
||||||
|
programLevel := new(slog.LevelVar) // Info by default
|
||||||
|
|
||||||
var programLevel = new(slog.LevelVar) // Info by default
|
envVar := "DEBUG"
|
||||||
|
if len(ConfigPrefix) > 0 {
|
||||||
|
envVar = ConfigPrefix + "_" + envVar
|
||||||
|
}
|
||||||
|
|
||||||
envVar := "DEBUG"
|
if opt := os.Getenv(envVar); len(opt) > 0 {
|
||||||
if len(ConfigPrefix) > 0 {
|
if debug, _ := strconv.ParseBool(opt); debug {
|
||||||
envVar = ConfigPrefix + "_" + envVar
|
programLevel.Set(slog.LevelDebug)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if opt := os.Getenv(envVar); len(opt) > 0 {
|
logOptions := &slog.HandlerOptions{Level: programLevel}
|
||||||
if debug, _ := strconv.ParseBool(opt); debug {
|
|
||||||
programLevel.Set(slog.LevelDebug)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logOptions := &slog.HandlerOptions{Level: programLevel}
|
if len(os.Getenv("INVOCATION_ID")) > 0 {
|
||||||
|
// don't add timestamps when running under systemd
|
||||||
|
log.Default().SetFlags(0)
|
||||||
|
|
||||||
if len(os.Getenv("INVOCATION_ID")) > 0 {
|
logOptions.ReplaceAttr = logRemoveTime
|
||||||
// don't add timestamps when running under systemd
|
}
|
||||||
log.Default().SetFlags(0)
|
|
||||||
|
|
||||||
logReplace := func(groups []string, a slog.Attr) slog.Attr {
|
logHandler := slogtraceid.OtelHandler{
|
||||||
// Remove time
|
Next: slog.NewTextHandler(os.Stderr, logOptions),
|
||||||
if a.Key == slog.TimeKey && len(groups) == 0 {
|
}
|
||||||
a.Key = ""
|
|
||||||
a.Value = slog.AnyValue(nil)
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
logOptions.ReplaceAttr = logReplace
|
return logHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
logHandler := slog.NewTextHandler(os.Stderr, logOptions)
|
func setupOtlpLogger() *slog.Logger {
|
||||||
|
setupOtlp.Do(func() {
|
||||||
|
otlpLogger = slog.New(
|
||||||
|
newLogFmtHandler(otelslog.NewHandler("common")),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return otlpLogger
|
||||||
|
}
|
||||||
|
|
||||||
// https://github.com/cyrusaf/ctxlog/pull/1
|
// SetupMultiLogger will setup and make default a logger that
|
||||||
// log := slog.New(ctxlog.NewHandler(logHandler))
|
// logs as described in Setup() as well as an OLTP logger.
|
||||||
log := slog.New(logHandler)
|
// The "multi logger" is made the default the first time
|
||||||
|
// this function is called
|
||||||
slog.SetDefault(log)
|
func SetupMultiLogger() *slog.Logger {
|
||||||
|
setupMulti.Do(func() {
|
||||||
rootLogger = log
|
textHandler := Setup().Handler()
|
||||||
|
otlpHandler := setupOtlpLogger().Handler()
|
||||||
|
|
||||||
|
multiHandler := slogmulti.Fanout(
|
||||||
|
textHandler,
|
||||||
|
otlpHandler,
|
||||||
|
)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
multiLogger = slog.New(multiHandler)
|
||||||
|
slog.SetDefault(multiLogger)
|
||||||
})
|
})
|
||||||
|
|
||||||
return rootLogger
|
return multiLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// NewContext adds the logger to the context. Use this
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
@ -2,8 +2,7 @@ package logger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stdLoggerish struct {
|
type stdLoggerish struct {
|
||||||
@ -28,10 +27,15 @@ func NewStdLog(key string, debug bool, log *slog.Logger) *stdLoggerish {
|
|||||||
return sl
|
return sl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l stdLoggerish) Println(msg ...interface{}) {
|
func (l stdLoggerish) Println(msg ...any) {
|
||||||
l.f(l.key, "msg", msg)
|
l.f(l.key, "msg", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l stdLoggerish) Printf(msg string, args ...interface{}) {
|
func (l stdLoggerish) Printf(msg string, args ...any) {
|
||||||
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,3 +1,8 @@
|
|||||||
|
// 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 (
|
||||||
@ -13,10 +18,13 @@ 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()
|
||||||
|
|
||||||
@ -27,12 +35,14 @@ func New() *Metrics {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Metrics) Registry() prometheus.Registerer {
|
// Registry returns the custom Prometheus registry.
|
||||||
|
// Use this to register your application's metrics collectors.
|
||||||
|
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{
|
||||||
@ -42,11 +52,9 @@ func (m *Metrics) Handler() http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenAndServe starts a goroutine with a server running on
|
// ListenAndServe starts a metrics server on the specified port and blocks until ctx is done.
|
||||||
// the specified port. The server will shutdown and return when
|
// The server exposes the metrics handler and shuts down gracefully when the context is cancelled.
|
||||||
// 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{
|
||||||
|
242
metricsserver/metrics_test.go
Normal file
242
metricsserver/metrics_test.go
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,10 @@
|
|||||||
all: Makefile scripts
|
all: Makefile scripts
|
||||||
|
|
||||||
Makefile: .PHONY
|
Makefile: .PHONY
|
||||||
cp ~/src/go/common/scripts/Makefile .
|
cp ~/src/go/ntp/common/scripts/Makefile .
|
||||||
|
|
||||||
scripts: .PHONY
|
scripts: .PHONY
|
||||||
cp ~/src/go/common/scripts/{fury-publish,run-goreleaser,download-release} .
|
cp ~/src/go/ntp/common/scripts/{fury-publish,run-goreleaser,download-release} .
|
||||||
|
|
||||||
|
|
||||||
.PHONY:
|
.PHONY:
|
||||||
|
@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
go install github.com/goreleaser/goreleaser/v2@v2.8.2
|
||||||
|
|
||||||
|
if [ ! -z "${harbor_username:-}" ]; then
|
||||||
|
DOCKER_FILE=~/.docker/config.json
|
||||||
|
if [ ! -e $DOCKER_FILE ]; then
|
||||||
|
mkdir -p ~/.docker/
|
||||||
|
export harbor_auth=`cat /dev/null | jq -s -r '[ env.harbor_username, env.harbor_password ] | join(":") | @base64'`
|
||||||
|
echo '{"auths":{"harbor.ntppool.org":{"auth":""}}}' | jq '.auths["harbor.ntppool.org"].auth=env.harbor_auth' > $DOCKER_FILE
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
DRONE_TAG=${DRONE_TAG-""}
|
DRONE_TAG=${DRONE_TAG-""}
|
||||||
|
|
||||||
is_snapshot=""
|
is_snapshot=""
|
||||||
@ -10,4 +21,4 @@ if [ -z "$DRONE_TAG" ]; then
|
|||||||
is_snapshot="--snapshot"
|
is_snapshot="--snapshot"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
goreleaser release $is_snapshot -p 6 --skip-publish
|
goreleaser release $is_snapshot -p 6 --skip=publish
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// Package timeutil provides JSON-serializable time utilities.
|
||||||
package timeutil
|
package timeutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -6,16 +7,39 @@ 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 interface{}
|
var v any
|
||||||
if err := json.Unmarshal(b, &v); err != nil {
|
if err := json.Unmarshal(b, &v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -18,5 +18,4 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
281
tracing/tracing.go
Normal file
281
tracing/tracing.go
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
package tracing
|
||||||
|
|
||||||
|
// todo, review:
|
||||||
|
// https://github.com/ttys3/tracing-go/blob/main/tracing.go#L136
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.ntppool.org/common/logger"
|
||||||
|
"go.ntppool.org/common/version"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/contrib/exporters/autoexport"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||||
|
logglobal "go.opentelemetry.io/otel/log/global"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
|
"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
|
||||||
|
|
||||||
|
type TpShutdownFunc func(ctx context.Context) error
|
||||||
|
|
||||||
|
func Tracer() trace.Tracer {
|
||||||
|
traceProvider := otel.GetTracerProvider()
|
||||||
|
return traceProvider.Tracer("ntppool-tracer")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
||||||
|
return Tracer().Start(ctx, spanName, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
|
||||||
|
|
||||||
|
type TracerConfig struct {
|
||||||
|
ServiceName string
|
||||||
|
Environment string
|
||||||
|
Endpoint string
|
||||||
|
EndpointURL string
|
||||||
|
|
||||||
|
CertificateProvider GetClientCertificate
|
||||||
|
RootCAs *x509.CertPool
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitTracer(ctx context.Context, cfg *TracerConfig) (TpShutdownFunc, error) {
|
||||||
|
// todo: setup environment from cfg
|
||||||
|
return SetupSDK(ctx, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupSDK(ctx context.Context, cfg *TracerConfig) (shutdown TpShutdownFunc, err error) {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = &TracerConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
log := logger.Setup()
|
||||||
|
|
||||||
|
if serviceName := os.Getenv(svcNameKey); len(serviceName) == 0 {
|
||||||
|
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
|
||||||
|
// need to shutdown the providers first,
|
||||||
|
// exporters after which is the opposite
|
||||||
|
// order they are setup.
|
||||||
|
slices.Reverse(shutdownFuncs)
|
||||||
|
for _, fn := range shutdownFuncs {
|
||||||
|
// log.Warn("shutting down", "fn", fn)
|
||||||
|
err = errors.Join(err, fn(ctx))
|
||||||
|
}
|
||||||
|
shutdownFuncs = nil
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("shutdown returned errors", "err", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleErr calls shutdown for cleanup and makes sure that all errors are returned.
|
||||||
|
handleErr := func(inErr error) {
|
||||||
|
err = errors.Join(inErr, shutdown(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
prop := newPropagator()
|
||||||
|
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 {
|
||||||
|
handleErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shutdownFuncs = append(shutdownFuncs, spanExporter.Shutdown)
|
||||||
|
|
||||||
|
logExporter, err := autoexport.NewLogExporter(ctx)
|
||||||
|
if err != nil {
|
||||||
|
handleErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shutdownFuncs = append(shutdownFuncs, logExporter.Shutdown)
|
||||||
|
|
||||||
|
// Set up trace provider.
|
||||||
|
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),
|
||||||
|
sdklog.WithProcessor(
|
||||||
|
sdklog.NewBatchProcessor(logExporter, sdklog.WithExportBufferSize(10)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
logglobal.SetLoggerProvider(logProvider)
|
||||||
|
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) {
|
||||||
|
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{
|
||||||
|
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
|
||||||
|
}
|
||||||
|
if tlsConfig != nil {
|
||||||
|
opts = append(opts, otlptracehttp.WithTLSClientConfig(tlsConfig))
|
||||||
|
}
|
||||||
|
if len(cfg.Endpoint) > 0 {
|
||||||
|
opts = append(opts, otlptracehttp.WithEndpoint(cfg.Endpoint))
|
||||||
|
}
|
||||||
|
if len(cfg.EndpointURL) > 0 {
|
||||||
|
opts = append(opts, otlptracehttp.WithEndpointURL(cfg.EndpointURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
client = otlptracehttp.NewClient(opts...)
|
||||||
|
default:
|
||||||
|
return nil, errInvalidOTLPProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
exporter, err := otlptrace.New(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorContext(ctx, "creating OTLP trace exporter", "err", err)
|
||||||
|
}
|
||||||
|
return exporter, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTraceProvider(traceExporter sdktrace.SpanExporter, res *resource.Resource) (*sdktrace.TracerProvider, error) {
|
||||||
|
traceProvider := sdktrace.NewTracerProvider(
|
||||||
|
sdktrace.WithResource(res),
|
||||||
|
sdktrace.WithBatcher(traceExporter,
|
||||||
|
sdktrace.WithBatchTimeout(time.Second*3),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return traceProvider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPropagator() propagation.TextMapPropagator {
|
||||||
|
return propagation.NewCompositeTextMapPropagator(
|
||||||
|
propagation.TraceContext{},
|
||||||
|
propagation.Baggage{},
|
||||||
|
)
|
||||||
|
}
|
20
tracing/tracing_test.go
Normal file
20
tracing/tracing_test.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package tracing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInit(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost")
|
||||||
|
|
||||||
|
shutdownFn, err := InitTracer(ctx, &TracerConfig{})
|
||||||
|
if err != nil {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
defer shutdownFn(ctx)
|
||||||
|
}
|
52
types/log_scores.go
Normal file
52
types/log_scores.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogScoreAttributes struct {
|
||||||
|
Leap int8 `json:"leap,omitempty"`
|
||||||
|
Stratum int8 `json:"stratum,omitempty"`
|
||||||
|
NoResponse bool `json:"no_response,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Warning string `json:"warning,omitempty"`
|
||||||
|
|
||||||
|
FromLSID int `json:"from_ls_id,omitempty"`
|
||||||
|
FromSSID int `json:"from_ss_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lsa *LogScoreAttributes) String() string {
|
||||||
|
b, err := json.Marshal(lsa)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lsa *LogScoreAttributes) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(lsa)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lsa *LogScoreAttributes) Scan(value any) error {
|
||||||
|
var source []byte
|
||||||
|
_t := LogScoreAttributes{}
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []uint8:
|
||||||
|
source = v
|
||||||
|
case string:
|
||||||
|
source = []byte(v)
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("incompatible type for StringInterfaceMap")
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(source, &_t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*lsa = _t
|
||||||
|
return nil
|
||||||
|
}
|
66
ulid/ulid.go
66
ulid/ulid.go
@ -1,48 +1,44 @@
|
|||||||
|
// 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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var monotonicPool = sync.Pool{
|
// MakeULID generates a new ULID with the specified timestamp using cryptographically secure randomness.
|
||||||
New: func() interface{} {
|
// The function is thread-safe and optimized for high-concurrency environments.
|
||||||
|
//
|
||||||
log := logger.Setup()
|
// This implementation prioritizes simplicity and performance over strict monotonicity within
|
||||||
|
// the same millisecond. Each ULID is guaranteed to be unique and lexicographically sortable
|
||||||
var seed int64
|
// across different timestamps.
|
||||||
err := binary.Read(cryptorand.Reader, binary.BigEndian, &seed)
|
//
|
||||||
if err != nil {
|
// Returns a pointer to the generated ULID or an error if generation fails.
|
||||||
log.Error("crypto/rand error", "err", err)
|
// Generation should only fail under extreme circumstances (entropy exhaustion).
|
||||||
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)
|
||||||
mono := monotonicPool.Get().(io.Reader)
|
if err != nil {
|
||||||
|
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,25 +1,336 @@
|
|||||||
package ulid
|
package ulid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
cryptorand "crypto/rand"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
oklid "github.com/oklog/ulid/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestULID(t *testing.T) {
|
func TestMakeULID(t *testing.T) {
|
||||||
tm := time.Now()
|
tm := time.Now()
|
||||||
ul1, err := MakeULID(tm)
|
ul1, err := MakeULID(tm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("makeULID failed: %s", err)
|
t.Fatalf("MakeULID failed: %s", err)
|
||||||
t.Fail()
|
|
||||||
}
|
}
|
||||||
ul2, err := MakeULID(tm)
|
ul2, err := MakeULID(tm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("MakeULID failed: %s", err)
|
t.Fatalf("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.Logf("ul1 and ul2 got the same string: %s", ul1.String())
|
t.Errorf("ul1 and ul2 should be different: %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,34 +1,53 @@
|
|||||||
|
// 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 has the current software version (set in the build process)
|
// VERSION contains the current software version (typically set during the build process via ldflags).
|
||||||
var VERSION string
|
// If not set, defaults to "dev-snapshot". The version should follow semantic versioning.
|
||||||
var buildTime string
|
var (
|
||||||
var gitVersion string
|
VERSION string // Semantic version (e.g., "1.0.0" or "v1.0.0")
|
||||||
var gitModified bool
|
buildTime string // Build timestamp (RFC3339 format)
|
||||||
|
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"`
|
Version string `json:",omitempty"` // Semantic version with "v" prefix
|
||||||
GitRev string `json:",omitempty"`
|
GitRev string `json:",omitempty"` // Full Git commit hash
|
||||||
GitRevShort string `json:",omitempty"`
|
GitRevShort string `json:",omitempty"` // Shortened Git commit hash (7 characters)
|
||||||
BuildTime string `json:",omitempty"`
|
BuildTime string `json:",omitempty"` // Build timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
||||||
info.BuildTime = buildTime
|
info.BuildTime = buildTime
|
||||||
info.GitRev = gitVersion
|
info.GitRev = gitVersion
|
||||||
|
|
||||||
@ -39,7 +58,7 @@ func init() {
|
|||||||
VERSION = "v" + VERSION
|
VERSION = "v" + VERSION
|
||||||
}
|
}
|
||||||
if !semver.IsValid(VERSION) {
|
if !semver.IsValid(VERSION) {
|
||||||
logger.Setup().Warn("invalid version number", "version", VERSION)
|
slog.Default().Warn("invalid version number", "version", VERSION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bi, ok := debug.ReadBuildInfo(); ok {
|
if bi, ok := debug.ReadBuildInfo(); ok {
|
||||||
@ -78,10 +97,16 @@ 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)
|
||||||
@ -90,8 +115,26 @@ func VersionCmd(name string) *cobra.Command {
|
|||||||
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 = name + "_build_info"
|
name = name + "_build_info"
|
||||||
} else {
|
} else {
|
||||||
name = "build_info"
|
name = "build_info"
|
||||||
@ -99,13 +142,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",
|
Help: "Build information including version, build time, and git revision",
|
||||||
},
|
},
|
||||||
[]string{
|
[]string{
|
||||||
"version",
|
"version", // Combined version/git format (e.g., "v1.0.0/abc123")
|
||||||
"buildtime",
|
"buildtime", // Build timestamp from ldflags
|
||||||
"gittime",
|
"gittime", // Git commit timestamp from VCS info
|
||||||
"git",
|
"git", // Full Git commit hash
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
registry.MustRegister(buildInfo)
|
registry.MustRegister(buildInfo)
|
||||||
@ -120,12 +163,20 @@ 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
|
||||||
@ -152,3 +203,27 @@ func Version() string {
|
|||||||
v = fmt.Sprintf("%s (%s)", v, strings.Join(extra, ", "))
|
v = fmt.Sprintf("%s (%s)", v, strings.Join(extra, ", "))
|
||||||
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 {
|
||||||
|
if version == "dev-snapshot" {
|
||||||
|
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 {
|
||||||
|
// log.Debug("version too old", "v", cl.Version.Version)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
311
version/version_test.go
Normal file
311
version/version_test.go
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
1
xff/fastlyxff/fastly.json
Normal file
1
xff/fastlyxff/fastly.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"addresses":["23.235.32.0/20","43.249.72.0/22","103.244.50.0/24","103.245.222.0/23","103.245.224.0/24","104.156.80.0/20","140.248.64.0/18","140.248.128.0/17","146.75.0.0/17","151.101.0.0/16","157.52.64.0/18","167.82.0.0/17","167.82.128.0/20","167.82.160.0/20","167.82.224.0/20","172.111.64.0/18","185.31.16.0/22","199.27.72.0/21","199.232.0.0/16"],"ipv6_addresses":["2a04:4e40::/32","2a04:4e42::/32"]}
|
51
xff/fastlyxff/xff.go
Normal file
51
xff/fastlyxff/xff.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package fastlyxff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FastlyXFF struct {
|
||||||
|
IPv4 []string `json:"addresses"`
|
||||||
|
IPv6 []string `json:"ipv6_addresses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrustedNets struct {
|
||||||
|
prefixes []netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(fileName string) (*FastlyXFF, error) {
|
||||||
|
b, err := os.ReadFile(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d := FastlyXFF{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(b, &d)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xff *FastlyXFF) EchoTrustOption() ([]echo.TrustOption, error) {
|
||||||
|
ranges := []echo.TrustOption{}
|
||||||
|
|
||||||
|
for _, s := range append(xff.IPv4, xff.IPv6...) {
|
||||||
|
_, cidr, err := net.ParseCIDR(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
trust := echo.TrustIPRange(cidr)
|
||||||
|
ranges = append(ranges, trust)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges, nil
|
||||||
|
}
|
20
xff/fastlyxff/xff_test.go
Normal file
20
xff/fastlyxff/xff_test.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package fastlyxff
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestFastlyIPRanges(t *testing.T) {
|
||||||
|
fastlyxff, err := New("fastly.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not load test data: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := fastlyxff.EchoTrustOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not parse test data: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < 10 {
|
||||||
|
t.Logf("only got %d prefixes, expected more", len(data))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user