Skip to content
This repository has been archived by the owner on Oct 2, 2022. It is now read-only.

Commit

Permalink
Overhauled logging
Browse files Browse the repository at this point in the history
  • Loading branch information
Janos Pasztor committed Feb 17, 2021
1 parent a975146 commit 0d3f9f5
Show file tree
Hide file tree
Showing 24 changed files with 1,233 additions and 589 deletions.
252 changes: 186 additions & 66 deletions README.md
Expand Up @@ -11,105 +11,225 @@ This library provides internal logging for ContainerSSH. Its functionality is ve

<p align="center"><strong>Note: This is a developer documentation.</strong><br />The user documentation for ContainerSSH is located at <a href="https://containerssh.github.io">containerssh.github.io</a>.</p>

## Getting a logger
## Basic concept

The main interface provided by this library is the `Logger` interface, which is described in [logger.go](logger.go).
This is not the logger you would expect from the [go-log](https://github.com/go-log/log) library. This library combines the Go errors and the log messages into one. In other words, the `Message` object can be used both as an error and a log message.

You could use it like this:
The main `Message` structure has several properties: a unique error code, a user-safe error message and a detailed error message for the system administrator. Additionally, it also may contain several key-value pairs called labels to convey additional information.

## Creating a message

If you want to create a message you can use the following methods:

### `log.NewMessage`

The `log.NewMessage` method creates a new `Message` structure as follows:

```go
type MyModule struct {
logger log.Logger
}
msg := log.NewMessage(
"E_SOME_ERROR_CODE",
"Dear user, an internal error happened.",
"Details about the error (%s)",
"Some string that will end up instead of %s."
)
```

func (m * MyModule) DoSomething() {
m.logger.Debug("This is a debug message")
}
- The first parameter is an error code that is unique so people can identify the error. This error code should be documented so users can find it easily.
- The second parameter is a string printed to the user. It does not support formatting characters.
- The third parameter is a string that can be logged for the system administrator. It can contain `fmt.Sprintf`-style formatting characters.
- All subsequent parameters are used to replace the formatting characters.

### `log.Error`

The `log.Error` method is a simplified version of `log.NewMessage` without the user-facing message. The user-facing message will always be `Internal Error.`. The method signature is the following:

```go
msg := log.NewMessage(
"E_SOME_ERROR_CODE",
"Details about the error (%s)",
"Some string that will end up instead of %s."
)
```

The logger provides logging functions for the following levels:
### `log.Wrap`

- `Debug`
- `Info`
- `Notice`
- `Warning`
- `Error`
- `Critical`
- `Alert`
- `Emergency`
The `log.Wrap` method can be used to create a wrapped error. It automatically appends the original error message to the administrator-facing message. The function signature is the following:

Each of these functions have the following 4 variants:
```go
msg := log.Wrap(
originalErr,
"E_SOME_CODE",
"Dear user, some error happened."
"Dear admin, an error happened. %s" +
"The error message will be appended to this message."
"This string will appear instead of %s in the admin-message."
)
```

- `Debug` logs a string message
- `Debuge` logs an error
- `Debugd` logs an arbitrary data structure (`interface{}`)
- `Debugf` performs a string formating with `fmt.Sprintf` before logging
### `log.WrapError`

In addition, the logger also provides a generic `Log(...interface{})` function for compatibility that logs in the `info` log level.
Like the `log.Error` method the `log.WrapError` will skip the user-visible error message and otherwise be identical to `log.Wrap`.

## Creating logger
```go
msg := log.WrapError(
originalErr,
"E_SOME_CODE",
"Dear admin, an error happened. %s" +
"The error message will be appended to this message."
"This string will appear instead of %s in the admin-message."
)
```

### Adding labels to messages

Labels are useful for recording extra information with messages that can be indexed by the logging system. These labels may or may not be recorded by the logging backend. For example, the syslog output doesn't support recording labels due to size constraints. In other words, the message itself should contain enough information for an administrator to interpret the error.

## Using messages

As mentioned before, the `Message` interface implements the `error` interface, so these messages can simply be returned like a normal error would.

## Logging

The simplest way to create a logger is to use the convenience functions:
This library also provides a `Logger` interface that can log all kinds of errors, including the `Message` interface. It provides the following methods for logging:

- `logger.Debug(err)`
- `logger.Info(err)`
- `logger.Notice(err)`
- `logger.Warning(err)`
- `logger.Error(err)`
- `logger.Critical(err)`
- `logger.Alert(err)`
- `logger.Emergency(err)`

We also provide the following compatibility methods:

- `logger.Log(v ...interface{})`
- `logger.Logf(format string, v ...interface{})`

We provide a method to create a child logger that has a different minimum log level. Messages below this level will be discarded:

```go
config := log.Config{
// Log levels are: Debug, Info, Notice, Warning, Error, Critical, Alert, Emergency
Level: log.LevelNotice,
// Supported formats: Text, LJSON
Format: log.FormatText,
}
// module is an optional module descriptor for log messages. Can be empty.
module := "someModule"
logger := log.New(config, module, os.Stdout)
loggerFactory := log.NewFactory(os.Stdout)
newLogger := logger.WithLevel(log.LevelInfo)
```

You can also create a custom pipeline if you wish:
We can also create a new logger copy with default labels added:

```go
writer := os.Stdout
minimumLogLevel := log.LevelInfo
logFormatter := log.NewLJsonLogFormatter()
module := "someModule"
p := pipeline.NewLoggerPipeline(minimumLogLevel, module, logFormatter, writer)
p.Warning("test")
newLogger := logger.WithLabel("label name", "label value")
```

This will create a pipeline that writes log messages to the standard output in newline-delimited JSON format. You can, of course, also implement your own log formatter by implementing the interface in [formatter.go](formatter.go).
Finally, the logger also supports calling the `Rotate()` and `Close()` methods. `Rotate()` instructs the output to close all handles and reopen them to facilitate rotating logs. `Close()` permanently closes the writer.

## Plugging in the go logger
## Creating a logger

This package also provides the facility to plug in the go logger. This can be done by creating a logger as follows:
The `Logger` interface is intended for generic implementations. The default implementation can be created as follows:

```go
import (
goLog "log"
"github.com/containerssh/log"
)
logger, err := log.NewLogger(config)
```

goLogWriter := log.NewGoLogWriter(logger)
goLogger := goLog.New(goLogWriter, "", 0)
goLogger.Println("Hello world!")
Alternatively, you can also use the `log.MustNewLogger` method to skip having to deal with the error. (It will `panic` if an error happens.)

If you need a factory you can use the `log.LoggerFactory` interface and the `log.NewLoggerFactory` to create a factory you can pass around. The `Make(config)` method will make a new logger when needed.

## Configuration

The configuration structure for the default logger implementation is contained in the `log.Config` structure.

### Configuring the output format

The most important configuration is where your logs will end up:

```go
log.Config{
Output: log.OutputStdout,
}
```

If you want to change the log facility globally:
The following options are possible:

- `log.OutputStdout` logs to the standard output or any other `io.Writer`
- `log.OutputFile` logs to a file on the local filesystem.
- `log.OutputSyslog` logs to a syslog server using a UNIX or UDP socket.

### Logging to stdout

If you set the `Output` option to `log.OutputStdout` the output will be written to the standard output in the format specified below (see "Changing the log format"). The destination can be overridden:

```go
import (
goLog "log"
"github.com/containerssh/log"
)
log.Config {
Output: log.OutputStdout,
Stdout: someOtherWriter,
}
```

### Logging to a file

goLogWriter := log.NewGoLogWriter(logger)
goLog.SetOutput(goLogWriter)
goLog.Println("Hello world!")
The file logger is configured as follows:

```go
log.Config {
Output: log.OutputFile,
File: "/var/log/containerssh.log",
}
```

## Log formats
You can call the `Rotate()` method on the logger to close and reopen the file. This allows for log rotation.

### Logging to syslog

We currently support two log formats: `text` and `ljson`
The syslog logger writes to a syslog daemon. Typically, this is located on the `/dev/log` UNIX socket, but sending logs over UDP is also supported. TCP, encryption, and other advanced Syslog features are not supported, so adding a Syslog daemon on the local node is strongly recommended.

### The `text` format
The configuration is the following:

```go
log.Config{
Output: log.OutputSyslog,
Facility: log.FacilityStringAuth, // See facilities list below
Tag: "ProgramName", // Add program name here
Pid: false, // Change to true to add the PID to the Tag
Hostname: "" // Change to override host name
}
```

The following facilities are supported:

- `log.FacilityStringKern`
- `log.FacilityStringUser`
- `log.FacilityStringMail`
- `log.FacilityStringDaemon`
- `log.FacilityStringAuth`
- `log.FacilityStringSyslog`
- `log.FacilityStringLPR`
- `log.FacilityStringNews`
- `log.FacilityStringUUCP`
- `log.FacilityStringCron`
- `log.FacilityStringAuthPriv`
- `log.FacilityStringFTP`
- `log.FacilityStringNTP`
- `log.FacilityStringLogAudit`
- `log.FacilityStringLogAlert`
- `log.FacilityStringClock`
- `log.FacilityStringLocal0`
- `log.FacilityStringLocal1`
- `log.FacilityStringLocal2`
- `log.FacilityStringLocal3`
- `log.FacilityStringLocal4`
- `log.FacilityStringLocal5`
- `log.FacilityStringLocal6`
- `log.FacilityStringLocal7`

### Changing the log format

We currently support two log formats: `text` and `ljson`. The format is applied for the stdout and file outputs and can be configured as follows:

```go
log.Config {
Format: log.FormatText|log.FormatLJSON,
}
```

#### The `text` format

The text format is structured as follows:

Expand All @@ -124,7 +244,7 @@ TIMESTAMP[TAB]LEVEL[TAB]MODULE[TAB]MESSAGE[NEWLINE]

This format is recommended for human consumption only.

### The `ljson` format
#### The `ljson` format

This format logs in a newline-delimited JSON format. Each message has the following format:

Expand All @@ -144,4 +264,4 @@ You can create a logger for testing purposes that logs using the `t *testing.T`

```go
logger := log.NewTestLogger(t)
```
```

0 comments on commit 0d3f9f5

Please sign in to comment.