rstudio / platform-lib

Shared Go Libraries

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

stdout output not showing up when using ComposeLogger

msarahan opened this issue · comments

On the mercury project, we want to be able to "tee" the log output between stdout and a file. At first glance, the CompositeLogger looks perfect. Unfortunately, the stdout output seems to get swallowed if an rslog.DefaultLogger() gets nested into a CompositeLogger alongside a file logger.

package main

import (
	"fmt"
	"github.com/rstudio/platform-lib/pkg/rslog"
	"gopkg.in/natefinch/lumberjack.v2"
	"os"
	"path"
	"strings"
)

func main() {
	logger := rslog.DefaultLogger()
	defer rslog.Flush()

	// this default location matches the standard location for the RStudio Platform, but for development we may want it
	// in a less global location
	logLocation := "/var/log/rstudio"
	// If we detect that we are in our code folder, then we assume that we are running in development mode, and
	//     we store the logs locally.
	cwd, err := os.Getwd()
	if err != nil {
		logger.Fatalf("unable to get current working directory: %s", err.Error())
	}
	// TODO: replace this config with cohesive config scheme. See https://github.com/rstudio/mercury/pull/45
	if os.Getenv("MERCURY_LOG_DIR") != "" {
		logLocation = os.Getenv("MERCURY_LOG_DIR")
		if !path.IsAbs(logLocation) {
			logLocation = path.Join(cwd, logLocation)
		}
	}

	err = os.MkdirAll(logLocation, 0o755)
	if err != nil {
		panic("unable to create log directory; error is: " + err.Error())
	}

	if !strings.Contains(logLocation, "NO_FILE_LOG") {
		fmt.Println("logging to " + logLocation)
		fileLogger := rslog.DefaultLogger()
		fileLogger.SetFormatter(rslog.JSONFormat)
		fileLogger.SetOutput(&lumberjack.Logger{
			Filename: path.Join(logLocation, "mercury.log"),
			MaxSize:  100,  // megabytes
			MaxAge:   30,   // days
			Compress: true, // disabled by default
		})
		logger = rslog.ComposeLoggers(logger, fileLogger)
	}

	logger.SetLevel(rslog.DebugLevel)
	logger.Infof("service starting")
}

Here, if you bypass the file logging by setting MERCURY_LOG_DIR env var to NO_FILE_LOG, you see the expected output. If the file logger gets added, though, it does not appear.

If you build the logger in a more basic way (multiple outputs), it seems to work fine:

package main

import (
	"fmt"
	"github.com/rstudio/platform-lib/pkg/rslog"
	"os"
	"path"
	"strings"
)

func main() {

	// this default location matches the standard location for the RStudio Platform, but for development we may want it
	// in a less global location
	logLocation := "/var/log/rstudio"
	// If we detect that we are in our code folder, then we assume that we are running in development mode, and
	//     we store the logs locally.
	cwd, err := os.Getwd()
	if err != nil {
		panic(fmt.Sprintf("unable to get current working directory: %s", err.Error()))
	}
	// TODO: replace this config with cohesive config scheme. See https://github.com/rstudio/mercury/pull/45
	if os.Getenv("MERCURY_LOG_DIR") != "" {
		logLocation = os.Getenv("MERCURY_LOG_DIR")
		if !path.IsAbs(logLocation) {
			logLocation = path.Join(cwd, logLocation)
		}
	}

	err = os.MkdirAll(logLocation, 0o755)
	if err != nil {
		panic("unable to create log directory; error is: " + err.Error())
	}

	outputs := []rslog.OutputDest{{Output: rslog.LogOutputStdout}}
	if !strings.Contains(logLocation, "NO_FILE_LOG") {
		outputs = append(outputs, rslog.OutputDest{Output: rslog.LogOutputFile, Filepath: path.Join(logLocation, "mercury.log")})
	}

	logger, err := rslog.NewLoggerImpl(rslog.LoggerOptionsImpl{
		Output: outputs,
		Format: rslog.JSONFormat,
		Level:  rslog.DebugLevel,
	}, rslog.NewOutputLogBuilder(rslog.ServerLog, ""))

	defer rslog.Flush()

	logger.Infof("service starting")
}

Is there an issue with the CompositeLogger, or am I just using it wrong?

@msarahan I haven't spent a lot of time looking at the examples you gave, but did you consider using a factory to create the default logger? See the example app here: https://github.com/rstudio/platform-lib/blob/main/examples/cmd/testlog/cmd/root.go#L46.

If I understand things correctly, the factory would be an alternative to the rslog.NewLoggerImpl call that I have in the second example. I can see that being much cleaner if we were creating other loggers in the application. I'm not clear on how it might be better in the case of just one logger.

@gbrlsnchs @marcosnav @zackverham, pinging you here on Jon's recommendation. What would you consider to be the idiomatic way to have a "tee" output to stdout and to a file?

@msarahan First things first, the default logger is a singleton, so when you use rslog.DefaultLogger() you are getting the same instance. By taking a look to your first example, I noticed that you are overriding the default logger while configuring the fileLogger and that's why it feels that the default stdout logger is swallowed, at that point logger and fileLogger are the same instance writing to a file.

For your use case, rslog.ComposeLoggers() is the way to go, you'll have to create an independent file logger with NewLoggerImpl like you noticed. E.g:

stdoutLgr := rslog.DefaultLogger()
stdoutLgr.SetLevel(rslog.DebugLevel)

fileLgr, err := rslog.NewLoggerImpl(rslog.LoggerOptionsImpl{
  //...
}, rslog.NewOutputLogBuilder(rslog.ServerLog, ""))

compLogger := rslog.ComposeLoggers(stdoutLgr, fileLgr)

The above example creates a composite logger but further usage of rslog.DefaultLogger() will still return only a stdout logger. If you plan on passing down the logger this will work just fine, but in case you want to use rslog.DefaultLogger() to pull a composite logger in other packages or files, you can use a factory like Jon suggested.

type compositeFactory struct{}

func (f *compositeFactory) DefaultLogger() rslog.Logger {
  stdoutLgr, err := rslog.NewLoggerImpl(rslog.LoggerOptionsImpl{
    //...
  }, rslog.NewOutputLogBuilder(rslog.ServerLog, ""))

  fileLgr, err := rslog.NewLoggerImpl(rslog.LoggerOptionsImpl{
    //...
  }, rslog.NewOutputLogBuilder(rslog.ServerLog, ""))

  compLogger := rslog.ComposeLoggers(stdoutLgr, fileLgr)
  return compLogger
}

rslog.DefaultLoggerFactory = &compositeFactory{}

logger := rslog.DefaultLogger() // this will now get the same composite logger everywhere
logger.Infof("xyz")

NOTE: It is important to set rslog.DefaultLoggerFactory = &compositeFactory{} before any call to rslog.DefaultLogger().

Hope this helps :)

That's very helpful. Thanks for the explanation. I will adapt your explanation here into the readme so you won't need to answer this question again.