Codevault initial commit
This commit is contained in:
139
internal/observability/logging/configure.go
Normal file
139
internal/observability/logging/configure.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/contrib/bridges/otelzap"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
|
||||
"go.opentelemetry.io/otel/log/global"
|
||||
"go.opentelemetry.io/otel/sdk/log"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/kanootoko/codevault/internal"
|
||||
"github.com/kanootoko/codevault/internal/config"
|
||||
"github.com/kanootoko/codevault/internal/observability"
|
||||
)
|
||||
|
||||
func ConfigureLogger(ctx context.Context, cfg config.LoggingConfig, isProductionEnv bool) (logger *zap.Logger, closeFunc func(), err error) {
|
||||
stderrLevel, err := cfg.Stderr.ToZapLevel()
|
||||
if err != nil && !errors.Is(err, config.ErrLoggingDisabled) {
|
||||
return nil, nil, fmt.Errorf("error on getting stderr logging level: %w", err)
|
||||
}
|
||||
fileLoggers := make(map[string]zapcore.Level)
|
||||
for level, file := range cfg.Files {
|
||||
l, err := level.ToZapLevel()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error on getting file logging level: %w", err)
|
||||
}
|
||||
fileLoggers[file] = l
|
||||
}
|
||||
var stderrEncoder zapcore.Encoder
|
||||
var fileEncoder zapcore.Encoder
|
||||
{
|
||||
var fileLoggerCfg zapcore.EncoderConfig
|
||||
var stderrLoggerCfg zapcore.EncoderConfig
|
||||
if isProductionEnv {
|
||||
fileLoggerCfg = zap.NewProductionEncoderConfig()
|
||||
stderrLoggerCfg = zap.NewProductionEncoderConfig()
|
||||
} else {
|
||||
fileLoggerCfg = zap.NewDevelopmentEncoderConfig()
|
||||
stderrLoggerCfg = zap.NewDevelopmentEncoderConfig()
|
||||
}
|
||||
fileLoggerCfg.TimeKey = "timestamp"
|
||||
fileLoggerCfg.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
|
||||
stderrLoggerCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
|
||||
stderrEncoder = zapcore.NewConsoleEncoder(stderrLoggerCfg)
|
||||
fileEncoder = zapcore.NewJSONEncoder(fileLoggerCfg)
|
||||
}
|
||||
|
||||
fileHandlers := make([]*os.File, 0, len(fileLoggers))
|
||||
zapWriters := make([]zapcore.Core, 0, len(fileLoggers)+1)
|
||||
for filename, level := range fileLoggers {
|
||||
fileHandler, err := os.Create(filename)
|
||||
if err != nil {
|
||||
for _, file := range fileHandlers[:len(fileHandlers)-1] {
|
||||
_ = file.Close()
|
||||
}
|
||||
return nil, nil, fmt.Errorf("error on log file creation: %w", err)
|
||||
}
|
||||
fileHandlers = append(fileHandlers, fileHandler)
|
||||
fileSync := zapcore.AddSync(fileHandler)
|
||||
zapWriters = append(zapWriters, zapcore.NewCore(fileEncoder, fileSync, zap.NewAtomicLevelAt(level)))
|
||||
}
|
||||
|
||||
stderrSync := zapcore.AddSync(os.Stderr)
|
||||
if stderrLevel != zapcore.InvalidLevel {
|
||||
zapWriters = append(zapWriters, zapcore.NewCore(stderrEncoder, stderrSync, zap.NewAtomicLevelAt(stderrLevel)))
|
||||
}
|
||||
|
||||
var otlpCloseFunc func()
|
||||
if cfg.Exporter.Level != "" {
|
||||
var otlpWriter zapcore.Core
|
||||
var err error
|
||||
otlpWriter, otlpCloseFunc, err = configureOtelLogging(ctx, cfg.Exporter)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot configure OpenTelemetry log exporter: %w", err)
|
||||
}
|
||||
zapWriters = append(zapWriters, otlpWriter)
|
||||
}
|
||||
|
||||
core := zapcore.NewTee(
|
||||
zapWriters...,
|
||||
)
|
||||
|
||||
logger = zap.New(core)
|
||||
|
||||
closeFunc = func() {
|
||||
_ = logger.Sync()
|
||||
for _, file := range fileHandlers {
|
||||
_ = file.Close()
|
||||
}
|
||||
if otlpCloseFunc != nil {
|
||||
otlpCloseFunc()
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("configured logging", zap.Any("config", cfg))
|
||||
return
|
||||
}
|
||||
|
||||
func configureOtelLogging(ctx context.Context, cfg config.LogsExporterConfig) (zapcore.Core, func(), error) {
|
||||
var exporter log.Exporter
|
||||
var err error
|
||||
if cfg.Transport == config.HTTPTransportType {
|
||||
exporter, err = otlploghttp.New(ctx, otlploghttp.WithEndpointURL(cfg.Endpoint))
|
||||
} else {
|
||||
exporter, err = otlploggrpc.New(ctx, otlploggrpc.WithEndpointURL(cfg.Endpoint))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot create OpenTelemetry logs exporter: %w", err)
|
||||
}
|
||||
res, err := observability.GetOtelResource()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot get OpenTelemetry resource: %w", err)
|
||||
}
|
||||
|
||||
provider := log.NewLoggerProvider(
|
||||
log.WithProcessor(log.NewBatchProcessor(exporter)),
|
||||
log.WithResource(res),
|
||||
)
|
||||
|
||||
closeFunc := func() {
|
||||
shutdownContext, cancel := context.WithTimeout(context.Background(), time.Millisecond*300)
|
||||
defer cancel()
|
||||
_ = provider.ForceFlush(shutdownContext)
|
||||
_ = provider.Shutdown(shutdownContext)
|
||||
<-shutdownContext.Done()
|
||||
}
|
||||
global.SetLoggerProvider(provider)
|
||||
|
||||
return otelzap.NewCore(internal.PackageName, otelzap.WithLoggerProvider(provider)), closeFunc, nil
|
||||
}
|
||||
41
internal/observability/logging/ctx.go
Normal file
41
internal/observability/logging/ctx.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type loggerKeyType int
|
||||
|
||||
const (
|
||||
loggerKey loggerKeyType = iota
|
||||
)
|
||||
|
||||
var ErrNotInitialized = errors.New("logger has not been initialized in the given context")
|
||||
|
||||
func CtxWithLogger(ctx context.Context, logger *zap.Logger) context.Context {
|
||||
if loggerCtx, ok := ctx.Value(loggerKey).(*zap.Logger); ok {
|
||||
if loggerCtx == logger {
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
return context.WithValue(ctx, loggerKey, logger)
|
||||
}
|
||||
|
||||
func LoggerFromCtx(ctx context.Context) (*zap.Logger, error) {
|
||||
logger := ctx.Value(loggerKey)
|
||||
if logger == nil {
|
||||
return nil, ErrNotInitialized
|
||||
}
|
||||
return logger.(*zap.Logger), nil
|
||||
}
|
||||
|
||||
func LoggerMustFromContext(ctx context.Context) *zap.Logger {
|
||||
logger, err := LoggerFromCtx(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
83
internal/observability/metrics/configure.go
Normal file
83
internal/observability/metrics/configure.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/prometheus"
|
||||
otelmetric "go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
|
||||
"github.com/kanootoko/codevault/internal"
|
||||
"github.com/kanootoko/codevault/internal/config"
|
||||
"github.com/kanootoko/codevault/internal/observability"
|
||||
"github.com/kanootoko/codevault/internal/utils/gshutdown"
|
||||
)
|
||||
|
||||
// Configure starts a Prometheus server which serves metrics and finishes on context finish
|
||||
func Configure(ctx context.Context, cfg config.MetricsConfig) error {
|
||||
if cfg.Host == "" {
|
||||
return nil
|
||||
}
|
||||
res, err := observability.GetOtelResource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get OpenTelemetry resource: %w", err)
|
||||
}
|
||||
|
||||
meterProvider, err := newMeterProvider(res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot instanciate OpenTelemetry meter provider: %w", err)
|
||||
}
|
||||
|
||||
otel.SetMeterProvider(meterProvider)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
server := http.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||
Handler: mux,
|
||||
ReadTimeout: time.Second * 20,
|
||||
ReadHeaderTimeout: time.Second * 10,
|
||||
WriteTimeout: time.Second * 20,
|
||||
}
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on creating listener for prometheus metrics: %w", err)
|
||||
}
|
||||
go func() { _ = server.Serve(listener) }()
|
||||
_ = gshutdown.SubtaskStarted(ctx, "prometheus")
|
||||
go func() {
|
||||
defer func() {
|
||||
_ = listener.Close()
|
||||
_ = gshutdown.SubtaskFinished(ctx, "prometheus")
|
||||
}()
|
||||
<-ctx.Done()
|
||||
_ = server.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetMeter() otelmetric.Meter {
|
||||
mp := otel.GetMeterProvider()
|
||||
return mp.Meter(internal.PackageName, otelmetric.WithInstrumentationVersion(internal.Version))
|
||||
}
|
||||
|
||||
func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {
|
||||
metricExporter, err := prometheus.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
meterProvider := metric.NewMeterProvider(
|
||||
metric.WithResource(res),
|
||||
metric.WithReader(metricExporter),
|
||||
)
|
||||
return meterProvider, nil
|
||||
}
|
||||
110
internal/observability/metrics/metrics.go
Normal file
110
internal/observability/metrics/metrics.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
|
||||
"github.com/kanootoko/codevault/internal"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
HTTP HTTPMetrics
|
||||
Value ValueMetrics
|
||||
}
|
||||
|
||||
func NewMetrics(meter metric.Meter) Metrics {
|
||||
metrics := Metrics{
|
||||
HTTP: newHTTPMetrics(meter),
|
||||
Value: newValueMetrics(meter),
|
||||
}
|
||||
addObservableMetrics(meter)
|
||||
return metrics
|
||||
}
|
||||
|
||||
type HTTPMetrics struct {
|
||||
// Total HTTP requests with [route, status]
|
||||
RequestsTotal metric.Int64Counter
|
||||
|
||||
// Request duration histogram in seconds with [route]
|
||||
RequestDuration metric.Float64Histogram
|
||||
}
|
||||
|
||||
func newHTTPMetrics(meter metric.Meter) HTTPMetrics {
|
||||
requestsTotal, _ := meter.Int64Counter(
|
||||
"http_requests_total",
|
||||
metric.WithDescription("Total number of HTTP requests"),
|
||||
)
|
||||
requestDuration, _ := meter.Float64Histogram(
|
||||
"http_request_duration_seconds",
|
||||
metric.WithDescription("HTTP request duration histogram in seconds"),
|
||||
metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0),
|
||||
metric.WithUnit("s"),
|
||||
)
|
||||
return HTTPMetrics{
|
||||
RequestsTotal: requestsTotal,
|
||||
RequestDuration: requestDuration,
|
||||
}
|
||||
}
|
||||
|
||||
type ValueMetrics struct {
|
||||
// Successful getValue lookups, with [code]. Only successful lookups carry a
|
||||
// code label since the set of stored codes is bounded, unlike guessed/failed ones.
|
||||
GetSuccessTotal metric.Int64Counter
|
||||
|
||||
// Failed getValue lookups, with [reason] (not_found|rate_limited).
|
||||
GetFailedTotal metric.Int64Counter
|
||||
|
||||
// Successful setValue writes, with [user].
|
||||
SetSuccessTotal metric.Int64Counter
|
||||
|
||||
// Failed setValue writes, with [reason] (bad_request|unauthorized).
|
||||
SetFailedTotal metric.Int64Counter
|
||||
}
|
||||
|
||||
func newValueMetrics(meter metric.Meter) ValueMetrics {
|
||||
getSuccessTotal, _ := meter.Int64Counter(
|
||||
"codevault_get_value_success_total",
|
||||
metric.WithDescription("Total number of successful getValue lookups"),
|
||||
)
|
||||
getFailedTotal, _ := meter.Int64Counter(
|
||||
"codevault_get_value_failed_total",
|
||||
metric.WithDescription("Total number of failed getValue lookups"),
|
||||
)
|
||||
setSuccessTotal, _ := meter.Int64Counter(
|
||||
"codevault_set_value_success_total",
|
||||
metric.WithDescription("Total number of successful setValue writes"),
|
||||
)
|
||||
setFailedTotal, _ := meter.Int64Counter(
|
||||
"codevault_set_value_failed_total",
|
||||
metric.WithDescription("Total number of failed setValue writes"),
|
||||
)
|
||||
return ValueMetrics{
|
||||
GetSuccessTotal: getSuccessTotal,
|
||||
GetFailedTotal: getFailedTotal,
|
||||
SetSuccessTotal: setSuccessTotal,
|
||||
SetFailedTotal: setFailedTotal,
|
||||
}
|
||||
}
|
||||
|
||||
func addObservableMetrics(meter metric.Meter) {
|
||||
timeStarted := time.Now()
|
||||
_, _ = meter.Int64ObservableGauge(
|
||||
"startup_time",
|
||||
metric.WithDescription("Startup timestamp"),
|
||||
metric.WithInt64Callback(func(ctx context.Context, io metric.Int64Observer) error {
|
||||
io.Observe(timeStarted.Unix(), metric.WithAttributes(attribute.String("version", internal.Version)))
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
_, _ = meter.Int64ObservableGauge(
|
||||
"last_update_time",
|
||||
metric.WithDescription("Current timestamp"),
|
||||
metric.WithInt64Callback(func(ctx context.Context, io metric.Int64Observer) error {
|
||||
io.Observe(time.Now().Unix(), metric.WithAttributes(attribute.String("version", internal.Version)))
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
18
internal/observability/otel.go
Normal file
18
internal/observability/otel.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package observability
|
||||
|
||||
import (
|
||||
"github.com/kanootoko/codevault/internal"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
|
||||
)
|
||||
|
||||
func GetOtelResource() (*resource.Resource, error) {
|
||||
return resource.Merge(
|
||||
resource.Default(),
|
||||
resource.NewWithAttributes(
|
||||
semconv.SchemaURL,
|
||||
semconv.ServiceName(internal.PackageName),
|
||||
semconv.ServiceVersion(internal.Version),
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user