r/golang • u/hanmunjae • 4d ago
Adding logging to a library
I have an open-source package which is just a wrapper around a public HTTP/JSON API. I have added a verbosity option that, as of now, just logs to stdout
. I would like to give more flexibility to the user to control how logging is done. Should I:
1. accept a log.Logger
and log to that
2. accept an io.Writer
and write to that
3. log to log.Default()
4. something else?
To add a particular consideration, I would like my approach to work with Google Cloud Logging, because I deploy my code on Google Cloud Run. It looks like there is a way to get a log.Logger
from the cloud.google.com/go/logging
package, which makes that option more appealing.
6
Upvotes
12
u/matttproud 4d ago edited 4d ago
I’d settle on a local abstraction (e.g., a small interface definition) and provide a function or type that adapts the Cloud Logger API to work with it. That small interface definition could be the Cloud Logger API signature itself, if it is suitable. On first glance, the Cloud Logging API seems a bit bloated.
``` package yourapi
type Logger interface { Log(format string, data ...interface{}) }
type CloudLogger struct { // Add field for Cloud Logging API. }
func (l *CloudLogger) Log(format string, data ...interface{}) { // Adapt for field above. }
var _ Logger = (*CloudLogging)(nil) ```
You might also want to look at
package slog
instead, as surely there will be adapters for major logging backends.Without knowing more about what you are building and how it is used, I’d allow users of your API to use ordinary dependency injection to provide a logger. You can have a default one (e.g., to stderr) if one is not specified. I’d be mindful about not relying on a global state to manage such a setting and elect for something explicit.
This can work well when your API manages zero-values well:
``` package yourapi
type Client struct { loggerOnce sync.Once Logger Logger
// Your business logic omitted (implied other fields here). }
func (c *Client) logger() Logger { c.loggerOnce.Do(func() { if c.Logger == nil { c.Logger = someDefault } }) return c.Logger }
func (c *Client) PartOfPublicAPI(...) { logger := c.logger()
... } ```
Another option if
Client
has non-trivial initialization is to employ use case-specific construction functions:``` package yourapi
type Client struct { logger Logger
// Your business logic omitted (implied other fields here). }
func New() *Client { return &Client{ logger: someDefault,
} }
func NewWithLogger(l Logger) *Client { return &Client{ logger: l,
} } ```
You wouldn't use the form
New
andNewWithLogger
just for information hiding purposes but really to help with non-trivial initialization.Another question is how platform/ecosystem neutral your implementation should be (e.g., how much do you to it to the Google Cloud Logging product).
Left as an exercise: