Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MdcLoggingDirectives (withMdcLogging, withMdcEntry, withMdcEntries, extractMarkerLog) #3974

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ target
.*.swp
*.vim
.ensime*
.bsp
jrudolph marked this conversation as resolved.
Show resolved Hide resolved
.tool-versions
alexklibisz marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package akka.http.scaladsl.server.directives

import akka.event.{ DiagnosticMarkerBusLoggingAdapter, MarkerLoggingAdapter }
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.RoutingSpec
import scala.jdk.CollectionConverters._

class MdcLoggingDirectivesSpec extends RoutingSpec {
alexklibisz marked this conversation as resolved.
Show resolved Hide resolved

"The `withMdcLogging` directive" should {
"provide a DiagnosticMarkerBusLoggingAdapter" in {
Get() ~> withMdcLogging {
extractLog {
case _: DiagnosticMarkerBusLoggingAdapter => completeOk
case other => failTest(s"expected a DiagnosticMarkerBusLoggingAdapter but found $other")
}
} ~> check {
response shouldEqual Ok
}
}
"provide a new DiagnosticMarkerBusLoggingAdapter for each request" in {
val route = withMdcLogging {
extractLog {
case l: DiagnosticMarkerBusLoggingAdapter => complete(l.hashCode().toString)
case other => failTest(s"expected a DiagnosticMarkerBusLoggingAdapter but found $other")
}
}
val reps = 100
val responseEntities = (1 to reps).map(_ => Get() ~> route ~> check {
status shouldEqual StatusCodes.OK
entityAs[String]
})
responseEntities.distinct.length shouldBe reps
}
"provide the same DiagnosticMarkerBusLoggingAdapter when nested multiple times" in {
Get() ~> withMdcLogging {
extractLog { log1 =>
withMdcLogging {
extractLog { log2 =>
if (log1 == log2) completeOk
else failTest(s"$log1 != $log2")
}
}
}
} ~> check {
response shouldEqual Ok
}
}
}

"The `extractMarkerLog` directive" should {
"provide a MarkerLoggingAdapter" in {
Get() ~> extractMarkerLog {
case _: MarkerLoggingAdapter => completeOk
case other => failTest(s"expected a MarkerLoggingAdapter but found $other")
}
}
"provide a new MarkerLoggingAdapter for each request" in {
val route = extractMarkerLog {
case log: MarkerLoggingAdapter => complete(log.hashCode().toString)
case other => failTest(s"expected a MarkerLoggingAdapter but found $other")
}
val reps = 100
val responseEntities = (1 to reps).map(_ => Get() ~> route ~> check {
status shouldEqual StatusCodes.OK
entityAs[String]
})
responseEntities.distinct.length shouldBe reps
}
"provide the same MarkerLoggingAdapter when nested multiple times" in {
Get() ~> extractMarkerLog { log1 =>
withMdcLogging {
extractMarkerLog { log2 =>
if (log1 == log2) completeOk
else failTest(s"$log1 != $log2")
}
}
} ~> check {
response shouldEqual Ok
}
}
}

"The `withMdcEntries` directive" should {
"append entries to the DiagnosticMarkerBusLoggingAdapter's MDC map" in {
Get() ~> withMdcEntries(("foo", "foo entry"), ("bar", "bar entry")) {
extractMarkerLog {
case log: DiagnosticMarkerBusLoggingAdapter =>
val map = log.getMDC.asScala
if (!map.get("foo").contains("foo entry")) failTest(s"missing or incorrect key 'foo' in $map")
else if (!map.get("bar").contains("bar entry")) failTest(s"missing or incorrect key 'bar' in $map")
else completeOk
case other => failTest(s"expected a DiagnosticMarkerBusLoggingAdapter but found $other")
}
} ~> check {
response shouldEqual Ok
}
}
"replace entries with same key when nested" in {
Get() ~> withMdcEntries(("foo", "foo entry 1")) {
extractMarkerLog {
case log: DiagnosticMarkerBusLoggingAdapter =>
val map = log.getMDC.asScala
if (!map.get("foo").contains("foo entry 1")) failTest(s"'foo' should be 'foo entry 1'")
else withMdcEntries(("foo", "foo entry 2")) {
val map = log.getMDC.asScala
if (!map.get("foo").contains("foo entry 2")) failTest(s"'foo' should be 'foo entry 2'")
else completeOk
}
case other => failTest(s"expected a DiagnosticMarkerBusLoggingAdapter but found $other")
}
} ~> check {
response shouldEqual Ok
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ trait Directives extends RouteConcatenation
with WebSocketDirectives
with FramedEntityStreamingDirectives
with AttributeDirectives
with MdcLoggingDirectives

/**
* Collects all default directives into one object for simple importing.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package akka.http.scaladsl.server.directives

import akka.event.{
DefaultLoggingFilter,
DiagnosticMarkerBusLoggingAdapter,
LogSource,
MarkerLoggingAdapter
}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._

/**
* @groupname mdc Mdc logging directives
* @groupprio mdc 240
*/
trait MdcLoggingDirectives {

def withMdcLogging: Directive0 =
alexklibisz marked this conversation as resolved.
Show resolved Hide resolved
extractActorSystem
.flatMap { sys =>
mapRequestContext { ctx =>
ctx.log match {
// If it's already a DiagnosticMarkerBusLoggingAdapter, this is a no-op.
case _: DiagnosticMarkerBusLoggingAdapter => ctx
// Otherwise, we need to give the context a DiagnosticLoggingAdapter.
case _ =>
val (str, cls) = LogSource.fromAnyRef(sys, sys)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LL 27 to 35 were purely type tetris on my part. They work, but I'm not 100% sure I setup the logger the right way. Very open to input here.

val loggingFilter =
new DefaultLoggingFilter(sys.settings, sys.eventStream)
val dmbla = new DiagnosticMarkerBusLoggingAdapter(
sys.eventStream,
str,
cls,
loggingFilter)
ctx.withLog(dmbla)
}
}
}

def extractMarkerLog: Directive1[MarkerLoggingAdapter] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need that, aren't all logger also MarkerLoggingAdapters anyway even without withMdcLogging enabled?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't all logger also MarkerLoggingAdapters anyway even without withMdcLogging enabled?

I don't think so. I'm assuming you're implying we could just use extractLog instead of having this.

extractLog returns a LoggingAdapter:

MarkerLoggingAdapter has a few extra methods, e.g., https://github.com/akka/akka/blob/main/akka-actor/src/main/scala/akka/event/Logging.scala#L1848-L1849

Maybe it's true that the LoggingAdapter that gets returned from extractLog happens to always also extend MarkerLoggingAdapter, but that's not specified in the return type, so we can't access those methods.

MdcLoggingDirectives.extractDiagnosticMarkerLog
.map(identity[MarkerLoggingAdapter])

def withMdcEntries(entries: (String, Any)*): Directive0 =
MdcLoggingDirectives.extractDiagnosticMarkerLog
.map(log => log.mdc(log.mdc ++ entries.toMap))

def withMdcEntry(key: String, value: Any): Directive0 =
withMdcEntries((key, value))
}

object MdcLoggingDirectives extends MdcLoggingDirectives {
// This is private because the only advantage of a DiagnosticLoggingAdapter is that you can mutate the MDC entries,
// which is better done through the withMdcEntry and withMdcEntries directive.
private val extractDiagnosticMarkerLog: Directive1[DiagnosticMarkerBusLoggingAdapter] =
withMdcLogging.tflatMap { _ =>
extractLog.flatMap { log =>
// This asInstanceOf is tested and virtually guaranteed to work, but is there any way to do it without casting?
val dmbla = log.asInstanceOf[DiagnosticMarkerBusLoggingAdapter]
alexklibisz marked this conversation as resolved.
Show resolved Hide resolved
provide(dmbla)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
|@ref[extractMaterializer](basic-directives/extractMaterializer.md) | Extracts the @apidoc[Materializer] from the @apidoc[RequestContext] |
|@ref[extractHost](host-directives/extractHost.md) | Extracts the hostname part of the Host request header value |
|@ref[extractLog](basic-directives/extractLog.md) | Extracts the @apidoc[LoggingAdapter] from the @apidoc[RequestContext] |
|@ref[extractMarkerLog](mdc-logging-directives/extractMarkerLog.md) | Calls @ref[withMdcLogging](mdc-logging-directives/withMdcLogging.md) and provides the resulting @apidoc[MarkerLoggingAdapter] from the @apidoc[RequestContext] |
|@ref[extractMethod](method-directives/extractMethod.md) | Extracts the request method |
|@ref[extractOfferedWsProtocols](websocket-directives/extractOfferedWsProtocols.md) | Extract the list of websocket subprotocols offered by the client in the `Sec-WebSocket-Protocol` header if this is a websocket request and otherwise rejects with an @apidoc[ExpectedWebSocketRequestRejection$] |
|@ref[extractParserSettings](basic-directives/extractParserSettings.md) | Extracts the @apidoc[ParserSettings] from the @apidoc[RequestContext] |
Expand Down Expand Up @@ -158,6 +159,9 @@
|@ref[withExecutionContext](basic-directives/withExecutionContext.md) | Runs its inner route with the given alternative `ExecutionContext` |
|@ref[withLog](basic-directives/withLog.md) | Runs its inner route with the given alternative @apidoc[LoggingAdapter] |
|@ref[withMaterializer](basic-directives/withMaterializer.md) | Runs its inner route with the given alternative @apidoc[Materializer] |
|@ref[withMdcEntries](mdc-logging-directives/withMdcEntries.md) | Adds one or more (key, value) entries to the current MDC logging context |
|@ref[withMdcEntry](mdc-logging-directives/withMdcEntry.md) | Adds a single (key, value) entry to the current MDC logging context |
|@ref[withMdcLogging](mdc-logging-directives/withMdcLogging.md) | Replaces the @apidoc[RequestContext]'s existing @apidoc[LoggingAdapter] with an MDC-compatible @apidoc[MarkerLoggingAdapter] |
|@ref[withPrecompressedMediaTypeSupport](coding-directives/withPrecompressedMediaTypeSupport.md) | Adds a `Content-Encoding: gzip` response header if the entity's media-type is precompressed with gzip header |
|@ref[withRangeSupport](range-directives/withRangeSupport.md) | Adds `Accept-Ranges: bytes` to responses to GET requests, produces partial responses if the initial request contained a valid `Range` header |
|@ref[withRequestTimeout](timeout-directives/withRequestTimeout.md) | Configures the @ref[request timeouts](../../common/timeouts.md#request-timeout) for a given route. |
Expand Down
4 changes: 4 additions & 0 deletions docs/src/main/paradox/routing-dsl/directives/by-trait.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ All predefined directives are organized into traits that form one part of the ov
@ref[TimeoutDirectives](timeout-directives/index.md)
: Configure request timeouts and automatic timeout responses.

@ref[MdcLoggingDirectives](mdc-logging-directives/index.md)
: Directives for mapped diagnostic context logging.

## List of predefined directives by trait

@@toc { depth=1 }
Expand Down Expand Up @@ -109,5 +112,6 @@ All predefined directives are organized into traits that form one part of the ov
* [security-directives/index](security-directives/index.md)
* [websocket-directives/index](websocket-directives/index.md)
* [timeout-directives/index](timeout-directives/index.md)
* [mdc-logging-directives/index](mdc-logging-directives/index.md)

@@@
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# extractMarkerLog

@@@ div { .group-scala }

## Signature

@@signature [MdcLoggingDirectives.scala](/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MdcLoggingDirectives.scala) { #extractMarkerLog }

@@@

## Description

Calls @ref[withMdcLogging](./withMdcLogging.md) and provides the resulting @apidoc[MarkerLoggingAdapter] from the @apidoc[RequestContext].

Nested calls will provide the same instance of `MarkerLoggingAdapter`.

## Example

Scala
: @@snip [MdcLoggingDirectivesExamplesSpec.scala](/docs/src/test/scala/docs/http/scaladsl/server/directives/MdcLoggingDirectivesExamplesSpec.scala) { #extractMarkerLog }
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# MdcLoggingDirectives

Directives for request-level mapped diagnostic context logging using the @apidoc[MarkerLoggingAdapter].

These directives provide an API to setup a new MDC-compatible logger for every request and append (key, value) MDC entries to be included in any emitted logs for the duration of the request

For example, one might extract a request ID from a header at the beginning of the request and append it as a (key, value) MDC entry.
Any subsequent logs emitted by this request would include the request ID as an entry.

@@toc { depth=1 }

@@@ index

* [withMdcLogging](withMdcLogging.md)
* [withMdcEntries](withMdcEntries.md)
* [withMdcEntry](withMdcEntry.md)
* [extractMarkerLog](extractMarkerLog.md)

@@@

## Structured JSON MDC Logging

In order to get structured (i.e., JSON) MDC logging, some additional configurations are necessary.
One possible configuration is as follows:

1. Add akka-slf4j, logback-classic, and logstash-logback-encoder as dependencies.
2. Add the configuration `akka.loggers = ["akka.event.slf4j.Slf4jLogger"]` to application.conf.
3. Create a `logback.xml` file with an appender that uses the LogstashEncoder:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# withMdcEntries

@@@ div { .group-scala }

## Signature

@@signature [MdcLoggingDirectives.scala](/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MdcLoggingDirectives.scala) { #withMdcEntries }

@@@

## Description

Adds one or more (key, value) entries to the current MDC logging context.

Nested calls will accumulate entries.

## Example

Scala
: @@snip [MdcLoggingDirectivesExamplesSpec.scala](/docs/src/test/scala/docs/http/scaladsl/server/directives/MdcLoggingDirectivesExamplesSpec.scala) { #withMdcEntries }
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# withMdcEntry

@@@ div { .group-scala }

## Signature

@@signature [MdcLoggingDirectives.scala](/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MdcLoggingDirectives.scala) { #withMdcEntry }

@@@

## Description

Adds a single (key, value) entry to the current MDC logging context.

Nested calls will accumulate entries.

## Example

Scala
: @@snip [MdcLoggingDirectivesExamplesSpec.scala](/docs/src/test/scala/docs/http/scaladsl/server/directives/MdcLoggingDirectivesExamplesSpec.scala) { #withMdcEntry }
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# withMdcLogging

@@@ div { .group-scala }

## Signature

@@signature [MdcLoggingDirectives.scala](/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MdcLoggingDirectives.scala) { #withMdcLogging }

@@@

## Description

Replaces the @apidoc[RequestContext]'s existing @apidoc[LoggingAdapter] with an MDC-compatible @apidoc[MarkerLoggingAdapter].

Nested calls will provide the same instance of `MarkerLoggingAdapter`.

## Example

Scala
: @@snip [MdcLoggingDirectivesExamplesSpec.scala](/docs/src/test/scala/docs/http/scaladsl/server/directives/MdcLoggingDirectivesExamplesSpec.scala) { #withMdcLogging }
Loading