Finish Logging Service Integration (#1346)

This commit is contained in:
Radosław Waśko 2020-12-15 09:49:58 +01:00 committed by GitHub
parent c1369ad044
commit de817af655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 825 additions and 434 deletions

View File

@ -621,26 +621,6 @@ lazy val `logging-service` = project
)
.dependsOn(`akka-native`)
ThisBuild / testOptions += Tests.Setup(_ =>
// Note [Logging Service in Tests]
sys.props("org.enso.loggingservice.test-log-level") = "2"
)
/* Note [Logging Service in Tests]
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* As migrating the runner to our new logging service has forced us to migrate
* other components that are related to it too, some tests that used to be
* configured by logback are not properly configured anymore and log a lot of
* debug information. This is a temporary fix to make sure that there are not
* too much logs in the CI - it sets the log level for all tests to be at most
* info by default. It can be overridden by particular tests if they set up a
* logging service.
*
* This is a temporary solution and it will be obsoleted once all of our
* components do a complete migration to the new logging service, which is
* planned in tasks #1144 and #1151.
*/
lazy val cli = project
.in(file("lib/scala/cli"))
.configs(Test)
@ -1170,7 +1150,6 @@ lazy val launcher = project
"nl.gn0s1s" %% "bump" % bumpVersion,
"org.apache.commons" % "commons-compress" % commonsCompressVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
akkaHttp,
akkaSLF4J
)
)
@ -1225,8 +1204,7 @@ lazy val `runtime-version-manager` = project
"nl.gn0s1s" %% "bump" % bumpVersion,
"org.apache.commons" % "commons-compress" % commonsCompressVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
akkaHttp,
akkaSLF4J
akkaHttp
)
)
.dependsOn(pkg)

View File

@ -3,4 +3,4 @@ files-to-copy:
- NOTICE
- README.md
directories-to-copy:
- components-licences
- THIRD-PARTY

View File

