Codevault initial commit

This commit is contained in:
2026-08-16 16:23:14 +03:00
commit 6ee80f1c8f
47 changed files with 3177 additions and 0 deletions

View 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
}

View 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
}

View 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
}

View 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
}),
)
}

View 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),
),
)
}