Doing code reviews, I have sometimes noticed libraries forcing dependencies on the users.

While it is perfectly acceptable if well justified and documented, it sometimes feels like adding unnecessary baggage.

A typical example would be something like the following snippet:

func (c *Client) HealthCheck(ctx context.Context) error {
    logrus.Debug("sending get request")
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url, nil)
    if err != nil {
        return err
    }

    resp, err := c.client.Do(req)
    if err != nil {
        return err
    }

    logrus.Debugf("http status code was %d", resp.StatusCode)

    defer resp.Body.Close()

    return nil
}

Using logrus, as example, picking this code, one would need to check how logrus works, which configurations are available, which version should be used and so on.

Russ Cox wrote an excellent blog post on what considerations one should make when picking software.

There are multiples ways to improve observability; for an in-depth post on the subject check Domain-Oriented Observability


Using the http.Client example, in Go it is possible to improve observability without poluting the code or adding dependencies by leveraging the http.RoundTripper and dependency injection.

One way to implement it could be:

type Logger func(args ...interface{})

type DebugTransport struct {
    rt     http.RoundTripper
    logger Logger
}

func (d *DebugTransport) RoundTrip(request *http.Request) (*http.Response, error) {
    requestBody, err := httputil.DumpRequestOut(request, true)
    if err != nil {
        return nil, err
    }

    d.logger(string(requestBody))

    resp, err := d.rt.RoundTrip(request)
    if err != nil {
        return nil, err
    }

    respBody, err := httputil.DumpResponse(resp, true)
    if err != nil {
        return nil, err
    }

    d.logger(string(respBody))

    return resp, nil
}

This code allows logging requests and responses without deciding which logger to use and remove logging calls from the library.

Example output:

GET / HTTP/1.1
Host: 127.0.0.1:43411
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

HTTP/1.1 500 Internal Server Error
Date: Tue, 08 Jun 2021 20:33:38 GMT
Content-Length: 0

Similar logic could be implemented to add tracing, for example, the way opentelemetry implements otelhttp.Transport

When developing libraries (or anything to be fair), one should think: Is this dependency needed? Can I implement a feature while not committing to any concrete implementation?

Your users will appreciate it.

Code examples available in github