diff --git a/asgard/heimdallr.go b/asgard/heimdallr.go index f9cf85f..ab4a476 100644 --- a/asgard/heimdallr.go +++ b/asgard/heimdallr.go @@ -13,7 +13,6 @@ package asgard import ( "context" "encoding/pem" - "log/slog" "net/http" "net/url" @@ -51,14 +50,14 @@ func Heimdallr(h HeaderName, ns uuid.UUID) func(http.Handler) http.Handler { certHeader := r.Header.Get(h.String()) if certHeader == "" { - slog.ErrorContext(ctx, "missing authorization header") + bifrost.Logger().ErrorContext(ctx, "missing authorization header") http.Error(w, errBadAuthHeader, http.StatusServiceUnavailable) return } certPEM, err := url.PathUnescape(certHeader) if err != nil { - slog.ErrorContext( + bifrost.Logger().ErrorContext( ctx, "error decoding header", "headerName", h.String(), "headerValue", certHeader, @@ -69,7 +68,7 @@ func Heimdallr(h HeaderName, ns uuid.UUID) func(http.Handler) http.Handler { block, _ := pem.Decode([]byte(certPEM)) if block == nil { - slog.ErrorContext( + bifrost.Logger().ErrorContext( ctx, "no PEM data found in authorization header", "headerName", h.String(), "headerValue", certPEM, @@ -80,13 +79,13 @@ func Heimdallr(h HeaderName, ns uuid.UUID) func(http.Handler) http.Handler { cert, err := bifrost.ParseCertificate(block.Bytes) if err != nil { - slog.ErrorContext(ctx, "error parsing client certificate", "error", err) + bifrost.Logger().ErrorContext(ctx, "error parsing client certificate", "error", err) http.Error(w, errBadAuthHeader, http.StatusServiceUnavailable) return } if cert.Namespace != ns { - slog.ErrorContext( + bifrost.Logger().ErrorContext( ctx, "client certificate namespace mismatch", "expected", ns, "actual", cert.Namespace, diff --git a/asgard/hofund.go b/asgard/hofund.go index 24a43f6..bde52e1 100644 --- a/asgard/hofund.go +++ b/asgard/hofund.go @@ -2,7 +2,6 @@ package asgard import ( "encoding/pem" - "log/slog" "net/http" "net/url" @@ -29,13 +28,14 @@ func Hofund(h HeaderName, ns uuid.UUID) func(http.Handler) http.Handler { cert, err := bifrost.NewCertificate(r.TLS.PeerCertificates[0]) if err != nil { - slog.ErrorContext(ctx, "error validating client certificate", "error", err) + bifrost.Logger(). + ErrorContext(ctx, "error validating client certificate", "error", err) http.Error(w, "invalid client certificate", http.StatusUnauthorized) return } if cert.Namespace != ns { - slog.ErrorContext( + bifrost.Logger().ErrorContext( ctx, "client certificate namespace mismatch", "expected", ns, "actual", cert.Namespace, diff --git a/bifrost.go b/bifrost.go new file mode 100644 index 0000000..e5f4fec --- /dev/null +++ b/bifrost.go @@ -0,0 +1,38 @@ +// Package bifrost contains an API client for the Bifrost CA service. +package bifrost + +import ( + "context" + "log/slog" + "sync/atomic" +) + +var ( + // LogLevel is the log level used by the bifrost logger. + LogLevel = new(slog.LevelVar) + + logger atomic.Pointer[slog.Logger] +) + +// Logger returns the global Bifrost logger. +func Logger() *slog.Logger { + return logger.Load() +} + +// SetLogger sets the [*slog.Logger] used by bifrost. +// The default handler disables logging. +func SetLogger(l *slog.Logger) { + logger.Store(l) +} + +func init() { + SetLogger(slog.New(discardHandler{})) +} + +// discardHandler is an [slog.Handler] which is always disabled and therefore logs nothing. +type discardHandler struct{} + +func (discardHandler) Enabled(context.Context, slog.Level) bool { return false } +func (discardHandler) Handle(context.Context, slog.Record) error { return nil } +func (d discardHandler) WithAttrs([]slog.Attr) slog.Handler { return d } +func (d discardHandler) WithGroup(string) slog.Handler { return d } diff --git a/client.go b/client.go index 64f362f..7c28ab7 100644 --- a/client.go +++ b/client.go @@ -54,7 +54,10 @@ func (cr *certRefresher) GetClientCertificate( } // If the certificate is nil or is going to expire soon, request a new one. - if cert := cr.cert.Load(); cert == nil || cert.NotAfter.Before(time.Now().Add(-time.Minute*10)) { + if cert := cr.cert.Load(); cert == nil || + cert.NotAfter.Before(time.Now().Add(-time.Minute*10)) { + Logger().DebugContext(ctx, "refreshing client certificate") + cert, err := RequestCertificate(ctx, cr.url, cr.privkey) if err != nil { return nil, err @@ -66,6 +69,7 @@ func (cr *certRefresher) GetClientCertificate( break } } + Logger().InfoContext(ctx, "got new client certificate") } tlsCert := X509ToTLSCertificate(cr.cert.Load().Certificate, cr.privkey.PrivateKey) diff --git a/cmd/bf/ca.go b/cmd/bf/ca.go index f0c6f37..5a7189b 100644 --- a/cmd/bf/ca.go +++ b/cmd/bf/ca.go @@ -8,12 +8,12 @@ import ( "encoding/pem" "errors" "fmt" - "log/slog" "net/http" "os" "os/signal" "time" + "github.com/RealImage/bifrost" "github.com/RealImage/bifrost/cafiles" "github.com/RealImage/bifrost/internal/webapp" "github.com/RealImage/bifrost/tinyca" @@ -82,10 +82,10 @@ var caServeCmd = &cli.Command{ Action: func(ctx context.Context, _ *cli.Command) error { cert, key, err := cafiles.GetCertKey(ctx, caCertUri, caPrivKeyUri) if err != nil { - slog.ErrorContext(ctx, "error reading cert/key", "error", err) + bifrost.Logger().ErrorContext(ctx, "error reading cert/key", "error", err) return cli.Exit("Error reading cert/key", 1) } - slog.DebugContext( + bifrost.Logger().DebugContext( ctx, "loaded CA certificate and private key", "subject", cert.Subject, "notBefore", cert.NotBefore, @@ -94,13 +94,13 @@ var caServeCmd = &cli.Command{ gauntlet, err := tinyca.LoadGauntlet(gauntletPlugin) if err != nil { - slog.ErrorContext(ctx, "error loading interceptor plugin", "error", err) + bifrost.Logger().ErrorContext(ctx, "error loading interceptor plugin", "error", err) return cli.Exit("Error loading interceptor plugin", 1) } ca, err := tinyca.New(cert, key, gauntlet) if err != nil { - slog.ErrorContext(ctx, "error creating CA", "error", err) + bifrost.Logger().ErrorContext(ctx, "error creating CA", "error", err) return cli.Exit("Error creating CA", 1) } defer ca.Close() @@ -115,13 +115,14 @@ var caServeCmd = &cli.Command{ } addr := fmt.Sprintf("%s:%d", caHost, caPort) - slog.InfoContext(ctx, "starting server", "address", addr, "namespace", cert.Namespace) + bifrost.Logger(). + InfoContext(ctx, "starting server", "address", addr, "namespace", cert.Namespace) server := http.Server{Addr: addr, Handler: hdlr} go func() { if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - slog.ErrorContext(ctx, "error starting server", "error", err) + bifrost.Logger().ErrorContext(ctx, "error starting server", "error", err) os.Exit(1) } }() @@ -134,11 +135,11 @@ var caServeCmd = &cli.Command{ ctx, cancel = context.WithTimeout(context.Background(), serverShutdownTimeout) defer cancel() - slog.DebugContext(ctx, "shutting down server") + bifrost.Logger().DebugContext(ctx, "shutting down server") if err := server.Shutdown(ctx); err != nil { return err } - slog.InfoContext(ctx, "server shut down") + bifrost.Logger().InfoContext(ctx, "server shut down") return nil }, @@ -174,20 +175,20 @@ var caIssueCmd = &cli.Command{ Action: func(ctx context.Context, _ *cli.Command) error { caCert, caKey, err := cafiles.GetCertKey(ctx, caCertUri, caPrivKeyUri) if err != nil { - slog.ErrorContext(ctx, "error reading cert/key", "error", err) + bifrost.Logger().ErrorContext(ctx, "error reading cert/key", "error", err) return cli.Exit("Error reading cert/key", 1) } ca, err := tinyca.New(caCert, caKey, nil) if err != nil { - slog.ErrorContext(ctx, "error creating CA", "error", err) + bifrost.Logger().ErrorContext(ctx, "error creating CA", "error", err) return cli.Exit("Error creating CA", 1) } defer ca.Close() clientKey, err := cafiles.GetPrivateKey(ctx, clientPrivKeyUri) if err != nil { - slog.ErrorContext(ctx, "error reading client key", "error", err) + bifrost.Logger().ErrorContext(ctx, "error reading client key", "error", err) return cli.Exit("Error reading client key", 1) } @@ -198,30 +199,30 @@ var caIssueCmd = &cli.Command{ }, }, clientKey) if err != nil { - slog.ErrorContext(ctx, "error creating certificate request", "error", err) + bifrost.Logger().ErrorContext(ctx, "error creating certificate request", "error", err) return cli.Exit("Error creating certificate request", 1) } notBefore, notAfter, err := tinyca.ParseValidity(notBeforeTime, notAfterTime) if err != nil { - slog.ErrorContext(ctx, "error parsing validity", "error", err) + bifrost.Logger().ErrorContext(ctx, "error parsing validity", "error", err) return cli.Exit("Error parsing validity", 1) } cert, err := ca.IssueCertificate(csr, notBefore, notAfter) if err != nil { - slog.ErrorContext(ctx, "error issuing certificate", "error", err) + bifrost.Logger().ErrorContext(ctx, "error issuing certificate", "error", err) return cli.Exit("Error issuing certificate", 1) } out, cls, err := getOutputWriter() if err != nil { - slog.ErrorContext(ctx, "error getting output writer", "error", err) + bifrost.Logger().ErrorContext(ctx, "error getting output writer", "error", err) return cli.Exit("Error getting output writer", 1) } defer func() { if err := cls(); err != nil { - slog.ErrorContext(ctx, "error closing output writer", "error", err) + bifrost.Logger().ErrorContext(ctx, "error closing output writer", "error", err) } }() diff --git a/cmd/bf/id.go b/cmd/bf/id.go index ab6629c..3b0e3bf 100644 --- a/cmd/bf/id.go +++ b/cmd/bf/id.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "log/slog" "os" "github.com/RealImage/bifrost" @@ -36,7 +35,7 @@ var idCmd = &cli.Command{ id, err := bifrost.ParseIdentity(data) if err != nil { - slog.ErrorContext(ctx, "error parsing id file", "error", err) + bifrost.Logger().ErrorContext(ctx, "error parsing id file", "error", err) return cli.Exit("Error parsing file", 1) } @@ -46,7 +45,8 @@ var idCmd = &cli.Command{ // Either we got a namespace from the file or the namespace flag is set if id.Namespace != uuid.Nil && namespace != uuid.Nil && id.Namespace != namespace { - slog.ErrorContext(ctx, "namespace mismatch", "file", id.Namespace, "flag", namespace) + bifrost.Logger(). + ErrorContext(ctx, "namespace mismatch", "file", id.Namespace, "flag", namespace) return cli.Exit("Namespace mismatch", 1) } @@ -54,7 +54,7 @@ var idCmd = &cli.Command{ id.Namespace = namespace } - slog.Debug("using", "namespace", id.Namespace) + bifrost.Logger().Debug("using", "namespace", id.Namespace) fmt.Println(id.UUID()) return nil diff --git a/cmd/bf/main.go b/cmd/bf/main.go index d9f6b30..fc17f46 100644 --- a/cmd/bf/main.go +++ b/cmd/bf/main.go @@ -5,15 +5,19 @@ import ( "log/slog" "os" + "github.com/RealImage/bifrost" "github.com/urfave/cli/v3" ) var version = "devel" func main() { - logLevel := new(slog.LevelVar) - hdlr := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}) - slog.SetDefault(slog.New(hdlr)) + logger := slog.New( + slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: bifrost.LogLevel}), + ) + + slog.SetDefault(logger) + bifrost.SetLogger(logger) cli := &cli.Command{ Name: "bifrost", @@ -27,7 +31,7 @@ func main() { Sources: cli.EnvVars("LOG_LEVEL"), Value: slog.LevelInfo.String(), Action: func(_ context.Context, _ *cli.Command, level string) error { - return logLevel.UnmarshalText([]byte(level)) + return bifrost.LogLevel.UnmarshalText([]byte(level)) }, }, }, diff --git a/cmd/bf/new.go b/cmd/bf/new.go index d9d05e0..562baf5 100644 --- a/cmd/bf/new.go +++ b/cmd/bf/new.go @@ -7,7 +7,6 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" - "log/slog" "github.com/RealImage/bifrost" "github.com/RealImage/bifrost/cafiles" @@ -35,7 +34,8 @@ var newCmd = &cli.Command{ } defer func() { if err := cls(); err != nil { - slog.ErrorContext(ctx, "error closing output writer", "error", err) + bifrost.Logger(). + ErrorContext(ctx, "error closing output writer", "error", err) } }() @@ -67,7 +67,8 @@ var newCmd = &cli.Command{ } defer func() { if err := cls(); err != nil { - slog.ErrorContext(ctx, "error closing output writer", "error", err) + bifrost.Logger(). + ErrorContext(ctx, "error closing output writer", "error", err) } }() @@ -110,7 +111,8 @@ var newCmd = &cli.Command{ } defer func() { if err := cls(); err != nil { - slog.ErrorContext(ctx, "error closing output writer", "error", err) + bifrost.Logger(). + ErrorContext(ctx, "error closing output writer", "error", err) } }() @@ -173,7 +175,8 @@ var newCmd = &cli.Command{ } defer func() { if err := cls(); err != nil { - slog.ErrorContext(ctx, "error closing output writer", "error", err) + bifrost.Logger(). + ErrorContext(ctx, "error closing output writer", "error", err) } }() diff --git a/cmd/bf/proxy.go b/cmd/bf/proxy.go index ff537ff..79fae1c 100644 --- a/cmd/bf/proxy.go +++ b/cmd/bf/proxy.go @@ -8,7 +8,6 @@ import ( "crypto/x509/pkix" "errors" "fmt" - "log/slog" "net" "net/http" "net/http/httputil" @@ -84,7 +83,7 @@ var proxyCmd = &cli.Command{ Action: func(ctx context.Context, _ *cli.Command) error { caCert, caKey, err := cafiles.GetCertKey(ctx, caCertUri, caPrivKeyUri) if err != nil { - slog.ErrorContext(ctx, "error reading cert/key", "error", err) + bifrost.Logger().ErrorContext(ctx, "error reading cert/key", "error", err) return cli.Exit("Error reading certificate/private key", 1) } @@ -94,7 +93,7 @@ var proxyCmd = &cli.Command{ burl, err := url.Parse(backendUrl) if err != nil { - slog.ErrorContext(ctx, "error parsing backend url", "error", err) + bifrost.Logger().ErrorContext(ctx, "error parsing backend url", "error", err) return cli.Exit("Error parsing backend URL", 1) } reverseProxy := &httputil.ReverseProxy{ @@ -121,19 +120,19 @@ var proxyCmd = &cli.Command{ serverKey, err := bifrost.NewPrivateKey() if err != nil { - slog.ErrorContext(ctx, "error creating key", "error", err) + bifrost.Logger().ErrorContext(ctx, "error creating key", "error", err) return cli.Exit("Error creating server key", 1) } serverCert, err := issueTLSCert(caCert, caKey, serverKey) if err != nil { - slog.ErrorContext(ctx, "error creating certificate", "error", err) + bifrost.Logger().ErrorContext(ctx, "error creating certificate", "error", err) return cli.Exit("Error creating server certificate", 1) } tlsCert, err := serverCert.ToTLSCertificate(*serverKey) if err != nil { - slog.ErrorContext(ctx, "error converting certificate", "error", err) + bifrost.Logger().ErrorContext(ctx, "error converting certificate", "error", err) return cli.Exit("Certificate error", 1) } @@ -149,7 +148,7 @@ var proxyCmd = &cli.Command{ }, } - slog.InfoContext(ctx, "proxying requests", + bifrost.Logger().InfoContext(ctx, "proxying requests", "from", "https://"+addr, "to", backendUrl, "namespace", caCert.Namespace.String(), @@ -158,7 +157,7 @@ var proxyCmd = &cli.Command{ go func() { if err := server.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { - slog.ErrorContext(ctx, "error starting server", "error", err) + bifrost.Logger().ErrorContext(ctx, "error starting server", "error", err) os.Exit(1) } }() @@ -174,7 +173,7 @@ var proxyCmd = &cli.Command{ if err := server.Shutdown(ctx); err != nil { return err } - slog.InfoContext(ctx, "shut down server") + bifrost.Logger().InfoContext(ctx, "shut down server") return nil }, diff --git a/cmd/bf/request.go b/cmd/bf/request.go index e2e76b1..8c3745b 100644 --- a/cmd/bf/request.go +++ b/cmd/bf/request.go @@ -4,7 +4,6 @@ import ( "context" "encoding/pem" "fmt" - "log/slog" "github.com/RealImage/bifrost" "github.com/RealImage/bifrost/cafiles" @@ -33,20 +32,20 @@ var requestCmd = &cli.Command{ if namespace == uuid.Nil { var err error if namespace, err = bifrost.GetNamespace(ctx, caUrl); err != nil { - slog.ErrorContext(ctx, "error fetching namespace", "error", err) + bifrost.Logger().ErrorContext(ctx, "error fetching namespace", "error", err) return cli.Exit("Namespace not provided and could not be fetched", 1) } } key, err := cafiles.GetPrivateKey(ctx, clientPrivKeyUri) if err != nil { - slog.ErrorContext(ctx, "error reading private key", "error", err) + bifrost.Logger().ErrorContext(ctx, "error reading private key", "error", err) return cli.Exit("Failed to read private key", 1) } cert, err := bifrost.RequestCertificate(ctx, caUrl, key) if err != nil { - slog.ErrorContext(ctx, "error requesting certificate", "error", err) + bifrost.Logger().ErrorContext(ctx, "error requesting certificate", "error", err) return cli.Exit("Failed to request certificate", 1) } @@ -57,17 +56,17 @@ var requestCmd = &cli.Command{ out, cls, err := getOutputWriter() if err != nil { - slog.ErrorContext(ctx, "error opening output file", "error", err) + bifrost.Logger().ErrorContext(ctx, "error opening output file", "error", err) return cli.Exit("Failed to open output file", 1) } defer func() { if err := cls(); err != nil { - slog.ErrorContext(ctx, "error closing output writer", "error", err) + bifrost.Logger().ErrorContext(ctx, "error closing output writer", "error", err) } }() if err := pem.Encode(out, block); err != nil { - slog.ErrorContext(ctx, "error writing certificate", "error", err) + bifrost.Logger().ErrorContext(ctx, "error writing certificate", "error", err) return cli.Exit("Failed to write certificate", 1) } diff --git a/doc.go b/doc.go deleted file mode 100644 index 8753016..0000000 --- a/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package bifrost contains an API client for the Bifrost CA service. -package bifrost diff --git a/internal/webapp/requestlog.go b/internal/webapp/requestlog.go index 840c86a..96b829c 100644 --- a/internal/webapp/requestlog.go +++ b/internal/webapp/requestlog.go @@ -5,6 +5,7 @@ import ( "log/slog" "net/http" + "github.com/RealImage/bifrost" "github.com/felixge/httpsnoop" ) @@ -21,7 +22,7 @@ func RequestLogger(h http.Handler) http.Handler { level = slog.LevelError } - slog.LogAttrs( + bifrost.Logger().LogAttrs( r.Context(), level, fmt.Sprintf("%s %s", r.Method, r.RequestURI), diff --git a/tinyca/ca.go b/tinyca/ca.go index 6841195..90106c4 100644 --- a/tinyca/ca.go +++ b/tinyca/ca.go @@ -17,13 +17,13 @@ import ( "errors" "fmt" "io" - "log/slog" "math" "math/big" "net/http" "time" "github.com/RealImage/bifrost" + "github.com/RealImage/bifrost/internal/webapp" "github.com/VictoriaMetrics/metrics" "github.com/google/uuid" @@ -163,7 +163,7 @@ func (ca *CA) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } if err != nil { - slog.ErrorContext(ctx, "error writing certificate response", "err", err) + bifrost.Logger().ErrorContext(ctx, "error writing certificate response", "err", err) } } @@ -177,7 +177,7 @@ func (ca *CA) AddRoutes(mux *http.ServeMux, metrics bool) { mux.Handle("POST /issue", ca) if metrics { - slog.Info("metrics enabled") + bifrost.Logger().Info("metrics enabled") mux.HandleFunc("GET /metrics", func(w http.ResponseWriter, r *http.Request) { bifrost.StatsForNerds.WritePrometheus(w) }) @@ -244,7 +244,7 @@ func (ca *CA) IssueCertificate(asn1CSR []byte, notBefore, notAfter time.Time) ([ ca.issueSize.Update(float64(len(certBytes))) ca.issuedTotal.Inc() - slog.Debug("certificate issued", "to", csr.ID, "duration", time.Since(issueStart)) + bifrost.Logger().Debug("certificate issued", "to", csr.ID, "duration", time.Since(issueStart)) return certBytes, nil } @@ -289,13 +289,13 @@ func getNamespaceHandler(ns uuid.UUID) http.Handler { } if err != nil { - slog.Error("error writing namespace", "err", err) + bifrost.Logger().Error("error writing namespace", "err", err) } }) } func writeHTTPError(ctx context.Context, w http.ResponseWriter, msg string, statusCode int) { - slog.ErrorContext(ctx, msg, "statusCode", statusCode) + bifrost.Logger().ErrorContext(ctx, msg, "statusCode", statusCode) http.Error(w, msg, statusCode) } diff --git a/tinyca/gauntlet.go b/tinyca/gauntlet.go index 8b8e89b..903c04d 100644 --- a/tinyca/gauntlet.go +++ b/tinyca/gauntlet.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "errors" "fmt" - "log/slog" "plugin" "strings" "sync" @@ -123,7 +122,7 @@ func (gh *gauntletThrower) throw(csr *bifrost.CertificateRequest) (*x509.Certifi defer close(result) defer func() { if r := recover(); r != nil { - slog.ErrorContext(ctx, "gauntlet panic", "recovered", r) + bifrost.Logger().ErrorContext(ctx, "gauntlet panic", "recovered", r) cancel(fmt.Errorf("%w, gauntlet panic('%v')", bifrost.ErrRequestAborted, r)) } }() @@ -131,7 +130,7 @@ func (gh *gauntletThrower) throw(csr *bifrost.CertificateRequest) (*x509.Certifi start := time.Now() template, err := gh.Gauntlet(ctx, csr) gh.duration.UpdateDuration(start) - slog.DebugContext(ctx, "threw gauntlet", "duration", time.Since(start)) + bifrost.Logger().DebugContext(ctx, "threw gauntlet", "duration", time.Since(start)) if err != nil { cancel(fmt.Errorf("%w, %s", bifrost.ErrRequestDenied, err))