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 }