Replace a custom logger with off the shelf implementation (#7559)

This change replaces Enso's custom logger with an existing, mostly off the shelf logging implementation. The change attempts to provide a 1:1 replacement for the existing solution while requiring only a minimal logic for the initialization.

Loggers are configured completely via `logging-server` section in `application.conf` HOCON file, all initial logback configuration has been removed. This opens up a lot of interesting opportunities because we can benefit from all the well maintained slf4j implementations without being to them in terms of functionality.

Most important differences have been outlined in `docs/infrastructure/logging.md`.

# Important Notes
Addresses:
- #7253
- #6739
This commit is contained in:
Hubert Plociniczak 2023-09-04 11:40:16 +02:00 committed by GitHub
parent 87ce78615a
commit 8a60bc6dcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
140 changed files with 2680 additions and 5085 deletions

View File

@ -932,6 +932,7 @@
- [Using official BigInteger support][7420]
- [Allow users to give a project other than Upper_Snake_Case name][7397]
- [Support renaming variable or function][7515]
- [Replace custom logging service with off the shelf library][7559]
- [Only use types as State keys][7585]
- [Allow Java Enums in case of branches][7607]
- [Notification about the project rename action][7613]
@ -1069,6 +1070,7 @@
[7420]: https://github.com/enso-org/enso/pull/7420
[7397]: https://github.com/enso-org/enso/pull/7397
[7515]: https://github.com/enso-org/enso/pull/7515
[7559]: https://github.com/enso-org/enso/pull/7559
[7585]: https://github.com/enso-org/enso/pull/7585
[7607]: https://github.com/enso-org/enso/pull/7607
[7613]: https://github.com/enso-org/enso/pull/7613

150
build.sbt
View File

@ -266,8 +266,11 @@ lazy val enso = (project in file("."))
`task-progress-notifications`,
`profiling-utils`,
`logging-utils`,
filewatcher,
`logging-config`,
`logging-service`,
`logging-service-logback`,
`logging-utils-akka`,
filewatcher,
`logging-truffle-connector`,
`locking-test-helper`,
`akka-native`,
@ -338,24 +341,26 @@ lazy val enso = (project in file("."))
// === Akka ===================================================================
def akkaPkg(name: String) = akkaURL %% s"akka-$name" % akkaVersion
def akkaHTTPPkg(name: String) = akkaURL %% s"akka-$name" % akkaHTTPVersion
def akkaPkg(name: String) = akkaURL %% s"akka-$name" % akkaVersion
def akkaHTTPPkg(name: String) = akkaURL %% s"akka-$name" % akkaHTTPVersion
val akkaURL = "com.typesafe.akka"
val akkaVersion = "2.6.20"
val akkaHTTPVersion = "10.2.10"
val akkaMockSchedulerVersion = "0.5.5"
val logbackClassicVersion = "1.3.7"
val akkaActor = akkaPkg("actor")
val akkaStream = akkaPkg("stream")
val akkaTyped = akkaPkg("actor-typed")
val akkaTestkit = akkaPkg("testkit")
val akkaSLF4J = akkaPkg("slf4j")
val akkaTestkitTyped = akkaPkg("actor-testkit-typed") % Test
val akkaHttp = akkaHTTPPkg("http")
val akkaSpray = akkaHTTPPkg("http-spray-json")
val akkaTest = Seq(
"ch.qos.logback" % "logback-classic" % logbackClassicVersion % Test
val logbackPkg = Seq(
"ch.qos.logback" % "logback-classic" % logbackClassicVersion,
"ch.qos.logback" % "logback-core" % logbackClassicVersion
)
val akkaActor = akkaPkg("actor")
val akkaStream = akkaPkg("stream")
val akkaTyped = akkaPkg("actor-typed")
val akkaTestkit = akkaPkg("testkit")
val akkaSLF4J = akkaPkg("slf4j")
val akkaTestkitTyped = akkaPkg("actor-testkit-typed") % Test
val akkaHttp = akkaHTTPPkg("http")
val akkaSpray = akkaHTTPPkg("http-spray-json")
val logbackTest = logbackPkg.map(_ % Test)
val akka =
Seq(
akkaActor,
@ -679,8 +684,9 @@ lazy val `logging-utils` = project
frgaalJavaCompilerSetting,
version := "0.1",
libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % scalatestVersion % Test
)
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.slf4j" % "slf4j-api" % slf4jVersion
) ++ logbackTest
)
lazy val `logging-service` = project
@ -690,27 +696,54 @@ lazy val `logging-service` = project
frgaalJavaCompilerSetting,
version := "0.1",
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % slf4jVersion,
"com.typesafe" % "config" % typesafeConfigVersion,
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
akkaStream,
akkaHttp,
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,
"junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.graalvm.nativeimage" % "svm" % graalMavenPackagesVersion % "provided"
"org.slf4j" % "slf4j-api" % slf4jVersion,
"com.typesafe" % "config" % typesafeConfigVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
akkaHttp
)
)
.settings(
if (Platform.isWindows)
(Compile / unmanagedSourceDirectories) += (Compile / sourceDirectory).value / "java-windows"
else
(Compile / unmanagedSourceDirectories) += (Compile / sourceDirectory).value / "java-unix"
)
.dependsOn(`akka-native`)
.dependsOn(`logging-utils`)
.dependsOn(`logging-config`)
lazy val `logging-config` = project
.in(file("lib/scala/logging-config"))
.configs(Test)
.settings(
frgaalJavaCompilerSetting,
version := "0.1",
libraryDependencies ++= Seq(
"com.typesafe" % "config" % typesafeConfigVersion,
"org.slf4j" % "slf4j-api" % slf4jVersion
)
)
lazy val `logging-service-logback` = project
.in(file("lib/scala/logging-service-logback"))
.configs(Test)
.settings(
frgaalJavaCompilerSetting,
version := "0.1",
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % slf4jVersion,
"io.sentry" % "sentry-logback" % "6.28.0",
"io.sentry" % "sentry" % "6.28.0",
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion % "provided"
) ++ logbackPkg
)
.dependsOn(`logging-config`)
.dependsOn(`logging-service`)
lazy val `logging-utils-akka` = project
.in(file("lib/scala/logging-utils-akka"))
.configs(Test)
.settings(
frgaalJavaCompilerSetting,
version := "0.1",
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % slf4jVersion,
"com.typesafe.akka" %% "akka-actor" % akkaVersion
)
)
lazy val filewatcher = project
.in(file("lib/scala/filewatcher"))
@ -722,7 +755,7 @@ lazy val filewatcher = project
"io.methvin" % "directory-watcher" % directoryWatcherVersion,
"commons-io" % "commons-io" % commonsIoVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
)
) ++ logbackTest
)
.dependsOn(testkit % Test)
@ -880,8 +913,11 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
.dependsOn(`polyglot-api`)
.dependsOn(`runtime-version-manager`)
.dependsOn(`library-manager`)
.dependsOn(`logging-utils-akka`)
.dependsOn(`logging-service`)
.dependsOn(pkg)
.dependsOn(`json-rpc-server`)
.dependsOn(`logging-service-logback` % Runtime)
.dependsOn(`json-rpc-server-test` % Test)
.dependsOn(testkit % Test)
.dependsOn(`runtime-version-manager-test` % Test)
@ -912,7 +948,7 @@ lazy val `json-rpc-server` = project
.in(file("lib/scala/json-rpc-server"))
.settings(
frgaalJavaCompilerSetting,
libraryDependencies ++= akka ++ akkaTest,
libraryDependencies ++= akka ++ logbackTest,
libraryDependencies ++= circe,
libraryDependencies ++= Seq(
"io.circe" %% "circe-literal" % circeVersion,
@ -953,11 +989,10 @@ lazy val searcher = project
.settings(
frgaalJavaCompilerSetting,
libraryDependencies ++= jmh ++ Seq(
"com.typesafe.slick" %% "slick" % slickVersion,
"org.xerial" % "sqlite-jdbc" % sqliteVersion,
"ch.qos.logback" % "logback-classic" % logbackClassicVersion % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
)
"com.typesafe.slick" %% "slick" % slickVersion,
"org.xerial" % "sqlite-jdbc" % sqliteVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
) ++ logbackTest
)
.configs(Benchmark)
.settings(
@ -1081,6 +1116,7 @@ lazy val `language-server` = (project in file("engine/language-server"))
commands += WithDebugCommand.withDebug,
frgaalJavaCompilerSetting,
libraryDependencies ++= akka ++ circe ++ Seq(
"org.slf4j" % "slf4j-api" % slf4jVersion,
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
"io.circe" %% "circe-generic-extras" % circeGenericExtrasVersion,
"io.circe" %% "circe-literal" % circeVersion,
@ -1117,7 +1153,7 @@ lazy val `language-server` = (project in file("engine/language-server"))
Test / javaOptions ++= {
// Note [Classpath Separation]
val runtimeClasspath =
(LocalProject("runtime") / Compile / fullClasspath).value
(LocalProject("runtime") / Runtime / fullClasspath).value
.map(_.data)
.mkString(File.pathSeparator)
Seq(
@ -1138,6 +1174,7 @@ lazy val `language-server` = (project in file("engine/language-server"))
.dependsOn(`library-manager`)
.dependsOn(`connected-lock-manager`)
.dependsOn(`edition-updater`)
.dependsOn(`logging-utils-akka`)
.dependsOn(`logging-service`)
.dependsOn(`polyglot-api`)
.dependsOn(`searcher`)
@ -1147,6 +1184,7 @@ lazy val `language-server` = (project in file("engine/language-server"))
.dependsOn(`profiling-utils`)
.dependsOn(filewatcher)
.dependsOn(testkit % Test)
.dependsOn(`logging-service-logback` % Test)
.dependsOn(`library-manager-test` % Test)
.dependsOn(`runtime-version-manager-test` % Test)
@ -1409,12 +1447,10 @@ lazy val runtime = (project in file("engine/runtime"))
.dependsOn(`interpreter-dsl`)
.dependsOn(`library-manager`)
.dependsOn(`logging-truffle-connector`)
.dependsOn(`logging-utils`)
.dependsOn(`polyglot-api`)
.dependsOn(`text-buffer`)
.dependsOn(`runtime-parser`)
.dependsOn(pkg)
.dependsOn(`edition-updater`)
.dependsOn(`connected-lock-manager`)
.dependsOn(testkit % Test)
@ -1640,8 +1676,8 @@ lazy val `engine-runner` = project
commands += WithDebugCommand.withDebug,
inConfig(Compile)(truffleRunOptionsSettings),
libraryDependencies ++= Seq(
"org.graalvm.sdk" % "polyglot-tck" % graalMavenPackagesVersion % "provided",
"org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided",
"org.graalvm.sdk" % "polyglot-tck" % graalMavenPackagesVersion % Provided,
"org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % Provided,
"commons-cli" % "commons-cli" % commonsCliVersion,
"com.monovore" %% "decline" % declineVersion,
"org.jline" % "jline" % jlineVersion,
@ -1670,12 +1706,9 @@ lazy val `engine-runner` = project
mainClass = Option("org.enso.runner.Main"),
cp = Option("runtime.jar"),
initializeAtRuntime = Seq(
// Note [WSLoggerManager Shutdown Hook]
"org.enso.loggingservice.WSLoggerManager$",
"org.jline.nativ.JLineLibrary",
"io.methvin.watchservice.jna.CarbonAPI",
"org.enso.syntax2.Parser",
"org.enso.loggingservice",
"zio.internal.ZScheduler$$anon$4",
"sun.awt",
"sun.java2d",
@ -1700,8 +1733,10 @@ lazy val `engine-runner` = project
.dependsOn(cli)
.dependsOn(`library-manager`)
.dependsOn(`language-server`)
.dependsOn(`polyglot-api`)
.dependsOn(`edition-updater`)
.dependsOn(`logging-service`)
.dependsOn(`logging-service-logback` % Runtime)
.dependsOn(`polyglot-api`)
lazy val launcher = project
.in(file("engine/launcher"))
@ -1725,10 +1760,6 @@ lazy val launcher = project
additionalOptions = Seq(
"-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.NoOpLog",
"-H:IncludeResources=.*Main.enso$"
),
initializeAtRuntime = Seq(
// Note [WSLoggerManager Shutdown Hook]
"org.enso.loggingservice.WSLoggerManager$"
)
)
.dependsOn(installNativeImage)
@ -1761,13 +1792,19 @@ lazy val launcher = project
.dependsOn(buildNativeImage)
.dependsOn(LauncherShimsForTest.prepare())
.value,
Test / parallelExecution := false
(Test / testOnly) := (Test / testOnly)
.dependsOn(buildNativeImage)
.dependsOn(LauncherShimsForTest.prepare())
.evaluated
)
.dependsOn(cli)
.dependsOn(`runtime-version-manager`)
.dependsOn(`version-output`)
.dependsOn(pkg)
.dependsOn(`logging-utils` % "test->test")
.dependsOn(`logging-service`)
.dependsOn(`logging-service-logback` % Test)
.dependsOn(`logging-service-logback` % Runtime)
.dependsOn(`distribution-manager` % Test)
.dependsOn(`runtime-version-manager-test` % Test)
@ -1996,7 +2033,6 @@ lazy val `library-manager` = project
.dependsOn(`distribution-manager`)
.dependsOn(downloader)
.dependsOn(testkit % Test)
.dependsOn(`logging-service` % Test)
lazy val `library-manager-test` = project
.in(file("lib/scala/library-manager-test"))
@ -2010,8 +2046,8 @@ lazy val `library-manager-test` = project
)
)
.dependsOn(`library-manager`)
.dependsOn(`logging-utils` % "test->test")
.dependsOn(testkit)
.dependsOn(`logging-service`)
lazy val `connected-lock-manager` = project
.in(file("lib/scala/connected-lock-manager"))
@ -2046,7 +2082,6 @@ lazy val `runtime-version-manager` = project
)
.dependsOn(pkg)
.dependsOn(downloader)
.dependsOn(`logging-service`)
.dependsOn(cli)
.dependsOn(`version-output`)
.dependsOn(`edition-updater`)
@ -2070,7 +2105,6 @@ lazy val `runtime-version-manager-test` = project
.value
)
.dependsOn(`runtime-version-manager`)
.dependsOn(`logging-service`)
.dependsOn(testkit)
.dependsOn(cli)
.dependsOn(`distribution-manager`)

View File

@ -10,250 +10,297 @@ order: 6
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 connect to it. The service
aggregates all logs in one place for easier analysis of the interaction between
components.
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" -->
- [Protocol](#protocol)
- [Types](#types)
- [Messages](#messages)
- [Examples](#examples)
- [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)
- [Configuration](#configuration)
- [Logging in Tests](#logging-in-tests)
<!-- /MarkdownTOC -->
## Protocol
## Configuration
The service relies on a WebSocket connection to a specified endpoint that
exchanges JSON-encoded text messages. The communication is uni-directional - the
only messages are log messages that are sent from a connected client to the
server that aggregates the logs.
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.
### Types
The configuration has two main sections:
##### `LogLevel`
- [custom log levels](#custom-log-levels)
- [applications' appenders](#appenders) (also known as configuration of log
events output target)
The log level encoded as a number. Possible values are:
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.
- 0 - indicating `ERROR` level,
- 1 - indicating `WARN` level,
- 2 - indicating `INFO` level,
- 3 - indicating `DEBUG` level,
- 4 - indicating `TRACE` level.
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
```typescript
type LogLevel = 0 | 1 | 2 | 3 | 4;
```
{
host = localhost
host = $ENSO_HOST
}
```
##### `UTCTime`
defines a `host` key once, except that `ENSO_HOST` values takes a precedence if
it is defined during loading of the config file.
Message timestamp encoded as milliseconds elapsed from the UNIX epoch, i.e.
1970-01-01T00:00:00Z.
### 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.
```typescript
type UTCTime = number;
```
##### `Exception`
Encodes an exception that is related to a log message.
The `cause` field may be omitted if the exception does not have another
exception as its cause.
```typescript
interface Exception {
// Name of the exception. In Java this can be the qualified classname.
name: String;
// Message associated with the exception. May be empty.
message: String;
// A stack trace indicating code location where the exception has originated
// from. May be empty if unavailable.
trace: [TraceElement];
// Optional, another exception that caused this one.
cause?: Exception;
}
```
##### `TraceElement`
Represents a single element of exception's stacktrace.
```typescript
interface TraceElement {
// Name of the stack location. For example, in Java this can be a qualified
// method name.
element: String;
// Code location of the element.
location: String;
}
```
In Java, the location is usually a filename and line number locating the code
that corresponds to the indicated stack location, for example `Main.java:123`.
Native methods may be handled differently, as well as code from different
languages, for example Enso also includes the columns - `Test.enso:4:3-19`.
### Messages
Currently, the service supports only one message type - `LogMessage`, messages
not conforming to this format will be ignored. The first non-conforming message
for each connection will emit a warning.
#### `LogMessage`
Describes the log message that the server should report and does not expect any
response.
##### Parameters
```typescript
{
// Log level associated with the message.
level: LogLevel;
// Timestamp indicating when the message was sent.
time: UTCTime;
// An identifier of a log group - the group should indicate which component
// the message originated from and any (possibly nested) context.
group: String;
// The actual log message.
message: String;
// Optional exception associated with the message.
exception?: Exception;
}
```
The `exception` field may be omitted if there is no exception associated with
the message.
In general, the `group` name can be arbitrary, but it is often the quallified
name of the class that the log message originates from and it is sometimes
extended with additional nested context, for example:
- `org.enso.launcher.cli.Main`
- `org.enso.compiler.pass.analyse.AliasAnalysis.analyseType`
### Examples
For example, an error message with an attached exception may look like this (the
class names are made up):
```json
{
"level": 0,
"time": 1600864353151,
"group": "org.enso.launcher.Main",
"message": "Failed to load a configuration file.",
"exception": {
"name": "org.enso.componentmanager.config.ConfigurationLoaderFailure",
"message": "Configuration file does not exist.",
"trace": [
{
"element": "org.enso.componentmanager.config.ConfigurationLoader.load",
"location": "ConfigurationLoader.scala:123"
},
{
"element": "org.enso.launcher.Main",
"location": "Main.scala:42"
}
],
"cause": {
"name": "java.io.FileNotFoundException",
"message": "config.yaml (No such file or directory)",
"trace": []
}
logging-service.logger {
akka.actor = info
akka.event = error
akka.io = error
slick {
jdbc.JdbcBackend.statement = debug
"*" = error
}
}
```
Another example could be an info message (without attached exceptions):
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.
```json
{
"level": 2,
"time": 1600864353151,
"group": "org.enso.launcher.Main",
"message": "Configuration file loaded successfully."
}
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 `$ENSO_APPENDER_DEFAULT`.
#### 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
A default implementation of both a client and server for the logger service are
provided for the JVM.
Enso's logging makes use of two logging APIs - `java.util.logging` and
`org.slf4j`. The former is being used 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
`LoggerFacotry` 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 `logging-service` provides a class `org.enso.loggingservice.WSLogger` which
implements the `org.slf4j.Logger` interface, so it is compatible with all code
using SLF4J logging. When the `logging-service` is added to a project, it
automatically binds its logger instance as the SLF4J backend. So from the
perspective of the user, all that they have to do is use SLF4J compliant logging
in the application.
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, but for Scala code, it is
much better to use the `com.typesafe.scalalogging.Logger` 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. Additionally, the
`logging-service` provides syntactic sugar for working with nested logging
contexts.
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.
```
package foo
import com.typesafe.scalalogging.Logger
import org.enso.logger.LoggerSyntax
```java
package foo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Foo {
private val logger = Logger[Foo]
public class Foo {
private Logger logger = LoggerFactory.getLogger(Foo.class);
def bar(): Unit = {
logger.info("Hello world") // Logs `Hello world` from context `foo.Foo`.
baz()
}
def baz(): Unit = {
val bazLogger = logger.enter("baz")
bazLogger.warn("Inner") // Logs `Inner` from context `foo.Foo.baz`
}
public void bar() {
logger.info("Hello world!");
}
}
```
The `enter` extension method follows the convention that each level of context
nesting is separated by `.`, much like package names. The root context is
usually the qualified name of the relevant class, but other components are free
to use other conventions if needed.
### Setting Up Logging
The logger described above must know where it should send its logs, and this is
handled by the `LoggingServiceManager`. It allows to configure the logging
location, log level and setup the logging service in one of three different
modes:
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.
- _Server mode_, that will listen on a given port, gather both local and remote
logs and print them to stderr and to a file.
- _Client mode_, that will connect to a specified server and send all of its
logs there. It will not print anything.
- _Fallback mode_, that will just write the logs to stderr (and optionally) a
file, without setting up any services or connections.
```java
package foo;
import org.enso.logger.LoggerSetup;
import org.slf4j.event.Level;
This logging mode initialization cannot usually happen at the time of static
initialization, since the connection details may depend on CLI arguments or
other configuration which may not be accessed immediately. To help with this,
the logger will buffer any log messages that are issued before the
initialization has happened and send them as soon as the service is initialized.
public class MyService {
In a rare situation where the service would not be initialized at all, a
shutdown hook is added that will print the pending log messages before exiting.
Some of the messages may be dropped, however, if more messages are buffered than
the buffer can hold.
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
@ -281,51 +328,8 @@ 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.
### Configuration
The Logging Service settings should be placed under the `logging-service` key of
the `application.conf` config.
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.
### Logging in Tests
The Logging Service provides several utilities for managing logs inside of
tests.
The primary method for setting log-level for all tests in a project is by
creating an `application.conf` file in `resources` of the `test` target with the
configuration key `logging-service.test-log-level` which should be set to a log
level name (possible values are: `off`, `error`, `warning`, `info`, `debug`,
`trace`). If this key is set to any value, the default logging queue is replaced
with a special test queue which handles the log messages depending on status of
the service. If a service has been set up, it just forwards them (so tests can
easily override the log handling). However if it has not been set up, the
enabled log messages are printed to STDERR and the rest is dropped.
Another useful tool is `TestLogger.gatherLogs` - a function that wraps an action
and will return a sequence of logs reported when performing that action. It can
be used to verify logs of an action inside of a test.
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.

View File

@ -1,3 +1,5 @@
## Language Server's application.conf
akka {
actor.debug.lifecycle = on
http {
@ -11,13 +13,34 @@ akka {
log-dead-letters-during-shutdown = off
}
logging-service.logger {
akka.actor = info
akka.event = error
akka.io = error
akka.stream = error
slick.jdbc.JdbcBackend.statement = error # log SQL queries on debug level
slick."*" = error
org.eclipse.jgit = error
io.methvin.watcher = error
logging-service {
logger {
akka.actor = info
akka.event = error
akka.routing = error
akka.io = error
akka.stream = error
slick.jdbc.JdbcBackend.statement = error # log SQL queries on debug level
slick."*" = error
org.eclipse.jgit = error
io.methvin.watcher = error
# Log levels to limit during very verbose setting:
#org.enso.languageserver.protocol.json.JsonConnectionController = debug
#org.enso.jsonrpc.JsonRpcServer = debug
#org.enso.languageserver.runtime.RuntimeConnector = debug
}
appenders = [
{
name = "socket"
hostname = "localhost"
hostname = ${?ENSO_LOGSERVER_HOSTNAME}
port = 6000
port = ${?ENSO_LOGSERVER_PORT}
},
{
name = "console"
}
]
default-appender = socket
default-appender = ${?ENSO_APPENDER_DEFAULT}
}

View File

@ -14,9 +14,8 @@ import org.enso.languageserver.runtime.RuntimeKiller.{
RuntimeShutdownResult,
ShutDownRuntime
}
import org.enso.loggingservice.LogLevel
import org.enso.profiling.{FileSampler, MethodsSampler, NoopSampler}
import org.slf4j.event.Level
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContextExecutor, Future}
@ -25,7 +24,7 @@ import scala.concurrent.{Await, ExecutionContextExecutor, Future}
* @param config a LS config
* @param logLevel log level for the Language Server
*/
class LanguageServerComponent(config: LanguageServerConfig, logLevel: LogLevel)
class LanguageServerComponent(config: LanguageServerConfig, logLevel: Level)
extends LifecycleComponent
with LazyLogging {

View File

@ -43,19 +43,21 @@ import org.enso.librarymanager.LibraryLocations
import org.enso.librarymanager.local.DefaultLocalLibraryProvider
import org.enso.librarymanager.published.PublishedLibraryCache
import org.enso.lockmanager.server.LockManagerService
import org.enso.logger.Converter
import org.enso.logger.masking.{MaskedPath, Masking}
import org.enso.loggingservice.{JavaLoggingLogHandler, LogLevel}
import org.enso.logger.JulHandler
import org.enso.logger.akka.AkkaConverter
import org.enso.polyglot.{HostAccessFactory, RuntimeOptions, RuntimeServerInfo}
import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo}
import org.enso.text.{ContentBasedVersioning, Sha3_224VersionCalculator}
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.io.MessageEndpoint
import org.slf4j.event.Level
import org.slf4j.LoggerFactory
import java.io.File
import java.net.URI
import java.time.Clock
import scala.concurrent.duration._
import scala.util.{Failure, Success}
@ -64,7 +66,7 @@ import scala.util.{Failure, Success}
* @param serverConfig configuration for the language server
* @param logLevel log level for the Language Server
*/
class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
class MainModule(serverConfig: LanguageServerConfig, logLevel: Level) {
private val log = LoggerFactory.getLogger(this.getClass)
log.info(
@ -294,7 +296,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
.option(RuntimeOptions.PROJECT_ROOT, serverConfig.contentRootPath)
.option(
RuntimeOptions.LOG_LEVEL,
JavaLoggingLogHandler.getJavaLogLevelFor(logLevel).getName
Converter.toJavaLevel(logLevel).getName
)
.option(RuntimeOptions.LOG_MASKING, Masking.isMaskingEnabled.toString)
.option(RuntimeOptions.EDITION_OVERRIDE, Info.currentEdition)
@ -306,9 +308,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
.out(stdOut)
.err(stdErr)
.in(stdIn)
.logHandler(
JavaLoggingLogHandler.create(JavaLoggingLogHandler.defaultLevelMapping)
)
.logHandler(JulHandler.get())
.serverTransport((uri: URI, peerEndpoint: MessageEndpoint) => {
if (uri.toString == RuntimeServerInfo.URI) {
val connection = new RuntimeConnector.Endpoint(
@ -322,7 +322,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
.build()
log.trace("Created Runtime context [{}].", context)
system.eventStream.setLogLevel(LogLevel.toAkka(logLevel))
system.eventStream.setLogLevel(AkkaConverter.toAkka(logLevel))
log.trace("Set akka log level to [{}].", logLevel)
val runtimeKiller =

View File

@ -2,11 +2,14 @@ package org.enso.languageserver.libraries
import org.enso.editions.LibraryName
import org.enso.libraryupload.DependencyExtractor
import org.enso.loggingservice.{JavaLoggingLogHandler, LogLevel}
import org.enso.logger.Converter
import org.enso.logger.JulHandler
import org.enso.pkg.Package
import org.enso.pkg.SourceFile
import org.enso.polyglot.{HostAccessFactory, PolyglotContext, RuntimeOptions}
import org.graalvm.polyglot.Context
import org.slf4j.event.Level
import java.io.File
@ -17,7 +20,7 @@ import java.io.File
* @param logLevel the log level to use for the runtime context that will do
* the parsing
*/
class CompilerBasedDependencyExtractor(logLevel: LogLevel)
class CompilerBasedDependencyExtractor(logLevel: Level)
extends DependencyExtractor[File] {
/** @inheritdoc */
@ -60,11 +63,9 @@ class CompilerBasedDependencyExtractor(logLevel: LogLevel)
.option("js.foreign-object-prototype", "true")
.option(
RuntimeOptions.LOG_LEVEL,
JavaLoggingLogHandler.getJavaLogLevelFor(logLevel).getName
)
.logHandler(
JavaLoggingLogHandler.create(JavaLoggingLogHandler.defaultLevelMapping)
Converter.toJavaLevel(logLevel).getName
)
.logHandler(JulHandler.get())
.build
new PolyglotContext(context)
}

View File

@ -19,7 +19,7 @@ import org.enso.languageserver.libraries.{
import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.util.UnhandledLogging
import org.enso.libraryupload.{auth, LibraryUploader}
import org.enso.loggingservice.LoggingServiceManager
import org.enso.logging.LoggingServiceManager
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration

View File

@ -2,16 +2,17 @@ package org.enso.languageserver.util
import akka.actor.Actor
import com.typesafe.scalalogging.LazyLogging
import org.enso.loggingservice.LogLevel
import org.enso.logger.akka.AkkaConverter
import org.slf4j.event.Level
trait UnhandledLogging extends LazyLogging { this: Actor =>
private val akkaLogLevel = LogLevel
private val akkaLogLevel = AkkaConverter
.fromString(context.system.settings.LogLevel)
.getOrElse(LogLevel.Error)
.orElse(Level.ERROR)
override def unhandled(message: Any): Unit = {
if (implicitly[Ordering[LogLevel]].lteq(LogLevel.Warning, akkaLogLevel)) {
if (Level.WARN.toInt <= akkaLogLevel.toInt) {
logger.warn("Received unknown message [{}].", message.getClass)
}
}

View File

@ -5,7 +5,28 @@ akka.loglevel = "ERROR"
akka.test.timefactor = ${?CI_TEST_TIMEFACTOR}
akka.test.single-expect-default = 5s
logging-service.test-log-level = warning
searcher.db.numThreads = 1
searcher.db.properties.journal_mode = "memory"
logging-service {
logger {
akka.actor = info
akka.event = error
akka.routing = error
akka.io = error
akka.stream = error
slick.jdbc.JdbcBackend.statement = error # log SQL queries on debug level
slick."*" = error
org.eclipse.jgit = error
io.methvin.watcher = error
}
appenders = [
{
name = "console"
pattern = "[%level] [%d{yyyy-MM-ddTHH:mm:ssXXX}] [%logger] %msg%n%nopex"
}
]
default-appender = console
log-level = "error"
}

View File

@ -45,7 +45,7 @@ import org.enso.languageserver.vcsmanager.{Git, VcsManager}
import org.enso.librarymanager.LibraryLocations
import org.enso.librarymanager.local.DefaultLocalLibraryProvider
import org.enso.librarymanager.published.PublishedLibraryCache
import org.enso.loggingservice.LogLevel
import org.enso.logger.LoggerSetup
import org.enso.pkg.PackageManager
import org.enso.polyglot.data.TypeGraph
import org.enso.polyglot.runtime.Runtime.Api
@ -57,10 +57,10 @@ import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo}
import org.enso.testkit.{EitherValue, WithTemporaryDirectory}
import org.enso.text.Sha3_224VersionCalculator
import org.scalatest.OptionValues
import org.slf4j.event.Level
import java.nio.file.{Files, Path}
import java.util.UUID
import scala.concurrent.Await
import scala.concurrent.duration._
@ -75,6 +75,8 @@ class BaseServerTest
val timeout: FiniteDuration = 10.seconds
LoggerSetup.get().setup()
def isFileWatcherEnabled: Boolean = false
val testContentRootId = UUID.randomUUID()
@ -322,7 +324,7 @@ class BaseServerTest
distributionManager,
resourceManager,
Some(languageHome),
new CompilerBasedDependencyExtractor(logLevel = LogLevel.Warning)
new CompilerBasedDependencyExtractor(logLevel = Level.WARN)
)
)

View File

@ -722,5 +722,41 @@
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
}
]
},
{
"name":"org.enso.logger.LogbackSetup",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.DateConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.LevelConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.LineSeparatorConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.LoggerConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.NopThrowableInformationConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.MessageConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.core.rolling.helper.DateTokenConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.core.rolling.helper.IntegerTokenConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
}
]

View File

@ -5,7 +5,10 @@
{ "pattern": "\\Qapplication.conf\\E" },
{ "pattern": "\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" },
{ "pattern": "\\Qreference.conf\\E" },
{ "pattern": "\\Qversion.conf\\E" }
{ "pattern": "\\Qversion.conf\\E" },
{ "pattern": "\\QMETA-INF/MANIFEST.MF\\E" },
{ "pattern": "\\QMETA-INF/services/org.enso.logger.LoggerSetup\\E" },
{ "pattern": "\\QMETA-INF/services/org.enso.logging.LogbackLoggingServiceFactory\\E" }
],
"bundles": []
}

View File

@ -1,12 +1,35 @@
## Launcher's application.conf
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
stdout-loglevel = "ERROR"
}
logging-service.logger {
akka.actor = info
akka.event = error
akka.io = error
akka.stream = error
logging-service {
logger {
akka.actor = info
akka.event = error
akka.io = error
akka.stream = error
}
appenders = [
{
name = "socket"
hostname = "localhost"
hostname = ${?ENSO_LOGSERVER_PORT}
port = 6000
port = ${?ENSO_LOGSERVER_PORT}
},
{
name = "file",
pattern = "[%level{lowercase=true}] [%d{yyyy-MM-dd'T'HH:mm:ssXXX}] [%logger] %msg%n"
},
{
name = "console"
pattern = "[%level{lowercase=true}] [%d{yyyy-MM-dd'T'HH:mm:ssXXX}] [%logger] %msg%n%nopex"
}
]
default-appender = file
default-appender = ${?ENSO_APPENDER_DEFAULT}
}

View File

@ -24,7 +24,7 @@ import org.enso.launcher.installation.{
}
import org.enso.launcher.project.ProjectManager
import org.enso.launcher.upgrade.LauncherUpgrader
import org.enso.loggingservice.LogLevel
import org.slf4j.event.Level
import org.enso.version.{VersionDescription, VersionDescriptionParameter}
/** Implements launcher commands that are run from CLI and can be affected by
@ -207,7 +207,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
def runRepl(
projectPath: Option[Path],
versionOverride: Option[SemVer],
logLevel: LogLevel,
logLevel: Level,
useSystemJVM: Boolean,
jvmOpts: Seq[(String, String)],
additionalArguments: Seq[String]
@ -251,7 +251,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
def runRun(
path: Option[Path],
versionOverride: Option[SemVer],
logLevel: LogLevel,
logLevel: Level,
useSystemJVM: Boolean,
jvmOpts: Seq[(String, String)],
additionalArguments: Seq[String]
@ -293,7 +293,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
options: LanguageServerOptions,
contentRoot: Path,
versionOverride: Option[SemVer],
logLevel: LogLevel,
logLevel: Level,
useSystemJVM: Boolean,
jvmOpts: Seq[(String, String)],
additionalArguments: Seq[String]
@ -331,7 +331,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
*/
def runInstallDependencies(
versionOverride: Option[SemVer],
logLevel: LogLevel,
logLevel: Level,
useSystemJVM: Boolean,
jvmOpts: Seq[(String, String)],
additionalArguments: Seq[String]
@ -396,7 +396,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
path: Option[Path],
uploadUrl: Option[String],
authToken: Option[String],
logLevel: LogLevel,
logLevel: Level,
useSystemJVM: Boolean,
jvmOpts: Seq[(String, String)],
additionalArguments: Seq[String]

View File

@ -1,10 +1,9 @@
package org.enso.launcher.cli
import akka.http.scaladsl.model.Uri
import org.enso.cli.arguments.{Argument, OptsParseError}
import org.enso.launcher.cli.GlobalCLIOptions.InternalOptions
import org.enso.loggingservice.ColorMode.{Always, Auto, Never}
import org.enso.loggingservice.{ColorMode, LogLevel}
import java.net.URI
import org.slf4j.event.Level
/** Gathers settings set by the global CLI options.
*
@ -15,7 +14,6 @@ import org.enso.loggingservice.{ColorMode, LogLevel}
* printed
* @param useJSON specifies if output should be in JSON format, if it is
* supported (currently only the version command supports JSON)
* @param colorMode specifies if console output should contain colors
* @param internalOptions options that are remembered to pass them to launcher
* child processes
*/
@ -23,7 +21,6 @@ case class GlobalCLIOptions(
autoConfirm: Boolean,
hideProgress: Boolean,
useJSON: Boolean,
colorMode: ColorMode,
internalOptions: InternalOptions
)
@ -31,14 +28,13 @@ object GlobalCLIOptions {
val HIDE_PROGRESS = "hide-progress"
val AUTO_CONFIRM = "auto-confirm"
val USE_JSON = "json"
val COLOR_MODE = "color"
/** Internal options that are remembered to pass them to launcher child
* processes.
*/
case class InternalOptions(
launcherLogLevel: Option[LogLevel],
loggerConnectUri: Option[Uri],
launcherLogLevel: Option[Level],
loggerConnectUri: Option[URI],
logMaskingDisabled: Boolean
) {
@ -73,39 +69,6 @@ object GlobalCLIOptions {
val hideProgress =
if (config.hideProgress) Seq(s"--$HIDE_PROGRESS") else Seq()
val useJSON = if (config.useJSON) Seq(s"--$USE_JSON") else Seq()
autoConfirm ++ hideProgress ++ useJSON ++
LauncherColorMode.toOptions(
config.colorMode
) ++ config.internalOptions.toOptions
}
}
object LauncherColorMode {
/** [[Argument]] instance used to parse [[ColorMode]] from CLI.
*/
implicit val argument: Argument[ColorMode] = {
case "never" => Right(Never)
case "no" => Right(Never)
case "auto" => Right(Auto)
case "always" => Right(Always)
case "yes" => Right(Always)
case other =>
OptsParseError.left(
s"Unknown color mode value `$other`. Supported values are: " +
s"never | no | auto | always | yes."
)
}
/** Creates command line options that can be passed to a launcher process to
* inherit our color mode.
*/
def toOptions(colorMode: ColorMode): Seq[String] = {
val name = colorMode match {
case Never => "never"
case Auto => "auto"
case Always => "always"
}
Seq(s"--${GlobalCLIOptions.COLOR_MODE}", name)
autoConfirm ++ hideProgress ++ useJSON ++ config.internalOptions.toOptions
}
}

View File

@ -1,6 +1,5 @@
package org.enso.launcher.cli
import akka.http.scaladsl.model.Uri
import cats.data.NonEmptyList
import cats.implicits._
import nl.gn0s1s.bump.SemVer
@ -9,18 +8,18 @@ import org.enso.cli.arguments.Opts.implicits._
import org.enso.cli.arguments._
import org.enso.distribution.config.DefaultVersion
import org.enso.distribution.config.DefaultVersion._
import org.enso.launcher.cli.LauncherColorMode.argument
import org.enso.launcher.distribution.DefaultManagers._
import org.enso.launcher.installation.DistributionInstaller
import org.enso.launcher.installation.DistributionInstaller.BundleAction
import org.enso.launcher.upgrade.LauncherUpgrader
import org.enso.launcher.{cli, Launcher}
import org.enso.loggingservice.{ColorMode, LogLevel}
import org.enso.runtimeversionmanager.cli.Arguments._
import org.enso.runtimeversionmanager.runner.LanguageServerOptions
import org.slf4j.event.Level
import java.nio.file.Path
import java.util.UUID
import java.net.URI
/** Defines the CLI commands and options for the program.
*
@ -127,12 +126,12 @@ object LauncherApplication {
}
private def engineLogLevel = {
Opts
.optionalParameter[LogLevel](
.optionalParameter[Level](
"log-level",
"(error | warning | info | debug | trace)",
"Sets logging verbosity for the engine. Defaults to info."
)
.withDefault(LogLevel.Info)
.withDefault(Level.INFO)
}
private def runCommand: Command[Config => Int] =
@ -606,14 +605,14 @@ object LauncherApplication {
"running actions. May be needed if program output is piped.",
showInUsage = false
)
val logLevel = Opts.optionalParameter[LogLevel](
val logLevel = Opts.optionalParameter[Level](
GlobalCLIOptions.LOG_LEVEL,
"(error | warning | info | debug | trace)",
"Sets logging verbosity for the launcher. If not provided, defaults to" +
"Sets logging verbosity for the launcher. If not provided, defaults to " +
s"${LauncherLogging.defaultLogLevel}."
)
val connectLogger = Opts
.optionalParameter[Uri](
.optionalParameter[URI](
GlobalCLIOptions.CONNECT_LOGGER,
"URI",
"Instead of starting its own logging service, " +
@ -627,18 +626,6 @@ object LauncherApplication {
"variable.",
showInUsage = false
)
val colorMode =
Opts
.aliasedOptionalParameter[ColorMode](
GlobalCLIOptions.COLOR_MODE,
"colour",
"colors"
)(
"(auto | yes | always | no | never)",
"Specifies if colors should be used in the output, defaults to auto."
)
.withDefault(ColorMode.Auto)
val internalOpts = InternalOpts.topLevelOptions
(
@ -650,8 +637,7 @@ object LauncherApplication {
hideProgress,
logLevel,
connectLogger,
noLogMasking,
colorMode
noLogMasking
) mapN {
(
internalOptsCallback,
@ -662,8 +648,7 @@ object LauncherApplication {
hideProgress,
logLevel,
connectLogger,
disableLogMasking,
colorMode
disableLogMasking
) => () =>
if (shouldEnsurePortable) {
Launcher.ensurePortable()
@ -673,7 +658,6 @@ object LauncherApplication {
autoConfirm = autoConfirm,
hideProgress = hideProgress,
useJSON = useJSON,
colorMode = colorMode,
internalOptions = GlobalCLIOptions.InternalOptions(
logLevel,
connectLogger,
@ -686,9 +670,7 @@ object LauncherApplication {
LauncherLogging.setup(
logLevel,
connectLogger,
globalCLIOptions.colorMode,
!disableLogMasking,
None
!disableLogMasking
)
initializeApp()

View File

@ -1,23 +1,18 @@
package org.enso.launcher.cli
import java.nio.file.Path
import org.enso.launcher.distribution.DefaultManagers
import org.enso.loggingservice.{
ColorMode,
LogLevel,
LoggingServiceManager,
LoggingServiceSetupHelper
}
import org.enso.logger.LoggerSetup
import org.slf4j.event.Level
import org.enso.logging.LoggingSetupHelper
import scala.concurrent.ExecutionContext.Implicits.global
/** Manages setting up the logging service within the launcher.
*/
object LauncherLogging extends LoggingServiceSetupHelper {
object LauncherLogging extends LoggingSetupHelper(global) {
/** @inheritdoc */
override val defaultLogLevel: LogLevel = LogLevel.Warning
override val defaultLogLevel: Level = Level.WARN
/** @inheritdoc */
override val logFileSuffix: String = "enso-launcher"
@ -35,10 +30,9 @@ object LauncherLogging extends LoggingServiceSetupHelper {
* This is necessary on Windows to ensure that the logs file is closed, so
* that the log directory can be removed.
*/
def prepareForUninstall(colorMode: ColorMode): Unit = {
def prepareForUninstall(logLevel: Option[Level]): Unit = {
waitForSetup()
LoggingServiceManager.replaceWithFallback(printers =
Seq(stderrPrinter(colorMode, printExceptions = true))
)
val actualLogLevel = logLevel.getOrElse(defaultLogLevel)
LoggerSetup.get().setupConsoleAppender(actualLogLevel)
}
}

View File

@ -8,11 +8,6 @@ import org.enso.launcher.upgrade.LauncherUpgrader
/** Defines the entry point for the launcher.
*/
object Main {
private def setup(): Unit =
System.setProperty(
"org.apache.commons.logging.Log",
"org.apache.commons.logging.impl.NoOpLog"
)
private def runAppHandlingParseErrors(args: Array[String]): Int =
LauncherApplication.application.run(args) match {
@ -29,7 +24,8 @@ object Main {
/** Entry point of the application.
*/
def main(args: Array[String]): Unit = {
setup()
// Disable logging prior to parsing arguments (may generate additional and unnecessary logs)
LauncherLogging.initLogger()
val exitCode =
try {
LauncherUpgrader.recoverUpgradeRequiredErrors(args) {
@ -37,7 +33,8 @@ object Main {
}
} catch {
case e: Exception =>
logger.error(s"A fatal error has occurred: $e", e)
LauncherLogging.setupFallback()
logger.error("A fatal error has occurred: {}", e.getMessage, e)
1
}
@ -46,7 +43,7 @@ object Main {
/** Exits the program in a safe way.
*
* This should be used ofer `sys.exit` to ensure that all services are
* This should be used after `sys.exit` to ensure that all services are
* terminated gracefully and locks are released quickly (as the OS cleanup
* may take a longer while). The only exception is for functions in the
* [[InternalOpts]], because they may need to terminate the program as

View File

@ -1,16 +1,17 @@
package org.enso.launcher.components
import akka.http.scaladsl.model.Uri
import nl.gn0s1s.bump.SemVer
import org.enso.distribution.{DistributionManager, Environment}
import org.enso.editions.updater.EditionManager
import org.enso.launcher.Constants
import org.enso.launcher.project.ProjectManager
import org.enso.logger.masking.MaskedPath
import org.enso.loggingservice.LogLevel
import java.net.URI
import org.enso.runtimeversionmanager.components.RuntimeVersionManager
import org.enso.runtimeversionmanager.config.GlobalRunnerConfigurationManager
import org.enso.runtimeversionmanager.runner._
import org.slf4j.event.Level
import java.nio.file.{Files, Path}
import scala.concurrent.Future
@ -25,7 +26,7 @@ class LauncherRunner(
componentsManager: RuntimeVersionManager,
editionManager: EditionManager,
environment: Environment,
loggerConnection: Future[Option[Uri]]
loggerConnection: Future[Option[URI]]
) extends Runner(
componentsManager,
distributionManager,
@ -42,7 +43,7 @@ class LauncherRunner(
def repl(
projectPath: Option[Path],
versionOverride: Option[SemVer],
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
additionalArguments: Seq[String]
): Try[RunSettings] =
@ -78,7 +79,7 @@ class LauncherRunner(
def run(
path: Option[Path],
versionOverride: Option[SemVer],
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
additionalArguments: Seq[String]
): Try[RunSettings] =
@ -131,7 +132,7 @@ class LauncherRunner(
}
private def setLogLevelArgs(
level: LogLevel,
level: Level,
logMasking: Boolean
): Seq[String] =
Seq("--log-level", level.name) ++
@ -145,7 +146,7 @@ class LauncherRunner(
options: LanguageServerOptions,
contentRootPath: Path,
versionOverride: Option[SemVer],
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
additionalArguments: Seq[String]
): Try[RunSettings] =
@ -204,7 +205,7 @@ class LauncherRunner(
uploadUrl: String,
token: Option[String],
hideProgress: Boolean,
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
additionalArguments: Seq[String]
): Try[RunSettings] =
@ -250,7 +251,7 @@ class LauncherRunner(
def installDependencies(
versionOverride: Option[SemVer],
hideProgress: Boolean,
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
additionalArguments: Seq[String]
): Try[RunSettings] =

View File

@ -199,7 +199,9 @@ class DistributionUninstaller(
dataRoot.toAbsolutePath.normalize
)
if (logsInsideData) {
LauncherLogging.prepareForUninstall(globalCLIOptions.colorMode)
LauncherLogging.prepareForUninstall(
globalCLIOptions.internalOptions.launcherLogLevel
)
}
for (dirName <- knownDataDirectories) {

View File

@ -25,8 +25,8 @@ import org.enso.runtimeversionmanager.releases.ReleaseProvider
import org.enso.launcher.releases.LauncherRepository
import org.enso.launcher.InfoLogger
import org.enso.launcher.distribution.DefaultManagers
import org.enso.logger.LoggerSyntax
import org.enso.runtimeversionmanager.locking.Resources
import org.slf4j.LoggerFactory
import scala.util.Try
import scala.util.control.NonFatal
@ -428,7 +428,9 @@ object LauncherUpgrader {
upgradeRequiredError: UpgradeRequiredError,
originalArguments: Array[String]
): Int = {
val logger = Logger[LauncherUpgrader].enter("auto-upgrade")
val logger = LoggerFactory.getLogger(
classOf[LauncherUpgrader]
)
val globalCLIOptions = cachedCLIOptions.getOrElse(
throw new IllegalStateException(
"Upgrade requested but application was not initialized properly."

View File

@ -1 +1,12 @@
logging-service.test-log-level = warning
logging-service {
appenders = [
{
name = "console"
pattern = "[%level] [%d{yyyy-MM-ddTHH:mm:ssXXX}] [%logger] %msg%n%nopex"
}
]
default-appender = console
log-level = "warn"
}

View File

@ -1,8 +1,8 @@
package org.enso.launcher.components
import java.nio.file.{Files, Path}
import java.net.URI
import java.util.UUID
import akka.http.scaladsl.model.Uri
import nl.gn0s1s.bump.SemVer
import org.enso.distribution.FileSystem.PathSyntax
import org.enso.editions.updater.EditionManager
@ -10,7 +10,9 @@ import org.enso.runtimeversionmanager.config.GlobalRunnerConfigurationManager
import org.enso.runtimeversionmanager.runner._
import org.enso.runtimeversionmanager.test.RuntimeVersionManagerTest
import org.enso.launcher.project.ProjectManager
import org.enso.loggingservice.{LogLevel, TestLogger}
import org.enso.logger.TestLogger
import org.slf4j.event.Level
import org.enso.testkit.FlakySpec
import scala.concurrent.Future
@ -21,7 +23,7 @@ import scala.concurrent.Future
class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
private val defaultEngineVersion = SemVer(0, 0, 0, Some("default"))
private val fakeUri = Uri("ws://test:1234/")
private val fakeUri = URI.create("ws://test:1234/")
def makeFakeRunner(
cwdOverride: Option[Path] = None,
@ -178,23 +180,25 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
val runner = makeFakeRunner()
val projectPath = getTestDirectory / "project2"
val nightlyVersion = SemVer(0, 0, 0, Some("SNAPSHOT.2000-01-01"))
val (_, logs) = TestLogger.gatherLogs {
runner
.newProject(
path = projectPath,
name = "ProjectName2",
engineVersion = nightlyVersion,
normalizedName = None,
projectTemplate = None,
authorName = None,
authorEmail = None,
additionalArguments = Seq()
)
.get
}
val (_, logs) = TestLogger.gather[Any, Runner](
classOf[Runner], {
runner
.newProject(
path = projectPath,
name = "ProjectName2",
engineVersion = nightlyVersion,
normalizedName = None,
projectTemplate = None,
authorName = None,
authorEmail = None,
additionalArguments = Seq()
)
.get
}
)
assert(
logs.exists(msg =>
msg.logLevel == LogLevel.Warning && msg.message.contains(
msg.level == Level.WARN && msg.msg.contains(
"Consider using a stable version."
)
)
@ -208,7 +212,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
projectPath = None,
versionOverride = None,
additionalArguments = Seq("arg", "--flag"),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -234,7 +238,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
projectPath = Some(projectPath),
versionOverride = None,
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -249,7 +253,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
projectPath = None,
versionOverride = None,
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -264,7 +268,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
projectPath = Some(projectPath),
versionOverride = Some(overridden),
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -293,7 +297,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
contentRootPath = projectPath,
versionOverride = None,
additionalArguments = Seq("additional"),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -315,7 +319,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
contentRootPath = projectPath,
versionOverride = Some(overridden),
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -335,7 +339,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
path = Some(projectPath),
versionOverride = None,
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -350,7 +354,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
path = None,
versionOverride = None,
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -365,7 +369,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
path = Some(projectPath),
versionOverride = Some(overridden),
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -380,7 +384,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
path = None,
versionOverride = None,
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.isFailure,
@ -407,7 +411,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
path = Some(outsideFile),
versionOverride = None,
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get
@ -432,7 +436,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec {
path = Some(insideFile),
versionOverride = None,
additionalArguments = Seq(),
logLevel = LogLevel.Info,
logLevel = Level.INFO,
logMasking = true
)
.get

View File

@ -215,5 +215,41 @@
{
"name": "org.apache.commons.compress.archivers.zip.Zip64ExtendedInformationExtraField",
"methods": [{ "name": "<init>", "parameterTypes": [] }]
},
{
"name": "org.apache.commons.compress.archivers.zip.Zip64ExtendedInformationExtraField",
"methods": [{ "name": "<init>", "parameterTypes": [] }]
},
{
"name":"ch.qos.logback.classic.pattern.DateConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.LevelConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.LineSeparatorConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.NopThrowableInformationConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.LoggerConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.pattern.MessageConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.core.rolling.helper.DateTokenConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.core.rolling.helper.IntegerTokenConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
}
]

View File

@ -27,6 +27,12 @@
},
{
"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
},
{
"pattern":"\\Qapplication.conf\\E"
},
{
"pattern":"\\Qch/qos/logback/classic/spi/Configurator.class\\E"
}
]},
"bundles":[]

View File

@ -278,9 +278,6 @@
{
"name":"org.enso.compiler.pass.resolve.TypeSignatures$Signature"
},
{
"name":"org.enso.data.Shifted"
},
{
"name":"org.enso.pkg.QualifiedName"
},

View File

@ -1,12 +1,14 @@
package org.enso.runner
import org.enso.loggingservice.{JavaLoggingLogHandler, LogLevel}
import org.enso.logger.Converter
import org.enso.logger.JulHandler
import org.enso.polyglot.debugger.{
DebugServerInfo,
DebuggerSessionManagerEndpoint
}
import org.enso.polyglot.{HostAccessFactory, PolyglotContext, RuntimeOptions}
import org.graalvm.polyglot.Context
import org.slf4j.event.Level
import java.io.{File, InputStream, OutputStream}
@ -36,7 +38,7 @@ class ContextFactory {
in: InputStream,
out: OutputStream,
repl: Repl,
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
enableIrCaches: Boolean,
strictErrors: Boolean = false,
@ -49,6 +51,7 @@ class ContextFactory {
executionEnvironment.foreach { name =>
options.put("enso.ExecutionEnvironment", name)
}
val logLevelName = Converter.toJavaLevel(logLevel).getName
val builder = Context
.newBuilder()
.allowExperimentalOptions(true)
@ -83,11 +86,9 @@ class ContextFactory {
}
.option(
RuntimeOptions.LOG_LEVEL,
JavaLoggingLogHandler.getJavaLogLevelFor(logLevel).getName
)
.logHandler(
JavaLoggingLogHandler.create(JavaLoggingLogHandler.defaultLevelMapping)
logLevelName
)
.logHandler(JulHandler.get())
val graalpy = new File(
new File(
new File(new File(new File(projectRoot), "polyglot"), "python"),

View File

@ -1,6 +1,7 @@
package org.enso.runner
import cats.implicits.toTraverseOps
import org.slf4j.event.Level
import com.typesafe.scalalogging.Logger
import org.enso.cli.ProgressBar
import org.enso.cli.task.{ProgressReporter, TaskProgress}
@ -16,7 +17,6 @@ import org.enso.editions.{DefaultEdition, EditionResolver}
import org.enso.languageserver.libraries.CompilerBasedDependencyExtractor
import org.enso.librarymanager.dependencies.DependencyResolver
import org.enso.librarymanager.{DefaultLibraryProvider, LibraryResolver}
import org.enso.loggingservice.LogLevel
import org.enso.pkg.PackageManager
import java.io.File
@ -28,7 +28,7 @@ object DependencyPreinstaller {
* to find all transitive dependencies and ensures that all of them are
* installed.
*/
def preinstallDependencies(projectRoot: File, logLevel: LogLevel): Unit = {
def preinstallDependencies(projectRoot: File, logLevel: Level): Unit = {
val logger = Logger[DependencyPreinstaller.type]
val pkg = PackageManager.Default.loadPackage(projectRoot).get

View File

@ -4,7 +4,7 @@ import org.enso.languageserver.boot.{
LanguageServerComponent,
LanguageServerConfig
}
import org.enso.loggingservice.LogLevel
import org.slf4j.event.Level
import java.util.concurrent.Semaphore
@ -26,7 +26,7 @@ object LanguageServerApp {
*/
def run(
config: LanguageServerConfig,
logLevel: LogLevel,
logLevel: Level,
deamonize: Boolean
): Unit = {
val server = new LanguageServerComponent(config, logLevel)

View File

@ -1,6 +1,6 @@
package org.enso.runner
import akka.http.scaladsl.model.{IllegalUriException, Uri}
import akka.http.scaladsl.model.{IllegalUriException}
import buildinfo.Info
import cats.implicits._
import com.typesafe.scalalogging.Logger
@ -14,13 +14,14 @@ import org.enso.languageserver.boot.{
StartupConfig
}
import org.enso.libraryupload.LibraryUploader.UploadFailedError
import org.enso.loggingservice.LogLevel
import org.slf4j.event.Level
import org.enso.pkg.{Contact, PackageManager, Template}
import org.enso.polyglot.{HostEnsoUtils, LanguageInfo, Module, PolyglotContext}
import org.enso.version.VersionDescription
import org.graalvm.polyglot.PolyglotException
import java.io.File
import java.net.URI
import java.nio.file.{Path, Paths}
import java.util.{HashMap, UUID}
import scala.Console.err
@ -524,7 +525,7 @@ object Main {
packagePath: String,
shouldCompileDependencies: Boolean,
shouldUseGlobalCache: Boolean,
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean
): Unit = {
val file = new File(packagePath)
@ -575,7 +576,7 @@ object Main {
path: String,
additionalArgs: Array[String],
projectPath: Option[String],
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
enableIrCaches: Boolean,
enableAutoParallelism: Boolean,
@ -660,7 +661,7 @@ object Main {
*/
private def genDocs(
projectPath: Option[String],
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
enableIrCaches: Boolean
): Unit = {
@ -682,7 +683,7 @@ object Main {
*/
private def generateDocsFrom(
path: String,
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
enableIrCaches: Boolean
): Unit = {
@ -724,7 +725,7 @@ object Main {
*/
private def preinstallDependencies(
projectPath: Option[String],
logLevel: LogLevel
logLevel: Level
): Unit = projectPath match {
case Some(path) =>
try {
@ -875,7 +876,7 @@ object Main {
*/
private def runRepl(
projectPath: Option[String],
logLevel: LogLevel,
logLevel: Level,
logMasking: Boolean,
enableIrCaches: Boolean
): Unit = {
@ -914,7 +915,7 @@ object Main {
* @param line a CLI line
* @param logLevel log level to set for the engine runtime
*/
private def runLanguageServer(line: CommandLine, logLevel: LogLevel): Unit = {
private def runLanguageServer(line: CommandLine, logLevel: Level): Unit = {
val maybeConfig = parseServerOptions(line)
maybeConfig match {
@ -1000,11 +1001,11 @@ object Main {
/** Parses the log level option.
*/
def parseLogLevel(levelOption: String): LogLevel = {
def parseLogLevel(levelOption: String): Level = {
val name = levelOption.toLowerCase
LogLevel.allLevels.find(_.toString.toLowerCase == name).getOrElse {
Level.values().find(_.name().toLowerCase() == name).getOrElse {
val possible =
LogLevel.allLevels.map(_.toString.toLowerCase).mkString(", ")
Level.values().map(_.toString.toLowerCase).mkString(", ")
System.err.println(s"Invalid log level. Possible values are $possible.")
exitFail()
}
@ -1012,9 +1013,9 @@ object Main {
/** Parses an URI that specifies the logging service connection.
*/
def parseUri(string: String): Uri =
def parseUri(string: String): URI =
try {
Uri(string)
URI.create(string)
} catch {
case _: IllegalUriException =>
System.err.println(s"`$string` is not a valid URI.")
@ -1023,7 +1024,7 @@ object Main {
/** Default log level to use if the LOG_LEVEL option is not provided.
*/
val defaultLogLevel: LogLevel = LogLevel.Error
val defaultLogLevel: Level = Level.ERROR
/** Main entry point for the CLI program.
*
@ -1179,7 +1180,7 @@ object Main {
}
}
/** Checks whether IR caching should be enabled.o
/** Checks whether IR caching should be enabled.
*
* The (mutually exclusive) flags can control it explicitly, otherwise it
* defaults to off in development builds and on in production builds.

View File

@ -5,8 +5,8 @@ import org.enso.cli.ProgressBar
import org.enso.cli.task.{ProgressReporter, TaskProgress}
import org.enso.languageserver.libraries.CompilerBasedDependencyExtractor
import org.enso.libraryupload.{auth, LibraryUploader}
import org.enso.loggingservice.LogLevel
import org.enso.pkg.PackageManager
import org.slf4j.event.Level
import java.nio.file.Path
@ -31,7 +31,7 @@ object ProjectUploader {
uploadUrl: String,
authToken: Option[String],
showProgress: Boolean,
logLevel: LogLevel
logLevel: Level
): Unit = {
import scala.concurrent.ExecutionContext.Implicits.global
val progressReporter = new ProgressReporter {
@ -69,7 +69,7 @@ object ProjectUploader {
* @param logLevel the log level to use for the context gathering
* dependencies
*/
def updateManifest(projectRoot: Path, logLevel: LogLevel): Unit = {
def updateManifest(projectRoot: Path, logLevel: Level): Unit = {
val pkg = PackageManager.Default.loadPackage(projectRoot.toFile).get
val dependencyExtractor = new CompilerBasedDependencyExtractor(logLevel)

View File

@ -1,18 +1,20 @@
package org.enso.runner
import akka.http.scaladsl.model.Uri
import java.net.URI
import com.typesafe.scalalogging.Logger
import org.enso.logger.LoggerSetup
import org.enso.logger.masking.Masking
import org.enso.loggingservice.printers.StderrPrinter
import org.enso.loggingservice.{LogLevel, LoggerMode, LoggingServiceManager}
import org.slf4j.event.Level
import scala.concurrent.Future
import scala.util.{Failure, Success}
import scala.concurrent.Future
/** Manages setting up the logging service within the runner.
*/
object RunnerLogging {
private val logger = Logger[RunnerLogging.type]
/** Sets up the runner's logging service.
*
* If `connectionUri` is provided it tries to connect to a logging service
@ -24,53 +26,53 @@ object RunnerLogging {
* @param logMasking switches log masking on and off
*/
def setup(
connectionUri: Option[Uri],
logLevel: LogLevel,
connectionUri: Option[URI],
logLevel: Level,
logMasking: Boolean
): Unit = {
import scala.concurrent.ExecutionContext.Implicits.global
Masking.setup(logMasking)
val loggerSetup = connectionUri match {
val loggerSetup = LoggerSetup.get()
val initializedLogger = connectionUri match {
case Some(uri) =>
LoggingServiceManager
.setup(
LoggerMode.Client(uri),
logLevel
Future {
loggerSetup.setupSocketAppender(
logLevel,
uri.getHost(),
uri.getPort()
)
.map { _ =>
logger.trace("Connected to logging service at [{}].", uri)
}
.recoverWith { _ =>
logger.error(
}
.map(success =>
if (success) {
logger.trace("Connected to logging service at [{}].", uri)
true
} else
throw new RuntimeException("Failed to connect to logging service")
)
.recoverWith[Boolean] { _ =>
System.err.println(
"Failed to connect to the logging service server, " +
"falling back to local logging."
)
setupLocalLogger(logLevel)
Future.successful(loggerSetup.setupConsoleAppender(logLevel))
}
case None =>
setupLocalLogger(logLevel)
Future.successful(loggerSetup.setupConsoleAppender(logLevel))
}
loggerSetup.onComplete {
initializedLogger.onComplete {
case Failure(exception) =>
System.err.println(s"Failed to initialize logging: $exception")
exception.printStackTrace()
case Success(_) =>
System.err.println("Logger setup: " + exception.getMessage)
case Success(success) =>
if (!success) {
System.err.println("Failed to initialize logging infrastructure")
}
}
}
private def setupLocalLogger(logLevel: LogLevel): Future[Unit] =
LoggingServiceManager
.setup(
LoggerMode.Local(
Seq(StderrPrinter.create(printExceptions = true))
),
logLevel
)
private val logger = Logger[RunnerLogging.type]
/** Shuts down the logging service gracefully.
*/
def tearDown(): Unit =
LoggingServiceManager.tearDown()
def tearDown(): Unit = {
LoggerSetup.get().teardown()
}
}

View File

@ -55,6 +55,10 @@ public class DebuggingEnsoTest {
RuntimeOptions.LANGUAGE_HOME_OVERRIDE,
Paths.get("../../distribution/component").toFile().getAbsolutePath()
)
.option(
RuntimeOptions.LOG_LEVEL,
"FINEST"
)
.logHandler(OutputStream.nullOutputStream())
.build();

View File

@ -83,6 +83,7 @@ class Application[Config](
additionalArguments,
applicationName = commandName
)
val finalResult = parseResult.flatMap {
case ((topLevelAction, commandResult), pluginIntercepted) =>
pluginIntercepted match {

View File

@ -12,7 +12,7 @@ import scala.util.{Failure, Success, Try, Using}
/** Manages the global configuration of the distribution. */
class GlobalConfigurationManager(distributionManager: DistributionManager) {
private val logger = Logger[this.type]
private val logger = Logger[GlobalConfigurationManager]
/** Location of the global configuration file. */
def configLocation: Path =

View File

@ -194,7 +194,7 @@ object MessageHandler {
*/
case class Connected(webConnection: ActorRef)
/** A control message usef to notify the controller about
/** A control message used to notify the controller about
* the connection being closed.
*/
case object Disconnected

View File

@ -1,12 +1,14 @@
package org.enso.librarymanager.published.repository
import org.enso.editions.Editions
import org.enso.loggingservice.TestLogger.TestLogMessage
import org.enso.loggingservice.{LogLevel, TestLogger}
import org.enso.librarymanager.published.cache.DownloadingLibraryCache
import org.enso.logger.TestLogMessage
import org.enso.pkg.PackageManager
import org.enso.testkit.WithTemporaryDirectory
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.slf4j.event.Level
import org.enso.logger.TestLogger
import java.nio.file.Files
@ -31,30 +33,36 @@ class LibraryDownloadTest
repo.testLib.version
) shouldBe empty
val (libPath, logs) = TestLogger.gatherLogs {
cache
.findOrInstallLibrary(
repo.testLib.libraryName,
repo.testLib.version,
Editions
.Repository("test_repo", s"http://localhost:$port/libraries")
val (_, allLogs) = TestLogger.gather[Any, DownloadingLibraryCache](
classOf[DownloadingLibraryCache], {
val libPath =
cache
.findOrInstallLibrary(
repo.testLib.libraryName,
repo.testLib.version,
Editions
.Repository(
"test_repo",
s"http://localhost:$port/libraries"
)
)
.get
val pkg =
PackageManager.Default.loadPackage(libPath.location.toFile).get
pkg.normalizedName shouldEqual "Bar"
val sources = pkg.listSources()
sources should have size 1
sources.head.file.getName shouldEqual "Main.enso"
assert(
Files.notExists(libPath / "LICENSE.md"),
"The license file should not exist as it was not provided " +
"in the repository."
)
.get
}
val pkg =
PackageManager.Default.loadPackage(libPath.location.toFile).get
pkg.normalizedName shouldEqual "Bar"
val sources = pkg.listSources()
sources should have size 1
sources.head.file.getName shouldEqual "Main.enso"
assert(
Files.notExists(libPath / "LICENSE.md"),
"The license file should not exist as it was not provided " +
"in the repository."
}
)
logs should contain(
allLogs should contain(
TestLogMessage(
LogLevel.Warning,
Level.WARN,
"License file for library [Foo.Bar:1.0.0] was missing."
)
)

View File

@ -0,0 +1,118 @@
package org.enso.logger;
import java.nio.file.Path;
import java.util.ServiceLoader;
import org.enso.logger.config.LoggingServiceConfig;
import org.enso.logger.config.MissingConfigurationField;
import org.slf4j.event.Level;
/** Base class to be implemented by the underlying logging implementation. */
public abstract class LoggerSetup {
private static volatile LoggerSetup _instance;
private static Object _lock = new Object();
public static LoggerSetup get() {
LoggerSetup result = _instance;
if (result == null) {
synchronized (_lock) {
result = _instance;
if (result == null) {
// Can't initialize in static initializer because Config has to be able to read runtime
// env vars
ServiceLoader<LoggerSetup> loader =
ServiceLoader.load(LoggerSetup.class, LoggerSetup.class.getClassLoader());
result = loader.findFirst().get();
_instance = result;
}
}
}
return result;
}
/** Returns parsed application config used to create this instance * */
public abstract LoggingServiceConfig getConfig();
/**
* Setup forwarding of logger's log event to a logging server.
*
* @param logLevel the maximal level of logs that will be forwarded
* @param hostname the name of the host where server is located
* @param port the port number where server is listening for messages
* @return true if logger was setup correctly, false otherwise
*/
public abstract boolean setupSocketAppender(Level logLevel, String hostname, int port);
/**
* Setup writing logger's log event to a file.
*
* @param logLevel the maximal level of logs that will be written
* @param logRoot the root directory where logs are located
* @param logPrefix the prefix used in the name of the log file
* @return true if logger was setup correctly, false otherwise
*/
public abstract boolean setupFileAppender(Level logLevel, Path logRoot, String logPrefix);
/**
* Setup writing logger's log event to a plain console.
*
* @param logLevel the maximal level of logs that will be displayed
* @return true if logger was setup correctly, false otherwise
*/
public abstract boolean setupConsoleAppender(Level logLevel);
/**
* Setup forwarding logger's log event to a sentry,io service. Requires the presence of the
* sentry's dependency appropriate to the logging implementation.
*
* @param logLevel the maximal level of logs that will be displayed
* @param logRoot the root directory where logs are located
* @return true if logger was setup correctly, false otherwise
*/
public abstract boolean setupSentryAppender(Level logLevel, Path logRoot);
/**
* Sets up loggers so that all events are being discarded.
*
* @return true unconditionally
*/
public abstract boolean setupNoOpAppender();
/**
* Sets up logging according to the application's config file.
*
* @return true if logger was setup correctly, false otherwise
* @throws MissingConfigurationField if application's config has been mis-configured
*/
public abstract boolean setup() throws MissingConfigurationField;
/**
* Sets up logging according to the application's config file while taking into account the
* provided log level.
*
* @param logLevel maximal log level allowed for log events
* @return true if logger was setup correctly, false otherwise
* @throws MissingConfigurationField if application's config has been mis-configured
*/
public abstract boolean setup(Level logLevel) throws MissingConfigurationField;
/**
* Sets up logging according to the provided application's config file and log level. If the
* default logging writes to a file, provided parameters will specify the exact location of the
* log file. This is more specific than {@link #setup(Level)} method.
*
* @param logLevel maximal log level allowed for log events
* @param logRoot the root directory where logs are located
* @param logPrefix the prefix used in the name of the log file
* @oaram config config file to be used to setup loggers (overriding the one returned by {@link
* #getConfig()}
* @return true if logger was setup correctly, false otherwise
* @throws MissingConfigurationField if application's config has been mis-configured
*/
public abstract boolean setup(
Level logLevel, Path logRoot, String logPrefix, LoggingServiceConfig config);
/** Shuts down all loggers. */
public abstract void teardown();
private static final String implClassKey = LoggerSetup.class.getName() + ".impl.class";
}

View File

@ -0,0 +1,77 @@
package org.enso.logger.config;
import com.typesafe.config.Config;
import java.nio.file.Path;
import org.enso.logger.LoggerSetup;
import org.slf4j.event.Level;
/**
* Base class for all appenders supported by Enso's logging configuration. Appenders determine what
* to do with the recorded log events
*/
public sealed abstract class Appender permits FileAppender, SocketAppender, SentryAppender, ConsoleAppender {
/**
* Returns the name of the appender
*
* @return
*/
public abstract String getName();
/**
* Parses config section and returns an appender's configuration.
*
* @param config section of the config to parse
* @return parsed and verified appender configuration
* @throws MissingConfigurationField if the config file was mis-configured
*/
public static Appender parse(Config config) throws MissingConfigurationField {
if (config != null) {
switch (config.getString(nameKey)) {
case FileAppender.appenderName:
return FileAppender.parse(config);
case SocketAppender.appenderName:
return SocketAppender.parse(config);
case SentryAppender.appenderName:
return SentryAppender.parse(config);
case ConsoleAppender.appenderName:
return ConsoleAppender.parse(config);
default:
return null;
}
}
return null;
}
/**
* Uses this appender's configuration to setup the logger.
*
* @param logLevel maximal level of logs that will be handled by logger
* @param loggerSetup logger's setup to be used to be invoked with this appender
* @return true if logger has been setup correctly using this configuration, false otherwise
*/
public boolean setup(Level logLevel, LoggerSetup loggerSetup) {
return false;
}
/**
* Uses this appender's configuration to setup the logger.
*
* @param logLevel maximal level of logs that will be handled by logger
* @param loggerSetup logger's setup to be used to be invoked with this appender
* @return true if logger has been setup correctly using this configuration, false otherwise
*/
public boolean setupForPath(
Level logLevel, Path logRoot, String logPrefix, LoggerSetup loggerSetup) {
return setup(logLevel, loggerSetup);
}
public boolean setupForURI(Level logLevel, String hostname, int port, LoggerSetup loggerSetup) {
return setup(logLevel, loggerSetup);
}
public static final String defaultPattern =
"[%level] [%d{yyyy-MM-dd'T'HH:mm:ssXXX}] [%logger] %msg%n";
protected static final String patternKey = "pattern";
private static final String nameKey = "name";
}

View File

@ -0,0 +1,37 @@
package org.enso.logger.config;
import com.typesafe.config.Config;
import org.enso.logger.LoggerSetup;
import org.slf4j.event.Level;
/** Config for log configuration that appends to the console */
public final class ConsoleAppender extends Appender {
private final String pattern;
private ConsoleAppender(String pattern) {
this.pattern = pattern;
}
public static ConsoleAppender parse(Config config) {
String pattern =
config.hasPath(patternKey) ? config.getString(patternKey) : Appender.defaultPattern;
return new ConsoleAppender(pattern);
}
@Override
public boolean setup(Level logLevel, LoggerSetup appenderSetup) {
return appenderSetup.setupConsoleAppender(logLevel);
}
public String getPattern() {
return pattern;
}
@Override
public String getName() {
return appenderName;
}
public static final String appenderName = "console";
}

View File

@ -0,0 +1,139 @@
package org.enso.logger.config;
import com.typesafe.config.Config;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.enso.logger.LoggerSetup;
import org.slf4j.event.Level;
/** Config for log configuration that appends to the file. */
public final class FileAppender extends Appender {
private final boolean append;
private final boolean immediateFlush;
private final String pattern;
private final LogLocation logLocation;
private final RollingPolicy rollingPolicy;
private FileAppender(
boolean append,
boolean immediateFlush,
String pattern,
LogLocation logLocation,
RollingPolicy rollingPolicy) {
this.append = append;
this.immediateFlush = immediateFlush;
this.pattern = pattern;
this.logLocation = logLocation;
this.rollingPolicy = rollingPolicy;
}
public static Appender parse(Config config) {
boolean append = config.hasPath(appendKey) ? config.getBoolean(appendKey) : true;
boolean immediateFlush =
config.hasPath(immediateFlushKey) ? config.getBoolean(immediateFlushKey) : false;
String pattern =
config.hasPath(patternKey) ? config.getString(patternKey) : Appender.defaultPattern;
LogLocation location;
if (config.hasPath(logLocationKey)
&& config.hasPath(logRootKey)
&& config.hasPath(logPrefixKey)) {
Config logLocationConfig = config.getConfig(logLocationKey);
location =
new LogLocation(
Paths.get(logLocationConfig.getString(logRootKey)),
logLocationConfig.getString(logPrefixKey));
} else {
location = new LogLocation(null, null);
}
RollingPolicy rollingPolicy;
if (config.hasPath(rollingPolicyKey)) {
Config c = config.getConfig(rollingPolicyKey);
rollingPolicy =
new RollingPolicy(
stringWithDefault(c, maxFileSizeKey, "50MB"),
initWithDefault(c, maxHistoryKey, 30),
stringWithDefault(c, maxTotalSizeKey, "2GB"));
} else {
rollingPolicy = null;
}
return new FileAppender(append, immediateFlush, pattern, location, rollingPolicy);
}
@Override
public boolean setup(Level logLevel, LoggerSetup appenderSetup) {
return appenderSetup.setupFileAppender(
logLevel, logLocation.logRoot(), logLocation.logPrefix());
}
@Override
public boolean setupForPath(
Level logLevel, Path componentLogPath, String componentLogPrefix, LoggerSetup loggerSetup) {
return loggerSetup.setupFileAppender(logLevel, componentLogPath, componentLogPrefix);
}
@Override
public String getName() {
return appenderName;
}
public boolean isAppend() {
return append;
}
public boolean isImmediateFlush() {
return immediateFlush;
}
public String getPattern() {
return pattern;
}
public RollingPolicy getRollingPolicy() {
return rollingPolicy;
}
public record LogLocation(Path logRoot, String logPrefix) {}
public record RollingPolicy(String maxFileSize, int maxHistory, String totalSizeCap) {}
private static int initWithDefault(Config c, String key, int defaultValue) {
if (c.hasPath(key)) return c.getInt(key);
else return defaultValue;
}
private static String stringWithDefault(Config c, String key, String defaultValue) {
if (c.hasPath(key)) return c.getString(key);
else return defaultValue;
}
@Override
public String toString() {
return "file-appender: pattern - "
+ pattern
+ ", immediate-flush - "
+ immediateFlush
+ ", rolling-policy - "
+ (rollingPolicy == null ? "no" : rollingPolicy.toString());
}
// Config keys
private static final String immediateFlushKey = "immediate-flush";
private static final String appendKey = "append";
private static final String patternKey = "pattern";
private static final String logLocationKey = "location";
private static final String logRootKey = "log-root";
private static final String logPrefixKey = "log-prefix";
private static final String rollingPolicyKey = "rolling-policy";
private static final String maxFileSizeKey = "max-file-size";
private static final String maxHistoryKey = "max-history";
private static final String maxTotalSizeKey = "max-total-size";
public static final String appenderName = "file";
}

View File

@ -0,0 +1,97 @@
package org.enso.logger.config;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.event.Level;
/** Encapsulates custom log levels that can be set via config file and environmental variables. */
public class LoggersLevels {
private Map<String, Level> loggers;
private LoggersLevels(Map<String, Level> loggers) {
this.loggers = loggers;
}
public Set<Map.Entry<String, Level>> entrySet() {
return loggers.entrySet();
}
public static LoggersLevels parse() {
return parse(ConfigFactory.empty());
}
public static LoggersLevels parse(Config config) {
// LinkedHashMap ensures that wildcard loggers are de-prioritized
Map<String, Level> loggers = systemLoggers();
Map<String, Level> fallbacks = new LinkedHashMap<>();
config
.entrySet()
.forEach(
entry -> {
String key = entry.getKey();
String v = config.getString(key);
Level level = Level.valueOf(v.toUpperCase());
String normalizedKey = normalizeKey(key);
if (normalizedKey.endsWith("*")) {
int idx = normalizedKey.indexOf('*');
fallbacks.put(normalizedKey.substring(0, idx), level);
} else {
loggers.put(normalizedKey, level);
}
});
if (!fallbacks.isEmpty()) {
loggers.putAll(fallbacks);
}
return new LoggersLevels(loggers);
}
public boolean isEmpty() {
return loggers.isEmpty();
}
/**
* Read any loggers' levels set via `-Dfoo.bar.Logger.level=<level>` env variables.
*
* @return a map of custom loggers' levels set on startup
*/
private static Map<String, Level> systemLoggers() {
Map<String, Level> loggers = new LinkedHashMap<>();
System.getProperties()
.forEach(
(keyObj, value) -> {
String key = keyObj.toString();
if (key.endsWith(SYS_PROP_SUFFIX)) {
int idx = key.lastIndexOf(SYS_PROP_SUFFIX);
String loggerName = key.substring(0, idx);
try {
loggers.put(loggerName, Level.valueOf(value.toString().toUpperCase()));
} catch (IllegalArgumentException e) {
System.err.println(
"Invalid log level `" + value + "` for " + loggerName + ". Skipping...");
}
}
});
return loggers;
}
@Override
public java.lang.String toString() {
return String.join(
"\n",
loggers.entrySet().stream()
.map(entry -> entry.getKey() + ": " + entry.getValue().toString())
.collect(Collectors.toList()));
}
private static String normalizeKey(String key) {
return key.replace("'", "").replace("\"", "");
}
private static String SYS_PROP_SUFFIX = ".Logger.level";
}

View File

@ -0,0 +1,34 @@
package org.enso.logger.config;
import com.typesafe.config.Config;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Configuration for the local server that collects logs from different services.
*
* @param port port of the local server that accepts logs
* @param appender appender's configuration describing how to transform received log events
* @param start if true, will be started by the service defining the configuration
*/
public record LoggingServer(int port, Map<String, Appender> appenders, String appender, Boolean start) {
public static LoggingServer parse(Config config) throws MissingConfigurationField {
int port = config.getInt("port");
Map<String, Appender> appendersMap = new HashMap<>();
if (config.hasPath("appenders")) {
List<? extends Config> configs = config.getConfigList("appenders");
for (Config c : configs) {
Appender a = Appender.parse(c);
appendersMap.put(a.getName(), a);
}
}
String defaultAppender = config.getString("default-appender");
boolean start = config.getBoolean("start");
return new LoggingServer(port, appendersMap, defaultAppender, start);
}
}

View File

@ -0,0 +1,139 @@
package org.enso.logger.config;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Parsed and verified representation of `logging-service` section of `application.conf`. Defines
* custom log levels, logging appenders and, optionally, logging server configuration.
*/
public class LoggingServiceConfig {
public static final String configurationRoot = "logging-service";
public static final String serverKey = "server";
public static final String loggersKey = "logger";
public static final String appendersKey = "appenders";
public static final String defaultAppenderKey = "default-appender";
public static final String logLevelKey = "log-level";
private final LoggersLevels loggers;
private final Map<String, Appender> appenders;
private final String defaultAppenderName;
private final Optional<String> logLevel;
private final LoggingServer server;
private LoggingServiceConfig(
LoggersLevels loggers,
Optional<String> logLevel,
Map<String, Appender> appenders,
String defaultAppender,
LoggingServer server) {
this.loggers = loggers;
this.appenders = appenders;
this.defaultAppenderName = defaultAppender;
this.logLevel = logLevel;
this.server = server;
}
public static LoggingServiceConfig parseConfig() throws MissingConfigurationField {
var empty = ConfigFactory.empty().atKey(configurationRoot);
var root = ConfigFactory.load().withFallback(empty).getConfig(configurationRoot);
LoggingServer server;
if (root.hasPath(serverKey)) {
Config serverConfig = root.getConfig(serverKey);
server = LoggingServer.parse(serverConfig);
} else {
server = null;
}
Map<String, Appender> appendersMap = new HashMap<>();
if (root.hasPath(appendersKey)) {
List<? extends Config> configs = root.getConfigList(appendersKey);
for (Config c : configs) {
Appender a = Appender.parse(c);
appendersMap.put(a.getName(), a);
}
}
LoggersLevels loggers;
if (root.hasPath(loggersKey)) {
loggers = LoggersLevels.parse(root.getConfig(loggersKey));
} else {
loggers = LoggersLevels.parse();
}
return new LoggingServiceConfig(
loggers,
getStringOpt(logLevelKey, root),
appendersMap,
root.getString(defaultAppenderKey),
server);
}
public static LoggingServiceConfig withSingleAppender(Appender appender) {
Map<String, Appender> map = new HashMap<>();
map.put(appender.getName(), appender);
return new LoggingServiceConfig(
LoggersLevels.parse(), Optional.empty(), map, appender.getName(), null);
}
public LoggersLevels getLoggers() {
return loggers;
}
public Appender getAppender() {
return appenders.get(defaultAppenderName);
}
public SocketAppender getSocketAppender() {
return (SocketAppender) appenders.getOrDefault(SocketAppender.appenderName, null);
}
public FileAppender getFileAppender() {
return (FileAppender) appenders.getOrDefault(FileAppender.appenderName, null);
}
public ConsoleAppender getConsoleAppender() {
return (ConsoleAppender) appenders.getOrDefault(ConsoleAppender.appenderName, null);
}
public SentryAppender getSentryAppender() {
return (SentryAppender) appenders.getOrDefault(SentryAppender.appenderName, null);
}
public boolean loggingServerNeedsBoot() {
return server != null && server.start();
}
private static Optional<String> getStringOpt(String key, Config config) {
try {
return Optional.ofNullable(config.getString(key));
} catch (ConfigException.Missing missing) {
return Optional.empty();
}
}
public Optional<String> getLogLevel() {
return logLevel;
}
public LoggingServer getServer() {
return server;
}
@Override
public String toString() {
return "Loggers: "
+ loggers
+ ", appenders: "
+ String.join(",", appenders.keySet())
+ ", default-appender: "
+ (defaultAppenderName == null ? "unknown" : defaultAppenderName)
+ ", logLevel: "
+ logLevel.orElseGet(() -> "default")
+ ", server: "
+ server;
}
}

View File

@ -0,0 +1,7 @@
package org.enso.logger.config;
public class MissingConfigurationField extends Exception {
public MissingConfigurationField(String name) {
super("Missing required configuration for field `" + name + "`");
}
}

View File

@ -0,0 +1,67 @@
package org.enso.logger.config;
import com.typesafe.config.Config;
import java.nio.file.Path;
import org.enso.logger.LoggerSetup;
import org.slf4j.event.Level;
/** Config for log configuration that sends logs to sentry.io service. */
public final class SentryAppender extends Appender {
public String getDsn() {
return dsn;
}
private String dsn;
public Integer getFlushTimeoutMs() {
return flushTimeoutMs;
}
private Integer flushTimeoutMs;
public boolean isDebugEnabled() {
return debugEnabled;
}
private boolean debugEnabled;
private SentryAppender(String dsn, Integer flushTimeoutMs, boolean debugEnabled) {
this.dsn = dsn;
this.flushTimeoutMs = flushTimeoutMs;
this.debugEnabled = debugEnabled;
}
public static Appender parse(Config config) throws MissingConfigurationField {
if (config.hasPath(dsnKey)) {
String dsn = config.getString(dsnKey);
Integer flushTimeoutMs =
config.hasPath(flushTimeoutKey) ? Integer.valueOf(config.getInt(flushTimeoutKey)) : null;
boolean debugEnabled = config.hasPath(debugKey) ? config.getBoolean(debugKey) : false;
return new SentryAppender(dsn, flushTimeoutMs, debugEnabled);
} else throw new MissingConfigurationField(dsnKey);
}
@Override
public boolean setup(Level logLevel, LoggerSetup loggerSetup) {
return loggerSetup.setupSentryAppender(logLevel, null);
}
@Override
public boolean setupForPath(
Level logLevel, Path logRoot, String logPrefix, LoggerSetup loggerSetup) {
return loggerSetup.setupSentryAppender(logLevel, logRoot);
}
@Override
public String getName() {
return appenderName;
}
private static final String dsnKey = "dsn";
private static final String flushTimeoutKey = "flush-timeout";
private static final String debugKey = "debug";
public static final String appenderName = "sentry";
}

View File

@ -0,0 +1,69 @@
package org.enso.logger.config;
import com.typesafe.config.Config;
import org.enso.logger.LoggerSetup;
import org.slf4j.event.Level;
/** Config for log configuration that forwards logs to the network socket as-is. */
public final class SocketAppender extends Appender {
private String name;
private String host;
/**
* Returns the name of the host of the network socket to connect to.
*
* @return
*/
public String getHost() {
return host;
}
/** Returns the port of the network socket to connect to. */
public int getPort() {
return port;
}
private int port;
/** Returns the number of miliseconds after a failed connection should be re-established. */
public int getReconnectionDelay() {
return reconnectionDelay;
}
private final int reconnectionDelay;
private SocketAppender(String host, int port, int reconnectionDelay) {
this.name = appenderName;
this.host = host;
this.port = port;
this.reconnectionDelay = reconnectionDelay;
}
@Override
public String getName() {
return name;
}
public static Appender parse(Config config) throws MissingConfigurationField {
if (!config.hasPath(hostKey)) throw new MissingConfigurationField(hostKey);
if (!config.hasPath(portKey)) throw new MissingConfigurationField(portKey);
int reconnectionDelay =
config.hasPath(reconnectionDelayKey) ? config.getInt(reconnectionDelayKey) : 10000;
return new SocketAppender(config.getString(hostKey), config.getInt(portKey), reconnectionDelay);
}
@Override
public boolean setup(Level logLevel, LoggerSetup loggerSetup) {
return loggerSetup.setupSocketAppender(logLevel, host, port);
}
@Override
public boolean setupForURI(Level logLevel, String host, int port, LoggerSetup loggerSetup) {
return loggerSetup.setupSocketAppender(logLevel, host, port);
}
private static final String hostKey = "hostname";
private static final String portKey = "port";
private static final String reconnectionDelayKey = "reconnection-delay";
public static final String appenderName = "socket";
}

View File

@ -0,0 +1,38 @@
package org.enso.logger;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
import org.enso.logger.config.LoggersLevels;
/**
* An implementation of ch.qos.logback.core.filter.Filter that is created from configuration's and
* user's custom logger levels.
*/
public class ApplicationFilter extends Filter<ILoggingEvent> {
private final LoggersLevels loggers;
private ApplicationFilter(LoggersLevels loggers) {
this.loggers = loggers;
}
@Override
public FilterReply decide(ILoggingEvent event) {
for (var entry : loggers.entrySet()) {
if (event.getLoggerName().startsWith(entry.getKey())) {
Level loggerLevel = Level.convertAnSLF4JLevel(entry.getValue());
if (event.getLevel().isGreaterOrEqual(loggerLevel)) {
return FilterReply.NEUTRAL;
} else {
return FilterReply.DENY;
}
}
}
return FilterReply.NEUTRAL;
}
public static Filter<ILoggingEvent> fromLoggers(LoggersLevels loggers) {
return new ApplicationFilter(loggers);
}
}

View File

@ -0,0 +1,295 @@
package org.enso.logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.net.SocketAppender;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.helpers.NOPAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
import ch.qos.logback.core.util.Duration;
import ch.qos.logback.core.util.FileSize;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.SystemOutLogger;
import io.sentry.logback.SentryAppender;
import java.io.File;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import org.enso.logger.config.*;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
@org.openide.util.lookup.ServiceProvider(service = LoggerSetup.class)
public final class LogbackSetup extends LoggerSetup {
private LogbackSetup(LoggingServiceConfig config, LoggerContext context) {
this.config = config;
this.context = context;
}
public LogbackSetup() throws MissingConfigurationField {
this(LoggingServiceConfig.parseConfig(), (LoggerContext) LoggerFactory.getILoggerFactory());
}
/**
* Create a logger setup for a provided context and a single appender configuration
* @param context context that will be initialized by this setup
* @param appender appender configuration to use during initialization
*/
public static LogbackSetup forContext(LoggerContext context, Appender appender) {
return new LogbackSetup(LoggingServiceConfig.withSingleAppender(appender), context);
}
public LoggingServiceConfig getConfig() {
return config;
}
private final LoggingServiceConfig config;
private final LoggerContext context;
@Override
public boolean setup() throws MissingConfigurationField {
LoggingServiceConfig config = LoggingServiceConfig.parseConfig();
return setup(config);
}
private boolean setup(LoggingServiceConfig config) {
Level defaultLogLevel = config.getLogLevel().map(name -> Level.valueOf(name.toUpperCase())).orElseGet(() -> Level.ERROR);
return setup(defaultLogLevel, config);
}
@Override
public boolean setup(Level logLevel) throws MissingConfigurationField {
return setup(logLevel, LoggingServiceConfig.parseConfig());
}
public boolean setup(Level logLevel, LoggingServiceConfig config) {
Appender defaultAppender = config.getAppender();
if (defaultAppender != null) {
return defaultAppender.setup(logLevel, this);
} else {
return setupConsoleAppender(logLevel);
}
}
@Override
public boolean setup(Level logLevel, Path componentLogPath, String componentLogPrefix, LoggingServiceConfig config) {
Appender defaultAppender = config.getAppender();
if (defaultAppender != null) {
return defaultAppender.setupForPath(logLevel, componentLogPath, componentLogPrefix, this);
} else {
return setupConsoleAppender(logLevel);
}
}
@Override
public boolean setupSocketAppender(
Level logLevel,
String hostname,
int port) {
LoggerAndContext env = contextInit(logLevel, config);
org.enso.logger.config.SocketAppender appenderConfig = config.getSocketAppender();
SocketAppender socketAppender = new SocketAppender();
socketAppender.setName("enso-socket");
socketAppender.setIncludeCallerData(false);
socketAppender.setRemoteHost(hostname);
socketAppender.setPort(port);
if (appenderConfig != null)
socketAppender.setReconnectionDelay(Duration.buildByMilliseconds(appenderConfig.getReconnectionDelay()));
env.finalizeAppender(socketAppender);
return true;
}
@Override
public boolean setupFileAppender(
Level logLevel,
Path logRoot,
String logPrefix) {
try {
LoggerAndContext env = contextInit(logLevel, config);
org.enso.logger.config.FileAppender appenderConfig = config.getFileAppender();
if (appenderConfig == null) {
throw new MissingConfigurationField(org.enso.logger.config.FileAppender.appenderName);
}
final PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setPattern(appenderConfig.getPattern());
env.finalizeEncoder(encoder);
FileAppender<ILoggingEvent> fileAppender;
if (appenderConfig != null && appenderConfig.getRollingPolicy() != null) {
RollingFileAppender<ILoggingEvent> rollingFileAppender = new RollingFileAppender<>();
fileAppender = rollingFileAppender;
fileAppender.setContext(env.ctx); // Context needs to be set prior to rolling policy initialization
String filePattern;
if (logRoot == null || logPrefix == null) {
filePattern = "enso-%d{yyyy-MM-dd}";
} else {
filePattern = logRoot.toAbsolutePath() + File.separator + logPrefix + "-" + "%d{yyyy-MM-dd}";
}
org.enso.logger.config.FileAppender.RollingPolicy rollingPolicy = appenderConfig.getRollingPolicy();
SizeAndTimeBasedRollingPolicy logbackRollingPolicy = new SizeAndTimeBasedRollingPolicy();
logbackRollingPolicy.setContext(env.ctx);
logbackRollingPolicy.setParent(fileAppender);
logbackRollingPolicy.setMaxFileSize(FileSize.valueOf(rollingPolicy.maxFileSize()));
logbackRollingPolicy.setMaxHistory(rollingPolicy.maxHistory());
logbackRollingPolicy.setTotalSizeCap(FileSize.valueOf(rollingPolicy.totalSizeCap()));
logbackRollingPolicy.setFileNamePattern(filePattern + ".%i.log.gz");
logbackRollingPolicy.start();
rollingFileAppender.setRollingPolicy(logbackRollingPolicy);
} else {
fileAppender = new FileAppender<>();
fileAppender.setName("enso-file");
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String currentDate = LocalDate.now().format(dtf);
String fullFilePath;
if (logRoot == null || logPrefix == null) {
fullFilePath = "enso-" + currentDate + ".log";
} else {
fullFilePath = logRoot.toAbsolutePath() + File.separator + logPrefix + "-" + currentDate + ".log";
}
fileAppender.setFile(fullFilePath);
}
fileAppender.setAppend(appenderConfig.isAppend());
fileAppender.setImmediateFlush(appenderConfig.isImmediateFlush());
fileAppender.setEncoder(encoder);
env.finalizeAppender(fileAppender);
} catch (Throwable e) {
e.printStackTrace();
return false;
}
return true;
}
@Override
public boolean setupConsoleAppender(Level logLevel) {
LoggerAndContext env = contextInit(logLevel, config);
org.enso.logger.config.ConsoleAppender appenderConfig = config.getConsoleAppender();
final PatternLayoutEncoder encoder = new PatternLayoutEncoder();
try {
encoder.setPattern(appenderConfig.getPattern());
} catch (Throwable e) {
e.printStackTrace();
encoder.setPattern(Appender.defaultPattern);
}
env.finalizeEncoder(encoder);
ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
consoleAppender.setName("enso-console");
consoleAppender.setEncoder(encoder);
env.finalizeAppender(consoleAppender);
return true;
}
@Override
public boolean setupSentryAppender(Level logLevel, Path logRoot) {
// TODO: handle proxy
// TODO: shutdown timeout configuration
try {
LoggerAndContext env = contextInit(logLevel, config);
org.enso.logger.config.SentryAppender appenderConfig = config.getSentryAppender();
if (appenderConfig == null) {
throw new MissingConfigurationField(org.enso.logger.config.SentryAppender.appenderName);
}
SentryAppender appender = new SentryAppender();
SentryOptions opts = new SentryOptions();
if (appenderConfig.isDebugEnabled()) {
opts.setDebug(true);
opts.setLogger(new SystemOutLogger());
opts.setDiagnosticLevel(SentryLevel.ERROR);
}
if (logRoot == null) {
opts.setCacheDirPath("sentry");
} else {
opts.setCacheDirPath(logRoot.resolve(".sentry").toAbsolutePath().toString());
}
if (appenderConfig.getFlushTimeoutMs() != null) {
opts.setFlushTimeoutMillis(appenderConfig.getFlushTimeoutMs());
}
appender.setMinimumEventLevel(ch.qos.logback.classic.Level.convertAnSLF4JLevel(logLevel));
opts.setDsn(appenderConfig.getDsn());
appender.setOptions(opts);
env.finalizeAppender(appender);
} catch (Throwable e) {
e.printStackTrace();
return false;
}
return true;
}
@Override
public boolean setupNoOpAppender() {
LoggerAndContext env = contextInit(Level.ERROR, null);
NOPAppender<ILoggingEvent> appender = new NOPAppender<>();
appender.setName("enso-noop");
env.finalizeAppender(appender);
return true;
}
@Override
public void teardown() {
// TODO: disable whatever appender is now in place and replace it with console
context.stop();
}
private LoggerAndContext contextInit(Level level, LoggingServiceConfig config) {
context.reset();
context.setName("enso-custom");
Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME);
Filter<ILoggingEvent> filter;
LoggersLevels loggers = config != null ? config.getLoggers() : null;
if (loggers != null && !loggers.isEmpty()) {
filter = ApplicationFilter.fromLoggers(loggers);
} else {
filter = null;
}
return new LoggerAndContext(level, context, rootLogger, filter);
}
private record LoggerAndContext(Level level, LoggerContext ctx, Logger logger, Filter<ILoggingEvent> filter) {
void finalizeEncoder(ch.qos.logback.core.encoder.Encoder<ILoggingEvent> encoder) {
encoder.setContext(ctx);
encoder.start();
}
void finalizeAppender(ch.qos.logback.core.Appender<ILoggingEvent> appender) {
logger.setLevel(ch.qos.logback.classic.Level.convertAnSLF4JLevel(level));
if (filter != null) {
appender.addFilter(filter);
filter.setContext(ctx);
filter.start();
}
appender.setContext(ctx);
appender.start();
logger.addAppender(appender);
}
}
}

View File

@ -0,0 +1,12 @@
package org.enso.logging;
import java.net.URI;
@org.openide.util.lookup.ServiceProvider(service = LoggingServiceFactory.class)
public class LogbackLoggingServiceFactory extends LoggingServiceFactory<URI> {
@Override
public LoggingService<URI> localServerFor(int port) {
return new LoggingServer(port);
}
}

View File

@ -0,0 +1,46 @@
package org.enso.logging;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.net.SimpleSocketServer;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import org.enso.logger.LogbackSetup;
import org.enso.logger.config.Appender;
import org.slf4j.event.Level;
class LoggingServer extends LoggingService<URI> {
private int port;
private SimpleSocketServer logServer;
public LoggingServer(int port) {
this.port = port;
this.logServer = null;
}
public URI start(Level level, Path path, String prefix, Appender appender) {
var lc = new LoggerContext();
var setup = LogbackSetup.forContext(lc, appender);
logServer = new SimpleSocketServer(lc, port);
logServer.start();
try {
setup.setup(level, path, prefix, setup.getConfig());
return new URI(null, null, "localhost", port, null, null, null);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
public boolean isSetup() {
return logServer != null;
}
@Override
public void teardown() {
if (logServer != null) {
logServer.close();
}
}
}

View File

@ -1,18 +0,0 @@
package org.enso.loggingservice.internal;
/**
* Provides a stub for enabling VT console mode.
*
* <p>We assume that VT is supported by default on UNIX platforms, so this function is never
* actually used. It is defined just to provide binary compatibility with the Windows counterpart,
* so that the code that uses it only on Windows, compiles on all platforms.
*/
public class NativeAnsiTerm {
/**
* Enables VT emulation within the connected console.
*
* <p>The UNIX variant does nothing, as we assume that VT is supported out of the box.
*/
public static void enableVT() {}
}

View File

@ -1,109 +0,0 @@
package org.enso.loggingservice.internal;
import org.graalvm.nativeimage.UnmanagedMemory;
import org.graalvm.nativeimage.c.function.CFunction;
import org.graalvm.nativeimage.c.type.CIntPointer;
import org.graalvm.word.PointerBase;
/** Provides access to the native WinApi calls that enable VT emulation in a connected console. */
public class NativeAnsiTerm {
/**
* Returns a handle to a console connected to one of the standard streams.
*
* <p>Refer to: https://docs.microsoft.com/en-us/windows/console/getstdhandle
*
* @param nStdHandle constant representing one of the standard streams
* @return pointer to the console handle or null if it could not be accessed
*/
@CFunction
private static native PointerBase GetStdHandle(int nStdHandle);
/**
* Returns current console mode.
*
* @param hConsoleHandle console handle from [[GetStdHandle]]
* @param lpMode pointer to an integer that will be set to the current mode on success
* @return non-zero integer on success
* @see <a
* href="https://docs.microsoft.com/en-us/windows/console/setconsolemodehttps://docs.microsoft.com/en-us/windows/console/getconsolemode>GetConsoleMode</a>
*/
@CFunction
private static native int GetConsoleMode(PointerBase hConsoleHandle, CIntPointer lpMode);
/**
* Sets console mode.
*
* @param hConsoleHandle console handle from [[GetStdHandle]]
* @param dwMode mode to set
* @return non-zero integer on success
* @see <a
* href="https://docs.microsoft.com/en-us/windows/console/setconsolemode">SetConsoleMode</a>
*/
@CFunction
private static native int SetConsoleMode(PointerBase hConsoleHandle, int dwMode);
/**
* Returns error code of last error.
*
* <p>Can be called if a function returns a zero exit code to get the error code.
*
* @see <a
* href="https://docs.microsoft.com/en-gb/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror">GetLastError</a>
*/
@CFunction
private static native int GetLastError();
/**
* Constant that can be used in [[GetStdHandle]] that refers to the standard error stream.
*
* @see <a href="https://docs.microsoft.com/en-us/windows/console/getstdhandle">GetStdHandle</a>
*/
private static final int STD_ERROR_HANDLE = -12;
/**
* Constant that can be used as part of a console mode which indicates that the output stream
* should handle VT escape codes.
*
* @see <a
* href="https://docs.microsoft.com/en-us/windows/console/setconsolemode">SetConsoleMode</a>
* @see <a
* href="https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences">Console
* Virtual Terminal Sequences</a>
*/
private static final int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
/**
* Enables VT emulation within the connected console.
*
* <p>May throw an exception if it is not possible to do so. Can only be called from native-image
* targets.
*/
public static void enableVT() {
CIntPointer modePtr = UnmanagedMemory.malloc(4);
try {
var handle = GetStdHandle(STD_ERROR_HANDLE);
if (handle.isNull()) {
throw new RuntimeException(
"Failed to get console handle. "
+ "Perhaps the console is not connected. "
+ "Error code: "
+ GetLastError());
}
if (GetConsoleMode(handle, modePtr) == 0) {
throw new RuntimeException(
"Failed to get console mode. " + "Error code: " + GetLastError());
}
var alteredMode = modePtr.read() | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if (SetConsoleMode(handle, alteredMode) == 0) {
throw new RuntimeException(
"Failed to set console mode. "
+ "Perhaps the console does not support VT codes. "
+ "Error code: "
+ GetLastError());
}
} finally {
UnmanagedMemory.free(modePtr);
}
}
}

View File

@ -1 +0,0 @@
package org.enso.logger.akka;

View File

@ -0,0 +1,7 @@
package org.enso.logging;
public class LoggerInitializationFailed extends RuntimeException {
public LoggerInitializationFailed() {
super("Logger initialization failed");
}
}

View File

@ -0,0 +1,28 @@
package org.enso.logging;
import java.nio.file.Path;
import org.enso.logger.config.Appender;
import org.slf4j.event.Level;
/**
* Base class for any logging service that accepts logs from other Enso's services
*
* @param <T> the type of the object describing on how to communicate with the service
*/
public abstract class LoggingService<T> {
/**
* Starts the service. The `appender` configuration specifies what to do with the received log
* events.
*
* @param level the maximal log level handled by this service
* @param path
* @param prefix
* @param appender
* @return
*/
public abstract T start(Level level, Path path, String prefix, Appender appender);
/** Shuts down the service. */
public abstract void teardown();
}

View File

@ -0,0 +1,7 @@
package org.enso.logging;
public class LoggingServiceAlreadySetup extends RuntimeException {
public LoggingServiceAlreadySetup() {
super("Logging Service already setup");
}
}

View File

@ -0,0 +1,32 @@
package org.enso.logging;
import java.net.URI;
import java.util.ServiceLoader;
public abstract class LoggingServiceFactory<T> {
private static volatile LoggingServiceFactory _loggingServiceFactory;
private static Object _lock = new Object();
public abstract LoggingService<T> localServerFor(int port);
@SuppressWarnings("unchecked")
public static LoggingServiceFactory<URI> get() {
LoggingServiceFactory<URI> result = _loggingServiceFactory;
if (result == null) {
synchronized (_lock) {
result = _loggingServiceFactory;
if (result == null) {
// Can't initialize in static initializer because Config has to be able to read runtime
// env vars
ServiceLoader<LoggingServiceFactory> loader =
ServiceLoader.load(
LoggingServiceFactory.class, LoggingServiceFactory.class.getClassLoader());
result = loader.findFirst().get();
_loggingServiceFactory = result;
}
}
}
return result;
}
}

View File

@ -0,0 +1,50 @@
package org.enso.logging;
import java.net.URI;
import java.nio.file.Path;
import org.enso.logger.config.Appender;
import org.enso.logger.config.LoggingServer;
import org.slf4j.event.Level;
import scala.concurrent.ExecutionContext;
import scala.concurrent.Future;
public class LoggingServiceManager {
private static LoggingService<?> loggingService = null;
private static Level currentLevel = Level.TRACE;
public static Level currentLogLevelForThisApplication() {
return currentLevel;
}
public static Future<URI> setupServer(
Level logLevel,
int port,
Path logPath,
String logFileSuffix,
LoggingServer config,
ExecutionContext ec) {
if (loggingService != null) {
throw new LoggingServiceAlreadySetup();
} else {
if (config.appenders().containsKey(config.appender())) {
currentLevel = logLevel;
return Future.apply(
() -> {
var server = LoggingServiceFactory.get().localServerFor(port);
loggingService = server;
Appender appender = config.appenders().get(config.appender());
return server.start(logLevel, logPath, logFileSuffix, appender);
},
ec);
} else {
throw new LoggerInitializationFailed();
}
}
}
public static void teardown() {
if (loggingService != null) {
loggingService.teardown();
}
}
}

View File

@ -0,0 +1,163 @@
package org.enso.logging;
import java.net.URI;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.enso.logger.LoggerSetup;
import org.enso.logger.config.MissingConfigurationField;
import org.enso.logger.masking.Masking;
import org.slf4j.event.Level;
import scala.Option;
import scala.Unit$;
import scala.concurrent.Await;
import scala.concurrent.ExecutionContext;
import scala.concurrent.Future;
import scala.concurrent.Promise;
import scala.concurrent.Promise$;
import scala.concurrent.duration.Duration$;
/**
* Base class for any Enso service that needs to setup its logging.
*
* <p>Note: if this looks ugly and not very Java-friendly, it's because it is. It's a 1:1
* translation from Scala.
*/
public abstract class LoggingSetupHelper {
public LoggingSetupHelper(ExecutionContext ec) {
this.ec = ec;
}
private ExecutionContext ec;
protected abstract Level defaultLogLevel();
protected abstract String logFileSuffix();
protected abstract Path logPath();
public Future<Option<URI>> loggingServiceEndpoint() {
return loggingServiceEndpointPromise.future();
}
private Promise<Option<URI>> loggingServiceEndpointPromise = Promise$.MODULE$.apply();
/**
* Initialize logging to console prior to establishing logging. Some logs may be added while
* inferring the parameters of logging infrastructure, leading to catch-22 situations.
*/
public void initLogger() {
LoggerSetup.get().setupNoOpAppender();
}
public void setupFallback() {
LoggerSetup.get().setupConsoleAppender(defaultLogLevel());
}
/**
* Starts a logging server, if necessary, that accepts logs from different components. Once
* started, logs in this service are being setup to be forwarded to that logging server.
*
* @param logLevel maximal level of log events to be forwarded
* @param logMasking true if masking of sensitive data should be applied to all log messages
*/
public void setup(Level logLevel, boolean logMasking) throws MissingConfigurationField {
initLogger();
var loggerSetup = LoggerSetup.get();
var config = loggerSetup.getConfig();
if (config.loggingServerNeedsBoot()) {
int actualPort = config.getServer().port();
LoggingServiceManager.setupServer(
logLevel, actualPort, logPath(), logFileSuffix(), config.getServer(), ec)
.onComplete(
(result) -> {
try {
if (result.isFailure()) {
setup(Option.apply(logLevel), Option.empty(), logMasking, loggerSetup);
} else {
URI uri = result.get();
Masking.setup(logMasking);
if (!loggerSetup.setup(logLevel)) {
LoggingServiceManager.teardown();
loggingServiceEndpointPromise.failure(new LoggerInitializationFailed());
} else {
loggingServiceEndpointPromise.success(Option.apply(uri));
}
}
return Unit$.MODULE$;
} catch (MissingConfigurationField e) {
throw new RuntimeException(e);
}
},
ec);
} else {
// Setup logger according to config
if (loggerSetup.setup(logLevel)) {
loggingServiceEndpointPromise.success(Option.empty());
}
}
}
/**
* Initializes logging for this service using the URI of the dedicated logging server. If
* connecting to the logging server failed, or the optional address is missing, log events will be
* handled purely based on configuration packaged with this service.
*
* @param logLevel optional maximal level of log events that will be handled by the logging
* infrastructure
* @param connectToExternalLogger optional address of the logging server
* @param logMasking true if sensitive data should be masked in log events, false otherwise
* @throws MissingConfigurationField if the config file has been mis-configured
*/
public void setup(Option<Level> logLevel, Option<URI> connectToExternalLogger, boolean logMasking)
throws MissingConfigurationField {
initLogger();
setup(logLevel, connectToExternalLogger, logMasking, LoggerSetup.get());
}
private void setup(
Option<Level> logLevel,
Option<URI> connectToExternalLogger,
boolean logMasking,
LoggerSetup loggerSetup)
throws MissingConfigurationField {
var actualLogLevel = logLevel.getOrElse(() -> defaultLogLevel());
if (connectToExternalLogger.isDefined()) {
var uri = connectToExternalLogger.get();
var initialized =
loggerSetup.setupSocketAppender(actualLogLevel, uri.getHost(), uri.getPort());
if (!initialized) {
// Fallback
initialized =
loggerSetup.setup(actualLogLevel, logPath(), logFileSuffix(), loggerSetup.getConfig());
if (!initialized) {
// Fallback to console
initialized = loggerSetup.setupConsoleAppender(actualLogLevel);
}
}
if (initialized) {
Masking.setup(logMasking);
loggingServiceEndpointPromise.success(Option.empty());
} else {
loggingServiceEndpointPromise.failure(new LoggerInitializationFailed());
}
} else {
if (loggerSetup.setup(actualLogLevel, logPath(), logFileSuffix(), loggerSetup.getConfig())) {
Masking.setup(logMasking);
loggingServiceEndpointPromise.success(Option.empty());
} else {
loggingServiceEndpointPromise.failure(new LoggerInitializationFailed());
}
}
}
public void waitForSetup() throws InterruptedException, TimeoutException {
Await.ready(
loggingServiceEndpointPromise.future(), Duration$.MODULE$.apply(5, TimeUnit.SECONDS));
}
public void tearDown() {
LoggingServiceManager.teardown();
}
}

View File

@ -1,32 +0,0 @@
package org.slf4j.impl;
import org.enso.loggingservice.LoggerFactory;
import org.slf4j.ILoggerFactory;
/**
* Binds the logging service as an SLF4J backend.
*
* <p>The public interface of this class must conform to what is expected by an SLF4J backend. See
* slf4j-simple for reference.
*/
public class StaticLoggerBinder {
/** Should be in sync with `slf4jVersion` in `build.sbt`. */
public static String REQUESTED_API_VERSION = "1.7.36";
private static final StaticLoggerBinder singleton = new StaticLoggerBinder();
public static StaticLoggerBinder getSingleton() {
return singleton;
}
private final LoggerFactory factory = new LoggerFactory();
private final String factoryClassStr = LoggerFactory.class.getName();
public ILoggerFactory getLoggerFactory() {
return factory;
}
public String getLoggerFactoryClassStr() {
return factoryClassStr;
}
}

View File

@ -1,33 +0,0 @@
package org.slf4j.impl;
import org.slf4j.helpers.NOPMDCAdapter;
import org.slf4j.spi.MDCAdapter;
/**
* Provides a no-op MDC adapter for the SLF4J backend.
*
* <p>MDC handling is an optional SLF4J feature and currently the logging service does not support
* it, so it provides a no-op adapter.
*
* <p>The public interface of this class must conform to what is expected by an SLF4J backend. See
* slf4j-simple for reference.
*/
public class StaticMDCBinder {
private static final StaticMDCBinder singleton = new StaticMDCBinder();
public static StaticMDCBinder getSingleton() {
return singleton;
}
private final MDCAdapter adapter = new NOPMDCAdapter();
private final String adapterClassStr = NOPMDCAdapter.class.getName();
public MDCAdapter getMDCA() {
return adapter;
}
public String getMDCAdapterClassStr() {
return adapterClassStr;
}
}

View File

@ -1,30 +0,0 @@
package org.slf4j.impl;
import org.slf4j.IMarkerFactory;
import org.slf4j.helpers.BasicMarkerFactory;
/**
* Provides a simple marker factory for the SLF4J backend.
*
* <p>The public interface of this class must conform to what is expected by an SLF4J backend. See
* slf4j-simple for reference.
*/
public class StaticMarkerBinder {
private static final StaticMarkerBinder singleton = new StaticMarkerBinder();
public static StaticMarkerBinder getSingleton() {
return singleton;
}
private final IMarkerFactory markerFactory = new BasicMarkerFactory();
private final String markerFactoryClassStr = BasicMarkerFactory.class.getName();
public IMarkerFactory getMarkerFactory() {
return markerFactory;
}
public String getMarkerFactoryClassStr() {
return markerFactoryClassStr;
}
}

View File

@ -1,18 +0,0 @@
package org.enso
import com.typesafe.scalalogging.Logger
package object logger {
/** Provides syntax for entering a sub-logger.
*/
implicit class LoggerSyntax(logger: Logger) {
/** Returns another [[Logger]] with name extended with a sub context.
*/
def enter(subContextName: String): Logger = {
val name = logger.underlying.getName + "." + subContextName
Logger(name)
}
}
}

View File

@ -1,20 +0,0 @@
package org.enso.loggingservice
/** Describes possible modes of color display in console output. */
sealed trait ColorMode
object ColorMode {
/** Never use color escape sequences in the output. */
case object Never extends ColorMode
/** Enable color output if it seems to be supported. */
case object Auto extends ColorMode
/** Always use escape sequences in the output, even if the program thinks they
* are unsupported.
*
* May be useful if output is piped to other programs that know how to handle
* the escape sequences.
*/
case object Always extends ColorMode
}

View File

@ -1,77 +0,0 @@
package org.enso.loggingservice
import org.enso.loggingservice.internal.{InternalLogMessage, LoggerConnection}
import java.util.logging.{Handler, Level, LogRecord, SimpleFormatter}
/** A [[Handler]] implementation that allows to use the logging service as a
* backend for [[java.util.logging]].
*/
class JavaLoggingLogHandler(
levelMapping: Level => LogLevel,
connection: LoggerConnection
) extends Handler {
/** @inheritdoc
*/
override def publish(record: LogRecord): Unit = {
val level = levelMapping(record.getLevel)
if (connection.isEnabled(record.getLoggerName, level)) {
val message = InternalLogMessage(
level = level,
timestamp = record.getInstant,
group = record.getLoggerName,
message = JavaLoggingLogHandler.formatter.formatMessage(record),
exception = Option(record.getThrown)
)
connection.send(message)
}
}
/** @inheritdoc */
override def flush(): Unit = {}
/** @inheritdoc */
override def close(): Unit = {}
}
object JavaLoggingLogHandler {
private val formatter = new SimpleFormatter()
/** Creates a [[Handler]] with the provided mapping from Java's log levels to
* our log levels.
*/
def create(mapping: Level => LogLevel): JavaLoggingLogHandler =
new JavaLoggingLogHandler(mapping, LoggingServiceManager.Connection)
/** Determines what is the smallest Java level that is still debug and not
* trace.
*/
private val defaultLevelDebugCutOff =
Seq(Level.FINE.intValue, Level.CONFIG.intValue).min
/** Default mapping of Java log levels to our log levels based
*/
def defaultLevelMapping(javaLevel: Level): LogLevel = {
val level = javaLevel.intValue
if (level == Level.OFF.intValue) LogLevel.Off
else if (level >= Level.SEVERE.intValue) LogLevel.Error
else if (level >= Level.WARNING.intValue) LogLevel.Warning
else if (level >= Level.INFO.intValue) LogLevel.Info
else if (level >= defaultLevelDebugCutOff) LogLevel.Debug
else LogLevel.Trace
}
/** Approximate-inverse of [[defaultLevelMapping]] that returns a Java log
* level corresponding to the given log level.
*/
def getJavaLogLevelFor(logLevel: LogLevel): Level =
logLevel match {
case LogLevel.Off => Level.OFF
case LogLevel.Error => Level.SEVERE
case LogLevel.Warning => Level.WARNING
case LogLevel.Info => Level.INFO
case LogLevel.Debug => Level.FINE
case LogLevel.Trace => Level.ALL
}
}

View File

@ -1,147 +0,0 @@
package org.enso.loggingservice
import io.circe.syntax._
import io.circe.{Decoder, DecodingFailure, Encoder}
/** Defines a log level for log messages. */
sealed abstract class LogLevel(final val name: String, final val level: Int) {
/** Determines if a component running on `this` log level should log the
* `other`.
*
* Log levels smaller or equal to component's log level are logged.
*/
def shouldLog(other: LogLevel): Boolean =
other.level <= level
/** @inheritdoc */
override def toString: String = name
}
object LogLevel {
/** This log level should not be used by messages, instead it can be set as
* component's log level to completely disable logging for it.
*/
case object Off extends LogLevel("off", -1)
/** Log level corresponding to severe errors, should be understandable to the
* end-user.
*/
case object Error extends LogLevel("error", 0)
/** Log level corresponding to important notices or issues that are not
* severe.
*/
case object Warning extends LogLevel("warning", 1)
/** Log level corresponding to usual information of what the application is
* doing.
*/
case object Info extends LogLevel("info", 2)
/** Log level used for debugging the application.
*
* The messages can be more complex and targeted at developers diagnosing the
* application.
*/
case object Debug extends LogLevel("debug", 3)
/** Log level used for advanced debugging, may be used for more throughout
* diagnostics.
*/
case object Trace extends LogLevel("trace", 4)
/** Lists all available log levels.
*
* Can be used for example to automate parsing.
*/
val allLevels = Seq(
LogLevel.Off,
LogLevel.Error,
LogLevel.Warning,
LogLevel.Info,
LogLevel.Debug,
LogLevel.Trace
)
/** [[Ordering]] instance for [[LogLevel]].
*
* The log levels are ordered from most severe. If a log level is enabled, it
* usually means that all levels smaller than it are enabled too.
*/
implicit val ord: Ordering[LogLevel] = (x, y) => x.level - y.level
/** [[Encoder]] instance for [[LogLevel]]. */
implicit val encoder: Encoder[LogLevel] = {
case Off =>
throw new IllegalArgumentException(
"`None` log level should never be used in actual log messages and it " +
"cannot be serialized to prevent that."
)
case level =>
level.level.asJson
}
/** [[Decoder]] instance for [[LogLevel]]. */
implicit val decoder: Decoder[LogLevel] = { json =>
json.as[Int].flatMap { level =>
fromInteger(level).toRight(
DecodingFailure(s"`$level` is not a valid log level.", json.history)
)
}
}
/** Creates a [[LogLevel]] from its integer representation.
*
* Returns None if the number does not represent a valid log level.
*/
def fromInteger(level: Int): Option[LogLevel] = level match {
case Off.level => Some(Off)
case Error.level => Some(Error)
case Warning.level => Some(Warning)
case Info.level => Some(Info)
case Debug.level => Some(Debug)
case Trace.level => Some(Trace)
case _ => None
}
/** Creates a [[LogLevel]] from its string representation.
*
* Returns None if the value does not represent a valid log level.
*/
def fromString(level: String): Option[LogLevel] =
level.toLowerCase match {
case Off.name => Some(Off)
case Error.name => Some(Error)
case Warning.name => Some(Warning)
case Info.name => Some(Info)
case Debug.name => Some(Debug)
case Trace.name => Some(Trace)
case _ => None
}
/** Converts our internal [[LogLevel]] to the corresponding instance of
* Akka-specific log level.
*/
def toAkka(logLevel: LogLevel): akka.event.Logging.LogLevel = logLevel match {
case Off => akka.event.Logging.LogLevel(Int.MinValue)
case Error => akka.event.Logging.ErrorLevel
case Warning => akka.event.Logging.WarningLevel
case Info => akka.event.Logging.InfoLevel
case Debug => akka.event.Logging.DebugLevel
case Trace => akka.event.Logging.DebugLevel
}
/** Converts our internal [[LogLevel]] to the corresponding instance of
* Java log level.
*/
def toJava(logLevel: LogLevel): java.util.logging.Level = logLevel match {
case Off => java.util.logging.Level.OFF
case Error => java.util.logging.Level.SEVERE
case Warning => java.util.logging.Level.WARNING
case Info => java.util.logging.Level.INFO
case Debug => java.util.logging.Level.FINER
case Trace => java.util.logging.Level.FINEST
}
}

View File

@ -1,315 +0,0 @@
package org.enso.loggingservice
import org.enso.logger.masking.Masking
import org.enso.loggingservice.internal.{InternalLogMessage, LoggerConnection}
import org.slf4j.helpers.MessageFormatter
import org.slf4j.{Marker, Logger => SLF4JLogger}
import scala.annotation.unused
/** A [[SLF4JLogger]] instance for the SLF4J backend which passes all log
* messages to a [[LoggerConnection]].
*
* @param name name of the logger
* @param connection the connection to pass the log messages to
* @param masking object that masks personally identifiable information
*/
class Logger(
name: String,
connection: LoggerConnection,
masking: Masking
) extends SLF4JLogger {
/** @inheritdoc */
override def getName: String = name
private def isEnabled(level: LogLevel): Boolean =
connection.isEnabled(name, level)
private def log(
level: LogLevel,
msg: String
): Unit = {
if (isEnabled(level)) {
connection.send(InternalLogMessage(level, name, msg, None))
}
}
private def log(
level: LogLevel,
format: String,
arg: AnyRef
): Unit = {
if (isEnabled(level)) {
val maskedArg = masking.mask(arg)
val fp = MessageFormatter.format(format, maskedArg)
connection.send(
InternalLogMessage(level, name, fp.getMessage, Option(fp.getThrowable))
)
}
}
private def log(
level: LogLevel,
format: String,
arg1: AnyRef,
arg2: AnyRef
): Unit = {
if (isEnabled(level)) {
val maskedArg1 = masking.mask(arg1)
val maskedArg2 = masking.mask(arg2)
val fp = MessageFormatter.format(format, maskedArg1, maskedArg2)
connection.send(
InternalLogMessage(level, name, fp.getMessage, Option(fp.getThrowable))
)
}
}
private def log(
level: LogLevel,
format: String,
args: Seq[AnyRef]
): Unit = {
if (isEnabled(level)) {
val maskedArgs = args.map(masking.mask)
val fp = MessageFormatter.arrayFormat(format, maskedArgs.toArray)
connection.send(
InternalLogMessage(level, name, fp.getMessage, Option(fp.getThrowable))
)
}
}
private def log(
level: LogLevel,
msg: String,
throwable: Throwable
): Unit = {
if (isEnabled(level)) {
connection.send(
InternalLogMessage(level, name, msg, Some(throwable))
)
}
}
override def isTraceEnabled: Boolean = isEnabled(LogLevel.Trace)
override def trace(msg: String): Unit = log(LogLevel.Trace, msg)
override def trace(format: String, arg: AnyRef): Unit =
log(LogLevel.Trace, format, arg)
override def trace(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
log(LogLevel.Trace, format, arg1, arg2)
override def trace(format: String, arguments: AnyRef*): Unit =
log(LogLevel.Trace, format, arguments)
override def trace(msg: String, t: Throwable): Unit =
log(LogLevel.Trace, msg, t)
override def isTraceEnabled(@unused marker: Marker): Boolean =
isEnabled(LogLevel.Trace)
override def trace(@unused marker: Marker, msg: String): Unit =
log(LogLevel.Trace, msg)
override def trace(
@unused marker: Marker,
format: String,
arg: AnyRef
): Unit =
log(LogLevel.Trace, format, arg)
override def trace(
@unused marker: Marker,
format: String,
arg1: AnyRef,
arg2: AnyRef
): Unit = log(LogLevel.Trace, format, arg1, arg2)
override def trace(
@unused marker: Marker,
format: String,
argArray: AnyRef*
): Unit =
log(LogLevel.Trace, format, argArray)
override def trace(@unused marker: Marker, msg: String, t: Throwable): Unit =
log(LogLevel.Trace, msg, t)
override def isDebugEnabled: Boolean = isEnabled(LogLevel.Debug)
override def debug(msg: String): Unit = log(LogLevel.Debug, msg)
override def debug(format: String, arg: AnyRef): Unit =
log(LogLevel.Debug, format, arg)
override def debug(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
log(LogLevel.Debug, format, arg1, arg2)
override def debug(format: String, arguments: AnyRef*): Unit =
log(LogLevel.Debug, format, arguments)
override def debug(msg: String, t: Throwable): Unit =
log(LogLevel.Debug, msg, t)
override def isDebugEnabled(@unused marker: Marker): Boolean =
isEnabled(LogLevel.Debug)
override def debug(@unused marker: Marker, msg: String): Unit =
log(LogLevel.Debug, msg)
override def debug(
@unused marker: Marker,
format: String,
arg: AnyRef
): Unit =
log(LogLevel.Debug, format, arg)
override def debug(
@unused marker: Marker,
format: String,
arg1: AnyRef,
arg2: AnyRef
): Unit = log(LogLevel.Debug, format, arg1, arg2)
override def debug(
@unused marker: Marker,
format: String,
arguments: AnyRef*
): Unit =
log(LogLevel.Debug, format, arguments)
override def debug(@unused marker: Marker, msg: String, t: Throwable): Unit =
log(LogLevel.Debug, msg, t)
override def isInfoEnabled: Boolean = isEnabled(LogLevel.Info)
override def info(msg: String): Unit = log(LogLevel.Info, msg)
override def info(format: String, arg: AnyRef): Unit =
log(LogLevel.Info, format, arg)
override def info(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
log(LogLevel.Info, format, arg1, arg2)
override def info(format: String, arguments: AnyRef*): Unit =
log(LogLevel.Info, format, arguments)
override def info(msg: String, t: Throwable): Unit =
log(LogLevel.Info, msg, t)
override def isInfoEnabled(@unused marker: Marker): Boolean =
isEnabled(LogLevel.Info)
override def info(@unused marker: Marker, msg: String): Unit =
log(LogLevel.Info, msg)
override def info(@unused marker: Marker, format: String, arg: AnyRef): Unit =
log(LogLevel.Info, format, arg)
override def info(
@unused marker: Marker,
format: String,
arg1: AnyRef,
arg2: AnyRef
): Unit = log(LogLevel.Info, format, arg1, arg2)
override def info(
@unused marker: Marker,
format: String,
arguments: AnyRef*
): Unit =
log(LogLevel.Info, format, arguments)
override def info(@unused marker: Marker, msg: String, t: Throwable): Unit =
log(LogLevel.Info, msg, t)
override def isWarnEnabled: Boolean = isEnabled(LogLevel.Warning)
override def warn(msg: String): Unit = log(LogLevel.Warning, msg)
override def warn(format: String, arg: AnyRef): Unit =
log(LogLevel.Warning, format, arg)
override def warn(format: String, arguments: AnyRef*): Unit =
log(LogLevel.Warning, format, arguments)
override def warn(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
log(LogLevel.Warning, format, arg1, arg2)
override def warn(msg: String, t: Throwable): Unit =
log(LogLevel.Warning, msg, t)
override def isWarnEnabled(@unused marker: Marker): Boolean =
isEnabled(LogLevel.Warning)
override def warn(@unused marker: Marker, msg: String): Unit =
log(LogLevel.Warning, msg)
override def warn(@unused marker: Marker, format: String, arg: AnyRef): Unit =
log(LogLevel.Warning, format, arg)
override def warn(
@unused marker: Marker,
format: String,
arg1: AnyRef,
arg2: AnyRef
): Unit = log(LogLevel.Warning, format, arg1, arg2)
override def warn(
@unused marker: Marker,
format: String,
arguments: AnyRef*
): Unit =
log(LogLevel.Warning, format, arguments)
override def warn(@unused marker: Marker, msg: String, t: Throwable): Unit =
log(LogLevel.Warning, msg, t)
override def isErrorEnabled: Boolean = isEnabled(LogLevel.Error)
override def error(msg: String): Unit = log(LogLevel.Error, msg)
override def error(format: String, arg: AnyRef): Unit =
log(LogLevel.Error, format, arg)
override def error(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
log(LogLevel.Error, format, arg1, arg2)
override def error(format: String, arguments: AnyRef*): Unit =
log(LogLevel.Error, format, arguments)
override def error(msg: String, t: Throwable): Unit =
log(LogLevel.Error, msg, t)
override def isErrorEnabled(@unused marker: Marker): Boolean =
isEnabled(LogLevel.Error)
override def error(@unused marker: Marker, msg: String): Unit =
log(LogLevel.Error, msg)
override def error(
@unused marker: Marker,
format: String,
arg: AnyRef
): Unit =
log(LogLevel.Error, format, arg)
override def error(
@unused marker: Marker,
format: String,
arg1: AnyRef,
arg2: AnyRef
): Unit = log(LogLevel.Error, format, arg1, arg2)
override def error(
@unused marker: Marker,
format: String,
arguments: AnyRef*
): Unit =
log(LogLevel.Error, format, arguments)
override def error(@unused marker: Marker, msg: String, t: Throwable): Unit =
log(LogLevel.Error, msg, t)
}

View File

@ -1,24 +0,0 @@
package org.enso.loggingservice
import org.enso.logger.masking.Masking
import org.slf4j.{ILoggerFactory, Logger => SLF4JLogger}
/** A [[ILoggerFactory]] instance for the SLF4J backend. */
class LoggerFactory extends ILoggerFactory {
private val loggers = scala.collection.concurrent.TrieMap[String, Logger]()
/** @inheritdoc */
override def getLogger(name: String): SLF4JLogger = {
loggers.getOrElseUpdate(
name, {
val newLogger =
new Logger(name, LoggingServiceManager.Connection, Masking())
if (!Masking.isMaskingEnabled) {
newLogger.warn("Log masking is disabled!")
}
newLogger
}
)
}
}

View File

@ -1,44 +0,0 @@
package org.enso.loggingservice
import akka.http.scaladsl.model.Uri
import org.enso.loggingservice.printers.Printer
/** Represents modes the logging service can be running in.
*
* @tparam InitializationResult type that is returned when
* [[LoggingServiceManager]] sets up the given
* mode
*/
sealed trait LoggerMode[InitializationResult]
object LoggerMode {
/** Forwards log messages to a logging service server.
*
* @param endpoint URI that is used to connect to the server via WebSockets
*/
case class Client(endpoint: Uri) extends LoggerMode[Unit]
/** Starts gathering messages from this and other components.
*
* Its initialization returns a [[ServerBinding]] that can be used to connect
* to the initialized server.
*
* @param printers a list of printers that process the incoming messages
* @param port optional port to listen at, if not provided, the OS will
* choose a default port; the chosen port can be extracted from
* the [[ServerBinding]] that is returned when the service is
* initialized
* @param interface interface to listen at
*/
case class Server(
printers: Seq[Printer],
port: Option[Int] = None,
interface: String = "localhost"
) extends LoggerMode[ServerBinding]
/** Processes log messages locally.
*
* @param printers a list of printers that process the incoming messages
*/
case class Local(printers: Seq[Printer]) extends LoggerMode[Unit]
}

View File

@ -1,7 +0,0 @@
package org.enso.loggingservice
case class LoggingServiceAlreadyInitializedException()
extends RuntimeException(
"The logging service was already initialized. " +
"If it should be restarted, it should be torn down first."
)

View File

@ -1,195 +0,0 @@
package org.enso.loggingservice
import org.enso.loggingservice.internal._
import org.enso.loggingservice.internal.service.{Client, Local, Server, Service}
import org.enso.loggingservice.printers.{Printer, StderrPrinter}
import scala.concurrent.{ExecutionContext, Future}
/** Manages the logging service.
*/
object LoggingServiceManager {
private var currentService: Option[Service] = None
private var currentLevel: LogLevel = LogLevel.Trace
/** Returns the log level that is currently set up for the application.
*
* Its result can change depending on initialization state.
*/
def currentLogLevelForThisApplication(): LogLevel = currentLevel
/** Creates an instance for the [[messageQueue]].
*
* Runs special workaround logic if test mode is detected.
*/
private def initializeMessageQueue(): BlockingConsumerMessageQueue = {
LoggingSettings.testLogLevel match {
case Some(logLevel) =>
val shouldOverride = () => currentService.isEmpty
System.err.println(
s"[Logging Service] Using test-mode logger at level $logLevel."
)
new TestMessageQueue(logLevel, shouldOverride)
case None =>
productionMessageQueue()
}
}
private def productionMessageQueue() = new BlockingConsumerMessageQueue()
private val messageQueue = initializeMessageQueue()
/** The default [[LoggerConnection]] that should be used by all backends which
* want to use the logging service.
*/
object Connection extends LoggerConnection {
/** @inheritdoc */
override def send(message: InternalLogMessage): Unit =
messageQueue.send(Left(message))
/** @inheritdoc */
override def logLevel: LogLevel = currentLevel
/** @inheritdoc */
override def loggers: Map[String, LogLevel] =
LoggingSettings.loggers
}
/** Sets up the logging service, but in a separate thread to avoid stalling
* the application.
*
* The returned [[InitializationResult]] depends on the mode.
*
* It is important to note that any printers passed inside of `mode` are from
* now on owned by the setup function and the created service, so if service
* creation fails, they will be shutdown alongside service termination. Any
* printers passed to this function must not be reused.
*
* @param mode [[LoggerMode]] to setup
* @param logLevel specifies which log level should be used for logs from
* this instance; this log level does not affect remote log
* levels in server mode
* @param executionContext execution context to run the initialization in
* @return a future that will complete once the logger is initialized
*/
def setup[InitializationResult](
mode: LoggerMode[InitializationResult],
logLevel: LogLevel
)(implicit
executionContext: ExecutionContext =
scala.concurrent.ExecutionContext.Implicits.global
): Future[InitializationResult] = {
currentLevel = logLevel
Future(doSetup(mode, logLevel))
}
/** Shuts down the logging service if it was initialized or runs
* [[handlePendingMessages]] to handle logs that would be dropped due to the
* logging service never being initialized.
*
* This method is also called as a shutdown hook, but it is good to call it
* before shutting down to ensure that everything has a chance to terminate
* correctly before the application exits. It can be safely called multiple
* times.
*/
def tearDown(): Unit = {
val service = this.synchronized {
val service = currentService
currentService = None
service
}
service match {
case Some(running) => running.terminate()
case None =>
}
handlePendingMessages()
}
/** Checks if the logging service has been set up. */
def isSetUp(): Boolean = this.synchronized {
currentService.isDefined
}
Runtime.getRuntime.addShutdownHook(new Thread(() => tearDown()))
/** Terminates the currently running logging service (if any) and replaces it
* with a fallback logging service.
*
* Can be used if the currently logging service fails after initialization
* and has to be shutdown.
*/
def replaceWithFallback(
printers: Seq[Printer] = Seq(StderrPrinter.create())
): Unit = {
val fallback =
Local.setup(currentLevel, messageQueue, printers)
val previousService = this.synchronized {
val previous = currentService
currentService = Some(fallback)
previous
}
previousService match {
case Some(service) =>
service.terminate()
case None =>
}
}
/** Removes any pending logs (so that [[handlePendingMessages]] will not print
* them).
*
* An internal method that is only used by [[TestLogger]].
*/
def dropPendingLogs(): Unit = messageQueue.drain(LogLevel.Off)
/** Prints any messages that have been buffered but have not been logged yet
* due to no loggers being active.
*/
private def handlePendingMessages(): Unit = {
val danglingMessages = messageQueue.drain(currentLevel)
if (danglingMessages.nonEmpty) {
InternalLogger.error(
"It seems that the logging service was never set up, " +
"or log messages were reported after it has been terminated. " +
"These messages are printed below:"
)
val stderrPrinter = StderrPrinter.create()
danglingMessages.foreach { message =>
stderrPrinter.print(message)
}
}
}
private def doSetup[InitializationResult](
mode: LoggerMode[InitializationResult],
logLevel: LogLevel
): InitializationResult = {
this.synchronized {
if (currentService.isDefined) {
throw new LoggingServiceAlreadyInitializedException()
}
val (service, result): (Service, InitializationResult) = mode match {
case LoggerMode.Client(endpoint) =>
(Client.setup(endpoint, messageQueue, logLevel), ())
case LoggerMode.Server(printers, port, interface) =>
val server = Server.setup(
interface,
port.getOrElse(0),
messageQueue,
printers,
logLevel
)
(server, server.getBinding())
case LoggerMode.Local(printers) =>
(Local.setup(logLevel, messageQueue, printers), ())
}
currentService = Some(service)
result
}
}
}

View File

@ -1,261 +0,0 @@
package org.enso.loggingservice
import akka.http.scaladsl.model.Uri
import com.typesafe.scalalogging.Logger
import org.enso.logger.masking.Masking
import org.enso.loggingservice.printers._
import java.nio.file.Path
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
import scala.util.control.NonFatal
import scala.util.{Failure, Success}
abstract class LoggingServiceSetupHelper(implicit
executionContext: ExecutionContext
) {
private val logger = Logger[this.type]
/** Default log level to use if none is provided. */
val defaultLogLevel: LogLevel
/** The location for storing the log files. */
def logPath: Path
/** A suffix added to created log files. */
val logFileSuffix: String
/** Sets up the logging service as either a server that gathers other
* component's logs or a client that forwards them further.
*
* Forwarding logs to another server is currently an internal,
* development-mode feature that is not designed to be used by end-users
* unless they specifically know what they are doing. Redirecting logs to an
* external server may result in some important information not being printed
* by the application, being forwarded instead.
*
* @param logLevel the log level to use for this application's logs; does not
* affect other component's log level, which has to be set
* separately
* @param connectToExternalLogger specifies an Uri of an external logging
* service that the application should forward
* its logs to; advanced feature, use with
* caution
* @param colorMode specifies how to handle colors in console output
* @param logMasking switches the masking on and off
*/
def setup(
logLevel: Option[LogLevel],
connectToExternalLogger: Option[Uri],
colorMode: ColorMode,
logMasking: Boolean,
profilingLog: Option[Path]
): Unit = {
val actualLogLevel = logLevel.getOrElse(defaultLogLevel)
Masking.setup(logMasking)
connectToExternalLogger match {
case Some(uri) =>
setupLoggingConnection(uri, actualLogLevel)
case None =>
setupLoggingServer(actualLogLevel, colorMode, profilingLog)
}
}
/** Sets up a fallback logger that just logs to stderr.
*
* It can be used when the application has failed to parse the CLI options
* and does not know which logger to set up.
*/
def setupFallback(): Unit = {
LoggingServiceManager
.setup(
LoggerMode.Local(Seq(fallbackPrinter)),
defaultLogLevel
)
.onComplete { _ =>
loggingServiceEndpointPromise.trySuccess(None)
}
}
def fallbackPrinter: Printer = StderrPrinter.create(printExceptions = true)
private val loggingServiceEndpointPromise = Promise[Option[Uri]]()
/** Returns a [[Uri]] of the logging service that launched components can
* connect to.
*
* Points to the local server if it has been set up, or to the endpoint that
* the launcher was told to connect to. May be empty if the initialization
* failed and local logging is used as a fallback.
*
* The future is completed once the
*/
def loggingServiceEndpoint(): Future[Option[Uri]] =
loggingServiceEndpointPromise.future
/** Returns a printer for outputting the logs to the standard error. */
def stderrPrinter(
colorMode: ColorMode,
printExceptions: Boolean
): Printer =
colorMode match {
case ColorMode.Never =>
StderrPrinter.create(printExceptions)
case ColorMode.Auto =>
StderrPrinterWithColors.colorPrinterIfAvailable(printExceptions)
case ColorMode.Always =>
StderrPrinterWithColors.forceCreate(printExceptions)
}
private def setupLoggingServer(
logLevel: LogLevel,
colorMode: ColorMode,
profilingLog: Option[Path]
): Unit = {
val printExceptionsInStderr =
implicitly[Ordering[LogLevel]].compare(logLevel, LogLevel.Debug) >= 0
/** Creates a stderr printer and a file printer if a log file can be opened.
*
* This is a `def` on purpose, as even if the service fails, the printers
* are shut down, so the fallback must create new instances.
*/
def createPrinters() =
try {
val filePrinter =
FileOutputPrinter.create(
logDirectory = logPath,
suffix = logFileSuffix,
printExceptions = true
)
val profilingPrinterOpt = profilingLog.map(new FileXmlPrinter(_))
Seq(
stderrPrinter(colorMode, printExceptionsInStderr),
filePrinter
) ++ profilingPrinterOpt
} catch {
case NonFatal(error) =>
logger.error(
"Failed to initialize the write-to-file logger, " +
"falling back to stderr only.",
error
)
Seq(stderrPrinter(colorMode, printExceptions = true))
}
LoggingServiceManager
.setup(LoggerMode.Server(createPrinters()), logLevel)
.onComplete {
case Failure(LoggingServiceAlreadyInitializedException()) =>
logger.warn(
"Failed to initialize the logger because the logging service " +
"was already initialized."
)
loggingServiceEndpointPromise.trySuccess(None)
case Failure(exception) =>
logger.error(
s"Failed to initialize the logging service server: $exception",
exception
)
logger.warn("Falling back to local-only logger.")
loggingServiceEndpointPromise.trySuccess(None)
LoggingServiceManager
.setup(
LoggerMode.Local(createPrinters()),
logLevel
)
.onComplete {
case Failure(LoggingServiceAlreadyInitializedException()) =>
logger.warn(
"Failed to initialize the fallback logger because the " +
"logging service was already initialized."
)
loggingServiceEndpointPromise.trySuccess(None)
case Failure(fallbackException) =>
System.err.println(
s"Failed to initialize the fallback logger: " +
s"$fallbackException"
)
fallbackException.printStackTrace()
case Success(_) =>
}
case Success(serverBinding) =>
val uri = serverBinding.toUri()
try {
loggingServiceEndpointPromise.success(Some(uri))
logger.trace(
s"Logging service has been set-up and is listening at `$uri`."
)
} catch {
case _: IllegalStateException =>
val earlierValue = loggingServiceEndpointPromise.future.value
logger.warn(
s"The logging service has been set-up at `$uri`, but the " +
s"logging URI has been initialized before that to " +
s"$earlierValue."
)
}
}
}
/** Connects this application to an external logging service.
*
* Currently, this is an internal function used mostly for testing purposes.
* It is not a user-facing API.
*/
private def setupLoggingConnection(uri: Uri, logLevel: LogLevel): Unit = {
LoggingServiceManager
.setup(
LoggerMode.Client(uri),
logLevel
)
.map(_ => true)
.recoverWith { _ =>
LoggingServiceManager
.setup(
LoggerMode.Local(Seq(fallbackPrinter)),
logLevel
)
.map(_ => false)
}
.onComplete {
case Failure(exception) =>
System.err.println(s"Failed to initialize the logger: $exception")
exception.printStackTrace()
loggingServiceEndpointPromise.trySuccess(None)
case Success(connected) =>
if (connected) {
try {
loggingServiceEndpointPromise.success(Some(uri))
System.err.println(
s"Log messages are forwarded to `$uri`."
)
} catch {
case _: IllegalStateException =>
val earlierValue = loggingServiceEndpointPromise.future.value
logger.warn(
s"The logging service has been set-up at `$uri`, but the " +
s"logging URI has been initialized before that to " +
s"$earlierValue."
)
}
} else {
loggingServiceEndpointPromise.trySuccess(None)
}
}
}
/** Waits until the logging service has been set-up.
*
* Due to limitations of how the logging service is implemented, it can only
* be terminated after it has been set up.
*/
def waitForSetup(): Unit = {
Await.ready(loggingServiceEndpointPromise.future, 5.seconds)
}
/** Shuts down the logging service gracefully.
*/
def tearDown(): Unit = LoggingServiceManager.tearDown()
}

View File

@ -1,13 +0,0 @@
package org.enso.loggingservice
import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.model.Uri.{Authority, Host, Path}
case class ServerBinding(port: Int) {
def toUri(host: String = "localhost"): Uri =
Uri(
scheme = "ws",
authority = Authority(host = Host(host), port = port),
path = Path./
)
}

View File

@ -1,54 +0,0 @@
package org.enso.loggingservice
import org.enso.loggingservice.printers.TestPrinter
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt
/** A helper object for handling logs in tests.
*/
object TestLogger {
/** A log message returned by [[gatherLogs]].
*
* It contains the loglevel and message, but ignores attached exceptions.
*/
case class TestLogMessage(logLevel: LogLevel, message: String)
/** Gathers logs logged during execution of `action`.
*
* This method should be used only inside of tests. Any tests using it should
* be ran with `parallelExecution` set to false, as global logger state has
* to be modified to gather the logs.
*/
def gatherLogs[R](action: => R): (R, Seq[TestLogMessage]) = {
LoggingServiceManager.dropPendingLogs()
if (LoggingServiceManager.isSetUp()) {
throw new IllegalStateException(
"gatherLogs called but another logging service has been already set " +
"up, this would lead to conflicts"
)
}
val printer = new TestPrinter
val future = LoggingServiceManager.setup(
LoggerMode.Local(Seq(printer)),
LogLevel.Trace
)
Await.ready(future, 1.second)
val result = action
Thread.sleep(100)
LoggingServiceManager.tearDown()
(result, printer.getLoggedMessages)
}
/** Drops any logs that are pending due to the logging service not being set
* up.
*
* This method should be used only inside of tests. Any tests using it should
* be ran with `parallelExecution` set to false, as global logger state has
* to be modified to gather the logs.
*/
def dropLogs(): Unit = {
LoggingServiceManager.dropPendingLogs()
}
}

View File

@ -1,28 +0,0 @@
package org.enso.loggingservice.internal
import org.enso.loggingservice.LogLevel
import scala.io.AnsiColor
/** Renders log messages in the same way as [[DefaultLogMessageRenderer]] but
* adds ANSI escape codes to display the log level in color.
*/
class ANSIColorsMessageRenderer(printExceptions: Boolean)
extends DefaultLogMessageRenderer(printExceptions) {
/** @inheritdoc
*/
override def renderLevel(logLevel: LogLevel): String = {
val color = logLevel match {
case LogLevel.Error => Some(AnsiColor.RED)
case LogLevel.Warning => Some(AnsiColor.YELLOW)
case LogLevel.Debug => Some(AnsiColor.CYAN)
case LogLevel.Trace => Some(AnsiColor.CYAN)
case _ => None
}
color match {
case Some(ansiColor) =>
s"$ansiColor${super.renderLevel(logLevel)}${AnsiColor.RESET}"
case None => super.renderLevel(logLevel)
}
}
}

View File

@ -1,54 +0,0 @@
package org.enso.loggingservice.internal
import com.typesafe.scalalogging.Logger
import org.graalvm.nativeimage.ImageInfo
/** Handles VT-compatible color output in the terminal.
*/
object AnsiTerminal {
/** Tries enabling ANSI colors in terminal output and returns true if it
* succeeded.
*
* We assume that ANSI colors are supported by default on UNIX platforms. On
* Windows, we use native calls to enable them, currently this is only
* supported in native-image builds. Currently ANSI colors are not supported
* on non-native Windows targets.
*/
def tryEnabling(): Boolean = {
if (isWindows) {
if (ImageInfo.inImageCode) {
try {
NativeAnsiTerm.enableVT()
true
} catch {
case error: RuntimeException =>
Logger[AnsiTerminal.type].warn(
s"Failed to initialize VT terminal (output will not contain " +
s"colors): ${error.getMessage}"
)
false
}
} else false
} else true
}
private def isWindows: Boolean =
System.getProperty("os.name").toLowerCase.contains("win")
/** Checks if output of this program may be piped.
*/
def isLikelyPiped: Boolean = System.console() == null
/** Checks if the output is connected to a terminal that can handle color
* output.
*
* On Windows, this function also enables color output, so any code that
* wants to use VT escape codes for colors (and is not assuming that its
* output is redirected) should first call this function to try enabling it
* and only use them if this function returned true.
*/
def canUseColors(): Boolean = {
!isLikelyPiped && AnsiTerminal.tryEnabling()
}
}

View File

@ -1,15 +0,0 @@
package org.enso.loggingservice.internal
import java.time.Instant
import org.enso.loggingservice.LogLevel
/** A base type for log messages parametrized by the exception representation.
*/
trait BaseLogMessage[ExceptionType] {
def level: LogLevel
def timestamp: Instant
def group: String
def message: String
def exception: Option[ExceptionType]
}

View File

@ -1,88 +0,0 @@
package org.enso.loggingservice.internal
import java.util.concurrent.ArrayBlockingQueue
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.protocol.WSLogMessage
import scala.annotation.tailrec
/** A message queue that can be consumed by a thread in a loop with a limited
* buffer.
*/
class BlockingConsumerMessageQueue(bufferSize: Int = 5000) {
/** Enqueues the `message` to be sent and returns immediately.
*
* If any underlying buffers are full, they may be removed and a warning will
* be issued.
*/
def send(message: Either[InternalLogMessage, WSLogMessage]): Unit = {
val inserted = queue.offer(message)
if (!inserted) {
queue.clear()
queue.offer(Left(queueOverflowMessage))
}
}
/** Returns next message in the queue, skipping messages that should be
* ignored and waiting if no messages are currently available.
*
* The distinction between internal and external messages is that internal
* messages should only be considered if they have log level that is enabled.
* However, all external log messages should be processed, regardless of
* their log level, because external messages come from other components
* whose log level is set independently.
*/
@tailrec
final def nextMessage(internalLogLevel: LogLevel): WSLogMessage = {
val (message, internal) = encodeMessage(queue.take())
if (isMessageRelevant(internalLogLevel)(message, internal))
message
else nextMessage(internalLogLevel)
}
/** Returns all currently enqueued messages, skipping ones that should be
* ignored.
*
* See [[nextMessage]] for explanation which messages are ignored.
*/
def drain(internalLogLevel: LogLevel): Seq[WSLogMessage] = {
val buffer = scala.collection.mutable
.Buffer[Either[InternalLogMessage, WSLogMessage]]()
import scala.jdk.CollectionConverters._
queue.drainTo(buffer.asJava)
buffer.toSeq
.map(encodeMessage)
.filter((isMessageRelevant(internalLogLevel) _).tupled)
.map(_._1)
}
/** All external messages are relevant, but internal messages relevancy depends
* on its log level.
*/
private def isMessageRelevant(
internalLogLevel: LogLevel
)(message: WSLogMessage, internal: Boolean): Boolean =
!internal || internalLogLevel.shouldLog(message.level)
/** Returns the encoded message and a boolean value indicating if it was
* internal.
*/
private def encodeMessage(
message: Either[InternalLogMessage, WSLogMessage]
): (WSLogMessage, Boolean) =
message.fold(msg => (msg.toLogMessage, true), (_, false))
private val queueOverflowMessage: InternalLogMessage =
InternalLogMessage(
level = LogLevel.Warning,
classOf[BlockingConsumerMessageQueue].getCanonicalName,
"The Logger does not keep up with processing log messages. " +
"Some log messages have been dropped.",
None
)
private val queue =
new ArrayBlockingQueue[Either[InternalLogMessage, WSLogMessage]](bufferSize)
}

View File

@ -1,75 +0,0 @@
package org.enso.loggingservice.internal
import java.time.format.DateTimeFormatter
import java.time.{Instant, ZoneOffset, ZonedDateTime}
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.protocol.{
SerializedException,
WSLogMessage
}
/** Renders the log message using the default format, including attached
* exceptions if [[printExceptions]] is set.
*/
class DefaultLogMessageRenderer(printExceptions: Boolean)
extends LogMessageRenderer {
/** @inheritdoc
*/
override def render(logMessage: WSLogMessage): String = {
val level = renderLevel(logMessage.level)
val timestamp = renderTimestamp(logMessage.timestamp)
val base =
s"[$level] [$timestamp] [${logMessage.group}] ${logMessage.message}"
addException(base, logMessage.exception)
}
private val timestampZone = ZoneOffset.UTC
/** Renders the timestamp.
*/
def renderTimestamp(timestamp: Instant): String =
ZonedDateTime
.ofInstant(timestamp, timestampZone)
.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
/** Adds attached exception's stack trace (if available) to the log if
* printing stack traces is enabled.
*/
def addException(
message: String,
exception: Option[SerializedException]
): String =
exception match {
case Some(e) if printExceptions =>
message + "\n" + renderException(e)
case _ => message
}
/** Renders an exception with its strack trace.
*/
def renderException(exception: SerializedException): String = {
val head = s"${exception.name}: ${exception.message}"
val trace = exception.stackTrace.map(elem =>
s" at ${elem.element}(${elem.location})"
)
val cause = exception.cause
.map(e => s"\nCaused by: ${renderException(e)}")
.getOrElse("")
head + trace.map("\n" + _).mkString + cause
}
/** Renders a log level.
*/
def renderLevel(logLevel: LogLevel): String =
logLevel match {
case LogLevel.Error => "error"
case LogLevel.Warning => "warn"
case LogLevel.Info => "info"
case LogLevel.Debug => "debug"
case LogLevel.Trace => "trace"
case LogLevel.Off => "off"
case _ => "error";
}
}

View File

@ -1,57 +0,0 @@
package org.enso.loggingservice.internal
import java.time.Instant
import java.time.temporal.ChronoUnit
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.protocol.{
SerializedException,
WSLogMessage
}
/** The internal log message that is used for local logging.
*
* @param level log level
* @param timestamp timestamp indicating when the message was created
* @param group group associated with the message
* @param message text message
* @param exception optional attached exception
*/
case class InternalLogMessage(
level: LogLevel,
timestamp: Instant,
group: String,
message: String,
exception: Option[Throwable]
) extends BaseLogMessage[Throwable] {
/** Converts to [[WSLogMessage]] by serializing the attached exception.
*/
def toLogMessage: WSLogMessage =
WSLogMessage(
level = level,
timestamp = timestamp.truncatedTo(ChronoUnit.MILLIS),
group = group,
message = message,
exception = exception.map(SerializedException.fromException)
)
}
object InternalLogMessage {
/** Creates a log message with the timestamp set to the current instant.
*/
def apply(
level: LogLevel,
group: String,
message: String,
exception: Option[Throwable]
): InternalLogMessage =
InternalLogMessage(
level = level,
timestamp = Instant.now(),
group = group,
message = message,
exception = exception
)
}

View File

@ -1,17 +0,0 @@
package org.enso.loggingservice.internal
/** An internal logger used for reporting errors within the logging service
* itself.
*
* As the logging service cannot be used to report its own errors (because a
* logging service error likely means that it is in a unusable state), its
* errors are printed to the standard error output.
*/
object InternalLogger {
/** Reports an internal logging service error with the given message.
*/
def error(message: String): Unit = {
System.err.println(s"[internal-logger-error] $message")
}
}

View File

@ -1,13 +0,0 @@
package org.enso.loggingservice.internal
import org.enso.loggingservice.internal.protocol.WSLogMessage
/** Specifies a strategy of rendering log messages to string.
*/
trait LogMessageRenderer {
/** Creates a string representation of the log message that can be written to
* an output.
*/
def render(logMessage: WSLogMessage): String
}

View File

@ -1,40 +0,0 @@
package org.enso.loggingservice.internal
import org.enso.loggingservice.LogLevel
/** An interface that allows to send log messages to the logging service. */
trait LoggerConnection {
/** Sends a message to the logging service.
*
* It should return immediately. Sending a message usually means that it is
* enqueued and will be encoded and sent to the logging service soon, but it
* is possible for messages to be dropped if too many messages are logged in
* a short period of time.
*/
def send(message: InternalLogMessage): Unit
/** Current log level.
*
* Only messages that have equal or smaller log level should be sent. Other
* messages will be ignored.
*/
def logLevel: LogLevel
/** Extra logger settings overriding the default log level.
*
* @return a mapping from a logger name to the log level that will be used
* for that logger.
*/
def loggers: Map[String, LogLevel]
/** Tells if messages with the provided log level should be sent. */
def isEnabled(name: String, level: LogLevel): Boolean = {
val loggerLevel =
loggers
.find(entry => name.startsWith(entry._1))
.map(_._2)
.getOrElse(logLevel)
implicitly[Ordering[LogLevel]].lteq(level, loggerLevel)
}
}

View File

@ -1,81 +0,0 @@
package org.enso.loggingservice.internal
import com.typesafe.config.{Config, ConfigFactory}
import org.enso.loggingservice.LogLevel
import scala.collection.immutable.ListMap
/** Reads logger settings from the resources.
*
* Currently these settings are used to configure logging inside of tests.
*/
object LoggingSettings {
private object Key {
val root = "logging-service"
val logger = "logger"
val testLogLevel = "test-log-level"
val GLOB = "*"
}
private lazy val configuration: Config = {
val empty = ConfigFactory.empty().atKey(Key.logger).atKey(Key.root)
ConfigFactory.load().withFallback(empty).getConfig(Key.root)
}
/** Log level settings overriding the default application log level.
*
* @return a mapping from a logger name to the log level that will be used
* for that logger.
*/
lazy val loggers: Map[String, LogLevel] = {
def normalize(key: String): String =
key.replace("'", "").replace("\"", "")
val loggerConfig = configuration.getConfig(Key.logger)
val builder = ListMap.newBuilder[String, LogLevel]
// `config` is unordered. To keep glob (*) entries at the end of the `ListMap`,
// gather them at the `fallback` map, and then append to the final `builder`
val fallback = ListMap.newBuilder[String, LogLevel]
loggerConfig.entrySet.forEach { entry =>
val key = entry.getKey
val value = loggerConfig.getString(key)
LogLevel.fromString(value) match {
case Some(logLevel) =>
val normalizedKey = normalize(key)
if (normalizedKey.endsWith(Key.GLOB)) {
fallback += normalizedKey.dropRight(Key.GLOB.length + 1) -> logLevel
} else {
builder += normalizedKey -> logLevel
}
case None =>
System.err.println(
s"Invalid log level for key [${normalize(key)}] set in " +
s"application config [$value]. Default log level will be used."
)
}
}
builder ++= fallback.result()
builder.result()
}
/** Indicates the log level to be used in test mode.
*
* If set to None, production logging should be used.
*/
lazy val testLogLevel: Option[LogLevel] = {
Option.when(configuration.hasPath(Key.testLogLevel)) {
val value = configuration.getString(Key.testLogLevel)
LogLevel.fromString(value).getOrElse {
System.err.println(
s"Invalid log level for key [${Key.testLogLevel}] set in " +
s"application config [$value], falling back to info."
)
LogLevel.Info
}
}
}
}

View File

@ -1,35 +0,0 @@
package org.enso.loggingservice.internal
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.protocol.WSLogMessage
import org.enso.loggingservice.printers.StderrPrinter
/** A message queue for use in testing.
*
* It has a smaller buffer and ignores messages from a certain log level.
*
* @param logLevel specifies which messages will be printed to stderr if no
* service is set-up
* @param isLoggingServiceSetUp a function used to check if a logging service
* is set up
*/
class TestMessageQueue(logLevel: LogLevel, isLoggingServiceSetUp: () => Boolean)
extends BlockingConsumerMessageQueue(bufferSize = 100) {
private def shouldKeepMessage(
message: Either[InternalLogMessage, WSLogMessage]
): Boolean = message match {
case Left(value) => logLevel.shouldLog(value.level)
case Right(value) => logLevel.shouldLog(value.level)
}
private val overridePrinter = StderrPrinter.create()
/** @inheritdoc */
override def send(message: Either[InternalLogMessage, WSLogMessage]): Unit =
if (isLoggingServiceSetUp()) {
if (shouldKeepMessage(message))
overridePrinter.print(message.fold(_.toLogMessage, identity))
} else {
super.send(message)
}
}

View File

@ -1,191 +0,0 @@
package org.enso.loggingservice.internal.protocol
import io.circe.syntax._
import io.circe._
/** Represents a language-agnostic exception that can be sent over the WebSocket
* connection.
*
* @param name name of the exception, corresponds to the qualified class name
* of the exception
* @param message message as returned by the exception's getMessage method
* @param stackTrace serialized stack trace
* @param cause optional serialized exception that caused this one; it must not
* point to itself, as this structure cannot be cyclic so that it
* can be serialized into JSON
*/
case class SerializedException(
name: String,
message: Option[String],
stackTrace: Seq[SerializedException.TraceElement],
cause: Option[SerializedException]
)
object SerializedException {
/** Creates a [[SerializedException]] with a cause.
*/
def apply(
name: String,
message: String,
stackTrace: Seq[SerializedException.TraceElement],
cause: SerializedException
): SerializedException =
SerializedException(
name = name,
message = Some(message),
stackTrace = stackTrace,
cause = Some(cause)
)
/** Creates a [[SerializedException]] without a cause.
*/
def apply(
name: String,
message: String,
stackTrace: Seq[SerializedException.TraceElement]
): SerializedException =
new SerializedException(
name = name,
message = Some(message),
stackTrace = stackTrace,
cause = None
)
/** Encodes a JVM [[Throwable]] as [[SerializedException]].
*/
def fromException(throwable: Throwable): SerializedException = {
val clazz = throwable.getClass
val cause =
if (throwable.getCause == throwable) None
else Option(throwable.getCause).map(fromException)
SerializedException(
name = Option(clazz.getCanonicalName).getOrElse(clazz.getName),
message = Option(throwable.getMessage),
stackTrace = throwable.getStackTrace.toSeq.map(encodeStackTraceElement),
cause = cause
)
}
/** Regular expression used to parse the stack trace elements format used by
* [[StackTraceElement#toString]].
*
* It assumes that the stack trace element has form `element(location)`, for
* example `foo.bar(Foo.java:123)` or `win32.foo(Native Method)`.
*
* This is the most robust way to get the location from the
* [[StackTraceElement]] without duplicating the standard library code. In
* case that the result of [[StackTraceElement#toString]] does not match the
* regex, an approximation based on its getters is used.
*/
private val stackTraceRegex = "(.*)\\((.*)\\)".r
/** Encodes a [[StackTraceElement]] as [[TraceElement]].
*/
private def encodeStackTraceElement(
stackTraceElement: StackTraceElement
): TraceElement = {
stackTraceElement.toString match {
case stackTraceRegex(element, location) =>
TraceElement(element = element, location = location)
case _ =>
val location = for {
filename <- Option(stackTraceElement.getFileName)
line <-
if (stackTraceElement.getLineNumber < 0) None
else Some(stackTraceElement.getLineNumber)
} yield s"$filename:$line"
val className = stackTraceElement.getClassName
val methodName = stackTraceElement.getMethodName
TraceElement(
s"$className.$methodName",
location.getOrElse("Unknown Source")
)
}
}
/** Represents an element of a stack trace attached to the
* [[SerializedException]].
*
* @param element name of the stack location; for example, in Java this is
* the qualified method name
* @param location code location of the element
*/
case class TraceElement(element: String, location: String)
private object JsonFields {
val Name = "name"
val Message = "message"
val StackTrace = "trace"
val Cause = "cause"
object TraceElement {
val Element = "element"
val Location = "location"
}
}
/** [[Encoder]] instance for [[SerializedException]].
*/
implicit val encoder: Encoder[SerializedException] = encodeException
/** Encodes a [[SerializedException]] as its JSON representation.
*/
private def encodeException(exception: SerializedException): Json = {
val base = JsonObject(
JsonFields.Name -> exception.name.asJson,
JsonFields.Message -> exception.message.asJson,
JsonFields.StackTrace -> exception.stackTrace.asJson
)
val result = exception.cause match {
case Some(cause) =>
base.+:((JsonFields.Cause, encodeException(cause)))
case None =>
base
}
result.asJson
}
/** [[Decoder]] instance for [[SerializedException]].
*/
implicit def decoder: Decoder[SerializedException] = decodeException
/** Tries to decode a [[SerializedException]] from its JSON representation.
*/
private def decodeException(
json: HCursor
): Decoder.Result[SerializedException] = {
for {
name <- json.get[String](JsonFields.Name)
message <- json.get[Option[String]](JsonFields.Message)
stackTrace <- json.get[Seq[TraceElement]](JsonFields.StackTrace)
cause <-
json.getOrElse[Option[SerializedException]](JsonFields.Cause)(None)
} yield SerializedException(
name = name,
message = message,
stackTrace = stackTrace,
cause = cause
)
}
/** [[Encoder]] instance for [[TraceElement]].
*/
implicit val traceEncoder: Encoder[TraceElement] = { traceElement =>
Json.obj(
JsonFields.TraceElement.Element -> traceElement.element.asJson,
JsonFields.TraceElement.Location -> traceElement.location.asJson
)
}
/** [[Decoder]] instance for [[TraceElement]].
*/
implicit val traceDecoder: Decoder[TraceElement] = { json =>
for {
element <- json.get[String](JsonFields.TraceElement.Element)
location <- json.get[String](JsonFields.TraceElement.Location)
} yield TraceElement(element = element, location = location)
}
}

View File

@ -1,80 +0,0 @@
package org.enso.loggingservice.internal.protocol
import java.time.Instant
import io.circe.syntax._
import io.circe.{Decoder, Encoder, JsonObject}
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.BaseLogMessage
/** The encoded log message that can be sent over the WebSocket connection or
* passed to a printer.
*
* @param level log level
* @param timestamp timestamp indicating when the message was created
* @param group group associated with the message
* @param message text message
* @param exception optional serialized exception attached to the message
*/
case class WSLogMessage(
level: LogLevel,
timestamp: Instant,
group: String,
message: String,
exception: Option[SerializedException]
) extends BaseLogMessage[SerializedException]
object WSLogMessage {
private object JsonFields {
val Level = "level"
val Timestamp = "time"
val Group = "group"
val Message = "message"
val Exception = "exception"
}
/** [[Encoder]] instance for [[WSLogMessage]].
*/
implicit val encoder: Encoder[WSLogMessage] = { message =>
var base = JsonObject(
JsonFields.Level -> message.level.asJson,
JsonFields.Timestamp -> message.timestamp.toEpochMilli.asJson
)
if (message.group != null) {
base = base.+:((JsonFields.Group -> message.group.asJson))
}
if (message.message != null) {
base = base.+:((JsonFields.Message -> message.message.asJson))
}
val result = message.exception match {
case Some(exception) =>
base.+:((JsonFields.Exception, exception.asJson))
case None =>
base
}
result.asJson
}
/** [[Decoder]] instance for [[WSLogMessage]].
*/
implicit val decoder: Decoder[WSLogMessage] = { json =>
for {
level <- json.get[LogLevel](JsonFields.Level)
timestamp <-
json.get[Long](JsonFields.Timestamp).map(Instant.ofEpochMilli)
group <- json.get[String](JsonFields.Group)
message <- json.get[String](JsonFields.Message)
exception <-
json.getOrElse[Option[SerializedException]](JsonFields.Exception)(None)
} yield WSLogMessage(
level = level,
timestamp = timestamp,
group = group,
message = message,
exception = exception
)
}
}

View File

@ -1,169 +0,0 @@
package org.enso.loggingservice.internal.service
import akka.Done
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ws.{Message, TextMessage, WebSocketRequest}
import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.stream.scaladsl.{Keep, Sink, Source, SourceQueueWithComplete}
import akka.stream.{OverflowStrategy, QueueOfferResult}
import io.circe.syntax._
import org.enso.loggingservice.internal.{
BlockingConsumerMessageQueue,
InternalLogger
}
import org.enso.loggingservice.internal.protocol.WSLogMessage
import org.enso.loggingservice.{LogLevel, LoggingServiceManager}
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, Future}
/** A client [[Service]] that passes incoming log messages to a server.
*
* @param serverUri uri of the server to connect to
* @param queue log message queue
* @param logLevel log level used to filter messages
*/
class Client(
serverUri: Uri,
protected val queue: BlockingConsumerMessageQueue,
protected val logLevel: LogLevel
) extends ThreadProcessingService
with ServiceWithActorSystem {
/** @inheritdoc
*/
override protected def actorSystemName: String = "logging-service-client"
/** Starts the client service by trying to connect to the server.
*
* Returns a future that is completed once the connection has been
* established.
*/
def start(): Future[Unit] = {
val request = WebSocketRequest(serverUri)
val flow = Http().webSocketClientFlow(request)
val incoming = Sink.ignore
val outgoing = Source.queue[Message](0, OverflowStrategy.backpressure)
val ((outgoingQueue, upgradeResponse), closed) =
outgoing.viaMat(flow)(Keep.both).toMat(incoming)(Keep.both).run()
import actorSystem.dispatcher
val connected = upgradeResponse.flatMap { upgrade =>
if (upgrade.response.status == StatusCodes.SwitchingProtocols) {
Future.successful(Done)
} else {
throw new RuntimeException(
s"Connection failed: ${upgrade.response.status}"
)
}
}
connected.map(_ => {
closedConnection = Some(closed)
webSocketQueue = Some(outgoingQueue)
closed.onComplete(_ => onDisconnected())
startQueueProcessor()
})
}
private var webSocketQueue: Option[SourceQueueWithComplete[Message]] = None
private var closedConnection: Option[Future[Done]] = None
/** Tries to send the log message to the server by appending it to the queue
* of outgoing messages.
*
* It waits for the offer to complete for a long time to handle the case in
* which the server is unresponsive for a longer time. Any pending messages
* are just enqueued onto the main [[BlockingConsumerMessageQueue]] and will
* be sent after this one.
*/
override protected def processMessage(message: WSLogMessage): Unit = {
val queue = webSocketQueue.getOrElse(
throw new IllegalStateException(
"Internal error: The queue processor thread has been started before " +
"the connection has been initialized."
)
)
val serializedMessage = message.asJson.noSpaces
val offerResult = queue.offer(TextMessage.Strict(serializedMessage))
try {
Await.result(offerResult, 30.seconds) match {
case QueueOfferResult.Enqueued =>
case QueueOfferResult.Dropped =>
InternalLogger.error(s"A log message has been dropped unexpectedly.")
case QueueOfferResult.Failure(cause) =>
InternalLogger.error(s"A log message could not be sent: $cause.")
case QueueOfferResult.QueueClosed => throw new InterruptedException
}
} catch {
case _: concurrent.TimeoutException =>
InternalLogger.error(
s"Adding a log message timed out. Messages may or may not be dropped."
)
}
}
@volatile private var shuttingDown: Boolean = false
/** If the remote server closes the connection, notifies the logging service
* to start the fallback logger.
*/
private def onDisconnected(): Unit = {
if (!shuttingDown) {
InternalLogger.error(
"Server has disconnected, logging is falling back to stderr."
)
LoggingServiceManager.replaceWithFallback()
}
}
/** Closes the connection.
*/
override protected def terminateUser(): Future[_] = {
shuttingDown = true
webSocketQueue match {
case Some(wsQueue) =>
import actorSystem.dispatcher
val promise = concurrent.Promise[Done]()
wsQueue.complete()
wsQueue.watchCompletion().onComplete(promise.tryComplete)
closedConnection.foreach(_.onComplete(promise.tryComplete))
promise.future
case None => Future.successful(Done)
}
}
/** No additional actions are performed after the thread has been shut down as
* termination happens in [[terminateUser()]].
*/
override protected def afterShutdown(): Unit = {}
}
object Client {
/** Waits for the [[Client]] to start up and returns it or throws an exception
* on setup failure.
*
* @param serverUri uri of the server to connect to
* @param queue log message queue
* @param logLevel log level used to filter messages
*/
def setup(
serverUri: Uri,
queue: BlockingConsumerMessageQueue,
logLevel: LogLevel
): Client = {
val client = new Client(serverUri, queue, logLevel)
try {
Await.result(client.start(), 3.seconds)
client
} catch {
case e: Throwable =>
client.terminate()
throw e
}
}
}

View File

@ -1,49 +0,0 @@
package org.enso.loggingservice.internal.service
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.BlockingConsumerMessageQueue
import org.enso.loggingservice.internal.protocol.WSLogMessage
import org.enso.loggingservice.printers.Printer
/** A local [[Service]] that handles all log messages locally.
*
* @param logLevel log level used to filter messages
* @param queue log message queue
* @param printers printers defining handling of the log messages
*/
case class Local(
logLevel: LogLevel,
queue: BlockingConsumerMessageQueue,
printers: Seq[Printer]
) extends ThreadProcessingService {
/** Passes each message to all printers.
*/
override protected def processMessage(message: WSLogMessage): Unit =
printers.foreach(_.print(message))
/** Shuts down the printers.
*/
override protected def afterShutdown(): Unit = {
printers.foreach(_.shutdown())
}
}
object Local {
/** Starts the [[Local]] service and returns it.
*
* @param logLevel log level used to filter messages
* @param queue log message queue
* @param printers printers defining handling of the log messages
*/
def setup(
logLevel: LogLevel,
queue: BlockingConsumerMessageQueue,
printers: Seq[Printer]
): Local = {
val local = new Local(logLevel, queue, printers)
local.startQueueProcessor()
local
}
}

View File

@ -1,174 +0,0 @@
package org.enso.loggingservice.internal.service
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.AttributeKeys
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.ws.{BinaryMessage, Message, TextMessage}
import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri}
import akka.stream.scaladsl.{Flow, Sink, Source}
import io.circe.{parser, Error}
import org.enso.loggingservice.internal.{
BlockingConsumerMessageQueue,
InternalLogger
}
import org.enso.loggingservice.internal.protocol.WSLogMessage
import org.enso.loggingservice.printers.Printer
import org.enso.loggingservice.{LogLevel, ServerBinding}
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, Future}
/** A server [[Service]] which handles both messages incoming from a WebSocket
* connection and local log messages.
*
* @param interface interface to bind to
* @param port port to bind to; if set to 0, the system will allocate some
* available port
* @param queue log message queue
* @param printers printers defining handling of the log messages
* @param logLevel log level used to filter the local messages (messages from
* clients are passed as-is as they may use different log
* levels)
*/
class Server(
interface: String,
port: Int,
queue: BlockingConsumerMessageQueue,
printers: Seq[Printer],
logLevel: LogLevel
) extends Local(logLevel, queue, printers)
with ServiceWithActorSystem {
/** @inheritdoc
*/
override protected def actorSystemName: String = "logging-service-server"
/** Immediately starts processing local messages and returns a [[Future]] that
* will complete once the server has been started.
*/
def start(): Future[Unit] = {
startQueueProcessor()
startWebSocketServer()
}
/** Starts the WebSocket server.
*/
private def startWebSocketServer(): Future[Unit] = {
val requestHandler: HttpRequest => HttpResponse = {
case req @ HttpRequest(GET, Uri.Path("/"), _, _, _) =>
req.attribute(AttributeKeys.webSocketUpgrade) match {
case Some(upgrade) =>
val flow = Flow.fromSinkAndSourceCoupled(
createMessageProcessor(),
Source.never
)
upgrade.handleMessages(flow)
case None =>
HttpResponse(400, entity = "Not a valid websocket request!")
}
case r: HttpRequest =>
r.discardEntityBytes()
HttpResponse(404, entity = "Unknown resource!")
}
import actorSystem.dispatcher
Http()
.newServerAt(interface, port)
.bindSync(requestHandler)
.map { serverBinding =>
bindingOption = Some(serverBinding)
}
}
/** Returns the binding that describes how to connect to the started server.
*
* This method can only be called after the future returned from [[start]]
* has completed.
*/
def getBinding(): ServerBinding = {
val binding = bindingOption.getOrElse(
throw new IllegalStateException(
"Binding requested before the server has been initialized."
)
)
ServerBinding(port = binding.localAddress.getPort)
}
private var bindingOption: Option[Http.ServerBinding] = None
/** Creates a separate message processor for each connection.
*
* Each connection will only report the first invalid message, all further
* invalid messages are silently ignored.
*/
private def createMessageProcessor() = {
@volatile var invalidWasReported: Boolean = false
def reportInvalidMessage(error: Throwable): Unit = {
if (!invalidWasReported) {
InternalLogger.error(s"Invalid message: $error.")
invalidWasReported = true
}
}
Sink.foreach[Message] {
case tm: TextMessage =>
val rawMessage = tm.textStream.fold("")(_ + _)
val decodedMessage = rawMessage.map(decodeMessage)
decodedMessage.runForeach {
case Left(error) => reportInvalidMessage(error)
case Right(message) => queue.send(Right(message))
}
case bm: BinaryMessage =>
reportInvalidMessage(
new IllegalStateException("Unexpected binary message.")
)
bm.dataStream.runWith(Sink.ignore)
}
}
private def decodeMessage(message: String): Either[Error, WSLogMessage] =
parser.parse(message).flatMap(_.as[WSLogMessage])
/** Shuts down the server.
*/
override protected def terminateUser(): Future[_] = {
bindingOption match {
case Some(binding) =>
binding.terminate(hardDeadline = 2.seconds)
case None => Future.successful(())
}
}
}
object Server {
/** Waits for the [[Server]] to start up and returns it or throws an exception
* on setup failure.
*
* @param interface interface to bind to
* @param port port to bind to; if set to 0, the system will allocate some
* available port
* @param queue log message queue
* @param printers printers defining handling of the log messages
* @param logLevel log level used to filter the local messages (messages from
* clients are passed as-is as they may use different log
* levels)
*/
def setup(
interface: String,
port: Int,
queue: BlockingConsumerMessageQueue,
printers: Seq[Printer],
logLevel: LogLevel
): Server = {
val server = new Server(interface, port, queue, printers, logLevel)
try {
Await.result(server.start(), 3.seconds)
server
} catch {
case e: Throwable =>
server.terminate()
throw e
}
}
}

View File

@ -1,10 +0,0 @@
package org.enso.loggingservice.internal.service
/** A service backend that is used to process incoming log messages.
*/
trait Service {
/** Terminates the service and releases its resources.
*/
def terminate(): Unit = {}
}

View File

@ -1,121 +0,0 @@
package org.enso.loggingservice.internal.service
import akka.actor.ActorSystem
import com.typesafe.config.{ConfigFactory, ConfigValueFactory}
import org.enso.loggingservice.internal.InternalLogger
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, Future}
/** A mix-in for implementing services that use an Akka [[ActorSystem]].
*/
trait ServiceWithActorSystem extends Service {
/** Name to use for the [[ActorSystem]].
*/
protected def actorSystemName: String
/** The [[ActorSystem]] that can be used by the service.
*/
implicit protected val actorSystem: ActorSystem =
initializeActorSystemForLoggingService(actorSystemName)
/** Initializes an [[ActorSystem]], overriding the default logging settings.
*
* The Actor System responsible for the logging service cannot use the
* default configured logger, because this logger is likely to be the one
* bound to the logging service itself. The logging service cannot use itself
* for logging, because if it failed, it could not log its own failure or
* there could be a risk of entering an infinite loop if writing a log
* message triggered another log message.
*
* To avoid these issues, the Actor System responsible for the logging
* service overrides its logger setting to use the default standard output
* logger and is configured to only log warnings or errors.
*/
private def initializeActorSystemForLoggingService(
name: String
): ActorSystem = {
import scala.jdk.CollectionConverters._
val loggers: java.lang.Iterable[String] =
Seq("akka.event.Logging$StandardOutLogger").asJava
val config = {
val baseConfig = ConfigFactory
.empty()
.withValue("akka.loggers", ConfigValueFactory.fromAnyRef(loggers))
.withValue(
"akka.logging-filter",
ConfigValueFactory.fromAnyRef("akka.event.DefaultLoggingFilter")
)
.withValue("akka.loglevel", ConfigValueFactory.fromAnyRef("WARNING"))
.withValue(
"akka.coordinated-shutdown.run-by-actor-system-terminate",
ConfigValueFactory.fromAnyRef("off")
)
.withValue("akka.daemonic", ConfigValueFactory.fromAnyRef("on"))
.withValue(
"akka.http.server.websocket.periodic-keep-alive-mode",
ConfigValueFactory.fromAnyRef("ping")
)
.withValue(
"akka.http.server.websocket.periodic-keep-alive-max-idle",
ConfigValueFactory.fromAnyRef("30 seconds")
)
val timeouts = Seq(
"akka.http.server.idle-timeout",
"akka.http.client.idle-timeout",
"akka.http.host-connection-pool.client.idle-timeout",
"akka.http.host-connection-pool.idle-timeout"
)
val configWithTimeouts = timeouts.foldLeft(baseConfig) {
case (config, key) =>
config.withValue(key, ConfigValueFactory.fromAnyRef("120 seconds"))
}
configWithTimeouts
}
ActorSystem(
name,
config,
classLoader =
classOf[Server].getClassLoader // Note [Actor System Class Loader]
)
}
/** Called before terminating the [[ActorSystem]], can be used to handle any
* actions that should happen before it is terminated.
*
* The actor system will wait with its termination until the returned future
* completes.
*/
protected def terminateUser(): Future[_]
/** Waits for up to 3 seconds for the [[terminateUser]] and [[ActorSystem]]
* termination, then handles any other termination logic.
*/
abstract override def terminate(): Unit = {
import actorSystem.dispatcher
val termination = terminateUser().map(_ => {
actorSystem.terminate()
})
try {
Await.result(termination, 3.seconds)
} catch {
case _: concurrent.TimeoutException =>
InternalLogger.error("The actor system did not terminate in time.")
} finally {
super.terminate()
}
}
}
/* Note [Actor System Class Loader]
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Without explicitly setting the ClassLoader, the ActorSystem initialization
* fails (at least if run in `sbt`) with `java.lang.ClassCastException:
* interface akka.event.LoggingFilter is not assignable from class
* akka.event.DefaultLoggingFilter` which is most likely caused by the two
* instances coming from distinct class loaders.
*/

View File

@ -1,98 +0,0 @@
package org.enso.loggingservice.internal.service
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.protocol.WSLogMessage
import org.enso.loggingservice.internal.{
BlockingConsumerMessageQueue,
DefaultLogMessageRenderer,
InternalLogger
}
import scala.util.control.NonFatal
/** A mix-in for implementing services that process messages from a
* [[BlockingConsumerMessageQueue]] in a separate thread.
*/
trait ThreadProcessingService extends Service {
/** The queue that is the source of messages.
*/
protected def queue: BlockingConsumerMessageQueue
/** Log level used for filtering messages from the queue.
* @return
*/
protected def logLevel: LogLevel
/** Logic responsible for processing each message from [[queue]].
*
* This function is guaranteed to be called synchronously from a single
* thread.
*/
protected def processMessage(message: WSLogMessage): Unit
/** Called after the message processing thread has been stopped, can be used
* to finish termination.
*/
protected def afterShutdown(): Unit
private var queueThread: Option[Thread] = None
/** Starts the thread processing messages from [[queue]].
*/
protected def startQueueProcessor(): Unit = {
if (queueThread.isDefined) {
throw new IllegalStateException(
"The processing thread has already been started."
)
}
val thread = new Thread(() => runQueue())
thread.setName("logging-service-processing-thread")
thread.setDaemon(true)
queueThread = Some(thread)
thread.start()
}
private lazy val renderer = new DefaultLogMessageRenderer(
printExceptions = false
)
/** The runner filters out internal messages that have disabled log levels,
* but passes through all external messages (as their log level is set
* independently and can be lower).
*/
private def runQueue(): Unit = {
try {
while (!Thread.currentThread().isInterrupted) {
val message = queue.nextMessage(logLevel)
try {
processMessage(message)
} catch {
case NonFatal(e) =>
InternalLogger.error(
s"One of the printers failed to write a message: $e"
)
InternalLogger.error(
s"The dropped message was: ${renderer.render(message)}"
)
}
}
} catch {
case _: InterruptedException =>
}
}
/** @inheritdoc */
abstract override def terminate(): Unit = {
super.terminate()
queueThread match {
case Some(thread) =>
thread.interrupt()
thread.join(100)
queueThread = None
afterShutdown()
case None =>
}
}
}

View File

@ -1,76 +0,0 @@
package org.enso.loggingservice.printers
import java.io.PrintWriter
import java.nio.file.{Files, Path, StandardOpenOption}
import java.time.format.DateTimeFormatter
import java.time.{Instant, LocalDateTime, ZoneId}
import org.enso.loggingservice.internal.DefaultLogMessageRenderer
import org.enso.loggingservice.internal.protocol.WSLogMessage
/** Creates a new file in [[logDirectory]] and writes incoming log messages to
* this file.
*
* @param logDirectory the directory to create the logfile in
* @param suffix a suffix to be added to the filename
* @param printExceptions whether to print exceptions attached to the log
* messages
*/
class FileOutputPrinter(
logDirectory: Path,
suffix: String,
printExceptions: Boolean
) extends Printer {
private val renderer = new DefaultLogMessageRenderer(printExceptions)
private val writer = initializeWriter()
/** @inheritdoc */
override def print(message: WSLogMessage): Unit = {
val lines = renderer.render(message)
writer.println(lines)
// TODO [RW] we may consider making flushing configurable as it is mostly
// useful for debugging crashes, whereas for usual usecases buffering could
// give slightly better performance
writer.flush()
}
/** @inheritdoc */
override def shutdown(): Unit = {
writer.flush()
writer.close()
}
/** Opens the log file for writing. */
private def initializeWriter(): PrintWriter = {
val logPath = logDirectory.resolve(makeLogFilename())
Files.createDirectories(logDirectory)
new PrintWriter(
Files.newBufferedWriter(
logPath,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE
)
)
}
/** Creates a log filename that is created based on the current timestamp. */
private def makeLogFilename(): String = {
val timestampZone = ZoneId.of("UTC")
val timestamp = LocalDateTime
.ofInstant(Instant.now(), timestampZone)
.format(DateTimeFormatter.ofPattern("YYYYMMdd-HHmmss-SSS"))
s"$timestamp-$suffix.log"
}
}
object FileOutputPrinter {
/** Creates a new [[FileOutputPrinter]]. */
def create(
logDirectory: Path,
suffix: String,
printExceptions: Boolean
): FileOutputPrinter =
new FileOutputPrinter(logDirectory, suffix, printExceptions)
}

View File

@ -1,61 +0,0 @@
package org.enso.loggingservice.printers
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.protocol.WSLogMessage
import java.io.PrintWriter
import java.nio.file.{Files, Path, StandardOpenOption}
import java.util.logging.{LogRecord, XMLFormatter}
/** Creates a new file in [[logPath]] and writes incoming log messages to
* this file in XML format.
*
* @param logPath the file path to log
*/
class FileXmlPrinter(logPath: Path) extends Printer {
private val writer = initializeWriter()
private val formatter = new XMLFormatter()
/** @inheritdoc */
override def print(message: WSLogMessage): Unit = {
val lines = formatter.format(toLogRecord(message))
writer.print(lines)
}
/** @inheritdoc */
override def shutdown(): Unit = {
writer.flush()
writer.close()
}
/** Opens the log file for writing. */
private def initializeWriter(): PrintWriter = {
Option(logPath.getParent).foreach(Files.createDirectories(_))
val writer = new PrintWriter(
Files.newBufferedWriter(
logPath,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE
)
)
writer.println(FileXmlPrinter.Header)
writer
}
/** Converts [[WSLogMessage]] to java [[LogRecord]]. */
private def toLogRecord(wsLogMessage: WSLogMessage): LogRecord = {
val record =
new LogRecord(LogLevel.toJava(wsLogMessage.level), wsLogMessage.message)
record.setInstant(wsLogMessage.timestamp)
record.setLoggerName(wsLogMessage.group)
record
}
}
object FileXmlPrinter {
private val Header: String =
"<?xml version='1.0' encoding='UTF-8'?><uigestures version='1.0'>"
}

View File

@ -1,20 +0,0 @@
package org.enso.loggingservice.printers
import org.enso.loggingservice.internal.protocol.WSLogMessage
/** Defines a strategy for outputting the log messages.
*
* It can output them to the console, a file, a database etc.
*/
trait Printer {
/** Outputs the log message.
*/
def print(message: WSLogMessage): Unit
/** Shuts down this output channel.
*
* It should flush any buffers and release resources.
*/
def shutdown(): Unit
}

View File

@ -1,32 +0,0 @@
package org.enso.loggingservice.printers
import org.enso.loggingservice.internal.DefaultLogMessageRenderer
import org.enso.loggingservice.internal.protocol.WSLogMessage
/** Prints the log messages to the standard error output.
*
* @param printExceptions specifies if attached exceptions should be printed
*/
class StderrPrinter(printExceptions: Boolean) extends Printer {
private val renderer = new DefaultLogMessageRenderer(printExceptions)
/** @inheritdoc
*/
override def print(logMessage: WSLogMessage): Unit = {
val lines = renderer.render(logMessage)
System.err.println(lines)
}
/** @inheritdoc
*/
override def shutdown(): Unit =
System.err.flush()
}
object StderrPrinter {
/** Creates an instance of [[StderrPrinter]].
*/
def create(printExceptions: Boolean = false): StderrPrinter =
new StderrPrinter(printExceptions)
}

View File

@ -1,66 +0,0 @@
package org.enso.loggingservice.printers
import com.typesafe.scalalogging.Logger
import org.enso.loggingservice.internal.{
ANSIColorsMessageRenderer,
AnsiTerminal
}
import org.enso.loggingservice.internal.protocol.WSLogMessage
import scala.io.AnsiColor
/** Prints the log messages to the standard error output with ANSI escape codes
* that allow to display log levels in color.
*
* @param printExceptions specifies if attached exceptions should be printed
*/
class StderrPrinterWithColors private (printExceptions: Boolean)
extends Printer {
private val renderer = new ANSIColorsMessageRenderer(printExceptions)
/** @inheritdoc
*/
override def print(message: WSLogMessage): Unit = {
val lines = renderer.render(message)
System.err.println(lines)
}
/** @inheritdoc
*/
override def shutdown(): Unit = {
System.err.print(AnsiColor.RESET)
System.err.flush()
}
}
object StderrPrinterWithColors {
/** Returns a color-supporting printer if color output is available in the
* console.
*/
def colorPrinterIfAvailable(printExceptions: Boolean): Printer =
if (AnsiTerminal.canUseColors())
new StderrPrinterWithColors(printExceptions)
else new StderrPrinter(printExceptions)
/** Returns a color-supporting printer regardless of if color output is
* available.
*
* Color output may be used even if it is unavailable in cases where the
* output is piped to another application that will support VT colors. This
* call tries to enable the color output in the local console anyway, in case
* it is used with the local console.
*
* If color support cannot be enabled and the output seems to not be piped, a
* warning is issued that colors are not supported.
*/
def forceCreate(printExceptions: Boolean): StderrPrinterWithColors = {
if (!AnsiTerminal.tryEnabling() && !AnsiTerminal.isLikelyPiped) {
Logger[StderrPrinterWithColors].warn(
"Color output requested on stderr console, but it is unavailable. " +
"Unless the output is handled in a special way, the log messages may " +
"be garbled."
)
}
new StderrPrinterWithColors(printExceptions)
}
}

Some files were not shown because too many files have changed in this diff Show More