package observability import ( "bytes" "encoding/json" "fmt" "net/http" "os" "path/filepath" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // NewLogger creates a zap production logger at the given level that writes to stderr. func NewLogger(level string, pretty bool) (*zap.Logger, error) { lvl, err := zapcore.ParseLevel(level) if err != nil { lvl = zapcore.InfoLevel } cfg := zap.NewProductionConfig() cfg.Level = zap.NewAtomicLevelAt(lvl) if !pretty { return cfg.Build() } encoder := zapcore.NewJSONEncoder(cfg.EncoderConfig) sink := &prettyJSONWriteSyncer{out: zapcore.Lock(zapcore.AddSync(os.Stderr))} core := zapcore.NewCore(encoder, sink, cfg.Level) return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)), nil } // NewLoggerWithFile creates a zap production logger at the given level. If path // is empty it behaves exactly like NewLogger. When path is set, the log file's // parent directory is created and log entries are written to that file // instead of stderr. func NewLoggerWithFile(level string, pretty bool, path string) (*zap.Logger, error) { if path == "" { return NewLogger(level, pretty) } lvl, err := zapcore.ParseLevel(level) if err != nil { lvl = zapcore.InfoLevel } if dir := filepath.Dir(path); dir != "" && dir != "." { if err := os.MkdirAll(dir, 0o755); err != nil { return nil, fmt.Errorf("mkdir log dir %s: %w", dir, err) } } file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { return nil, fmt.Errorf("open log file %s: %w", path, err) } cfg := zap.NewProductionConfig() cfg.Level = zap.NewAtomicLevelAt(lvl) var sink zapcore.WriteSyncer = zapcore.Lock(zapcore.AddSync(file)) if pretty { sink = &prettyJSONWriteSyncer{out: sink} } encoder := zapcore.NewJSONEncoder(cfg.EncoderConfig) core := zapcore.NewCore(encoder, sink, cfg.Level) return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)), nil } type prettyJSONWriteSyncer struct { out zapcore.WriteSyncer } func (w *prettyJSONWriteSyncer) Write(p []byte) (int, error) { trimmed := bytes.TrimSpace(p) if len(trimmed) == 0 { return len(p), nil } var pretty bytes.Buffer if err := json.Indent(&pretty, trimmed, "", " "); err != nil { _, writeErr := w.out.Write(p) return len(p), writeErr } pretty.WriteByte('\n') _, err := w.out.Write(pretty.Bytes()) return len(p), err } func (w *prettyJSONWriteSyncer) Sync() error { return w.out.Sync() } // ServeMetrics starts a Prometheus /metrics endpoint on the given port. // It blocks until the server exits. func ServeMetrics(port int) error { mux := http.NewServeMux() mux.Handle("/metrics", promhttp.Handler()) mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) addr := fmt.Sprintf(":%d", port) return http.ListenAndServe(addr, mux) }