diff --git a/CHANGELOG.md b/CHANGELOG.md index 64fa45855755..65a92f4dc30b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] @@ -1067,6 +1068,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 diff --git a/build.sbt b/build.sbt index ac8083cff276..96c6a256a5a4 100644 --- a/build.sbt +++ b/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 ) ) + .dependsOn(`logging-utils`) + .dependsOn(`logging-config`) + +lazy val `logging-config` = project + .in(file("lib/scala/logging-config")) + .configs(Test) .settings( - if (Platform.isWindows) - (Compile / unmanagedSourceDirectories) += (Compile / sourceDirectory).value / "java-windows" - else - (Compile / unmanagedSourceDirectories) += (Compile / sourceDirectory).value / "java-unix" + 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 + ) ) - .dependsOn(`akka-native`) - .dependsOn(`logging-utils`) 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`) diff --git a/docs/infrastructure/logging.md b/docs/infrastructure/logging.md index 600eef4b1612..77c5291aeab6 100644 --- a/docs/infrastructure/logging.md +++ b/docs/infrastructure/logging.md @@ -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. -- [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) -## 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 -```typescript -type UTCTime = number; +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 + } +} ``` -##### `Exception` +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. -Encodes an exception that is related to a log message. +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. -The `cause` field may be omitted if the exception does not have another -exception as its cause. +Additionally, custom log events can be provided during runtime via system +properties, without re-packaging the updated config file. For example ```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; -} +akka.actor = info; ``` -##### `TraceElement` - -Represents a single element of exception's stacktrace. +is equivalent to ```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; -} + -Dakka.actor.Logger.level=info ``` -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`. +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. -### Messages +Currently supported are -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. +- 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 -#### `LogMessage` +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`. -Describes the log message that the server should report and does not expect any -response. +#### Format -##### Parameters +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 -{ - // 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; -} + + appenders = [ + { + name = "console" + pattern = "[%level{lowercase=true}] [%d{yyyy-MM-dd'T'HH:mm:ssXXX}] [%logger] %msg%n%nopex" + } + ... + ] ``` -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": [] +#### File Appender + +Enabled with `ENSO_APPENDER_DEFAULT=file` environment variable. + +File appender directs all log events to a log file: + +``` + { + name = "file" + append = + immediate-flush = + pattern = + rolling-policy { + max-file-size = + max-history = + max-total-size = } } -} ``` -Another example could be an info message (without attached exceptions): +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). -```json -{ - "level": 2, - "time": 1600864353151, - "group": "org.enso.launcher.Main", - "message": "Configuration file loaded successfully." -} +#### Socket Appender + +Enabled with `ENSO_APPENDER_DEFAULT=socket` environment variable. + +Configuration + +``` + { + name = "socket" + hostname = + port = + } ``` +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 = + flush-timeout = + debug = + } +``` + +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. -### SLF4J Interface +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: -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. +- file logging requires Enso-specific directory which is only known during + runtime +- centralized logging +- modifying log levels without recompilation -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. +### SLF4J Interface -``` -package foo -import com.typesafe.scalalogging.Logger -import org.enso.logger.LoggerSyntax +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. -class Foo { - private val logger = Logger[Foo] +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. - def bar(): Unit = { - logger.info("Hello world") // Logs `Hello world` from context `foo.Foo`. - baz() - } +```java +package foo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; - def baz(): Unit = { - val bazLogger = logger.enter("baz") - bazLogger.warn("Inner") // Logs `Inner` from context `foo.Foo.baz` - } +public class Foo { + private Logger logger = LoggerFactory.getLogger(Foo.class); + + 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: - -- _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. - -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. - -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. +The `org.slf4j.Logger` instances have to know where to send log events. This +setting is typically performed once, when the service starts, and applies +globally during its execution. Currently, it is not possible to dynamically +change where log events are being stored. The main (abstract) class used for +setting up logging is `org.enso.logger.LoggerSetup`. An instance of that class +can be retrieved with the thread-safe `org.enso.logger.LoggerSetup.get` factory +method. `org.enso.logger.LoggerSetup` provides a number of `setupXYZAppender` +methods that will direct loggers to send log events to an `XYZ` appender. +Setting a specific hard-coded appender programmatically should however be +avoided by the users. Instead, one should invoke one of the overloaded `setup` +variants that initialize loggers based on the provided `logging-service` +configuration. + +```java +package foo; +import org.enso.logger.LoggerSetup; +import org.slf4j.event.Level; + +public class MyService { + + private Logger logger = LoggerFactory.getLogger(Foo.class); + ... + public void start(Level logLevel) { + LoggerSetup.get().setup(logLevel); + logger.info("My service is starting..."); + ... + } + ... +} +``` + +`org.enso.logging.LoggingSetupHelper` class was introduced to help with the most +common use cases - establishing a file-based logging in the Enso's dedicated +directories or connecting to an existing logging server once it starts accepting +connections. That is why services don't call `LoggerSetup` directly but instead +provide a service-specific implementation of +`org.enso.logging.LoggingSetupHelper`. `LoggingSetupHelper` and `LoggerSetup` +provide `teardown` methods to properly dispose of log events. ### Log Masking @@ -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. diff --git a/engine/language-server/src/main/resources/application.conf b/engine/language-server/src/main/resources/application.conf index 24026ee7d4ba..247cb24bbae2 100644 --- a/engine/language-server/src/main/resources/application.conf +++ b/engine/language-server/src/main/resources/application.conf @@ -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} } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala index c02b0be2bce3..800056b2c4cd 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala @@ -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 { diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala index 4e0820451f2a..504a5a3591e0 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala @@ -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 = diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/CompilerBasedDependencyExtractor.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/CompilerBasedDependencyExtractor.scala index 1724559a5d67..077cae40d869 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/CompilerBasedDependencyExtractor.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/CompilerBasedDependencyExtractor.scala @@ -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) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala index 265bd0cda400..cb715bdc1947 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala @@ -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 diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/util/UnhandledLogging.scala b/engine/language-server/src/main/scala/org/enso/languageserver/util/UnhandledLogging.scala index 7b6d69535156..3e70882a0c22 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/util/UnhandledLogging.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/util/UnhandledLogging.scala @@ -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) } } diff --git a/engine/language-server/src/test/resources/application.conf b/engine/language-server/src/test/resources/application.conf index 08bfee1a55b4..244c41c38cab 100644 --- a/engine/language-server/src/test/resources/application.conf +++ b/engine/language-server/src/test/resources/application.conf @@ -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" +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala index 99e5208d718c..1d9afa4682bf 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala @@ -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() @@ -318,7 +320,7 @@ class BaseServerTest distributionManager, resourceManager, Some(languageHome), - new CompilerBasedDependencyExtractor(logLevel = LogLevel.Warning) + new CompilerBasedDependencyExtractor(logLevel = Level.WARN) ) ) diff --git a/engine/launcher/src/main/resources/META-INF/native-image/org/enso/launcher/reflect-config.json b/engine/launcher/src/main/resources/META-INF/native-image/org/enso/launcher/reflect-config.json index 353bee804b36..d44c38e93aac 100644 --- a/engine/launcher/src/main/resources/META-INF/native-image/org/enso/launcher/reflect-config.json +++ b/engine/launcher/src/main/resources/META-INF/native-image/org/enso/launcher/reflect-config.json @@ -722,5 +722,41 @@ "parameterTypes": ["java.lang.Boolean", "java.lang.Object"] } ] + }, + { + "name":"org.enso.logger.LogbackSetup", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.DateConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LevelConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LineSeparatorConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LoggerConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.NopThrowableInformationConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.MessageConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.core.rolling.helper.DateTokenConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.core.rolling.helper.IntegerTokenConverter", + "methods":[{"name":"","parameterTypes":[] }] } ] diff --git a/engine/launcher/src/main/resources/META-INF/native-image/org/enso/launcher/resource-config.json b/engine/launcher/src/main/resources/META-INF/native-image/org/enso/launcher/resource-config.json index 044086ab6f65..01c3f1881636 100644 --- a/engine/launcher/src/main/resources/META-INF/native-image/org/enso/launcher/resource-config.json +++ b/engine/launcher/src/main/resources/META-INF/native-image/org/enso/launcher/resource-config.json @@ -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": [] } diff --git a/engine/launcher/src/main/resources/application.conf b/engine/launcher/src/main/resources/application.conf index 71ed309c6873..59f758c9e1a9 100644 --- a/engine/launcher/src/main/resources/application.conf +++ b/engine/launcher/src/main/resources/application.conf @@ -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} } diff --git a/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala b/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala index cfd85e5fbd45..bdca701c7c87 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala @@ -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] diff --git a/engine/launcher/src/main/scala/org/enso/launcher/cli/GlobalCLIOptions.scala b/engine/launcher/src/main/scala/org/enso/launcher/cli/GlobalCLIOptions.scala index 9fec4a526df9..c46a9269f2fe 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/cli/GlobalCLIOptions.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/cli/GlobalCLIOptions.scala @@ -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 } } diff --git a/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala b/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala index e6af9dd408db..5c1d4266f6b7 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala @@ -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() diff --git a/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherLogging.scala b/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherLogging.scala index 65dfeeaea469..a49ebdc435f5 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherLogging.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherLogging.scala @@ -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) } } diff --git a/engine/launcher/src/main/scala/org/enso/launcher/cli/Main.scala b/engine/launcher/src/main/scala/org/enso/launcher/cli/Main.scala index e5b49200dbf7..cc9762a7af0f 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/cli/Main.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/cli/Main.scala @@ -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 diff --git a/engine/launcher/src/main/scala/org/enso/launcher/components/LauncherRunner.scala b/engine/launcher/src/main/scala/org/enso/launcher/components/LauncherRunner.scala index 9e3884700bfe..0c7115438a01 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/components/LauncherRunner.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/components/LauncherRunner.scala @@ -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] = diff --git a/engine/launcher/src/main/scala/org/enso/launcher/installation/DistributionUninstaller.scala b/engine/launcher/src/main/scala/org/enso/launcher/installation/DistributionUninstaller.scala index cf50e9877626..fe1c0eca01fe 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/installation/DistributionUninstaller.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/installation/DistributionUninstaller.scala @@ -199,7 +199,9 @@ class DistributionUninstaller( dataRoot.toAbsolutePath.normalize ) if (logsInsideData) { - LauncherLogging.prepareForUninstall(globalCLIOptions.colorMode) + LauncherLogging.prepareForUninstall( + globalCLIOptions.internalOptions.launcherLogLevel + ) } for (dirName <- knownDataDirectories) { diff --git a/engine/launcher/src/main/scala/org/enso/launcher/upgrade/LauncherUpgrader.scala b/engine/launcher/src/main/scala/org/enso/launcher/upgrade/LauncherUpgrader.scala index 3080e753246d..18e9419daf5a 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/upgrade/LauncherUpgrader.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/upgrade/LauncherUpgrader.scala @@ -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." diff --git a/engine/launcher/src/test/resources/application.conf b/engine/launcher/src/test/resources/application.conf index af4e27167781..e6e206cd8021 100644 --- a/engine/launcher/src/test/resources/application.conf +++ b/engine/launcher/src/test/resources/application.conf @@ -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" +} diff --git a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala index 934b4b79c19b..5745cc2ee7e3 100644 --- a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala +++ b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala @@ -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 diff --git a/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/reflect-config.json b/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/reflect-config.json index a1216566d6ce..ab8d6a6b1b29 100644 --- a/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/reflect-config.json +++ b/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/reflect-config.json @@ -215,5 +215,41 @@ { "name": "org.apache.commons.compress.archivers.zip.Zip64ExtendedInformationExtraField", "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "org.apache.commons.compress.archivers.zip.Zip64ExtendedInformationExtraField", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name":"ch.qos.logback.classic.pattern.DateConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LevelConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LineSeparatorConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.NopThrowableInformationConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LoggerConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.MessageConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.core.rolling.helper.DateTokenConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.core.rolling.helper.IntegerTokenConverter", + "methods":[{"name":"","parameterTypes":[] }] } ] diff --git a/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/resource-config.json b/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/resource-config.json index ff677c20809b..429aea7bd773 100644 --- a/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/resource-config.json +++ b/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/resource-config.json @@ -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":[] diff --git a/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/serialization-config.json b/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/serialization-config.json index cafbbaf9d5ea..fbf84f6c509a 100644 --- a/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/serialization-config.json +++ b/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/serialization-config.json @@ -278,9 +278,6 @@ { "name":"org.enso.compiler.pass.resolve.TypeSignatures$Signature" }, - { - "name":"org.enso.data.Shifted" - }, { "name":"org.enso.pkg.QualifiedName" }, diff --git a/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala b/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala index 9b5b2df70f7d..04c2888fe14d 100644 --- a/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala +++ b/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala @@ -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"), diff --git a/engine/runner/src/main/scala/org/enso/runner/DependencyPreinstaller.scala b/engine/runner/src/main/scala/org/enso/runner/DependencyPreinstaller.scala index b357d9726032..488a79ab5ba0 100644 --- a/engine/runner/src/main/scala/org/enso/runner/DependencyPreinstaller.scala +++ b/engine/runner/src/main/scala/org/enso/runner/DependencyPreinstaller.scala @@ -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 diff --git a/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala b/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala index 70101fccc8ae..adeada439688 100644 --- a/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala +++ b/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala @@ -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) diff --git a/engine/runner/src/main/scala/org/enso/runner/Main.scala b/engine/runner/src/main/scala/org/enso/runner/Main.scala index 4cd1604f9174..485aa8a84306 100644 --- a/engine/runner/src/main/scala/org/enso/runner/Main.scala +++ b/engine/runner/src/main/scala/org/enso/runner/Main.scala @@ -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. diff --git a/engine/runner/src/main/scala/org/enso/runner/ProjectUploader.scala b/engine/runner/src/main/scala/org/enso/runner/ProjectUploader.scala index 9b21fae7fe37..11632bfdbac1 100644 --- a/engine/runner/src/main/scala/org/enso/runner/ProjectUploader.scala +++ b/engine/runner/src/main/scala/org/enso/runner/ProjectUploader.scala @@ -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) diff --git a/engine/runner/src/main/scala/org/enso/runner/RunnerLogging.scala b/engine/runner/src/main/scala/org/enso/runner/RunnerLogging.scala index b59221b8435d..ccaee4120a60 100644 --- a/engine/runner/src/main/scala/org/enso/runner/RunnerLogging.scala +++ b/engine/runner/src/main/scala/org/enso/runner/RunnerLogging.scala @@ -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() + } } diff --git a/engine/runtime/src/test/java/org/enso/interpreter/test/DebuggingEnsoTest.java b/engine/runtime/src/test/java/org/enso/interpreter/test/DebuggingEnsoTest.java index 72a848f568ac..d2c38b342d01 100644 --- a/engine/runtime/src/test/java/org/enso/interpreter/test/DebuggingEnsoTest.java +++ b/engine/runtime/src/test/java/org/enso/interpreter/test/DebuggingEnsoTest.java @@ -55,6 +55,10 @@ public void initContext() { RuntimeOptions.LANGUAGE_HOME_OVERRIDE, Paths.get("../../distribution/component").toFile().getAbsolutePath() ) + .option( + RuntimeOptions.LOG_LEVEL, + "FINEST" + ) .logHandler(OutputStream.nullOutputStream()) .build(); diff --git a/lib/scala/cli/src/main/scala/org/enso/cli/arguments/Application.scala b/lib/scala/cli/src/main/scala/org/enso/cli/arguments/Application.scala index 537001d7e607..90674d9bc230 100644 --- a/lib/scala/cli/src/main/scala/org/enso/cli/arguments/Application.scala +++ b/lib/scala/cli/src/main/scala/org/enso/cli/arguments/Application.scala @@ -83,6 +83,7 @@ class Application[Config]( additionalArguments, applicationName = commandName ) + val finalResult = parseResult.flatMap { case ((topLevelAction, commandResult), pluginIntercepted) => pluginIntercepted match { diff --git a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfigurationManager.scala b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfigurationManager.scala index 3125140b9591..dcff3dec66a8 100644 --- a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfigurationManager.scala +++ b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/config/GlobalConfigurationManager.scala @@ -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 = diff --git a/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala index 3b455d3a247d..a71b48947f09 100644 --- a/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala +++ b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala @@ -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 diff --git a/lib/scala/library-manager-test/src/test/scala/org/enso/librarymanager/published/repository/LibraryDownloadTest.scala b/lib/scala/library-manager-test/src/test/scala/org/enso/librarymanager/published/repository/LibraryDownloadTest.scala index 86c82bca92d3..e9ac9658ba56 100644 --- a/lib/scala/library-manager-test/src/test/scala/org/enso/librarymanager/published/repository/LibraryDownloadTest.scala +++ b/lib/scala/library-manager-test/src/test/scala/org/enso/librarymanager/published/repository/LibraryDownloadTest.scala @@ -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." ) ) diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/LoggerSetup.java b/lib/scala/logging-config/src/main/java/org/enso/logger/LoggerSetup.java new file mode 100644 index 000000000000..07cd36488dfa --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/LoggerSetup.java @@ -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 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"; +} diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/config/Appender.java b/lib/scala/logging-config/src/main/java/org/enso/logger/config/Appender.java new file mode 100644 index 000000000000..df66888ed25f --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/config/Appender.java @@ -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"; +} diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/config/ConsoleAppender.java b/lib/scala/logging-config/src/main/java/org/enso/logger/config/ConsoleAppender.java new file mode 100644 index 000000000000..c6966f6507c5 --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/config/ConsoleAppender.java @@ -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"; +} diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/config/FileAppender.java b/lib/scala/logging-config/src/main/java/org/enso/logger/config/FileAppender.java new file mode 100644 index 000000000000..c1fe2a70098c --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/config/FileAppender.java @@ -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"; +} diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/config/LoggersLevels.java b/lib/scala/logging-config/src/main/java/org/enso/logger/config/LoggersLevels.java new file mode 100644 index 000000000000..934692b1ed61 --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/config/LoggersLevels.java @@ -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 loggers; + + private LoggersLevels(Map loggers) { + this.loggers = loggers; + } + + public Set> 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 loggers = systemLoggers(); + Map 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=` env variables. + * + * @return a map of custom loggers' levels set on startup + */ + private static Map systemLoggers() { + Map 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"; +} diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/config/LoggingServer.java b/lib/scala/logging-config/src/main/java/org/enso/logger/config/LoggingServer.java new file mode 100644 index 000000000000..c3bfc522a766 --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/config/LoggingServer.java @@ -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 appenders, String appender, Boolean start) { + + public static LoggingServer parse(Config config) throws MissingConfigurationField { + int port = config.getInt("port"); + + Map appendersMap = new HashMap<>(); + if (config.hasPath("appenders")) { + List 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); + } + +} diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/config/LoggingServiceConfig.java b/lib/scala/logging-config/src/main/java/org/enso/logger/config/LoggingServiceConfig.java new file mode 100644 index 000000000000..263b4aee398b --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/config/LoggingServiceConfig.java @@ -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 appenders; + + private final String defaultAppenderName; + private final Optional logLevel; + private final LoggingServer server; + + private LoggingServiceConfig( + LoggersLevels loggers, + Optional logLevel, + Map 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 appendersMap = new HashMap<>(); + if (root.hasPath(appendersKey)) { + List 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 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 getStringOpt(String key, Config config) { + try { + return Optional.ofNullable(config.getString(key)); + } catch (ConfigException.Missing missing) { + return Optional.empty(); + } + } + + public Optional 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; + } +} diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/config/MissingConfigurationField.java b/lib/scala/logging-config/src/main/java/org/enso/logger/config/MissingConfigurationField.java new file mode 100644 index 000000000000..95749dd6d93c --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/config/MissingConfigurationField.java @@ -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 + "`"); + } +} diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/config/SentryAppender.java b/lib/scala/logging-config/src/main/java/org/enso/logger/config/SentryAppender.java new file mode 100644 index 000000000000..35fa6022aa9c --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/config/SentryAppender.java @@ -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"; +} diff --git a/lib/scala/logging-config/src/main/java/org/enso/logger/config/SocketAppender.java b/lib/scala/logging-config/src/main/java/org/enso/logger/config/SocketAppender.java new file mode 100644 index 000000000000..372c9ca2d585 --- /dev/null +++ b/lib/scala/logging-config/src/main/java/org/enso/logger/config/SocketAppender.java @@ -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"; +} diff --git a/lib/scala/logging-service-logback/src/main/java/org/enso/logger/ApplicationFilter.java b/lib/scala/logging-service-logback/src/main/java/org/enso/logger/ApplicationFilter.java new file mode 100644 index 000000000000..59a105ab1944 --- /dev/null +++ b/lib/scala/logging-service-logback/src/main/java/org/enso/logger/ApplicationFilter.java @@ -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 { + 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 fromLoggers(LoggersLevels loggers) { + return new ApplicationFilter(loggers); + } +} diff --git a/lib/scala/logging-service-logback/src/main/java/org/enso/logger/LogbackSetup.java b/lib/scala/logging-service-logback/src/main/java/org/enso/logger/LogbackSetup.java new file mode 100644 index 000000000000..4a0343f8dc10 --- /dev/null +++ b/lib/scala/logging-service-logback/src/main/java/org/enso/logger/LogbackSetup.java @@ -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 fileAppender; + + + if (appenderConfig != null && appenderConfig.getRollingPolicy() != null) { + RollingFileAppender 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 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 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 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 filter) { + + void finalizeEncoder(ch.qos.logback.core.encoder.Encoder encoder) { + encoder.setContext(ctx); + encoder.start(); + } + void finalizeAppender(ch.qos.logback.core.Appender 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); + } + } +} diff --git a/lib/scala/logging-service-logback/src/main/java/org/enso/logging/LogbackLoggingServiceFactory.java b/lib/scala/logging-service-logback/src/main/java/org/enso/logging/LogbackLoggingServiceFactory.java new file mode 100644 index 000000000000..495031a6151f --- /dev/null +++ b/lib/scala/logging-service-logback/src/main/java/org/enso/logging/LogbackLoggingServiceFactory.java @@ -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 { + + @Override + public LoggingService localServerFor(int port) { + return new LoggingServer(port); + } +} diff --git a/lib/scala/logging-service-logback/src/main/java/org/enso/logging/LoggingServer.java b/lib/scala/logging-service-logback/src/main/java/org/enso/logging/LoggingServer.java new file mode 100644 index 000000000000..95248a9f0d8b --- /dev/null +++ b/lib/scala/logging-service-logback/src/main/java/org/enso/logging/LoggingServer.java @@ -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 { + + 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(); + } + } +} diff --git a/lib/scala/logging-service/src/main/java-unix/org/enso/loggingservice/internal/NativeAnsiTerm.java b/lib/scala/logging-service/src/main/java-unix/org/enso/loggingservice/internal/NativeAnsiTerm.java deleted file mode 100755 index 1faedf91152a..000000000000 --- a/lib/scala/logging-service/src/main/java-unix/org/enso/loggingservice/internal/NativeAnsiTerm.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.enso.loggingservice.internal; - -/** - * Provides a stub for enabling VT console mode. - * - *

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. - * - *