@ -79,8 +79,7 @@ extraction-location
│ └── lts-2.0.8.yaml
├── README.md # Information on layout and usage of the Enso distribution.
├── .enso.portable # A file that allows the universal launcher to detect that if it is run from this directory, it should run in portable distribution mode.
├── NOTICE # A copyright notice regarding components that are included in the distribution of the universal launcher.
└── components-licences # Contains licences of distributed components, as described in the NOTICE.
└── THIRD-PARTY # Contains licences of distributed components, including the NOTICE file.
```
### Installed Enso Distribution Layout

View File

@ -251,3 +251,23 @@ 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.
### 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 a `logging.properties` file in `resources` of the `test` target.
Currently only one property is supported - `test-log-level` which should be set
to a log level name (possible values are: `off`, `error`, `warning`, `info`,
`debug`, `trace`). If this property 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.

View File

@ -0,0 +1 @@
test-log-level=info

View File

@ -3,7 +3,8 @@ 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.LogLevel
import org.enso.loggingservice.ColorMode.{Always, Auto, Never}
import org.enso.loggingservice.{ColorMode, LogLevel}
/** Gathers settings set by the global CLI options.
*
@ -45,7 +46,7 @@ object GlobalCLIOptions {
*/
def toOptions: Seq[String] = {
val level = launcherLogLevel
.map(level => Seq(s"--$LOG_LEVEL", level.toString))
.map(level => Seq(s"--$LOG_LEVEL", level.name))
.getOrElse(Seq())
val uri = loggerConnectUri
.map(uri => Seq(s"--$CONNECT_LOGGER", uri.toString))
@ -66,30 +67,13 @@ object GlobalCLIOptions {
if (config.hideProgress) Seq(s"--$HIDE_PROGRESS") else Seq()
val useJSON = if (config.useJSON) Seq(s"--$USE_JSON") else Seq()
autoConfirm ++ hideProgress ++ useJSON ++
ColorMode.toOptions(config.colorMode) ++ config.internalOptions.toOptions
LauncherColorMode.toOptions(
config.colorMode
) ++ config.internalOptions.toOptions
}
}
/** 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
object LauncherColorMode {
/** [[Argument]] instance used to parse [[ColorMode]] from CLI.
*/

View File

@ -10,6 +10,7 @@ import nl.gn0s1s.bump.SemVer
import org.enso.cli._
import org.enso.cli.arguments.Opts.implicits._
import org.enso.cli.arguments._
import org.enso.launcher.cli.LauncherColorMode.argument
import org.enso.runtimeversionmanager.cli.Arguments._
import org.enso.runtimeversionmanager.config.DefaultVersion
import org.enso.runtimeversionmanager.runner.LanguageServerOptions
@ -18,7 +19,7 @@ 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.LogLevel
import org.enso.loggingservice.{ColorMode, LogLevel}
/** Defines the CLI commands and options for the program.
*
@ -558,7 +559,11 @@ object LauncherApplication {
internalOptsCallback(globalCLIOptions)
LauncherUpgrader.setCLIOptions(globalCLIOptions)
LauncherLogging.setup(logLevel, connectLogger, globalCLIOptions)
LauncherLogging.setup(
logLevel,
connectLogger,
globalCLIOptions.colorMode
)
initializeApp()
if (version) {

View File

@ -1,219 +1,30 @@
package org.enso.launcher.cli
import akka.http.scaladsl.model.Uri
import com.typesafe.scalalogging.Logger
import java.nio.file.Path
import org.enso.launcher.distribution.DefaultManagers
import org.enso.loggingservice.printers.{
FileOutputPrinter,
Printer,
StderrPrinter,
StderrPrinterWithColors
import org.enso.loggingservice.{
ColorMode,
LogLevel,
LoggingServiceManager,
LoggingServiceSetupHelper
}
import org.enso.loggingservice.{LogLevel, LoggerMode, LoggingServiceManager}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, Future, Promise}
import scala.util.control.NonFatal
import scala.util.{Failure, Success}
/** Manages setting up the logging service within the launcher.
*/
object LauncherLogging {
private val logger = Logger[LauncherLogging.type]
object LauncherLogging extends LoggingServiceSetupHelper {
/** Default logl level to use if none is provided.
*/
val defaultLogLevel: LogLevel = LogLevel.Warning
/** @inheritdoc */
override val defaultLogLevel: LogLevel = LogLevel.Warning
/** Sets up launcher's logging service as either a server that gathers other
* component's logs or a client that forwards them further.
*
* Forwarding logs to another server in the launcher is 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 launcher, being forwarded instead.
*
* @param logLevel the log level to use for launcher'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 launcher should forward
* its logs to; advanced feature, use with
* caution
*/
def setup(
logLevel: Option[LogLevel],
connectToExternalLogger: Option[Uri],
globalCLIOptions: GlobalCLIOptions
): Unit = {
val actualLogLevel = logLevel.getOrElse(defaultLogLevel)
connectToExternalLogger match {
case Some(uri) =>
setupLoggingConnection(uri, actualLogLevel)
case None =>
setupLoggingServer(actualLogLevel, globalCLIOptions)
}
}
/** @inheritdoc */
override val logFileSuffix: String = "enso-launcher"
/** 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)
}
}
private def fallbackPrinter = 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.
*/
private def stderrPrinter(
globalCLIOptions: GlobalCLIOptions,
printExceptions: Boolean
): Printer =
globalCLIOptions.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,
globalCLIOptions: GlobalCLIOptions
): 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(
DefaultManagers.distributionManager.paths.logs
)
Seq(
stderrPrinter(globalCLIOptions, printExceptionsInStderr),
filePrinter
)
} catch {
case NonFatal(error) =>
logger.error(
"Failed to initialize the write-to-file logger, " +
"falling back to stderr only.",
error
)
Seq(stderrPrinter(globalCLIOptions, printExceptions = true))
}
LoggingServiceManager
.setup(LoggerMode.Server(createPrinters()), logLevel)
.onComplete {
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(fallbackException) =>
System.err.println(
s"Failed to initialize the fallback logger: " +
s"$fallbackException"
)
fallbackException.printStackTrace()
case Success(_) =>
}
case Success(serverBinding) =>
val uri = serverBinding.toUri()
loggingServiceEndpointPromise.success(Some(uri))
logger.trace(
s"Logging service has been set-up and is listening at `$uri`."
)
}
}
/** Connects this launcher 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) {
loggingServiceEndpointPromise.success(Some(uri))
System.err.println(
s"Log messages from this launcher are forwarded to `$uri`."
)
} 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)
}
/** @inheritdoc */
override lazy val logPath: Path =
DefaultManagers.distributionManager.paths.logs
/** Turns off the main logging service, falling back to just a stderr backend.
*
@ -224,15 +35,10 @@ object LauncherLogging {
* This is necessary on Windows to ensure that the logs file is closed, so
* that the log directory can be removed.
*/
def prepareForUninstall(globalCLIOptions: GlobalCLIOptions): Unit = {
def prepareForUninstall(colorMode: ColorMode): Unit = {
waitForSetup()
LoggingServiceManager.replaceWithFallback(printers =
Seq(stderrPrinter(globalCLIOptions, printExceptions = true))
Seq(stderrPrinter(colorMode, printExceptions = true))
)
}
/** Shuts down the logging service gracefully.
*/
def tearDown(): Unit =
LoggingServiceManager.tearDown()
}

View File

@ -68,7 +68,7 @@ class LauncherRunner(
}
RunSettings(
version,
arguments ++ Seq("--log-level", logLevel.toString)
arguments ++ setLogLevelArgs(logLevel)
++ additionalArguments,
connectLoggerIfAvailable = true
)
@ -128,12 +128,15 @@ class LauncherRunner(
}
RunSettings(
version,
arguments ++ Seq("--log-level", logLevel.toString)
arguments ++ setLogLevelArgs(logLevel)
++ additionalArguments,
connectLoggerIfAvailable = true
)
}
private def setLogLevelArgs(level: LogLevel): Seq[String] =
Seq("--log-level", level.name)
/** Creates [[RunSettings]] for launching the Language Server.
*
* See [[org.enso.launcher.Launcher.runLanguageServer]] for more details.

View File

@ -50,8 +50,8 @@ class DistributionInstaller(
*
* These files are assumed to be located at the data root.
*/
private val nonEssentialFiles = Seq("README.md", "NOTICE")
private val nonEssentialDirectories = Seq("components-licences")
private val nonEssentialFiles = Seq("README.md")
private val nonEssentialDirectories = Seq("THIRD-PARTY")
private val enginesDirectory =
installed.dataDirectory / manager.ENGINES_DIRECTORY

View File

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

View File

@ -0,0 +1 @@
test-log-level=warning

View File

@ -2,4 +2,4 @@ minimum-version-for-upgrade: 0.0.0
files-to-copy:
- README.md
directories-to-copy:
- components-licences
- THIRD-PARTY

View File

@ -94,7 +94,7 @@ class UpgradeSpec
val root = launcherPath.getParent.getParent
FileSystem.writeTextFile(root / ".enso.portable", "mark")
}
Thread.sleep(250)
Thread.sleep(1000)
}
/** Path to the launcher executable in the temporary distribution.
@ -190,7 +190,7 @@ class UpgradeSpec
checkVersion() shouldEqual SemVer(0, 0, 1)
TestHelpers.readFileContent(root / "README.md").trim shouldEqual "Content"
TestHelpers
.readFileContent(root / "components-licences" / "test-license.txt")
.readFileContent(root / "THIRD-PARTY" / "test-license.txt")
.trim shouldEqual "Test license"
}
@ -214,7 +214,7 @@ class UpgradeSpec
.readFileContent(dataRoot / "README.md")
.trim shouldEqual "Content"
TestHelpers
.readFileContent(dataRoot / "components-licences" / "test-license.txt")
.readFileContent(dataRoot / "THIRD-PARTY" / "test-license.txt")
.trim shouldEqual "Test license"
}

View File

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

View File

@ -5,7 +5,7 @@ import io.circe.{Decoder, DecodingFailure, Encoder}
/** Defines a log level for log messages.
*/
sealed abstract class LogLevel(final val level: Int) {
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`.
@ -14,6 +14,9 @@ sealed abstract class LogLevel(final val level: Int) {
*/
def shouldLog(other: LogLevel): Boolean =
other.level <= level
/** @inheritdoc */
override def toString: String = name
}
object LogLevel {
@ -21,46 +24,34 @@ 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(-1) {
override def toString: String = "off"
}
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(0) {
override def toString: String = "error"
}
case object Error extends LogLevel("error", 0)
/** Log level corresponding to important notices or issues that are not
* severe.
*/
case object Warning extends LogLevel(1) {
override def toString: String = "warning"
}
case object Warning extends LogLevel("warning", 1)
/** Log level corresponding to usual information of what the application is
* doing.
*/
case object Info extends LogLevel(2) {
override def toString: String = "info"
}
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(3) {
override def toString: String = "debug"
}
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(4) {
override def toString: String = "trace"
}
case object Trace extends LogLevel("trace", 4)
/** Lists all available log levels.
*
@ -99,6 +90,7 @@ object LogLevel {
* 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)
@ -107,6 +99,20 @@ object LogLevel {
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 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
}
/** [[Decoder]] instance for [[LogLevel]].
*/
implicit val decoder: Decoder[LogLevel] = { json =>

View File

@ -9,31 +9,29 @@ import scala.concurrent.{ExecutionContext, Future}
/** Manages the logging service.
*/
object LoggingServiceManager {
private val testLoggingPropertyKey = "org.enso.loggingservice.test-log-level"
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 = {
sys.props.get(testLoggingPropertyKey) match {
case Some(value) =>
val logLevel =
value.toIntOption.flatMap(LogLevel.fromInteger).getOrElse {
System.err.println(
s"Invalid log level for $testLoggingPropertyKey, " +
s"falling back to info."
)
LogLevel.Info
}
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()
case None =>
productionMessageQueue()
}
}
@ -94,7 +92,7 @@ object LoggingServiceManager {
* times.
*/
def tearDown(): Unit = {
val service = currentService.synchronized {
val service = this.synchronized {
val service = currentService
currentService = None
service
@ -108,6 +106,11 @@ object LoggingServiceManager {
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
@ -121,7 +124,7 @@ object LoggingServiceManager {
): Unit = {
val fallback =
Local.setup(currentLevel, messageQueue, printers)
val previousService = currentService.synchronized {
val previousService = this.synchronized {
val previous = currentService
currentService = Some(fallback)
previous
@ -163,7 +166,7 @@ object LoggingServiceManager {
mode: LoggerMode[InitializationResult],
logLevel: LogLevel
): InitializationResult = {
currentService.synchronized {
this.synchronized {
if (currentService.isDefined) {
throw new IllegalStateException(
"The logging service has already been set up."

View File

@ -0,0 +1,227 @@
package org.enso.loggingservice
import java.nio.file.Path
import akka.http.scaladsl.model.Uri
import com.typesafe.scalalogging.Logger
import org.enso.loggingservice.printers.{
FileOutputPrinter,
Printer,
StderrPrinter,
StderrPrinterWithColors
}
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
import scala.concurrent.duration.DurationInt
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
*/
def setup(
logLevel: Option[LogLevel],
connectToExternalLogger: Option[Uri],
colorMode: ColorMode
): Unit = {
val actualLogLevel = logLevel.getOrElse(defaultLogLevel)
connectToExternalLogger match {
case Some(uri) =>
setupLoggingConnection(uri, actualLogLevel)
case None =>
setupLoggingServer(actualLogLevel, colorMode)
}
}
/** 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
): 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
)
Seq(
stderrPrinter(colorMode, printExceptionsInStderr),
filePrinter
)
} 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(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(fallbackException) =>
System.err.println(
s"Failed to initialize the fallback logger: " +
s"$fallbackException"
)
fallbackException.printStackTrace()
case Success(_) =>
}
case Success(serverBinding) =>
val uri = serverBinding.toUri()
loggingServiceEndpointPromise.success(Some(uri))
logger.trace(
s"Logging service has been set-up and is listening at `$uri`."
)
}
}
/** 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) {
loggingServiceEndpointPromise.success(Some(uri))
System.err.println(
s"Log messages are forwarded to `$uri`."
)
} else {
loggingServiceEndpointPromise.trySuccess(None)
}
}
}
/** Waits until the logging service has been set-up.
*
* Due to limitations of how the logging service is implemented, it can only
* be terminated after it has been set up.
*/
def waitForSetup(): Unit = {
Await.ready(loggingServiceEndpointPromise.future, 5.seconds)
}
/** Shuts down the logging service gracefully.
*/
def tearDown(): Unit = LoggingServiceManager.tearDown()
}

View File

@ -2,6 +2,9 @@ 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 {
@ -20,12 +23,18 @@ object TestLogger {
*/
def gatherLogs(action: => Unit): Seq[TestLogMessage] = {
LoggingServiceManager.dropPendingLogs()
LoggingServiceManager.tearDown()
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
LoggingServiceManager.setup(
val future = LoggingServiceManager.setup(
LoggerMode.Local(Seq(printer)),
LogLevel.Debug
)
Await.ready(future, 1.second)
action
LoggingServiceManager.tearDown()
printer.getLoggedMessages

View File

@ -0,0 +1,42 @@
package org.enso.loggingservice.internal
import java.util.Properties
import org.enso.loggingservice.LogLevel
import scala.util.Using
/** Reads logger settings from the resources.
*
* Currently these settings are used to configure logging inside of tests.
*/
object LoggingSettings {
private val propertiesFilename = "logging.properties"
private val testLoggingPropertyKey = "test-log-level"
private val loggingProperties: Properties = {
val props = new Properties
Option(this.getClass.getClassLoader.getResourceAsStream(propertiesFilename))
.foreach { stream =>
val _ = Using(stream) { stream =>
props.load(stream)
}
}
props
}
/** Indicates the log level to be used in test mode.
*
* If set to None, production logging should be used.
*/
val testLogLevel: Option[LogLevel] = Option(
loggingProperties.getProperty(testLoggingPropertyKey)
).map { string =>
LogLevel.fromString(string).getOrElse {
System.err.println(
s"Invalid log level for $testLoggingPropertyKey set in " +
s"$propertiesFilename, falling back to info."
)
LogLevel.Info
}
}
}

View File

@ -7,10 +7,12 @@ import org.enso.loggingservice.printers.StderrPrinter
*
* It has a smaller buffer and ignores messages from a certain log level.
*
* @param logLevel
* @param shouldOverride
* @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, shouldOverride: () => Boolean)
class TestMessageQueue(logLevel: LogLevel, isLoggingServiceSetUp: () => Boolean)
extends BlockingConsumerMessageQueue(bufferSize = 100) {
private def shouldKeepMessage(
@ -24,10 +26,10 @@ class TestMessageQueue(logLevel: LogLevel, shouldOverride: () => Boolean)
/** @inheritdoc */
override def send(message: Either[InternalLogMessage, WSLogMessage]): Unit =
if (shouldKeepMessage(message)) {
if (shouldOverride())
if (isLoggingServiceSetUp()) {
if (shouldKeepMessage(message))
overridePrinter.print(message.fold(_.toLogMessage, identity))
else
super.send(message)
} else {
super.send(message)
}
}

View File

@ -47,6 +47,10 @@ trait ServiceWithActorSystem extends Service {
ConfigValueFactory.fromAnyRef("akka.event.DefaultLoggingFilter")
)
.withValue("akka.loglevel", ConfigValueFactory.fromAnyRef("WARNING"))
.withValue(
"akka.coordinated-shutdown.run-by-actor-system-terminate",
ConfigValueFactory.fromAnyRef("off")
)
ActorSystem(
name,
config,

View File

@ -73,8 +73,7 @@ trait ThreadProcessingService extends Service {
}
}
/** @inheritdoc
*/
/** @inheritdoc */
abstract override def terminate(): Unit = {
super.terminate()
queueThread match {

View File

@ -12,31 +12,32 @@ import org.enso.loggingservice.internal.protocol.WSLogMessage
* 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, printExceptions: Boolean)
extends Printer {
class FileOutputPrinter(
logDirectory: Path,
suffix: String,
printExceptions: Boolean
) extends Printer {
private val renderer = new DefaultLogMessageRenderer(printExceptions)
private val writer = initializeWriter()
/** @inheritdoc
*/
/** @inheritdoc */
override def print(message: WSLogMessage): Unit = {
val lines = renderer.render(message)
writer.println(lines)
}
/** @inheritdoc
*/
/** @inheritdoc */
override def shutdown(): Unit = {
writer.flush()
writer.close()
}
/** Opens the log file for writing.
*/
/** Opens the log file for writing. */
private def initializeWriter(): PrintWriter = {
val logPath = logDirectory.resolve(makeLogFilename())
Files.createDirectories(logDirectory)
@ -49,24 +50,23 @@ class FileOutputPrinter(logDirectory: Path, printExceptions: Boolean)
)
}
/** Creates a log filename that is created based on the current timestamp.
*/
/** 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-enso.log"
s"$timestamp-$suffix.log"
}
}
object FileOutputPrinter {
/** Creates a new [[FileOutputPrinter]].
*/
/** Creates a new [[FileOutputPrinter]]. */
def create(
logDirectory: Path,
suffix: String,
printExceptions: Boolean = true
): FileOutputPrinter =
new FileOutputPrinter(logDirectory, printExceptions)
new FileOutputPrinter(logDirectory, suffix, printExceptions)
}

View File

@ -1 +1 @@
Args=--initialize-at-run-time=com.typesafe.config.impl.ConfigImpl$EnvVariablesHolder,com.typesafe.config.impl.ConfigImpl$SystemPropertiesHolder
Args=--initialize-at-run-time=com.typesafe.config.impl.ConfigImpl$EnvVariablesHolder,com.typesafe.config.impl.ConfigImpl$SystemPropertiesHolder,org.enso.loggingservice.LoggingServiceManager$

View File

@ -1,22 +1,31 @@
package org.enso.projectmanager.boot
import org.enso.loggingservice.printers.StderrPrinterWithColors
import org.enso.loggingservice.{LogLevel, LoggerMode, LoggingServiceManager}
import java.nio.file.Path
import scala.concurrent.{ExecutionContext, Future}
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 scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
/** A helper for setting up the logging service in the Project Manager. */
object Logging {
object Logging extends LoggingServiceSetupHelper {
/** Sets up the logging service for local logging. */
def setup(
logLevel: LogLevel,
executionContext: ExecutionContext
): Future[Unit] = {
// TODO [RW] setting up the logging server will be added in #1151
val printer = StderrPrinterWithColors.colorPrinterIfAvailable(false)
LoggingServiceManager.setup(LoggerMode.Local(Seq(printer)), logLevel)(
executionContext
)
/** @inheritdoc */
override val defaultLogLevel: LogLevel = LogLevel.Info
/** @inheritdoc */
override lazy val logPath: Path =
DefaultDistributionConfiguration.distributionManager.paths.logs
/** @inheritdoc */
override val logFileSuffix: String = "enso-project-manager"
object GlobalLoggingService extends LoggingServiceDescriptor {
/** @inheritdoc */
override def getEndpoint: Future[Option[Uri]] = loggingServiceEndpoint()
}
}

View File

@ -24,13 +24,7 @@ import org.enso.projectmanager.protocol.{
}
import org.enso.projectmanager.service.config.GlobalConfigService
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementService
import org.enso.projectmanager.service.{
MonadicProjectValidator,
ProjectCreationService,
ProjectService,
ProjectServiceFailure,
ValidationFailure
}
import org.enso.projectmanager.service._
import org.enso.projectmanager.versionmanagement.DefaultDistributionConfiguration
import scala.concurrent.ExecutionContext
@ -71,6 +65,9 @@ class MainModule[
gen
)
val distributionConfiguration = DefaultDistributionConfiguration
val loggingService = Logging.GlobalLoggingService
lazy val languageServerRegistry =
system.actorOf(
LanguageServerRegistry
@ -79,7 +76,8 @@ class MainModule[
config.bootloader,
config.supervision,
config.timeout,
DefaultDistributionConfiguration,
distributionConfiguration,
loggingService,
ExecutorWithUnlimitedPool
),
"language-server-registry"
@ -99,10 +97,13 @@ class MainModule[
)
lazy val projectCreationService =
new ProjectCreationService[F](DefaultDistributionConfiguration)
new ProjectCreationService[F](
distributionConfiguration,
loggingService
)
lazy val globalConfigService =
new GlobalConfigService[F](DefaultDistributionConfiguration)
new GlobalConfigService[F](distributionConfiguration)
lazy val projectService =
new ProjectService[F](
@ -114,11 +115,11 @@ class MainModule[
clock,
gen,
languageServerGateway,
DefaultDistributionConfiguration
distributionConfiguration
)
lazy val runtimeVersionManagementService =
new RuntimeVersionManagementService[F](DefaultDistributionConfiguration)
new RuntimeVersionManagementService[F](distributionConfiguration)
lazy val clientControllerFactory =
new ManagerClientControllerFactory[F](
@ -126,6 +127,7 @@ class MainModule[
projectService = projectService,
globalConfigService = globalConfigService,
runtimeVersionManagementService = runtimeVersionManagementService,
loggingServiceDescriptor = loggingService,
timeoutConfig = config.timeout
)

View File

@ -6,7 +6,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor
import akka.http.scaladsl.Http
import com.typesafe.scalalogging.LazyLogging
import org.apache.commons.cli.CommandLine
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.{ColorMode, LogLevel}
import org.enso.projectmanager.boot.Globals.{
ConfigFilename,
ConfigNamespace,
@ -132,8 +132,15 @@ object ProjectManager extends App with LazyLogging {
case 1 => LogLevel.Debug
case _ => LogLevel.Trace
}
// TODO [RW] at some point we may want to allow customization of color
// output in CLI flags
val colorMode = ColorMode.Auto
ZIO
.fromFuture(executionContext => Logging.setup(level, executionContext))
.effect {
Logging.setup(Some(level), None, colorMode)
}
.catchAll { exception =>
putStrLnErr(s"Failed to setup the logger: $exception")
}
@ -144,7 +151,8 @@ object ProjectManager extends App with LazyLogging {
): ZIO[Console, Nothing, ExitCode] = {
val versionDescription = VersionDescription.make(
"Enso Project Manager",
includeRuntimeJVMInfo = true
includeRuntimeJVMInfo = false,
enableNativeImageOSWorkaround = true
)
putStrLn(versionDescription.asString(useJson)) *>
ZIO.succeed(SuccessExitCode)
@ -153,7 +161,8 @@ object ProjectManager extends App with LazyLogging {
private def logServerStartup(): UIO[Unit] =
effectTotal {
logger.info(
s"Started server at ${config.server.host}:${config.server.port}, press enter to kill server"
s"Started server at ${config.server.host}:${config.server.port}, " +
s"press enter to kill server"
)
}

View File

@ -7,11 +7,10 @@ import java.util.concurrent.Executors
import akka.actor.ActorRef
import com.typesafe.scalalogging.Logger
import org.apache.commons.lang3.concurrent.BasicThreadFactory
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.LoggingServiceManager
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
import org.enso.runtimeversionmanager.runner.{LanguageServerOptions, Runner}
import scala.concurrent.Future
import scala.util.Using
object ExecutorWithUnlimitedPool extends LanguageServerExecutor {
@ -72,9 +71,8 @@ object ExecutorWithUnlimitedPool extends LanguageServerExecutor {
val versionManager = RuntimeVersionManagerFactory(distributionConfiguration)
.makeRuntimeVersionManager(progressTracker)
// TODO [RW] logging #1151
val loggerConnection = Future.successful(None)
val logLevel = LogLevel.Info
val inheritedLogLevel =
LoggingServiceManager.currentLogLevelForThisApplication()
val options = LanguageServerOptions(
rootId = descriptor.rootId,
interface = descriptor.networkConfig.interface,
@ -85,14 +83,14 @@ object ExecutorWithUnlimitedPool extends LanguageServerExecutor {
val runner = new Runner(
versionManager,
distributionConfiguration.environment,
loggerConnection
descriptor.deferredLoggingServiceEndpoint
)
val runSettings = runner
.startLanguageServer(
options = options,
projectPath = descriptor.rootPath,
version = descriptor.engineVersion,
logLevel = logLevel,
logLevel = inheritedLogLevel,
additionalArguments = Seq()
)
.get

View File

@ -32,6 +32,7 @@ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerContr
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
import org.enso.projectmanager.model.Project
import org.enso.projectmanager.service.LoggingServiceDescriptor
import org.enso.projectmanager.util.UnhandledLogging
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
@ -47,6 +48,7 @@ import org.enso.projectmanager.versionmanagement.DistributionConfiguration
* @param supervisionConfig a supervision config
* @param timeoutConfig a timeout config
* @param distributionConfiguration configuration of the distribution
* @param loggingServiceDescriptor a logging service configuration descriptor
* @param executor an executor service used to start the language server
* process
*/
@ -59,6 +61,7 @@ class LanguageServerController(
supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
loggingServiceDescriptor: LoggingServiceDescriptor,
executor: LanguageServerExecutor
) extends Actor
with ActorLogging
@ -69,14 +72,15 @@ class LanguageServerController(
private val descriptor =
LanguageServerDescriptor(
name = s"language-server-${project.id}",
rootId = UUID.randomUUID(),
rootPath = project.path.get,
networkConfig = networkConfig,
distributionConfiguration = distributionConfiguration,
engineVersion = engineVersion,
jvmSettings = distributionConfiguration.defaultJVMSettings,
discardOutput = distributionConfiguration.shouldDiscardChildOutput
name = s"language-server-${project.id}",
rootId = UUID.randomUUID(),
rootPath = project.path.get,
networkConfig = networkConfig,
distributionConfiguration = distributionConfiguration,
engineVersion = engineVersion,
jvmSettings = distributionConfiguration.defaultJVMSettings,
discardOutput = distributionConfiguration.shouldDiscardChildOutput,
deferredLoggingServiceEndpoint = loggingServiceDescriptor.getEndpoint
)
override def supervisorStrategy: SupervisorStrategy =
@ -313,6 +317,7 @@ object LanguageServerController {
* @param distributionConfiguration configuration of the distribution
* @param executor an executor service used to start the language server
* process
* @param loggingServiceDescriptor a logging service configuration descriptor
* @return a configuration object
*/
def props(
@ -324,6 +329,7 @@ object LanguageServerController {
supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
loggingServiceDescriptor: LoggingServiceDescriptor,
executor: LanguageServerExecutor
): Props =
Props(
@ -336,6 +342,7 @@ object LanguageServerController {
supervisionConfig,
timeoutConfig,
distributionConfiguration,
loggingServiceDescriptor,
executor
)
)

View File

@ -2,11 +2,14 @@ package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
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 scala.concurrent.Future
/** A descriptor specifying options related to starting a Language Server.
*
* @param name a name of the LS
@ -20,6 +23,11 @@ import org.enso.runtimeversionmanager.runner.JVMSettings
* @param jvmSettings settings to use for the JVM that will host the engine
* @param discardOutput specifies if the process output should be discarded or
* printed to parent's streams
* @param deferredLoggingServiceEndpoint a future that is completed once the
* 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
*/
case class LanguageServerDescriptor(
name: String,
@ -29,5 +37,6 @@ case class LanguageServerDescriptor(
distributionConfiguration: DistributionConfiguration,
engineVersion: SemVer,
jvmSettings: JVMSettings,
discardOutput: Boolean
discardOutput: Boolean,
deferredLoggingServiceEndpoint: Future[Option[Uri]]
)

View File

@ -19,6 +19,7 @@ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProto
StopServer
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
import org.enso.projectmanager.service.LoggingServiceDescriptor
import org.enso.projectmanager.util.UnhandledLogging
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
@ -31,6 +32,7 @@ import org.enso.projectmanager.versionmanagement.DistributionConfiguration
* @param supervisionConfig a supervision config
* @param timeoutConfig a timeout config
* @param distributionConfiguration configuration of the distribution
* @param loggingServiceDescriptor a logging service configuration descriptor
* @param executor an executor service used to start the language server
* process
*/
@ -40,6 +42,7 @@ class LanguageServerRegistry(
supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
loggingServiceDescriptor: LoggingServiceDescriptor,
executor: LanguageServerExecutor
) extends Actor
with ActorLogging
@ -65,6 +68,7 @@ class LanguageServerRegistry(
supervisionConfig,
timeoutConfig,
distributionConfiguration,
loggingServiceDescriptor,
executor
),
s"language-server-controller-${project.id}"
@ -129,6 +133,7 @@ object LanguageServerRegistry {
* @param distributionConfiguration configuration of the distribution
* @param executor an executor service used to start the language server
* process
* @param loggingServiceDescriptor a logging service configuration descriptor
* @return a configuration object
*/
def props(
@ -137,6 +142,7 @@ object LanguageServerRegistry {
supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
loggingServiceDescriptor: LoggingServiceDescriptor,
executor: LanguageServerExecutor
): Props =
Props(
@ -146,6 +152,7 @@ object LanguageServerRegistry {
supervisionConfig,
timeoutConfig,
distributionConfiguration,
loggingServiceDescriptor,
executor
)
)

View File

@ -13,9 +13,12 @@ import org.enso.projectmanager.event.ClientEvent.{
}
import org.enso.projectmanager.protocol.ProjectManagementApi._
import org.enso.projectmanager.requesthandler._
import org.enso.projectmanager.service.ProjectServiceApi
import org.enso.projectmanager.service.config.GlobalConfigServiceApi
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementServiceApi
import org.enso.projectmanager.service.{
LoggingServiceDescriptor,
ProjectServiceApi
}
import org.enso.projectmanager.util.UnhandledLogging
import scala.annotation.unused
@ -28,6 +31,7 @@ import scala.concurrent.duration._
* @param projectService a project service
* @param globalConfigService global configuration service
* @param runtimeVersionManagementService version management service
* @param loggingServiceDescriptor a logging service configuration descriptor
* @param timeoutConfig a request timeout config
*/
class ClientController[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
@ -35,6 +39,7 @@ class ClientController[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
projectService: ProjectServiceApi[F],
globalConfigService: GlobalConfigServiceApi[F],
runtimeVersionManagementService: RuntimeVersionManagementServiceApi[F],
loggingServiceDescriptor: LoggingServiceDescriptor,
timeoutConfig: TimeoutConfig
) extends Actor
with ActorLogging
@ -87,7 +92,10 @@ class ClientController[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
.props(globalConfigService, timeoutConfig.requestTimeout),
ConfigDelete -> ConfigDeleteHandler
.props(globalConfigService, timeoutConfig.requestTimeout),
LoggingServiceGetEndpoint -> NotImplementedHandler.props
LoggingServiceGetEndpoint -> LoggingServiceEndpointRequestHandler.props(
loggingServiceDescriptor,
timeoutConfig.requestTimeout
)
)
override def receive: Receive = {
@ -124,6 +132,7 @@ object ClientController {
* @param globalConfigService global configuration service
* @param runtimeVersionManagementService version management service
* @param timeoutConfig a request timeout config
* @param loggingServiceDescriptor a logging service configuration descriptor
* @return a configuration object
*/
def props[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
@ -131,6 +140,7 @@ object ClientController {
projectService: ProjectServiceApi[F],
globalConfigService: GlobalConfigServiceApi[F],
runtimeVersionManagementService: RuntimeVersionManagementServiceApi[F],
loggingServiceDescriptor: LoggingServiceDescriptor,
timeoutConfig: TimeoutConfig
): Props =
Props(
@ -139,6 +149,7 @@ object ClientController {
projectService = projectService,
globalConfigService = globalConfigService,
runtimeVersionManagementService = runtimeVersionManagementService,
loggingServiceDescriptor = loggingServiceDescriptor,
timeoutConfig = timeoutConfig
)
)

View File

@ -7,9 +7,12 @@ import org.enso.jsonrpc.ClientControllerFactory
import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.effect.{ErrorChannel, Exec}
import org.enso.projectmanager.service.ProjectServiceApi
import org.enso.projectmanager.service.config.GlobalConfigServiceApi
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementServiceApi
import org.enso.projectmanager.service.{
LoggingServiceDescriptor,
ProjectServiceApi
}
/** Project manager client controller factory.
*
@ -17,6 +20,7 @@ import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagemen
* @param projectService a project service
* @param globalConfigService global configuration service
* @param runtimeVersionManagementService version management service
* @param loggingServiceDescriptor a logging service configuration descriptor
* @param timeoutConfig a request timeout config
*/
class ManagerClientControllerFactory[
@ -26,6 +30,7 @@ class ManagerClientControllerFactory[
projectService: ProjectServiceApi[F],
globalConfigService: GlobalConfigServiceApi[F],
runtimeVersionManagementService: RuntimeVersionManagementServiceApi[F],
loggingServiceDescriptor: LoggingServiceDescriptor,
timeoutConfig: TimeoutConfig
) extends ClientControllerFactory {
@ -42,6 +47,7 @@ class ManagerClientControllerFactory[
projectService,
globalConfigService,
runtimeVersionManagementService,
loggingServiceDescriptor,
timeoutConfig
),
s"jsonrpc-connection-controller-$clientId"

View File

@ -0,0 +1,111 @@
package org.enso.projectmanager.requesthandler
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Status}
import akka.http.scaladsl.model.Uri
import akka.pattern.pipe
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult}
import org.enso.projectmanager.protocol.ProjectManagementApi.{
LoggingServiceGetEndpoint,
LoggingServiceUnavailable
}
import org.enso.projectmanager.service.LoggingServiceDescriptor
import org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration
/** A request handler for `logging-service/get-endpoint` commands.
*
* @param loggingServiceDescriptor a logging service configuration descriptor
* @param requestTimeout timeout for the request
*/
class LoggingServiceEndpointRequestHandler(
loggingServiceDescriptor: LoggingServiceDescriptor,
requestTimeout: FiniteDuration
) extends Actor
with ActorLogging
with UnhandledLogging {
private def method = LoggingServiceGetEndpoint
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = {
case Request(LoggingServiceGetEndpoint, id, _) =>
loggingServiceDescriptor.getEndpoint
.map(LoggingServiceInitialized)
.pipeTo(self)
val timeoutCancellable = context.system.scheduler.scheduleOnce(
requestTimeout,
self,
RequestTimeout
)
context.become(responseStage(id, sender(), timeoutCancellable))
}
private def responseStage(
id: Id,
replyTo: ActorRef,
timeoutCancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
log.error(ex, s"Failure during $method operation:")
replyTo ! ResponseError(
Some(id),
LoggingServiceUnavailable(s"Logging service failed to set up: $ex")
)
timeoutCancellable.cancel()
context.stop(self)
case RequestTimeout =>
log.error(s"Request $method with $id timed out")
replyTo ! ResponseError(
Some(id),
LoggingServiceUnavailable(
"Logging service has not been set up within the timeout."
)
)
context.stop(self)
case LoggingServiceInitialized(maybeUri) =>
maybeUri match {
case Some(uri) =>
replyTo ! ResponseResult(
LoggingServiceGetEndpoint,
id,
LoggingServiceGetEndpoint.Result(uri.toString)
)
case None =>
replyTo ! ResponseError(
Some(id),
LoggingServiceUnavailable("Logging service is not available.")
)
}
timeoutCancellable.cancel()
context.stop(self)
}
private case class LoggingServiceInitialized(endpoint: Option[Uri])
}
object LoggingServiceEndpointRequestHandler {
/** Creates a configuration object used to create a
* [[LoggingServiceEndpointRequestHandler]].
*
* @param loggingServiceDescriptor a logging service configuration descriptor
* @param requestTimeout timeout for the request
* @return a configuration object
*/
def props(
loggingServiceDescriptor: LoggingServiceDescriptor,
requestTimeout: FiniteDuration
): Props = Props(
new LoggingServiceEndpointRequestHandler(
loggingServiceDescriptor,
requestTimeout
)
)
}

View File

@ -2,6 +2,7 @@ package org.enso.projectmanager.requesthandler
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Stash, Status}
import akka.pattern.pipe
import com.typesafe.scalalogging.Logger
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc.{
HasParams,
@ -100,7 +101,7 @@ abstract class RequestHandler[
context.stop(self)
case Left(failure: FailureType) =>
log.error(s"Request $id failed due to $failure")
log.error(s"Request $method with $id failed due to $failure")
val error = implicitly[FailureMapper[FailureType]].mapFailure(failure)
replyTo ! ResponseError(Some(id), error)
timeoutCancellable.foreach(_.cancel())
@ -130,7 +131,13 @@ abstract class RequestHandler[
replyTo: ActorRef,
timeoutCancellable: Option[Cancellable]
): Unit = {
timeoutCancellable.foreach(_.cancel())
timeoutCancellable.foreach { cancellable =>
cancellable.cancel()
Logger[this.type].trace(
s"The operation $method ($id) reported starting a long-running task, " +
s"its request-timeout has been cancelled."
)
}
context.become(responseStage(id, replyTo, None))
}
}

View File

@ -0,0 +1,16 @@
package org.enso.projectmanager.service
import akka.http.scaladsl.model.Uri
import scala.concurrent.Future
/** A service descriptor that provides information on the logging service setup.
*/
trait LoggingServiceDescriptor {
/** Returns a future that will yield the logging service endpoint once it is
* initialized or None if the logging service does not expect incoming
* connections.
*/
def getEndpoint: Future[Option[Uri]]
}

View File

@ -14,15 +14,14 @@ import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFa
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.runner.Runner
import scala.concurrent.Future
/** A service for creating new project structures using the runner of the
* specific engine version selected for the project.
*/
class ProjectCreationService[
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap
](
distributionConfiguration: DistributionConfiguration
distributionConfiguration: DistributionConfiguration,
loggingServiceDescriptor: LoggingServiceDescriptor
) extends ProjectCreationServiceApi[F] {
/** @inheritdoc */
@ -41,7 +40,7 @@ class ProjectCreationService[
new Runner(
versionManager,
distributionConfiguration.environment,
Future.successful(None)
loggingServiceDescriptor.getEndpoint
)
val settings =

View File

@ -55,8 +55,6 @@ trait DistributionConfiguration {
* or piped to parent's streams.
*
* This option is used to easily turn off logging in tests.
*
* TODO [RW] It will likely become obsolete once #1151 (or #1144) is done.
*/
def shouldDiscardChildOutput: Boolean
}

View File

@ -0,0 +1 @@
test-log-level=warning

View File

@ -40,7 +40,7 @@ import org.enso.projectmanager.service.{
}
import org.enso.projectmanager.test.{ObservableGenerator, ProgrammableClock}
import org.enso.runtimeversionmanager.OS
import org.enso.runtimeversionmanager.test.{DropLogs, FakeReleases}
import org.enso.runtimeversionmanager.test.FakeReleases
import org.scalatest.BeforeAndAfterAll
import pureconfig.ConfigSource
import pureconfig.generic.auto._
@ -50,10 +50,7 @@ import zio.{Runtime, Semaphore, ZEnv, ZIO}
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
class BaseServerSpec
extends JsonRpcServerTestKit
with DropLogs
with BeforeAndAfterAll {
class BaseServerSpec extends JsonRpcServerTestKit with BeforeAndAfterAll {
override def protocol: Protocol = JsonRpc.protocol
@ -121,6 +118,8 @@ class BaseServerSpec
discardChildOutput = !debugChildLogs
)
val loggingService = new TestLoggingService
lazy val languageServerRegistry =
system.actorOf(
LanguageServerRegistry
@ -130,6 +129,7 @@ class BaseServerSpec
supervisionConfig,
timeoutConfig,
distributionConfiguration,
loggingService,
ExecutorWithUnlimitedPool
)
)
@ -146,7 +146,10 @@ class BaseServerSpec
)
lazy val projectCreationService =
new ProjectCreationService[ZIO[ZEnv, +*, +*]](distributionConfiguration)
new ProjectCreationService[ZIO[ZEnv, +*, +*]](
distributionConfiguration,
loggingService
)
lazy val globalConfigService = new GlobalConfigService[ZIO[ZEnv, +*, +*]](
distributionConfiguration
@ -176,6 +179,7 @@ class BaseServerSpec
projectService = projectService,
globalConfigService = globalConfigService,
runtimeVersionManagementService = runtimeVersionManagementService,
loggingServiceDescriptor = loggingService,
timeoutConfig = timeoutConfig
)
}

View File

@ -59,7 +59,7 @@ trait ProjectManagementOps { this: BaseServerSpec =>
}
}
""")
val Right(openReply) = parse(client.expectMessage(10.seconds.dilated))
val Right(openReply) = parse(client.expectMessage(20.seconds.dilated))
val socket = for {
result <- openReply.hcursor.downExpectedField("result")
addr <- result.downExpectedField("languageServerJsonAddress")

View File

@ -0,0 +1,24 @@
package org.enso.projectmanager
import akka.http.scaladsl.model.Uri
import org.enso.projectmanager.service.LoggingServiceDescriptor
import scala.concurrent.Future
class TestLoggingService extends LoggingServiceDescriptor {
private var currentFuture: Future[Option[Uri]] = Future.successful(None)
override def getEndpoint: Future[Option[Uri]] = currentFuture
def withOverriddenEndpoint[R](
future: Future[Option[Uri]]
)(action: => R): Unit = {
val oldValue = currentFuture
currentFuture = future
try {
action
} finally {
currentFuture = oldValue
}
}
}

View File

@ -0,0 +1,76 @@
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
class LoggingServiceEndpointSpec extends BaseServerSpec with FlakySpec {
class TestException extends RuntimeException {
override def toString: String = "test-exception"
}
"logging-service/get-endpoint" should {
"fail if endpoint is not setup" in {
implicit val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "logging-service/get-endpoint",
"id": 0
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":0,
"error": { "code": 4013, "message": "Logging service is not available." }
}
""")
}
"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/")))
) {
client.send(json"""
{ "jsonrpc": "2.0",
"method": "logging-service/get-endpoint",
"id": 0
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":0,
"result" : {
"uri": "ws://test-uri/"
}
}
""")
}
}
"report logging service setup failures" in {
implicit val client = new WsTestClient(address)
loggingService.withOverriddenEndpoint(Future.failed(new TestException)) {
client.send(json"""
{ "jsonrpc": "2.0",
"method": "logging-service/get-endpoint",
"id": 0
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":0,
"error": { "code": 4013, "message": "Logging service failed to set up: test-exception" }
}
""")
}
}
}
}

View File

@ -1,17 +0,0 @@
package org.enso.runtimeversionmanager.test
import org.enso.loggingservice.TestLogger
import org.scalatest.{BeforeAndAfterAll, Suite}
/** Ensures that any pending logs that were not processed when executing tests
* are ignored instead of being dumped to stderr.
*
* This does not affect tests that set up the loggings service themselves or
* capture the logs. It only affects logs that were not handled at all.
*/
trait DropLogs extends BeforeAndAfterAll { self: Suite =>
override def afterAll(): Unit = {
super.afterAll()
TestLogger.dropLogs()
}
}

View File

@ -24,8 +24,7 @@ class RuntimeVersionManagerTest
with Matchers
with OptionValues
with WithTemporaryDirectory
with FakeEnvironment
with DropLogs {
with FakeEnvironment {
/** Creates the [[DistributionManager]], [[RuntimeVersionManager]] and an
* [[Environment]] for use in the tests.

View File

@ -0,0 +1 @@
test-log-level=error

View File

@ -4,7 +4,6 @@ import io.circe.Json
import nl.gn0s1s.bump.SemVer
import org.enso.runtimeversionmanager.distribution.DistributionManager
import org.enso.runtimeversionmanager.test.{
DropLogs,
FakeEnvironment,
WithTemporaryDirectory
}
@ -17,8 +16,7 @@ class GlobalConfigurationManagerSpec
with Matchers
with WithTemporaryDirectory
with FakeEnvironment
with OptionValues
with DropLogs {
with OptionValues {
def makeConfigManager(): GlobalConfigurationManager = {
val env = fakeInstalledEnvironment()
val distributionManager = new DistributionManager(env)

View File

@ -9,7 +9,6 @@ import org.enso.runtimeversionmanager.distribution.{
PortableDistributionManager
}
import org.enso.runtimeversionmanager.test.{
DropLogs,
FakeEnvironment,
WithTemporaryDirectory
}
@ -20,8 +19,7 @@ class DistributionManagerSpec
extends AnyWordSpec
with Matchers
with WithTemporaryDirectory
with FakeEnvironment
with DropLogs {
with FakeEnvironment {
"DistributionManager" should {
"detect portable distribution" in {

View File

@ -22,7 +22,7 @@ import org.enso.runtimeversionmanager.releases.engine.{
import org.enso.runtimeversionmanager.releases.graalvm.GraalCEReleaseProvider
import org.enso.runtimeversionmanager.releases.testing.FakeReleaseProvider
import org.enso.runtimeversionmanager.test._
import org.enso.testkit.RetrySpec
import org.enso.testkit.{FlakySpec, RetrySpec}
import org.scalatest.BeforeAndAfterEach
import org.scalatest.concurrent.TimeLimitedTests
import org.scalatest.matchers.should.Matchers
@ -38,9 +38,9 @@ class ConcurrencyTest
with WithTemporaryDirectory
with FakeEnvironment
with BeforeAndAfterEach
with DropLogs
with TimeLimitedTests
with RetrySpec {
with RetrySpec
with FlakySpec {
/** This is an upper bound to avoid stalling the tests forever, but particular
* operations have smaller timeouts usually.
@ -170,7 +170,8 @@ class ConcurrencyTest
)._2
"locks" should {
"synchronize parallel installations with the same runtime" taggedAs Retry in {
"synchronize parallel installations " +
"with the same runtime".taggedAs(Flaky, Retry) in {
/** Two threads start installing different engines in parallel, but these
* engines use the same runtime. The second thread is stalled on
@ -246,7 +247,7 @@ class ConcurrencyTest
)
}
"synchronize installation and usage" taggedAs Retry in {
"synchronize installation and usage".taggedAs(Flaky, Retry) in {
/** The first thread starts installing the engine, but is suspended when
* downloading the package. The second thread then tries to use it, but
@ -304,7 +305,7 @@ class ConcurrencyTest
)
}
"synchronize uninstallation and usage" taggedAs Retry in {
"synchronize uninstallation and usage".taggedAs(Flaky, Retry) in {
/** The first thread starts using the engine, while in the meantime
* another thread starts uninstalling it. The second thread has to wait

View File

@ -271,6 +271,7 @@ class RuntimeVersionManager(
def listInstalledGraalRuntimes(): Seq[GraalRuntime] =
FileSystem
.listDirectory(distributionManager.paths.runtimes)
.filter(isNotIgnoredDirectory)
.map(path => (path, loadGraalRuntime(path)))
.flatMap(handleErrorsAsWarnings[GraalRuntime]("A runtime"))
@ -278,10 +279,18 @@ class RuntimeVersionManager(
def listInstalledEngines(): Seq[Engine] = {
FileSystem
.listDirectory(distributionManager.paths.engines)
.filter(isNotIgnoredDirectory)
.map(path => (path, loadEngine(path)))
.flatMap(handleErrorsAsWarnings[Engine]("An engine"))
}
private def isNotIgnoredDirectory(path: Path): Boolean = {
val ignoreList = Seq(".DS_Store")
val fileName = path.getFileName.toString
val isIgnored = ignoreList.contains(fileName)
!isIgnored
}
/** A helper function that is used when listing components.
*
* A component error is non-fatal in context of listing, so it is issued as a

View File

@ -157,7 +157,8 @@ class DistributionManager(val env: Environment) {
TMP_DIRECTORY,
LOG_DIRECTORY,
LOCK_DIRECTORY,
"components-licences"
"THIRD-PARTY",
".DS_Store"
)
/** Config directory for an installed distribution.

View File

@ -99,14 +99,12 @@ class Runner(
"--data-port",
options.dataPort.toString,
"--log-level",
logLevel.toString
logLevel.name
)
RunSettings(
version,
arguments ++ additionalArguments,
// TODO [RW] set to true when language server gets logging support
// (#1144)
connectLoggerIfAvailable = false
connectLoggerIfAvailable = true
)
}