Proper logging implementation in Golang package

Viewed 526

I have small Golang package which does some work. This work suppose a high amount of errors could be produced and this is OK. Currently all errors are ignored. Yes it may look strange, but visit the link and check the main purpose of package. I'd like to extend functionality of the package and provide ability to see errors occurred during runtime. But due to lack of software design skills I have some questions with no answers.

At first, I thought to implement logging inside the package using the existing logging (zerolog, zap or whatever else). But, will it be ok for package's users? Because they might want to use other logging packages and would like to modify output format. Maybe it's possible to provide a way to user to inject it's own logging?

I'd like to achieve the ability to provide easy-configurable way for logging which could be switched on or off on users demands.

2 Answers

Some go lib use logging like this

in your packge definite a logger interface

type Yourlogging interface{
      Errorf(...)
      Warningf(...)
      Infof(...)
      Debugf(...)
}

and definite a variable for this interface

  var mylogger Yourlogging
  func SetLogger(l yourlogging)error{
       mylogger = l
  }

in your func, you can call them for logging

  mylogger.Infof(..)

  mylogger.Errorf(...)

you don't need implement the interface, but you can use them who implement this interface

 for example:
     SetLogger(os.Stdout)    //logging output to stdout
     SetLogger(logrus.New()) // logging output to logrus  (github.com/sirupsen/logrus)
  

In Go, you will see some libraries implement logging interfaces like other answers have suggested. However, you could completely avoid your packages needing to log if you structured your application differently, for your example.

For example, in your example application you linked, your main application runtime calls idleexacts.Run(), which starts this function.

// startLoop starts workload using passed settings and database connection.
func startLoop(ctx context.Context, log log.Logger, pool db.DB, tables []string, jobs uint16, minTime, maxTime time.Duration) error {
    rand.Seed(time.Now().UnixNano())

    // Increment maxTime up to 1 due to rand.Int63n() never return max value.
    maxTime++

    // While running, keep required number of workers using channel.
    // Run new workers only until there is any free slot.
    guard := make(chan struct{}, jobs)
    for {
        select {
        // Run workers only when it's possible to write into channel (channel is limited by number of jobs).
        case guard <- struct{}{}:
            go func() {
                table := selectRandomTable(tables)
                naptime := time.Duration(rand.Int63n(maxTime.Nanoseconds()-minTime.Nanoseconds()) + minTime.Nanoseconds())

                err := startSingleIdleXact(ctx, pool, table, naptime)
                if err != nil {
                    log.Warnf("start idle xact failed: %s", err)
                }

                // When worker finishes, read from the channel to allow starting another worker.
                <-guard
            }()
        case <-ctx.Done():

            return nil
        }
    }
}

The problem here is all of the orchestration of your logic is happening inside of your packages. Instead, this loop should be running in your main application, and this package should provide users with simple actions such as selectRandomTable() or createTempTable().

If the orchestration of code was in your main application and the package only provided simple actions. It would be much easier to return errors to the user as part of the function calls.

It would also make your packages easier for others to reuse because they have simple actions and open users to use them in other ways than you intended.

Related