The UNIX variant does nothing, as we assume that VT is supported out of the box. - */ - public static void enableVT() {} -} diff --git a/lib/scala/logging-service/src/main/java-windows/org/enso/loggingservice/internal/NativeAnsiTerm.java b/lib/scala/logging-service/src/main/java-windows/org/enso/loggingservice/internal/NativeAnsiTerm.java deleted file mode 100755 index 91f846f29ba6..000000000000 --- a/lib/scala/logging-service/src/main/java-windows/org/enso/loggingservice/internal/NativeAnsiTerm.java +++ /dev/null @@ -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. - * - *

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 SetConsoleMode - */ - @CFunction - private static native int SetConsoleMode(PointerBase hConsoleHandle, int dwMode); - - /** - * Returns error code of last error. - * - *

Can be called if a function returns a zero exit code to get the error code. - * - * @see GetLastError - */ - @CFunction - private static native int GetLastError(); - - /** - * Constant that can be used in [[GetStdHandle]] that refers to the standard error stream. - * - * @see GetStdHandle - */ - 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 SetConsoleMode - * @see Console - * Virtual Terminal Sequences - */ - private static final int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; - - /** - * Enables VT emulation within the connected console. - * - *

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); - } - } -} diff --git a/lib/scala/logging-service/src/main/java/org/enso/logger/akka/package-info.java b/lib/scala/logging-service/src/main/java/org/enso/logger/akka/package-info.java deleted file mode 100644 index 6d03f00ecdf8..000000000000 --- a/lib/scala/logging-service/src/main/java/org/enso/logger/akka/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package org.enso.logger.akka; diff --git a/lib/scala/logging-service/src/main/java/org/enso/logging/LoggerInitializationFailed.java b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggerInitializationFailed.java new file mode 100644 index 000000000000..94da192c6f74 --- /dev/null +++ b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggerInitializationFailed.java @@ -0,0 +1,7 @@ +package org.enso.logging; + +public class LoggerInitializationFailed extends RuntimeException { + public LoggerInitializationFailed() { + super("Logger initialization failed"); + } +} diff --git a/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingService.java b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingService.java new file mode 100644 index 000000000000..3c7aca74f1e2 --- /dev/null +++ b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingService.java @@ -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 the type of the object describing on how to communicate with the service + */ +public abstract class LoggingService { + + /** + * 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(); +} diff --git a/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingServiceAlreadySetup.java b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingServiceAlreadySetup.java new file mode 100644 index 000000000000..159b8191e012 --- /dev/null +++ b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingServiceAlreadySetup.java @@ -0,0 +1,7 @@ +package org.enso.logging; + +public class LoggingServiceAlreadySetup extends RuntimeException { + public LoggingServiceAlreadySetup() { + super("Logging Service already setup"); + } +} diff --git a/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingServiceFactory.java b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingServiceFactory.java new file mode 100644 index 000000000000..97513a8fa80c --- /dev/null +++ b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingServiceFactory.java @@ -0,0 +1,32 @@ +package org.enso.logging; + +import java.net.URI; +import java.util.ServiceLoader; + +public abstract class LoggingServiceFactory { + + private static volatile LoggingServiceFactory _loggingServiceFactory; + private static Object _lock = new Object(); + + public abstract LoggingService localServerFor(int port); + + @SuppressWarnings("unchecked") + public static LoggingServiceFactory get() { + LoggingServiceFactory 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 loader = + ServiceLoader.load( + LoggingServiceFactory.class, LoggingServiceFactory.class.getClassLoader()); + result = loader.findFirst().get(); + _loggingServiceFactory = result; + } + } + } + return result; + } +} diff --git a/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingServiceManager.java b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingServiceManager.java new file mode 100644 index 000000000000..13bf884c2311 --- /dev/null +++ b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingServiceManager.java @@ -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 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(); + } + } +} diff --git a/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingSetupHelper.java b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingSetupHelper.java new file mode 100644 index 000000000000..35380c816fbf --- /dev/null +++ b/lib/scala/logging-service/src/main/java/org/enso/logging/LoggingSetupHelper.java @@ -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. + * + *

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> loggingServiceEndpoint() { + return loggingServiceEndpointPromise.future(); + } + + private Promise> 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 logLevel, Option connectToExternalLogger, boolean logMasking) + throws MissingConfigurationField { + initLogger(); + setup(logLevel, connectToExternalLogger, logMasking, LoggerSetup.get()); + } + + private void setup( + Option logLevel, + Option 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(); + } +} diff --git a/lib/scala/logging-service/src/main/java/org/slf4j/impl/StaticLoggerBinder.java b/lib/scala/logging-service/src/main/java/org/slf4j/impl/StaticLoggerBinder.java deleted file mode 100644 index 7018a88b7894..000000000000 --- a/lib/scala/logging-service/src/main/java/org/slf4j/impl/StaticLoggerBinder.java +++ /dev/null @@ -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. - * - *

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; - } -} diff --git a/lib/scala/logging-service/src/main/java/org/slf4j/impl/StaticMDCBinder.java b/lib/scala/logging-service/src/main/java/org/slf4j/impl/StaticMDCBinder.java deleted file mode 100644 index 55f3e7c8b7e1..000000000000 --- a/lib/scala/logging-service/src/main/java/org/slf4j/impl/StaticMDCBinder.java +++ /dev/null @@ -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. - * - *

MDC handling is an optional SLF4J feature and currently the logging service does not support - * it, so it provides a no-op adapter. - * - *

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; - } -} diff --git a/lib/scala/logging-service/src/main/java/org/slf4j/impl/StaticMarkerBinder.java b/lib/scala/logging-service/src/main/java/org/slf4j/impl/StaticMarkerBinder.java deleted file mode 100644 index b55cac48e634..000000000000 --- a/lib/scala/logging-service/src/main/java/org/slf4j/impl/StaticMarkerBinder.java +++ /dev/null @@ -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. - * - *

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; - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/logger/package.scala b/lib/scala/logging-service/src/main/scala/org/enso/logger/package.scala deleted file mode 100644 index a202e35b0324..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/logger/package.scala +++ /dev/null @@ -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) - } - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/ColorMode.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/ColorMode.scala deleted file mode 100644 index 1a7e1fbb9565..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/ColorMode.scala +++ /dev/null @@ -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 -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/JavaLoggingLogHandler.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/JavaLoggingLogHandler.scala deleted file mode 100644 index 85a069f9e872..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/JavaLoggingLogHandler.scala +++ /dev/null @@ -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 - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LogLevel.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LogLevel.scala deleted file mode 100644 index 6949d1767565..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LogLevel.scala +++ /dev/null @@ -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 - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/Logger.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/Logger.scala deleted file mode 100644 index 34a6219d288f..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/Logger.scala +++ /dev/null @@ -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) -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggerFactory.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggerFactory.scala deleted file mode 100644 index da3b40a790ce..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggerFactory.scala +++ /dev/null @@ -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 - } - ) - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggerMode.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggerMode.scala deleted file mode 100644 index 5d37f6fc2299..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggerMode.scala +++ /dev/null @@ -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] -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceAlreadyInitializedException.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceAlreadyInitializedException.scala deleted file mode 100644 index 29ffeaef2552..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceAlreadyInitializedException.scala +++ /dev/null @@ -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." - ) diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceManager.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceManager.scala deleted file mode 100644 index 7a35b2c1d618..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceManager.scala +++ /dev/null @@ -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 - } - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceSetupHelper.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceSetupHelper.scala deleted file mode 100644 index ed6a06f2de35..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceSetupHelper.scala +++ /dev/null @@ -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() -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/ServerBinding.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/ServerBinding.scala deleted file mode 100644 index 935405f36362..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/ServerBinding.scala +++ /dev/null @@ -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./ - ) -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/TestLogger.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/TestLogger.scala deleted file mode 100644 index 6bb6026575f4..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/TestLogger.scala +++ /dev/null @@ -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() - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/ANSIColorsMessageRenderer.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/ANSIColorsMessageRenderer.scala deleted file mode 100644 index b0a1cf1041a7..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/ANSIColorsMessageRenderer.scala +++ /dev/null @@ -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) - } - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/AnsiTerminal.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/AnsiTerminal.scala deleted file mode 100755 index 5282b0d9d2f8..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/AnsiTerminal.scala +++ /dev/null @@ -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() - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/BaseLogMessage.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/BaseLogMessage.scala deleted file mode 100644 index a7e80040ef3e..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/BaseLogMessage.scala +++ /dev/null @@ -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] -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/BlockingConsumerMessageQueue.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/BlockingConsumerMessageQueue.scala deleted file mode 100644 index 1d0d2ee9ee70..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/BlockingConsumerMessageQueue.scala +++ /dev/null @@ -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) -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/DefaultLogMessageRenderer.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/DefaultLogMessageRenderer.scala deleted file mode 100644 index fb95fccbfaef..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/DefaultLogMessageRenderer.scala +++ /dev/null @@ -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"; - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/InternalLogMessage.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/InternalLogMessage.scala deleted file mode 100644 index ae3083dcd711..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/InternalLogMessage.scala +++ /dev/null @@ -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 - ) -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/InternalLogger.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/InternalLogger.scala deleted file mode 100644 index d3d3f5fe7f59..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/InternalLogger.scala +++ /dev/null @@ -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") - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/LogMessageRenderer.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/LogMessageRenderer.scala deleted file mode 100644 index f67692e19cd0..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/LogMessageRenderer.scala +++ /dev/null @@ -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 -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/LoggerConnection.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/LoggerConnection.scala deleted file mode 100644 index 6c28abdc020d..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/LoggerConnection.scala +++ /dev/null @@ -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) - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/LoggingSettings.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/LoggingSettings.scala deleted file mode 100644 index e218358eab5f..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/LoggingSettings.scala +++ /dev/null @@ -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 - } - } - } - -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/TestMessageQueue.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/TestMessageQueue.scala deleted file mode 100644 index 6ca9ce2d8dcf..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/TestMessageQueue.scala +++ /dev/null @@ -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) - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/SerializedException.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/SerializedException.scala deleted file mode 100644 index 8e2ca6783008..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/SerializedException.scala +++ /dev/null @@ -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) - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/WSLogMessage.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/WSLogMessage.scala deleted file mode 100644 index 1a733335d690..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/WSLogMessage.scala +++ /dev/null @@ -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 - ) - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Client.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Client.scala deleted file mode 100644 index b7ed6547a96b..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Client.scala +++ /dev/null @@ -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 - } - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Local.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Local.scala deleted file mode 100644 index 66bbe50299e3..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Local.scala +++ /dev/null @@ -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 - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Server.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Server.scala deleted file mode 100644 index c1b942d358be..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Server.scala +++ /dev/null @@ -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 - } - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Service.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Service.scala deleted file mode 100644 index d7298521d9c7..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/Service.scala +++ /dev/null @@ -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 = {} -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/ServiceWithActorSystem.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/ServiceWithActorSystem.scala deleted file mode 100644 index 2a592db65833..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/ServiceWithActorSystem.scala +++ /dev/null @@ -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. - */ diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/ThreadProcessingService.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/ThreadProcessingService.scala deleted file mode 100644 index 9976878ec196..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/service/ThreadProcessingService.scala +++ /dev/null @@ -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 => - } - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/FileOutputPrinter.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/FileOutputPrinter.scala deleted file mode 100644 index 6b7716eafe3a..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/FileOutputPrinter.scala +++ /dev/null @@ -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) -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/FileXmlPrinter.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/FileXmlPrinter.scala deleted file mode 100644 index 575ca1d86e8f..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/FileXmlPrinter.scala +++ /dev/null @@ -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 = - "" -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/Printer.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/Printer.scala deleted file mode 100644 index f8b8857d0144..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/Printer.scala +++ /dev/null @@ -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 -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/StderrPrinter.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/StderrPrinter.scala deleted file mode 100644 index bc86b02ea1c3..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/StderrPrinter.scala +++ /dev/null @@ -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) -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/StderrPrinterWithColors.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/StderrPrinterWithColors.scala deleted file mode 100644 index 5d36427c9071..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/StderrPrinterWithColors.scala +++ /dev/null @@ -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) - } -} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/TestPrinter.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/TestPrinter.scala deleted file mode 100644 index 303f9d598f72..000000000000 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/printers/TestPrinter.scala +++ /dev/null @@ -1,38 +0,0 @@ -package org.enso.loggingservice.printers - -import org.enso.loggingservice.TestLogger.TestLogMessage -import org.enso.loggingservice.internal.protocol.WSLogMessage - -/** A [[Printer]] instance that may be used in tests to gather reported log - * messages. - */ -class TestPrinter extends Printer { - private val messages = - scala.collection.mutable.ListBuffer.empty[TestLogMessage] - - /** @inheritdoc - */ - override def print(message: WSLogMessage): Unit = { - val testMessage = - TestLogMessage(logLevel = message.level, message = message.message) - messages.synchronized { - messages.append(testMessage) - } - } - - @volatile private var alreadyShutdown = false - - /** @inheritdoc - */ - def shutdown(): Unit = { - alreadyShutdown = true - } - - /** Reports if [[shutdown]] has been called. - */ - def wasShutdown: Boolean = alreadyShutdown - - /** Returns all log messages that have been gathered so far. - */ - def getLoggedMessages: Seq[TestLogMessage] = messages.toSeq -} diff --git a/lib/scala/logging-service/src/test/java/org/enso/loggingservice/JavaLoggingLogHandlerTest.java b/lib/scala/logging-service/src/test/java/org/enso/loggingservice/JavaLoggingLogHandlerTest.java deleted file mode 100644 index 6051a95d95b7..000000000000 --- a/lib/scala/logging-service/src/test/java/org/enso/loggingservice/JavaLoggingLogHandlerTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.enso.loggingservice; - -import static org.junit.Assert.assertEquals; - -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.LogRecord; -import org.enso.loggingservice.internal.InternalLogMessage; -import org.enso.loggingservice.internal.LoggerConnection; -import org.junit.Test; -import scala.collection.immutable.Map; - -public class JavaLoggingLogHandlerTest { - - @Test - public void verifyFormatting() { - var c = - new LoggerConnection() { - final List messages = new ArrayList<>(); - - @Override - public void send(InternalLogMessage message) { - messages.add(message); - } - - @Override - public LogLevel logLevel() { - throw new UnsupportedOperationException(); - } - - @Override - public Map loggers() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isEnabled(String name, LogLevel level) { - return true; - } - }; - var h = new JavaLoggingLogHandler((v1) -> LogLevel.Debug$.MODULE$, c); - - LogRecord r = new LogRecord(Level.SEVERE, "Init {0} done"); - r.setParameters(new Object[] {"js"}); - - h.publish(r); - - assertEquals("One message: " + c.messages, 1, c.messages.size()); - assertEquals("Init js done", c.messages.get(0).message()); - } -} diff --git a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/DefaultLogMessageRendererSpec.scala b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/DefaultLogMessageRendererSpec.scala deleted file mode 100644 index 34d067d537db..000000000000 --- a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/DefaultLogMessageRendererSpec.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.enso.loggingservice.internal - -import org.enso.loggingservice.LogLevel -import org.enso.loggingservice.internal.protocol.{ - SerializedException, - WSLogMessage -} -import org.scalatest.OptionValues -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import java.time.Instant -import java.time.temporal.ChronoUnit - -class DefaultLogMessageRendererSpec - extends AnyWordSpec - with Matchers - with OptionValues { - - "DefaultLogMessageRenderer" should { - "render NullPointerException" in { - val renderer = new DefaultLogMessageRenderer(printExceptions = true) - val ts = Instant.now().truncatedTo(ChronoUnit.MILLIS) - - val exception = - SerializedException.fromException(new NullPointerException) - val message = - WSLogMessage(LogLevel.Trace, ts, "group", "message", Some(exception)) - - noException should be thrownBy renderer.render(message) - } - - "render NullPointerException with null message" in { - val renderer = new DefaultLogMessageRenderer(printExceptions = true) - val ts = Instant.now().truncatedTo(ChronoUnit.MILLIS) - - val exception = - SerializedException.fromException(new NullPointerException) - val message = - WSLogMessage(LogLevel.Trace, ts, null, null, Some(exception)) - - val txt = renderer.render(message) - txt.toString() should equal(txt) - } - - "JSONize NullPointerException with null message" in { - val ts = Instant.now().truncatedTo(ChronoUnit.MILLIS) - - val exception = - SerializedException.fromException(new NullPointerException) - val message = - WSLogMessage(LogLevel.Trace, ts, null, null, Some(exception)) - - import io.circe.syntax._ - - message.asJson.noSpaces - } - } -} diff --git a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/SerializedExceptionSpec.scala b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/SerializedExceptionSpec.scala deleted file mode 100644 index c5773066c34a..000000000000 --- a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/SerializedExceptionSpec.scala +++ /dev/null @@ -1,48 +0,0 @@ -package org.enso.loggingservice.internal - -import io.circe.syntax._ -import org.enso.loggingservice.internal.protocol.SerializedException -import org.scalatest.OptionValues -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class SerializedExceptionSpec - extends AnyWordSpec - with Matchers - with OptionValues { - - "SerializedException" should { - "serialize and deserialize with nested causes" in { - val cause = SerializedException( - "cause", - "msg", - Seq(SerializedException.TraceElement("e1", "loc1")) - ) - - val exception = SerializedException( - "root", - "msg2", - Seq( - SerializedException.TraceElement("e2", "loc2"), - SerializedException.TraceElement("e3", "loc3") - ), - cause - ) - - exception.asJson - .as[SerializedException] - .toOption - .value shouldEqual exception - } - - "be created from NullPointerException" in { - val exception = new NullPointerException() - val result = SerializedException.fromException(exception) - - result.name shouldEqual exception.getClass.getName - result.message shouldEqual None - result.cause shouldEqual None - result.stackTrace shouldBe Symbol("nonEmpty") - } - } -} diff --git a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/WSLogMessageSpec.scala b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/WSLogMessageSpec.scala deleted file mode 100644 index e19c5736b6a4..000000000000 --- a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/WSLogMessageSpec.scala +++ /dev/null @@ -1,44 +0,0 @@ -package org.enso.loggingservice.internal - -import java.time.Instant -import java.time.temporal.ChronoUnit - -import io.circe.syntax._ -import org.enso.loggingservice.LogLevel -import org.enso.loggingservice.internal.protocol.{ - SerializedException, - WSLogMessage -} -import org.scalatest.OptionValues -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -class WSLogMessageSpec extends AnyWordSpec with Matchers with OptionValues { - - "WSLogMessage" should { - "serialize and deserialize to the same thing" in { - val ts = Instant.now().truncatedTo(ChronoUnit.MILLIS) - - val message1 = WSLogMessage(LogLevel.Trace, ts, "group", "message", None) - message1.asJson.as[WSLogMessage].toOption.value shouldEqual message1 - noException should be thrownBy message1.asJson.noSpaces - - val exception = SerializedException("name", "message", Seq()) - val message2 = - WSLogMessage(LogLevel.Trace, ts, "group", "message", Some(exception)) - message2.asJson.as[WSLogMessage].toOption.value shouldEqual message2 - noException should be thrownBy message2.asJson.noSpaces - } - - "serialize NullPointerException" in { - val ts = Instant.now().truncatedTo(ChronoUnit.MILLIS) - - val exception = - SerializedException.fromException(new NullPointerException) - val message = - WSLogMessage(LogLevel.Trace, ts, "group", "message", Some(exception)) - message.asJson.as[WSLogMessage].toOption.value shouldEqual message - noException should be thrownBy message.asJson.noSpaces - } - } -} diff --git a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/service/ClientServerServiceSpec.scala b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/service/ClientServerServiceSpec.scala deleted file mode 100644 index e08451e1b40a..000000000000 --- a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/service/ClientServerServiceSpec.scala +++ /dev/null @@ -1,81 +0,0 @@ -package org.enso.loggingservice.internal.service - -import java.time.Instant -import java.util.concurrent.{Semaphore, TimeUnit} - -import org.enso.loggingservice.LogLevel -import org.enso.loggingservice.TestLogger.TestLogMessage -import org.enso.loggingservice.internal.protocol.WSLogMessage -import org.enso.loggingservice.internal.{ - BlockingConsumerMessageQueue, - InternalLogMessage -} -import org.enso.loggingservice.printers.TestPrinter - -class ClientServerServiceSpec extends ServiceTest { - "Client and Server" should { - "communicate" in { - val serverQueue = new BlockingConsumerMessageQueue() - val clientQueue = new BlockingConsumerMessageQueue() - val semaphore = new Semaphore(0) - val testPrinter = new TestPrinter { - override def print(message: WSLogMessage): Unit = { - super.print(message) - semaphore.release() - } - } - val message = - InternalLogMessage( - LogLevel.Debug, - Instant.now(), - "group", - "message", - None - ) - val skippedMessage = - InternalLogMessage( - LogLevel.Trace, - Instant.now(), - "group", - "message", - None - ) - - val server = - Server.setup( - "localhost", - 0, - serverQueue, - Seq(testPrinter), - LogLevel.Off - ) - try { - val uri = server.getBinding().toUri() - val client = Client.setup(uri, clientQueue, LogLevel.Debug) - try { - clientQueue.send(Left(message)) - clientQueue.send(Left(skippedMessage)) - assert( - semaphore.tryAcquire(1, 5000, TimeUnit.MILLISECONDS), - "; Waiting for messages timed out." - ) - testPrinter.getLoggedMessages shouldEqual Seq( - TestLogMessage(LogLevel.Debug, "message") - ) - } finally { - client.terminate() - } - } finally { - server.terminate() - } - } - } - - "Server" should { - "also gather local messages" in { - testServiceMessageGathering { (logLevel, queue, printers) => - Server.setup("localhost", 0, queue, printers, logLevel) - } - } - } -} diff --git a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/service/LocalServiceSpec.scala b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/service/LocalServiceSpec.scala deleted file mode 100644 index 803d3decf09d..000000000000 --- a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/service/LocalServiceSpec.scala +++ /dev/null @@ -1,11 +0,0 @@ -package org.enso.loggingservice.internal.service - -class LocalServiceSpec extends ServiceTest { - "Local service" should { - "gather messages" in { - testServiceMessageGathering { (logLevel, queue, printers) => - Local.setup(logLevel, queue, printers) - } - } - } -} diff --git a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/service/ServiceTest.scala b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/service/ServiceTest.scala deleted file mode 100644 index 7b45651627bf..000000000000 --- a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/service/ServiceTest.scala +++ /dev/null @@ -1,75 +0,0 @@ -package org.enso.loggingservice.internal.service - -import java.time.Instant -import java.util.concurrent.{Semaphore, TimeUnit} - -import org.enso.loggingservice.LogLevel -import org.enso.loggingservice.TestLogger.TestLogMessage -import org.enso.loggingservice.internal.{ - BlockingConsumerMessageQueue, - InternalLogMessage -} -import org.enso.loggingservice.internal.protocol.WSLogMessage -import org.enso.loggingservice.printers.{Printer, TestPrinter} -import org.scalatest.OptionValues -import org.scalatest.concurrent.TimeLimitedTests -import org.scalatest.matchers.should.Matchers -import org.scalatest.time.Span -import org.scalatest.time.SpanSugar.convertIntToGrainOfTime -import org.scalatest.wordspec.AnyWordSpec - -trait ServiceTest - extends AnyWordSpec - with Matchers - with OptionValues - with TimeLimitedTests { - - override def timeLimit: Span = 20.seconds - - def testServiceMessageGathering( - serviceConstructor: ( - LogLevel, - BlockingConsumerMessageQueue, - Seq[Printer] - ) => Service - ): Unit = { - val queue = new BlockingConsumerMessageQueue() - val semaphore = new Semaphore(0) - val testPrinter = new TestPrinter { - override def print(message: WSLogMessage): Unit = { - super.print(message) - semaphore.release() - } - } - val message = - InternalLogMessage( - LogLevel.Debug, - Instant.now(), - "group", - "message", - None - ) - val skippedMessage = - InternalLogMessage( - LogLevel.Trace, - Instant.now(), - "group", - "message", - None - ) - - queue.send(Left(message)) - val service = serviceConstructor(LogLevel.Debug, queue, Seq(testPrinter)) - queue.send(Left(skippedMessage)) - assert( - semaphore.tryAcquire(1, 5000, TimeUnit.MILLISECONDS), - "; Waiting for messages timed out." - ) - service.terminate() - assert(testPrinter.wasShutdown) - - testPrinter.getLoggedMessages shouldEqual Seq( - TestLogMessage(LogLevel.Debug, "message") - ) - } -} diff --git a/lib/scala/logging-utils-akka/src/main/java/org/enso/logger/akka/AkkaConverter.java b/lib/scala/logging-utils-akka/src/main/java/org/enso/logger/akka/AkkaConverter.java new file mode 100644 index 000000000000..d262fb2e2c45 --- /dev/null +++ b/lib/scala/logging-utils-akka/src/main/java/org/enso/logger/akka/AkkaConverter.java @@ -0,0 +1,62 @@ +package org.enso.logger.akka; + +import static org.slf4j.event.Level.*; + +import java.util.Optional; +import org.slf4j.event.Level; + +public class AkkaConverter { + + /** + * Converts SLF4K's Level to an Akka one. + * + * @param level a SLF4J's Level to convert + * @return an equivalnet of `level` in Akka's LogLevel + */ + public static akka.event.Logging.LogLevel toAkka(Level level) { + int lvl; + switch (level) { + case ERROR: + lvl = akka.event.Logging$.MODULE$.ErrorLevel(); + break; + case WARN: + lvl = akka.event.Logging$.MODULE$.WarningLevel(); + break; + case INFO: + lvl = akka.event.Logging$.MODULE$.InfoLevel(); + break; + case DEBUG: + lvl = akka.event.Logging$.MODULE$.DebugLevel(); + break; + case TRACE: + lvl = akka.event.Logging$.MODULE$.DebugLevel(); + break; + default: + lvl = akka.event.Logging$.MODULE$.ErrorLevel(); + } + return new akka.event.Logging.LogLevel(lvl); + } + + /** + * Converts a string representation of Akka's LogLevel to an SLF4J one. + * + * @param level a string representation of level to convert + * @return an equivalent representation in Akka's LogLevel + */ + public static Optional fromString(String level) { + switch (level.toLowerCase()) { + case "warning": + return Optional.of(WARN); + case "error": + return Optional.of(ERROR); + case "info": + return Optional.of(INFO); + case "debug": + return Optional.of(DEBUG); + case "trace": + return Optional.of(TRACE); + default: + return Optional.empty(); + } + } +} diff --git a/lib/scala/logging-service/src/main/scala/org/enso/logger/akka/ActorLoggingReceive.scala b/lib/scala/logging-utils-akka/src/main/scala/org/enso/logger/akka/ActorLoggingReceive.scala similarity index 100% rename from lib/scala/logging-service/src/main/scala/org/enso/logger/akka/ActorLoggingReceive.scala rename to lib/scala/logging-utils-akka/src/main/scala/org/enso/logger/akka/ActorLoggingReceive.scala diff --git a/lib/scala/logging-service/src/main/scala/org/enso/logger/akka/ActorMessageLogging.scala b/lib/scala/logging-utils-akka/src/main/scala/org/enso/logger/akka/ActorMessageLogging.scala similarity index 94% rename from lib/scala/logging-service/src/main/scala/org/enso/logger/akka/ActorMessageLogging.scala rename to lib/scala/logging-utils-akka/src/main/scala/org/enso/logger/akka/ActorMessageLogging.scala index b33065b132fd..a54e1332788e 100644 --- a/lib/scala/logging-service/src/main/scala/org/enso/logger/akka/ActorMessageLogging.scala +++ b/lib/scala/logging-utils-akka/src/main/scala/org/enso/logger/akka/ActorMessageLogging.scala @@ -1,6 +1,7 @@ package org.enso.logger.akka -import akka.actor.{Actor, ActorContext} +import akka.actor.Actor +import akka.actor.ActorContext import org.slf4j.LoggerFactory /** A trait providing functions for logging received actor messages. */ diff --git a/lib/scala/logging-utils/src/main/java/org/enso/logger/Converter.java b/lib/scala/logging-utils/src/main/java/org/enso/logger/Converter.java new file mode 100644 index 000000000000..ad0f4a74670b --- /dev/null +++ b/lib/scala/logging-utils/src/main/java/org/enso/logger/Converter.java @@ -0,0 +1,46 @@ +package org.enso.logger; + +import static org.slf4j.event.Level.*; + +import org.slf4j.event.Level; + +public class Converter { + + /** Determines what is the smallest Java level that is still debug and not trace. */ + private static int defaultLevelDebugCutOff = + Math.min(java.util.logging.Level.FINE.intValue(), java.util.logging.Level.CONFIG.intValue()); + + /** + * Converts SLF4J's Level to java.util one. + * + * @param level the SLF4J's level to convert. + * @return an equivalent in java.util.logging.Level terms + */ + public static java.util.logging.Level toJavaLevel(Level level) { + switch (level) { + case ERROR: + return java.util.logging.Level.SEVERE; + case WARN: + return java.util.logging.Level.WARNING; + case INFO: + return java.util.logging.Level.INFO; + case DEBUG: + return java.util.logging.Level.FINE; + case TRACE: + return java.util.logging.Level.FINEST; + default: + return java.util.logging.Level.ALL; + } + } + + /** Default mapping of Java log levels to our log levels based */ + public static Level fromJavaLevel(java.util.logging.Level javaLevel) { + int level = javaLevel.intValue(); + if (level == java.util.logging.Level.OFF.intValue()) return ERROR; + else if (level >= java.util.logging.Level.SEVERE.intValue()) return ERROR; + else if (level >= java.util.logging.Level.WARNING.intValue()) return WARN; + else if (level >= java.util.logging.Level.INFO.intValue()) return INFO; + else if (level >= defaultLevelDebugCutOff) return DEBUG; + else return TRACE; + } +} diff --git a/lib/scala/logging-utils/src/main/java/org/enso/logger/JulHandler.java b/lib/scala/logging-utils/src/main/java/org/enso/logger/JulHandler.java new file mode 100644 index 000000000000..ea6a8550c8e4 --- /dev/null +++ b/lib/scala/logging-utils/src/main/java/org/enso/logger/JulHandler.java @@ -0,0 +1,73 @@ +package org.enso.logger; + +import static java.util.logging.Level.*; + +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.logging.SimpleFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** java.util.logging.Handler that propagates all events to the equivalent SLF4J implementation. */ +public final class JulHandler extends Handler { + + private static final Handler _handler; + + static { + _handler = new JulHandler(); + } + + private Formatter formattter; + + private JulHandler() { + this.formattter = new SimpleFormatter(); + } + + public static final Handler get() { + return _handler; + } + + @Override + public void publish(LogRecord record) { + Logger logger = LoggerFactory.getLogger(record.getLoggerName()); + java.util.logging.Level julLevel = record.getLevel(); + String msg; + boolean hasThrowable; + if (record.getThrown() != null) { + hasThrowable = true; + msg = formattter.formatMessage(record); + } else { + hasThrowable = false; + msg = record.getMessage().replaceAll("\\{\\d+\\}", "{}"); + } + if (julLevel.intValue() == SEVERE.intValue()) { + if (hasThrowable) logger.error(msg, record.getThrown()); + else logger.error(msg, record.getParameters()); + } else if (julLevel.intValue() == INFO.intValue()) { + if (hasThrowable) logger.info(msg, record.getThrown()); + else logger.info(msg, record.getParameters()); + } else if (julLevel.intValue() == WARNING.intValue()) { + if (hasThrowable) logger.warn(msg, record.getThrown()); + else logger.warn(msg, record.getParameters()); + } else if (julLevel.intValue() == ALL.intValue()) { + if (hasThrowable) logger.trace(msg, record.getThrown()); + else logger.trace(msg, record.getParameters()); + } else if (julLevel.intValue() == FINE.intValue()) { + if (hasThrowable) logger.debug(msg, record.getThrown()); + else logger.debug(msg, record.getParameters()); + } else if (julLevel.intValue() == FINER.intValue()) { + if (hasThrowable) logger.trace(msg, record.getThrown()); + else logger.trace(msg, record.getParameters()); + } else if (julLevel.intValue() == FINEST.intValue()) { + if (hasThrowable) logger.trace(msg, record.getThrown()); + else logger.trace(msg, record.getParameters()); + } + } + + @Override + public void flush() {} + + @Override + public void close() throws SecurityException {} +} diff --git a/lib/scala/logging-utils/src/main/java/org/enso/logger/LoggerUtils.java b/lib/scala/logging-utils/src/main/java/org/enso/logger/LoggerUtils.java new file mode 100644 index 000000000000..5a245846b08d --- /dev/null +++ b/lib/scala/logging-utils/src/main/java/org/enso/logger/LoggerUtils.java @@ -0,0 +1,12 @@ +package org.enso.logger; + +public class LoggerUtils { + public static String backwardCompatibleName(String name) { + switch (name) { + case "warning": + return "warn"; + default: + return name; + } + } +} diff --git a/lib/scala/logging-utils/src/test/scala/org/enso/logger/TestAppender.scala b/lib/scala/logging-utils/src/test/scala/org/enso/logger/TestAppender.scala new file mode 100644 index 000000000000..b41fa3336be1 --- /dev/null +++ b/lib/scala/logging-utils/src/test/scala/org/enso/logger/TestAppender.scala @@ -0,0 +1,19 @@ +package org.enso.logger + +import ch.qos.logback.core.read.ListAppender +import ch.qos.logback.classic.spi.ILoggingEvent + +import scala.jdk.CollectionConverters._ + +class TestAppender extends ListAppender[ILoggingEvent] { + + def size(): Int = { + this.list.size(); + } + + def allEvents(): List[TestLogMessage] = { + this.list.asScala.toList.map(event => + TestLogMessage(event.getLevel(), event.getFormattedMessage()) + ) + } +} diff --git a/lib/scala/logging-utils/src/test/scala/org/enso/logger/TestLogMessage.scala b/lib/scala/logging-utils/src/test/scala/org/enso/logger/TestLogMessage.scala new file mode 100644 index 000000000000..5c098e1add21 --- /dev/null +++ b/lib/scala/logging-utils/src/test/scala/org/enso/logger/TestLogMessage.scala @@ -0,0 +1,17 @@ +package org.enso.logger + +import org.slf4j.event.Level +import ch.qos.logback.classic.{Level => LogbackLevel} + +case class TestLogMessage(level: Level, msg: String) +object TestLogMessage { + def apply(level: LogbackLevel, msg: String): TestLogMessage = { + val translatedLevel = level match { + case LogbackLevel.INFO => Level.INFO + case LogbackLevel.DEBUG => Level.DEBUG + case LogbackLevel.WARN => Level.WARN + case LogbackLevel.TRACE => Level.TRACE + } + TestLogMessage(translatedLevel, msg) + } +} diff --git a/lib/scala/logging-utils/src/test/scala/org/enso/logger/TestLogger.scala b/lib/scala/logging-utils/src/test/scala/org/enso/logger/TestLogger.scala new file mode 100644 index 000000000000..1d53ae68de6f --- /dev/null +++ b/lib/scala/logging-utils/src/test/scala/org/enso/logger/TestLogger.scala @@ -0,0 +1,29 @@ +package org.enso.logger + +import org.slf4j.LoggerFactory +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.LoggerContext + +object TestLogger { + + /** Gathers all logs of a specified type while executing a closure. + * + * @param of class of logs to collect + * @param action a generic closure to execute + * @tparam T the return type of executing a closure + * @tparam A the type of logs to collect + * @return a tuple with the result of executing the closure and the list of log events collected + */ + def gather[T, A](of: Class[A], action: => T): (T, List[TestLogMessage]) = { + val logger = LoggerFactory.getLogger(of).asInstanceOf[Logger] + val appender = new TestAppender() + appender.setContext( + LoggerFactory.getILoggerFactory().asInstanceOf[LoggerContext] + ) + logger.addAppender(appender) + appender.start() + val result = action + (result, appender.allEvents()) + } + +} diff --git a/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/native-image.properties b/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/native-image.properties index 9848711dc334..f3643d3a6bb6 100644 --- a/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/native-image.properties +++ b/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/native-image.properties @@ -1 +1 @@ -Args=--initialize-at-run-time=com.typesafe.config.impl.ConfigImpl$EnvVariablesHolder,com.typesafe.config.impl.ConfigImpl$SystemPropertiesHolder,org.enso.loggingservice.LoggingServiceManager$ +Args=--initialize-at-run-time=com.typesafe.config.impl.ConfigImpl$EnvVariablesHolder,com.typesafe.config.impl.ConfigImpl$SystemPropertiesHolder diff --git a/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/reflect-config.json b/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/reflect-config.json index c695bde11fa4..16f472b6eacc 100644 --- a/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/reflect-config.json +++ b/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/reflect-config.json @@ -565,5 +565,41 @@ "parameterTypes": ["java.lang.Boolean", "java.lang.Object"] } ] + }, + { + "name":"org.enso.logger.LogbackSetup", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.DateConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LevelConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LineSeparatorConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LoggerConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.NopThrowableInformationConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.MessageConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.core.rolling.helper.DateTokenConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.core.rolling.helper.IntegerTokenConverter", + "methods":[{"name":"","parameterTypes":[] }] } ] diff --git a/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/resource-config.json b/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/resource-config.json index 53c6ce2416be..cdda671e57d4 100644 --- a/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/resource-config.json +++ b/lib/scala/project-manager/src/main/resources/META-INF/native-image/org/enso/projectmanager/resource-config.json @@ -51,6 +51,9 @@ "pattern": "\\Qorg/enso/projectmanager/service/versionmanagement/RuntimeVersionManagerMixin$ErrorRecovery.class\\E" }, { "pattern": "\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" }, + { "pattern": "\\QMETA-INF/services/org.enso.logger.LoggerSetup\\E" }, + { "pattern": "\\QMETA-INF/services/org.enso.logging.LogbackLoggingServiceFactory\\E" }, + { "pattern": "\\Qch/qos/logback/classic/spi/Configurator.class\\E" }, { "pattern": "\\Qreference.conf\\E" }, { "pattern": "\\Qversion.conf\\E" }, { "pattern": "\\Qzio/App$$anon$1.class\\E" }, diff --git a/lib/scala/project-manager/src/main/resources/application.conf b/lib/scala/project-manager/src/main/resources/application.conf index dff5a54f21c1..f2b7302ed9d0 100644 --- a/lib/scala/project-manager/src/main/resources/application.conf +++ b/lib/scala/project-manager/src/main/resources/application.conf @@ -1,3 +1,5 @@ +## Project Manager's application.conf + akka { actor.debug.lifecycle = on http { @@ -11,11 +13,58 @@ akka { log-dead-letters-during-shutdown = off } -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.http = warn + akka.stream = error + akka.routing = error + } + appenders = [ + { + name = "socket" + hostname = "localhost" + hostname = ${?ENSO_LOGSERVER_HOSTNAME} + port = 6000 + port = ${?ENSO_LOGSERVER_PORT} + }, + { + name = "file", + }, + { + name = "console" + } + ] + default-appender = ${?ENSO_APPENDER_DEFAULT} + default-appender = socket + server { + start = true + start = ${?ENSO_LOGSERVER_START} + port = 6000 + port = ${?ENSO_LOGSERVER_PORT} + appenders = [ # file/console/socket/sentry + { + name = "file" + rolling-policy { + max-file-size = "100MB" + max-history = 30 + max-total-size = "2GB" + } + }, + { + name = "sentry" + dsn = "" + dsn = ${?ENSO_APPENDER_SENTRY_DSN} + }, + { + name = "console" + } + ] + default-appender = file + default-appender = ${?ENSO_LOGSERVER_APPENDER} + } } project-manager { diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/Logging.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/Logging.scala index c0b89fa59aa5..a2b7b649a18f 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/Logging.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/Logging.scala @@ -1,20 +1,21 @@ package org.enso.projectmanager.boot import java.nio.file.Path - -import akka.http.scaladsl.model.Uri -import org.enso.loggingservice.{LogLevel, LoggingServiceSetupHelper} import org.enso.projectmanager.service.LoggingServiceDescriptor import org.enso.projectmanager.versionmanagement.DefaultDistributionConfiguration +import org.slf4j.event.Level + +import java.net.URI -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future +import org.enso.logging.LoggingSetupHelper +import scala.concurrent.ExecutionContext.Implicits.global /** A helper for setting up the logging service in the Project Manager. */ -object Logging extends LoggingServiceSetupHelper { +object Logging extends LoggingSetupHelper(global) { /** @inheritdoc */ - override val defaultLogLevel: LogLevel = LogLevel.Info + override val defaultLogLevel: Level = Level.INFO /** @inheritdoc */ override lazy val logPath: Path = @@ -26,6 +27,6 @@ object Logging extends LoggingServiceSetupHelper { object GlobalLoggingService extends LoggingServiceDescriptor { /** @inheritdoc */ - override def getEndpoint: Future[Option[Uri]] = loggingServiceEndpoint() + override def getEndpoint: Future[Option[URI]] = loggingServiceEndpoint() } } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/MainModule.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/MainModule.scala index 369aa03fa33c..b546742b3fe0 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/MainModule.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/MainModule.scala @@ -4,7 +4,7 @@ import akka.actor.ActorSystem import akka.stream.SystemMaterializer import cats.MonadError import org.enso.jsonrpc.JsonRpcServer -import org.enso.loggingservice.LogLevel +import org.enso.logger.akka.AkkaConverter import org.enso.projectmanager.boot.configuration.{ MainProcessConfig, ProjectManagerConfig @@ -53,7 +53,7 @@ class MainModule[ implicit val system: ActorSystem = ActorSystem("project-manager", None, None, Some(computeExecutionContext)) - system.eventStream.setLogLevel(LogLevel.toAkka(processConfig.logLevel)) + system.eventStream.setLogLevel(AkkaConverter.toAkka(processConfig.logLevel)) implicit val materializer: SystemMaterializer = SystemMaterializer.get(system) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/ProjectManager.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/ProjectManager.scala index a41ff8230e66..ef371d8bc03d 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/ProjectManager.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/ProjectManager.scala @@ -3,7 +3,7 @@ package org.enso.projectmanager.boot import akka.http.scaladsl.Http import com.typesafe.scalalogging.LazyLogging import org.apache.commons.cli.CommandLine -import org.enso.loggingservice.{ColorMode, LogLevel} + import org.enso.projectmanager.boot.Globals.{ ConfigFilename, ConfigNamespace, @@ -15,6 +15,7 @@ import org.enso.projectmanager.boot.configuration.{ ProjectManagerConfig } import org.enso.version.VersionDescription +import org.slf4j.event.Level import pureconfig.ConfigSource import pureconfig.generic.auto._ import zio.Console.{printLine, printLineError, readLine} @@ -22,7 +23,7 @@ import zio.interop.catz.core._ import zio.{ExitCode, Runtime, Scope, UIO, ZAny, ZIO, ZIOAppArgs, ZIOAppDefault} import java.io.{EOFException, IOException} -import java.nio.file.{FileAlreadyExistsException, Files, Path, Paths} +import java.nio.file.{FileAlreadyExistsException, Files, Paths} import java.util.concurrent.ScheduledThreadPoolExecutor import scala.concurrent.duration._ @@ -126,7 +127,9 @@ object ProjectManager extends ZIOAppDefault with LazyLogging { case Right(opts) => runOpts(opts).catchAll(th => ZIO.succeed( - logger.error("An error occurred during the program startup", th) + System.err.println( + s"An error occurred during the program startup: ${th.getMessage}" + ) ) *> ZIO.succeed(FailureExitCode) ) @@ -134,7 +137,9 @@ object ProjectManager extends ZIOAppDefault with LazyLogging { (printLine(error) *> ZIO.succeed(Cli.printHelp()) *> ZIO.succeed(FailureExitCode)).catchAll(th => - ZIO.succeed(logger.error("Unexpected error", th)) *> + ZIO.succeed( + System.err.println(s"Unexpected error: ${th.getMessage}") + ) *> ZIO.succeed(FailureExitCode) ) } @@ -229,14 +234,10 @@ object ProjectManager extends ZIOAppDefault with LazyLogging { } else { val verbosity = options.getOptions.count(_ == Cli.option.verbose) val logMasking = !options.hasOption(Cli.NO_LOG_MASKING) - logger.info( - "Starting {}", - makeVersionDescription.asString(useJson = false) - ) for { - opts <- parseOpts(options) - profilingLog = opts.profilingPath.map(getSiblingFile(_, ".log")) - logLevel <- setupLogging(verbosity, logMasking, profilingLog) + _ <- displayVersion(false) + opts <- parseOpts(options) + logLevel <- setupLogging(verbosity, logMasking) procConf = MainProcessConfig( logLevel, opts.profilingRuntimeEventsLog, @@ -256,25 +257,21 @@ object ProjectManager extends ZIOAppDefault with LazyLogging { private def setupLogging( verbosityLevel: Int, - logMasking: Boolean, - profilingLog: Option[Path] - ): ZIO[ZAny, IOException, LogLevel] = { + logMasking: Boolean + ): ZIO[ZAny, IOException, Level] = { val level = verbosityLevel match { - case 0 => LogLevel.Info - case 1 => LogLevel.Debug - case _ => LogLevel.Trace + case 0 => Level.INFO + case 1 => Level.DEBUG + case _ => Level.TRACE } - - // TODO [RW] at some point we may want to allow customization of color - // output in CLI flags - val colorMode = ColorMode.Auto - ZIO .attempt { - Logging.setup(Some(level), None, colorMode, logMasking, profilingLog) + Logging.setup(level, logMasking) + Logging.waitForSetup() + () } .catchAll { exception => - printLineError(s"Failed to setup the logger: $exception") + printLineError(s"Failed to setup logger: ${exception.getMessage()}") } .as(level) } @@ -312,12 +309,4 @@ object ProjectManager extends ZIOAppDefault with LazyLogging { ) } - private def getSiblingFile(file: Path, ext: String): Path = { - val fileName = file.getFileName.toString - val extensionIndex = fileName.lastIndexOf(".") - val newName = - if (extensionIndex > 0) fileName.substring(0, extensionIndex) + ext - else fileName + ext - file.getParent.resolve(newName) - } } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration.scala index 5cfdeefeec51..647788d2ee40 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration.scala @@ -1,10 +1,9 @@ package org.enso.projectmanager.boot -import org.enso.loggingservice.LogLevel +import org.slf4j.event.Level import java.io.File import java.nio.file.Path - import scala.concurrent.duration.FiniteDuration object configuration { @@ -18,7 +17,7 @@ object configuration { * @param profilingTime the time limiting the profiling duration */ case class MainProcessConfig( - logLevel: LogLevel, + logLevel: Level, profilingEventsLogPath: Option[Path], profilingPath: Option[Path], profilingTime: Option[FiniteDuration] diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ExecutorWithUnlimitedPool.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ExecutorWithUnlimitedPool.scala index c97abe131f2a..708b70cbe35d 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ExecutorWithUnlimitedPool.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ExecutorWithUnlimitedPool.scala @@ -4,7 +4,7 @@ import akka.actor.ActorRef import com.typesafe.scalalogging.Logger import org.apache.commons.lang3.concurrent.BasicThreadFactory import org.enso.logger.masking.Masking -import org.enso.loggingservice.LoggingServiceManager +import org.enso.logging.LoggingServiceManager import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory import org.enso.runtimeversionmanager.config.GlobalRunnerConfigurationManager import org.enso.runtimeversionmanager.runner.{LanguageServerOptions, Runner} diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerDescriptor.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerDescriptor.scala index 5e3d06e91f14..8b31da1707ae 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerDescriptor.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerDescriptor.scala @@ -1,14 +1,13 @@ package org.enso.projectmanager.infrastructure.languageserver -import akka.http.scaladsl.model.Uri import nl.gn0s1s.bump.SemVer import org.enso.projectmanager.boot.configuration.NetworkConfig import org.enso.projectmanager.versionmanagement.DistributionConfiguration import org.enso.runtimeversionmanager.runner.JVMSettings +import java.net.URI import java.nio.file.Path import java.util.UUID - import scala.concurrent.Future import scala.concurrent.duration.FiniteDuration @@ -32,7 +31,7 @@ import scala.concurrent.duration.FiniteDuration * logging service has been fully set-up; * if the child component should connect * to the logging service, it should - * contain the Uri to connect to + * contain the URI to connect to * @param skipGraalVMUpdater indicates if the check and installation of GraalVM * should be skipped */ @@ -48,6 +47,6 @@ case class LanguageServerDescriptor( profilingEventsLogPath: Option[Path], profilingPath: Option[Path], profilingTime: Option[FiniteDuration], - deferredLoggingServiceEndpoint: Future[Option[Uri]], + deferredLoggingServiceEndpoint: Future[Option[URI]], skipGraalVMUpdater: Boolean ) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/LoggingServiceEndpointRequestHandler.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/LoggingServiceEndpointRequestHandler.scala index f5dbea52ee0b..a7b146d9b7ca 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/LoggingServiceEndpointRequestHandler.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/LoggingServiceEndpointRequestHandler.scala @@ -1,7 +1,6 @@ package org.enso.projectmanager.requesthandler import akka.actor.{Actor, ActorRef, Cancellable, Props, Status} -import akka.http.scaladsl.model.Uri import akka.pattern.pipe import com.typesafe.scalalogging.LazyLogging import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult} @@ -12,6 +11,7 @@ import org.enso.projectmanager.protocol.ProjectManagementApi.{ import org.enso.projectmanager.service.LoggingServiceDescriptor import org.enso.projectmanager.util.UnhandledLogging +import java.net.URI import scala.concurrent.duration.FiniteDuration /** A request handler for `logging-service/get-endpoint` commands. @@ -88,7 +88,7 @@ class LoggingServiceEndpointRequestHandler( context.stop(self) } - private case class LoggingServiceInitialized(endpoint: Option[Uri]) + private case class LoggingServiceInitialized(endpoint: Option[URI]) } object LoggingServiceEndpointRequestHandler { diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/LoggingServiceDescriptor.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/LoggingServiceDescriptor.scala index d4f6298e5870..5d0cc6bb5d66 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/LoggingServiceDescriptor.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/LoggingServiceDescriptor.scala @@ -1,7 +1,6 @@ package org.enso.projectmanager.service -import akka.http.scaladsl.model.Uri - +import java.net.URI import scala.concurrent.Future /** A service descriptor that provides information on the logging service setup. @@ -12,5 +11,5 @@ trait LoggingServiceDescriptor { * initialized or None if the logging service does not expect incoming * connections. */ - def getEndpoint: Future[Option[Uri]] + def getEndpoint: Future[Option[URI]] } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/util/UnhandledLogging.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/util/UnhandledLogging.scala index 35ff3b5230fd..d48262a4ba04 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/util/UnhandledLogging.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/util/UnhandledLogging.scala @@ -2,16 +2,17 @@ package org.enso.projectmanager.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 //LogLevel .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) } } diff --git a/lib/scala/project-manager/src/test/resources/application.conf b/lib/scala/project-manager/src/test/resources/application.conf index a2cb10e49091..b1a7f8f0c7ac 100644 --- a/lib/scala/project-manager/src/test/resources/application.conf +++ b/lib/scala/project-manager/src/test/resources/application.conf @@ -5,7 +5,20 @@ akka.loglevel = "ERROR" akka.test.timefactor = ${?CI_TEST_TIMEFACTOR} akka.test.single-expect-default = 5s -logging-service.test-log-level = warning +logging-service { + logger { + akka = error + } + appenders = [ + { + name = "console" + pattern = "[%level{lowercase=true}] [%d{yyyy-MM-dd'T'HH:mm:ssXXX}] [%logger] %msg%n%nopex" + } + ] + default-appender = console + log-level = "warn" +} + project-manager { diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/BaseServerSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/BaseServerSpec.scala index 8e504b6b3283..041f056e33bd 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/BaseServerSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/BaseServerSpec.scala @@ -16,8 +16,7 @@ import org.enso.editions.Editions import org.enso.cli.OS import org.enso.jsonrpc.test.JsonRpcServerTestKit import org.enso.jsonrpc.{ClientControllerFactory, ProtocolFactory} -import org.enso.loggingservice.printers.StderrPrinterWithColors -import org.enso.loggingservice.{LogLevel, LoggerMode, LoggingServiceManager} +import org.enso.logger.LoggerSetup import org.enso.pkg.{Config, PackageManager} import org.enso.projectmanager.boot.Globals.{ConfigFilename, ConfigNamespace} import org.enso.projectmanager.boot.configuration._ @@ -47,6 +46,7 @@ import org.enso.projectmanager.test.{ObservableGenerator, ProgrammableClock} import org.enso.runtimeversionmanager.components.GraalVMVersion import org.enso.runtimeversionmanager.test.FakeReleases import org.scalatest.BeforeAndAfterAll +import org.slf4j.event.Level import pureconfig.ConfigSource import pureconfig.generic.auto._ import zio.interop.catz.core._ @@ -85,7 +85,7 @@ class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll { val processConfig: MainProcessConfig = MainProcessConfig( - logLevel = if (debugLogs) LogLevel.Trace else LogLevel.Off, + logLevel = if (debugLogs) Level.TRACE else Level.ERROR, profilingPath = profilingPath, profilingTime = None, profilingEventsLogPath = None @@ -236,17 +236,14 @@ class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll { override def beforeAll(): Unit = { super.beforeAll() - setupEditions() - if (debugLogs) { - LoggingServiceManager.setup( - LoggerMode.Local( - Seq(StderrPrinterWithColors.colorPrinterIfAvailable(true)) - ), - LogLevel.Trace - ) + LoggerSetup.get().setup(Level.TRACE) + } else { + LoggerSetup.get().setup() } + setupEditions() + engineToInstall.foreach(preInstallEngine) } diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/TestLoggingService.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/TestLoggingService.scala index b63065561b49..fa65470fd5d7 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/TestLoggingService.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/TestLoggingService.scala @@ -1,17 +1,17 @@ package org.enso.projectmanager -import akka.http.scaladsl.model.Uri import org.enso.projectmanager.service.LoggingServiceDescriptor +import java.net.URI import scala.concurrent.Future class TestLoggingService extends LoggingServiceDescriptor { - private var currentFuture: Future[Option[Uri]] = Future.successful(None) + private var currentFuture: Future[Option[URI]] = Future.successful(None) - override def getEndpoint: Future[Option[Uri]] = currentFuture + override def getEndpoint: Future[Option[URI]] = currentFuture def withOverriddenEndpoint[R]( - future: Future[Option[Uri]] + future: Future[Option[URI]] )(action: => R): Unit = { val oldValue = currentFuture currentFuture = future diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerSupervisorSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerSupervisorSpec.scala index cc82f4aad2b0..42e235881e86 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerSupervisorSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerSupervisorSpec.scala @@ -3,6 +3,7 @@ package org.enso.projectmanager.infrastructure.languageserver import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{ImplicitSender, TestActor, TestKit, TestProbe} import com.miguno.akka.testing.VirtualTime +import org.enso.logger.LoggerSetup import org.enso.projectmanager.boot.configuration.SupervisionConfig import org.enso.projectmanager.infrastructure.http.AkkaBasedWebSocketConnectionFactory import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.ServerBooted @@ -106,6 +107,8 @@ class LanguageServerSupervisorSpec trait TestCtx { + LoggerSetup.get().setup() + val VerificationTimeout = 120000 val virtualTime = new VirtualTime diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/LoggingServiceEndpointSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/LoggingServiceEndpointSpec.scala index 14e3d2fd4c7c..006afcb20d3c 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/LoggingServiceEndpointSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/LoggingServiceEndpointSpec.scala @@ -1,11 +1,11 @@ package org.enso.projectmanager.protocol -import akka.http.scaladsl.model.Uri import io.circe.literal.JsonStringContext import org.enso.projectmanager.BaseServerSpec import org.enso.testkit.FlakySpec import scala.concurrent.Future +import java.net.URI class LoggingServiceEndpointSpec extends BaseServerSpec with FlakySpec { @@ -34,7 +34,7 @@ class LoggingServiceEndpointSpec extends BaseServerSpec with FlakySpec { "return the endpoint if it has been set-up" in { implicit val client = new WsTestClient(address) loggingService.withOverriddenEndpoint( - Future.successful(Some(Uri("ws://test-uri/"))) + Future.successful(Some(URI.create("ws://test-uri/"))) ) { client.send(json""" { "jsonrpc": "2.0", diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/cli/Arguments.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/cli/Arguments.scala index d7f5b8376dd7..a20026002c1e 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/cli/Arguments.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/cli/Arguments.scala @@ -2,10 +2,14 @@ package org.enso.runtimeversionmanager.cli import akka.http.scaladsl.model.{IllegalUriException, Uri} import org.enso.cli.arguments.{Argument, OptsParseError} -import org.enso.loggingservice.LogLevel +import org.enso.logger.LoggerUtils + +import java.net.URI +import java.net.URISyntaxException +import org.slf4j.event.Level object Arguments { - implicit val uriArgument: Argument[Uri] = (string: String) => + implicit val uriAkkaArgument: Argument[Uri] = (string: String) => try { Right(Uri(string)) } catch { @@ -13,9 +17,20 @@ object Arguments { Left(OptsParseError(s"`$string` is not a valid Uri: $error.")) } - implicit val logLevelArgument: Argument[LogLevel] = (string: String) => { - val provided = string.toLowerCase - LogLevel.allLevels + implicit val uriArgument: Argument[URI] = (string: String) => + try { + Right(URI.create(string)) + } catch { + case error: IllegalArgumentException => + Left(OptsParseError(s"`$string` is not a valid URI: $error.")) + case error: URISyntaxException => + Left(OptsParseError(s"`$string` is not a valid URI: $error.")) + } + + implicit val logLevelArgument: Argument[Level] = (string: String) => { + val provided = LoggerUtils.backwardCompatibleName(string.toLowerCase) + Level + .values() .find(_.toString.toLowerCase == provided) .toRight( OptsParseError(s"`$string` is not a valid log level.") diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/testing/FakeReleaseProvider.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/testing/FakeReleaseProvider.scala index 645705d42800..96df4cff4e42 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/testing/FakeReleaseProvider.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/testing/FakeReleaseProvider.scala @@ -1,6 +1,12 @@ package org.enso.runtimeversionmanager.releases.testing -import java.nio.file.{Files, Path, StandardCopyOption} +import java.nio.file.{ + FileAlreadyExistsException, + Files, + NoSuchFileException, + Path, + StandardCopyOption +} import org.enso.cli.task.{ProgressListener, TaskProgress} import org.enso.distribution.FileSystem import org.enso.distribution.locking.{LockManager, LockType} @@ -147,11 +153,16 @@ case class FakeAsset( } for (sourceToCopy <- pathsToCopy) { - Files.copy( - sourceToCopy, - innerRoot.resolve(sourceToCopy.getFileName), - StandardCopyOption.REPLACE_EXISTING - ) + try { + Files.copy( + sourceToCopy, + innerRoot.resolve(sourceToCopy.getFileName), + StandardCopyOption.REPLACE_EXISTING + ) + } catch { + case _: FileAlreadyExistsException => + case _: NoSuchFileException => + } } TestArchivePackager.packArchive(source, destination) } diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala index 2e9135578f6f..7c78b0d23a65 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala @@ -1,13 +1,14 @@ package org.enso.runtimeversionmanager.runner -import akka.http.scaladsl.model.Uri import com.typesafe.scalalogging.Logger import nl.gn0s1s.bump.SemVer import org.enso.distribution.{DistributionManager, Environment} import org.enso.editions.updater.EditionManager import org.enso.editions.{DefaultEnsoVersion, SemVerEnsoVersion} import org.enso.logger.masking.MaskedString -import org.enso.loggingservice.LogLevel +import org.slf4j.event.Level + +import java.net.URI import org.enso.runtimeversionmanager.components.Manifest.JVMOptionsContext import org.enso.runtimeversionmanager.components.{ Engine, @@ -31,7 +32,7 @@ class Runner( globalConfigurationManager: GlobalRunnerConfigurationManager, editionManager: EditionManager, environment: Environment, - loggerConnection: Future[Option[Uri]] + loggerConnection: Future[Option[URI]] ) { /** The current working directory that is a starting point when checking if @@ -86,7 +87,7 @@ class Runner( options: LanguageServerOptions, project: Project, versionOverride: Option[SemVer], - logLevel: LogLevel, + logLevel: Level, logMasking: Boolean, additionalArguments: Seq[String] ): Try[RunSettings] = { @@ -107,7 +108,7 @@ class Runner( options: LanguageServerOptions, projectPath: String, version: SemVer, - logLevel: LogLevel, + logLevel: Level, logMasking: Boolean, additionalArguments: Seq[String] ): Try[RunSettings] = diff --git a/project/NativeImage.scala b/project/NativeImage.scala index 16131130b7a9..d4a64e31aee3 100644 --- a/project/NativeImage.scala +++ b/project/NativeImage.scala @@ -36,7 +36,8 @@ object NativeImage { "zio", "enumeratum", "akka", - "nl" + "nl", + "ch.qos.logback" ) /** Creates a task that builds a native image for the current project. diff --git a/tools/simple-library-server/package-lock.json b/tools/simple-library-server/package-lock.json index 946531acfd69..e040ca7f67da 100644 --- a/tools/simple-library-server/package-lock.json +++ b/tools/simple-library-server/package-lock.json @@ -1,956 +1,8 @@ { "name": "simple-library-server", "version": "1.0.0", - "lockfileVersion": 2, + "lockfileVersion": 1, "requires": true, - "packages": { - "": { - "name": "simple-library-server", - "version": "1.0.0", - "license": "Apache-2.0", - "dependencies": { - "compression": "^1.7.4", - "express": "^4.17.1", - "multer": "^1.4.2", - "semver": "^7.5.2", - "yargs": "^17.0.1" - }, - "engines": { - "node": ">=14.17.2" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.9.7", - "raw-body": "2.4.3", - "type-is": "~1.6.18" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", - "dependencies": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" - }, - "node_modules/dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", - "dependencies": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", - "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.19.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.4.2", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.9.7", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", - "setprototypeof": "1.2.0", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/multer": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", - "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", - "deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^0.2.11", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "on-finished": "^2.3.0", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", - "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "1.8.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", - "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz", - "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - } - }, "dependencies": { "accepts": { "version": "1.3.8", @@ -1553,11 +605,6 @@ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==" }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" - }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1568,6 +615,11 @@ "strip-ansi": "^6.0.1" } }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",