5

I would like to log errors from net/http in my own format. In net/http package I have found Server struct:

type Server struct {
        //...
        ErrorLog *log.Logger
}

I would like to substitute logger with my own implementation:

type AppLogger struct {
    log *zap.SugaredLogger
}

func (l *AppLogger) Error(message string, keyAndValues ...interface{}) {
    l.log.Errorw(message, keyAndValues...)
}

What is the correct way of implementing this?


Update:

I have zap logger with following config:

cfg := zap.Config{
    Encoding:         encoding,
    Level:            zap.NewAtomicLevelAt(zap.DebugLevel),
    OutputPaths:      []string{"stdout"},
    ErrorOutputPaths: []string{"stdout"},
    EncoderConfig:    encCfg,
}
logger, err := cfg.Build()

It configured to write in json format. I would like errors from net/http be written in the same way as zap. I create following:

type serverJsonWriter struct {
    io.Writer
}

// ListenAndServeTLS - with custom log Writer
func ListenAndServeTLS(addr, certFile, keyFile string, handler http.Handler) error {
    server := &http.Server{
        Addr: addr,
        Handler: handler,
        ErrorLog: logger.New(serverJsonWriter{}, "", 0),
    }
}

func (w serverJsonWriter) Write(p []byte) (n int, err error){
    // {"error":{"type":"net/http error","message":"header too long"}}
}

Questions:

  1. What should be the body of serverJsonWriter method?
  2. Should I retrieve zap io.Writer in order to pass it log.Logger? How to do this?
Rudziankoŭ
  • 10,681
  • 20
  • 92
  • 192
  • 3
    Unfortunately, this is not possible, since `*log.Logger` is a concrete type. The closest you can do is use a logger that logs to your own `io.Writer`. – Jonathan Hall Sep 12 '18 at 11:57
  • 4
    There is [a proposal](https://github.com/golang/go/issues/13182) to make log.Logger an interface, which would make this much easier. – Jonathan Hall Sep 12 '18 at 12:08
  • How do you want to transform the single error message logged by `net/http` to `message` and `keyAndValues` which are the parameters of `SugaredLogger.Errorw()`? – icza Sep 12 '18 at 14:36
  • @icza, I am going to skip `keyAndValues`. I would like to pass only `message` from `net/http` – Rudziankoŭ Sep 12 '18 at 14:46

3 Answers3

6

This is easily doable, because the log.Logger type guarantees that each log message is delivered to the destination io.Writer with a single Writer.Write() call:

Each logging operation makes a single call to the Writer's Write method. A Logger can be used simultaneously from multiple goroutines; it guarantees to serialize access to the Writer.

So basically you just need to create a type which implements io.Writer, and whose Write() method simply calls your logger.

Here's a simple implementation which does that:

type fwdToZapWriter struct {
    logger *zap.SugaredLogger
}

func (fw *fwdToZapWriter) Write(p []byte) (n int, err error) {
    fw.logger.Errorw(string(p))
    return len(p), nil
}

And that's all. You can "install" this writer at your http.Server like this:

server := &http.Server{
    Addr:     addr,
    Handler:  handler,
    ErrorLog: logger.New(&fwdToZapWriter{logger}, "", 0),
}

logger in the above example is from your example: logger, err := cfg.Build()

If you want, you can just as easily forward to your AppLogger instead of logger.

See similar question: Go: Create io.Writer inteface for logging to mongodb database

icza
  • 389,944
  • 63
  • 907
  • 827
  • what is the reason for returning `len(p), nil` if we're just trying to log? – TheRealFakeNews Dec 03 '21 at 20:58
  • @TheRealFakeNews Because the `Write()` method is to implement [`io.Writer`](https://pkg.go.dev/io#Writer), and its _contract_ requires us to return the number of writter bytes and the error of writing (if there was any). – icza Dec 03 '21 at 21:04
  • I tried following your example, but when I include `ErrorLog: logger.New(&netHTTPLoggerToZap{logger}, "", 0)` (just renamed it), I get an error for `New` that says `logger.New(&netHTTPLoggerToZap{logger}, "", 0),` and an error for the second `logger`: `cannot use logger (variable of type *zap.Logger) as *zap.SugaredLogger value in struct literalcompilerIncompatibleAssign`. Although I have chosen to use `logger, err := zap.NewProduction()`...Not sure if that makes a difference. – TheRealFakeNews Dec 03 '21 at 21:30
  • The error should be self-explanatory: you try to assign a value to `fwdToZapWriter.logger` field that has a different type. – icza Dec 03 '21 at 21:38
  • Okay, I see. So I pass in sugar like this then: `logger.New(&fwdToZapWriter{sugar}, "", 0)`. However, I still get the error under `New`: `logger.New undefined (type *zap.Logger has no field or method New)compilerMissingFieldOrMethod`. Thank you so much for helping! – TheRealFakeNews Dec 03 '21 at 21:47
  • I deleted `pkg/util/logger.go` just in case and renamed my variables, but the error persists. I have posted my question here: https://stackoverflow.com/questions/70221214/unable-to-implement-custom-logger-for-http-server-no-new-method-on-logger – TheRealFakeNews Dec 03 '21 at 22:14
  • It seems the zap logger API has changed since this answer, this may not be working now. – icza Dec 03 '21 at 22:36
3

You can use zap.NewStdLog() to get a new instance of a *log.Logger.

https://godoc.org/go.uber.org/zap#NewStdLog

logger := zap.NewExample()
defer logger.Sync()

std := zap.NewStdLog(logger)
std.Print("standard logger wrapper")

// Output:
// {"level":"info","msg":"standard logger wrapper"}
Xeoncross
  • 55,620
  • 80
  • 262
  • 364
0
 type serverErrorLogWriter struct{}
    
    func (*serverErrorLogWriter) Write(p []byte) (int, error) {
        m := string(p)
        if strings.HasPrefix(m, "http: TLS handshake error") && strings.HasSuffix(m, ": EOF\n") {
            // handle EOF error
        } else {
            // handle other errors
        }
        return len(p), nil
    }
    
    func newServerErrorLog() *log.Logger {
        return log.New(&serverErrorLogWriter{}, "", 0)
    }

And then

   server := &http.Server{
    Addr:     addr,
    Handler:  handler,
    ErrorLog: log.New(&serverErrorLogWriter{}, "", 0),

}

Nizar
  • 500
  • 3
  • 12