enso/docs/infrastructure/logging.md

369 lines
12 KiB
Markdown
Raw Normal View History

---
layout: developer-doc
title: Logging Service
category: infrastructure
tags: [infrastructure, logging, debug]
order: 6
---
# Logging
The Enso project features a centralised logging service to allow for the
aggregation of logs from multiple components. This service can be started with
one of the main components, allowing other components to connect to it. The
service aggregates all logs in one place for easier analysis of the interaction
between components. Components can also log to console or files directly without
involving the centralized logging service.
<!-- MarkdownTOC levels="2,3" autolink="true" -->
- [Configuration](#configuration)
- [Custom Log Levels](#custom-log-levels)
- [Appenders](#appenders)
- [Format](#format)
- [File](#file-appender)
- [Network](#socket-appender)
- [Sentry.io](#sentry-appender)
- [JVM Architecture](#jvm-architecture)
- [SLF4J Interface](#slf4j-interface)
- [Setting Up Logging](#setting-up-logging)
- [Log Masking](#log-masking)
- [Logging in Tests](#logging-in-tests)
Always log to console and file (#7825) * Always log verbose to a file The change adds an option by default to always log to a file with verbose log level. The implementation is a bit tricky because in the most common use-case we have to always log in verbose mode to a socket and only later apply the desired log levels. Previously socket appender would respect the desired log level already before forwarding the log. If by default we log to a file, verbose mode is simply ignored and does not override user settings. To test run `project-manager` with `ENSO_LOGSERVER_APPENDER=console` env variable. That will output to the console with the default `INFO` level and `TRACE` log level for the file. * add docs * changelog * Address some PR requests 1. Log INFO level to CONSOLE by default 2. Change runner's default log level from ERROR to WARN Took a while to figure out why the correct log level wasn't being passed to the language server, therefore ignoring the (desired) verbose logs from the log file. * linter * 3rd party uses log4j for logging Getting rid of the warning by adding a log4j over slf4j bridge: ``` ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console... ``` * legal review update * Make sure tests use test resources Having `application.conf` in `src/main/resources` and `test/resources` does not guarantee that in Tests we will pick up the latter. Instead, by default it seems to do some kind of merge of different configurations, which is far from desired. * Ensure native launcher test log to console only Logging to console and (temporary) files is problematic for Windows. The CI also revealed a problem with the native configuration because it was not possible to modify the launcher via env variables as everything was initialized during build time. * Adapt to method changes * Potentially deal with Windows failures
2023-09-26 12:32:04 +03:00
- [Logging to file](#logging-to-file)
<!-- /MarkdownTOC -->
## Configuration
The logging settings should be placed under the `logging-service` key of the
`application.conf` config. Each of the main components can customize format and
output target via section in `application.conf` configuration file. The
configuration is using HOCON-style, as defined by
[lightbend/config](https://github.com/lightbend/config). Individual values
accepted in the config are inspired by SLF4J's properties, formatting and
implementations.
The configuration has two main sections:
- [custom log levels](#custom-log-levels)
- [applications' appenders](#appenders) (also known as configuration of log
events output target)
During component's setup, its `application.conf` config file is parsed. The
config's keys and values are validated and, if correct, the parsed
representation is available as an instance of
`org.enso.logger.config.LoggingServiceConfig` class. The class encapsulates the
`logging-service` section of `application.conf` file and is used to
programmatically initialize loggers.
As per [configuration schema](https://github.com/lightbend/config) any key can
have a default value that can be overridden by an environment variable. For
example
```
{
host = localhost
host = $ENSO_HOST
}
```
defines a `host` key once, except that `ENSO_HOST` values takes a precedence if
it is defined during loading of the config file.
### Custom Log Levels
The `logging-service.logger` configuration provides an ability to override the
default application log level for particular loggers. In the `logger` subconfig
the key specifies the logger name (or it's prefix) and the value specifies the
log level for that logger.
```
logging-service.logger {
akka.actor = info
akka.event = error
akka.io = error
slick {
jdbc.JdbcBackend.statement = debug
"*" = error
}
}
```
For example, the config above limits all `akka.actor.*` loggers to the info
level logging, and `akka.event.*` loggers can emit only the error level
messages.
Config supports globs (`*`). For example, the config above sets
`jdbc.JdbcBackend.statement` SQL statements logging to debug level, and the rest
of the slick loggers to error level.
Additionally, custom log events can be provided during runtime via system
properties, without re-packaging the updated config file. For example
```typescript
akka.actor = info;
```
is equivalent to
```typescript
-Dakka.actor.Logger.level=info
```
Any custom log level is therefore defined with `-Dx.y.Z.Logger.level` where `x`,
`y` and `Z` refer to the package elements and class name, respectively. System
properties always have a higher priority over those defined in the
`application.conf` file.
### Appenders
Log output target is also configured in the `application.conf` files in the
"appenders" section ("appender" is equivalent to `java.util.logging.Handler`
semantics). Each appender section can provide further required and optional
key/value pairs, to better customize the log target output.
Currently supported are
- console appender - the most basic appender that prints log events to stdout
- [file appender](#file-appender) - appender that writes log events to a file,
with optional rolling file policy
- [socket appender](#socket-appender) - appender that forwards log events to
some logging server
- [sentry.io appender](#sentry-appender) - appender that forwards log events to
a sentry.io service
The appenders are defined by the `logging-service.appenders`. Currently only a
single appender can be selected at a time. The selection may also be done via an
environmental variable but it depends on which component we are executing.
- `project-manager` - project manager by default starts a centralized logging
server that collects logs (as defined in `logging-service.server` config key)
and the logs output can be overwritten by `ENSO_LOGSERVER_APPENDER` env
variable
- `ensoup` or `enso` - the default log output can be overwritten by defining the
`ENSO_APPENDER_DEFAULT` env variable
For example, for the project manager to output to `console` one simply executes
```
ENSO_LOGSERVER_APPENDER=console ./project-manager
```
#### Format
The pattern follows the classic's
[PatternLayout](https://logback.qos.ch/manual/layouts.html#ClassicPatternLayout)
format.
Appenders that store/display log events can specify the format of the log
message via `pattern` field e.g.
```typescript
appenders = [
{
name = "console"
pattern = "[%level{lowercase=true}] [%d{yyyy-MM-dd'T'HH:mm:ssXXX}] [%logger] %msg%n%nopex"
}
...
]
```
#### File Appender
Enabled with `ENSO_APPENDER_DEFAULT=file` environment variable.
File appender directs all log events to a log file:
```
{
name = "file"
append = <boolean, optional>
immediate-flush = <boolean, optional>
pattern = <string, optional>
rolling-policy {
max-file-size = <string, optional>
max-history = <int, optional>
max-total-size = <string, optional>
}
}
```
Rolling policy is a fully optional property of File Appender that would trigger
automatic log rotation. All properties are optional with some reasonable
defaults if missing (defined in `org.enso.logger.config.FileAppender` config
class).
#### Socket Appender
Enabled with `ENSO_APPENDER_DEFAULT=socket` environment variable.
Configuration
```
{
name = "socket"
hostname = <string, required>
port = <string, required>
}
```
The two fields can be overridden via environment variables:
- `hostname` has an equivalent `$ENSO_LOGSERVER_HOSTNAME` variable
- `port` has an equivalent `$ENSO_LOGSERVER_PORT` variable
#### Sentry Appender
Enabled with `ENSO_APPENDER_DEFAULT=sentry` environment variable.
```
{
name = "sentry"
dsn = <string, required>
flush-timeout = <int, optional>
debug = <boolean, optional>
}
```
Sentry's Appender has a single required field, `dsn`. The `dsn` value can be
provided via an environment variable `ENSO_APPENDER_SENTRY_DSN`. `flush-timeout`
determines how often logger should send its collected events to sentry.io
service. If `debug` value is `true`, logging will print to stdout additional
trace information of the logging process itself.
## JVM Architecture
Enso's logging makes use of two logging APIs - `java.util.logging` and
Upgrade enso to GraalVM for jdk 21 (#7991) Upgrade to GraalVM JDK 21. ``` > java -version openjdk version "21" 2023-09-19 OpenJDK Runtime Environment GraalVM CE 21+35.1 (build 21+35-jvmci-23.1-b15) OpenJDK 64-Bit Server VM GraalVM CE 21+35.1 (build 21+35-jvmci-23.1-b15, mixed mode, sharing) ``` With SDKMan, download with `sdk install java 21-graalce`. # Important Notes - After this PR, one can theoretically run enso with any JRE with version at least 21. - Removed `sbt bootstrap` hack and all the other build time related hacks related to the handling of GraalVM distribution. - `project-manager` remains backward compatible - it can open older engines with runtimes. New engines now do no longer require a separate runtime to be downloaded. - sbt does not support compilation of `module-info.java` files in mixed projects - https://github.com/sbt/sbt/issues/3368 - Which means that we can have `module-info.java` files only for Java-only projects. - Anyway, we need just a single `module-info.class` in the resulting `runtime.jar` fat jar. - `runtime.jar` is assembled in `runtime-with-instruments` with a custom merge strategy (`sbt-assembly` plugin). Caching is disabled for custom merge strategies, which means that re-assembly of `runtime.jar` will be more frequent. - Engine distribution contains multiple JAR archives (modules) in `component` directory, along with `runner/runner.jar` that is hidden inside a nested directory. - The new entry point to the engine runner is [EngineRunnerBootLoader](https://github.com/enso-org/enso/pull/7991/files#diff-9ab172d0566c18456472aeb95c4345f47e2db3965e77e29c11694d3a9333a2aa) that contains a custom ClassLoader - to make sure that everything that does not have to be loaded from a module is loaded from `runner.jar`, which is not a module. - The new command line for launching the engine runner is in [distribution/bin/enso](https://github.com/enso-org/enso/pull/7991/files#diff-0b66983403b2c329febc7381cd23d45871d4d555ce98dd040d4d1e879c8f3725) - [Newest version of Frgaal](https://repo1.maven.org/maven2/org/frgaal/compiler/20.0.1/) (20.0.1) does not recognize `--source 21` option, only `--source 20`.
2023-11-17 21:02:36 +03:00
`org.slf4j`. The former is being used by the Truffle runtime, which itself
relies on `jul`, while the latter is used everywhere else. The implementation of
the logging is using off the shelf `Logback` implementation with some custom
setup methods. The two APIss cooperate by essentially forwarding log messages
from the former to the latter.
While typically any SLF4J customization would be performed via custom
Upgrade enso to GraalVM for jdk 21 (#7991) Upgrade to GraalVM JDK 21. ``` > java -version openjdk version "21" 2023-09-19 OpenJDK Runtime Environment GraalVM CE 21+35.1 (build 21+35-jvmci-23.1-b15) OpenJDK 64-Bit Server VM GraalVM CE 21+35.1 (build 21+35-jvmci-23.1-b15, mixed mode, sharing) ``` With SDKMan, download with `sdk install java 21-graalce`. # Important Notes - After this PR, one can theoretically run enso with any JRE with version at least 21. - Removed `sbt bootstrap` hack and all the other build time related hacks related to the handling of GraalVM distribution. - `project-manager` remains backward compatible - it can open older engines with runtimes. New engines now do no longer require a separate runtime to be downloaded. - sbt does not support compilation of `module-info.java` files in mixed projects - https://github.com/sbt/sbt/issues/3368 - Which means that we can have `module-info.java` files only for Java-only projects. - Anyway, we need just a single `module-info.class` in the resulting `runtime.jar` fat jar. - `runtime.jar` is assembled in `runtime-with-instruments` with a custom merge strategy (`sbt-assembly` plugin). Caching is disabled for custom merge strategies, which means that re-assembly of `runtime.jar` will be more frequent. - Engine distribution contains multiple JAR archives (modules) in `component` directory, along with `runner/runner.jar` that is hidden inside a nested directory. - The new entry point to the engine runner is [EngineRunnerBootLoader](https://github.com/enso-org/enso/pull/7991/files#diff-9ab172d0566c18456472aeb95c4345f47e2db3965e77e29c11694d3a9333a2aa) that contains a custom ClassLoader - to make sure that everything that does not have to be loaded from a module is loaded from `runner.jar`, which is not a module. - The new command line for launching the engine runner is in [distribution/bin/enso](https://github.com/enso-org/enso/pull/7991/files#diff-0b66983403b2c329febc7381cd23d45871d4d555ce98dd040d4d1e879c8f3725) - [Newest version of Frgaal](https://repo1.maven.org/maven2/org/frgaal/compiler/20.0.1/) (20.0.1) does not recognize `--source 21` option, only `--source 20`.
2023-11-17 21:02:36 +03:00
`LoggerFactory` and `Logger` implementation that is returned via a
`StaticLoggerBinder` instance, this is not possible for our use-case:
- file logging requires Enso-specific directory which is only known during
runtime
- centralized logging
- modifying log levels without recompilation
### SLF4J Interface
The user code must not be calling any of the underlying implementations, such as
Log4J or Logback, and should only request loggers via factory methods.
One can use the `org.slf4j.LoggerFactory` directly to retrieve class-specific
logger. For Scala code, it is recommended to use the
`com.typesafe.scalalogging.Logger` instead which wraps the SLF4J logger with
macros that compute the log messages only if the given logging level is enabled,
and allows much prettier initialisation.
```java
package foo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Foo {
private Logger logger = LoggerFactory.getLogger(Foo.class);
public void bar() {
logger.info("Hello world!");
}
}
```
### Setting Up Logging
The `org.slf4j.Logger` instances have to know where to send log events. This
setting is typically performed once, when the service starts, and applies
globally during its execution. Currently, it is not possible to dynamically
change where log events are being stored. The main (abstract) class used for
setting up logging is `org.enso.logger.LoggerSetup`. An instance of that class
can be retrieved with the thread-safe `org.enso.logger.LoggerSetup.get` factory
method. `org.enso.logger.LoggerSetup` provides a number of `setupXYZAppender`
methods that will direct loggers to send log events to an `XYZ` appender.
Setting a specific hard-coded appender programmatically should however be
avoided by the users. Instead, one should invoke one of the overloaded `setup`
variants that initialize loggers based on the provided `logging-service`
configuration.
```java
package foo;
import org.enso.logger.LoggerSetup;
import org.slf4j.event.Level;
public class MyService {
private Logger logger = LoggerFactory.getLogger(Foo.class);
...
public void start(Level logLevel) {
LoggerSetup.get().setup(logLevel);
logger.info("My service is starting...");
...
}
...
}
```
`org.enso.logging.LoggingSetupHelper` class was introduced to help with the most
common use cases - establishing a file-based logging in the Enso's dedicated
directories or connecting to an existing logging server once it starts accepting
connections. That is why services don't call `LoggerSetup` directly but instead
provide a service-specific implementation of
`org.enso.logging.LoggingSetupHelper`. `LoggingSetupHelper` and `LoggerSetup`
provide `teardown` methods to properly dispose of log events.
### Log Masking
Logs should not contain personally identifiable information (PII). The following
is considered PII:
- User code
- Values of executed expressions
- Values of user environment variables. Although variable names are not
considered PII and can be logged.
- File paths inside the user project directory. System and distribution paths
and a path to the user project can be logged.
Project logging library implements masking of PII. To utilize it
1. Logged object should implement an interface that defines custom log-string
representation of this object
2. The logging should be performed by supplying a template string with `{}`
placeholders, and the arguments
```scala
log.debug("Created {} at [{}].", obj, path)
```
String interpolation in log statements `s"Created $obj"` should be avoided
because it uses default `toString` implementation and can leak critical
information even if the object implements custom interface for masked logging.
### Logging in Tests
The Logging Service provides a helper function `TestLogger.gatherLogs` that will
execute the closure and collect all logs reported in the specified class. That
way it can verify that all logs are being reported within the provided code.
Always log to console and file (#7825) * Always log verbose to a file The change adds an option by default to always log to a file with verbose log level. The implementation is a bit tricky because in the most common use-case we have to always log in verbose mode to a socket and only later apply the desired log levels. Previously socket appender would respect the desired log level already before forwarding the log. If by default we log to a file, verbose mode is simply ignored and does not override user settings. To test run `project-manager` with `ENSO_LOGSERVER_APPENDER=console` env variable. That will output to the console with the default `INFO` level and `TRACE` log level for the file. * add docs * changelog * Address some PR requests 1. Log INFO level to CONSOLE by default 2. Change runner's default log level from ERROR to WARN Took a while to figure out why the correct log level wasn't being passed to the language server, therefore ignoring the (desired) verbose logs from the log file. * linter * 3rd party uses log4j for logging Getting rid of the warning by adding a log4j over slf4j bridge: ``` ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console... ``` * legal review update * Make sure tests use test resources Having `application.conf` in `src/main/resources` and `test/resources` does not guarantee that in Tests we will pick up the latter. Instead, by default it seems to do some kind of merge of different configurations, which is far from desired. * Ensure native launcher test log to console only Logging to console and (temporary) files is problematic for Windows. The CI also revealed a problem with the native configuration because it was not possible to modify the launcher via env variables as everything was initialized during build time. * Adapt to method changes * Potentially deal with Windows failures
2023-09-26 12:32:04 +03:00
### Logging to file
By default Enso will attempt to persist (verbose) logs into a designated log
file. This means that even though a user might be shown `WARNING` level logs in
the console, logs with up to `DEBUG` level will be dumped into the log file. A
Always log to console and file (#7825) * Always log verbose to a file The change adds an option by default to always log to a file with verbose log level. The implementation is a bit tricky because in the most common use-case we have to always log in verbose mode to a socket and only later apply the desired log levels. Previously socket appender would respect the desired log level already before forwarding the log. If by default we log to a file, verbose mode is simply ignored and does not override user settings. To test run `project-manager` with `ENSO_LOGSERVER_APPENDER=console` env variable. That will output to the console with the default `INFO` level and `TRACE` log level for the file. * add docs * changelog * Address some PR requests 1. Log INFO level to CONSOLE by default 2. Change runner's default log level from ERROR to WARN Took a while to figure out why the correct log level wasn't being passed to the language server, therefore ignoring the (desired) verbose logs from the log file. * linter * 3rd party uses log4j for logging Getting rid of the warning by adding a log4j over slf4j bridge: ``` ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console... ``` * legal review update * Make sure tests use test resources Having `application.conf` in `src/main/resources` and `test/resources` does not guarantee that in Tests we will pick up the latter. Instead, by default it seems to do some kind of merge of different configurations, which is far from desired. * Ensure native launcher test log to console only Logging to console and (temporary) files is problematic for Windows. The CI also revealed a problem with the native configuration because it was not possible to modify the launcher via env variables as everything was initialized during build time. * Adapt to method changes * Potentially deal with Windows failures
2023-09-26 12:32:04 +03:00
user can disable this parallel logging to a file by setting the environment
variable:
```
ENSO_LOG_TO_FILE=false project-manager ...
```
Users can also modify the default maximal log level, `DEBUG`, used when logging
to a log file by setting the environment variable:
```
ENSO_LOG_TO_FILE_LOG_LEVEL=trace project-manager ...
```