mirror of
https://github.com/enso-org/enso.git
synced 2024-08-16 16:30:33 +03:00
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:
parent
87ce78615a
commit
8a60bc6dcd
@ -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
150
build.sbt
@ -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`)
|
||||
|
@ -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.
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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":[] }]
|
||||
}
|
||||
]
|
||||
|
@ -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": []
|
||||
}
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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] =
|
||||
|
@ -199,7 +199,9 @@ class DistributionUninstaller(
|
||||
dataRoot.toAbsolutePath.normalize
|
||||
)
|
||||
if (logsInsideData) {
|
||||
LauncherLogging.prepareForUninstall(globalCLIOptions.colorMode)
|
||||
LauncherLogging.prepareForUninstall(
|
||||
globalCLIOptions.internalOptions.launcherLogLevel
|
||||
)
|
||||
}
|
||||
|
||||
for (dirName <- knownDataDirectories) {
|
||||
|
@ -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."
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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":[] }]
|
||||
}
|
||||
]
|
||||
|
@ -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":[]
|
||||
|
@ -278,9 +278,6 @@
|
||||
{
|
||||
"name":"org.enso.compiler.pass.resolve.TypeSignatures$Signature"
|
||||
},
|
||||
{
|
||||
"name":"org.enso.data.Shifted"
|
||||
},
|
||||
{
|
||||
"name":"org.enso.pkg.QualifiedName"
|
||||
},
|
||||
|
@ -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"),
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -83,6 +83,7 @@ class Application[Config](
|
||||
additionalArguments,
|
||||
applicationName = commandName
|
||||
)
|
||||
|
||||
val finalResult = parseResult.flatMap {
|
||||
case ((topLevelAction, commandResult), pluginIntercepted) =>
|
||||
pluginIntercepted match {
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
@ -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."
|
||||
)
|
||||
)
|
||||
|
@ -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";
|
||||
}
|
@ -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";
|
||||
}
|
@ -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";
|
||||
}
|
@ -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";
|
||||
}
|
@ -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";
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 + "`");
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
@ -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";
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
package org.enso.logger.akka;
|
@ -0,0 +1,7 @@
|
||||
package org.enso.logging;
|
||||
|
||||
public class LoggerInitializationFailed extends RuntimeException {
|
||||
public LoggerInitializationFailed() {
|
||||
super("Logger initialization failed");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package org.enso.logging;
|
||||
|
||||
public class LoggingServiceAlreadySetup extends RuntimeException {
|
||||
public LoggingServiceAlreadySetup() {
|
||||
super("Logging Service already setup");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
@ -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."
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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./
|
||||
)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
@ -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)
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = {}
|
||||
}
|
@ -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.
|
||||
*/
|
@ -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 =>
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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'>"
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user