├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt ├── src └── main │ ├── java │ └── com │ │ └── github │ │ └── rishabh9 │ │ ├── EnableMDC.java │ │ ├── MappedDiagnosticContextAction.java │ │ └── MappedDiagnosticContextFilter.java │ └── scala │ └── com │ └── github │ └── rishabh9 │ └── MDCPropagatingDispatcherConfigurator.scala └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | *.class 4 | 5 | # Log file 6 | *.log 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # Mobile Tools for Java (J2ME) 12 | .mtj.tmp/ 13 | 14 | # Package Files # 15 | *.jar 16 | *.war 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | ### JetBrains template 25 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 26 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 27 | 28 | .idea/ 29 | *.iml 30 | 31 | # User-specific stuff: 32 | .idea/**/workspace.xml 33 | .idea/**/tasks.xml 34 | 35 | # Sensitive or high-churn files: 36 | .idea/**/dataSources/ 37 | .idea/**/dataSources.ids 38 | .idea/**/dataSources.xml 39 | .idea/**/dataSources.local.xml 40 | .idea/**/sqlDataSources.xml 41 | .idea/**/dynamic.xml 42 | .idea/**/uiDesigner.xml 43 | 44 | # Gradle: 45 | .idea/**/gradle.xml 46 | .idea/**/libraries 47 | 48 | # Mongo Explorer plugin: 49 | .idea/**/mongoSettings.xml 50 | 51 | ## File-based project format: 52 | *.iws 53 | 54 | ## Plugin-specific files: 55 | 56 | # IntelliJ 57 | /out/ 58 | 59 | # mpeltonen/sbt-idea plugin 60 | .idea_modules/ 61 | 62 | # JIRA plugin 63 | atlassian-ide-plugin.xml 64 | 65 | # Crashlytics plugin (for Android Studio and IntelliJ) 66 | com_crashlytics_export_strings.xml 67 | crashlytics.properties 68 | crashlytics-build.properties 69 | fabric.properties 70 | ### SBT template 71 | # Simple Build Tool 72 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 73 | 74 | target/ 75 | lib_managed/ 76 | src_managed/ 77 | project/boot/ 78 | .history 79 | .cache 80 | ### Scala template 81 | *.class 82 | *.log 83 | 84 | # sbt specific 85 | .cache 86 | .history 87 | .lib/ 88 | dist/* 89 | target/ 90 | lib_managed/ 91 | src_managed/ 92 | project/boot/ 93 | project/plugins/project/ 94 | 95 | # Scala-IDE specific 96 | .ensime 97 | .ensime_cache/ 98 | .scala_dependencies 99 | .worksheet 100 | 101 | # ENSIME specific 102 | .ensime_cache/ 103 | .ensime 104 | 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with 4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 5 | 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 8 | language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Mapped Diagnostic Context (MDC) Propagation Akka Dispatcher 2 | ============= 3 | 4 | A Mapped Diagnostic Context (MDC) propagation Akka Dispatcher for the asynchronous environment of the Play Framework. 5 | 6 | Took the idea from here: 7 | 8 | 1. http://yanns.github.io/blog/2014/05/04/slf4j-mapped-diagnostic-context-mdc-with-play-framework/ 9 | 2. https://github.com/jroper/thread-local-context-propagation/ 10 | 11 | The provided code logs an UUID, per request, by default. 12 | 13 | Refer the code and customize to your whims and fancy. 14 | 15 | > Created this project so that I can have the dispatcher in a separate jar, to make my development easier. 16 | 17 | #### How To Use 18 | 19 | ###### Add the dependency 20 | ```scala 21 | libraryDependencies += "com.github.rishabh9" %% "mdc-propagation-dispatcher" % "0.0.8" 22 | ``` 23 | 24 | ###### Either add 'MappedDiagnosticContextFilter' to Filters.java 25 | ```java 26 | import com.github.rishabh9.MappedDiagnosticContextFilter; 27 | 28 | @Singleton 29 | public class Filters implements HttpFilters { 30 | private final MappedDiagnosticContextFilter mdcFilter; 31 | 32 | @Inject 33 | public Filters(MappedDiagnosticContextFilter mdcFilter) { 34 | this.mdcFilter = mdcFilter; 35 | } 36 | 37 | @Override 38 | public EssentialFilter[] filters() { 39 | final EssentialFilter[] filters = { 40 | mdcFilter.asJava() 41 | }; 42 | return filters; 43 | } 44 | } 45 | ``` 46 | 47 | ###### Or annotate your controllers/methods with 'EnableMDC' annotation 48 | ```java 49 | import com.github.rishabh9.EnableMDC; 50 | import play.mvc.Controller; 51 | 52 | @EnableMDC 53 | public class MyController extends Controller { 54 | // ... 55 | } 56 | ``` 57 | 58 | ###### Update your logging configuration 59 | ```xml 60 | %d{HH:mm:ss.SSS} %coloredLevel %logger{35} %mdc{X-UUID:--} - %msg%n%rootException 61 | ``` 62 | 63 | ###### Update your application.conf 64 | ```hocon 65 | play { 66 | akka { 67 | actor { 68 | default-dispatcher { 69 | type = "com.github.rishabh9.MDCPropagatingDispatcherConfigurator" 70 | } 71 | } 72 | } 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations._ 2 | 3 | organization := "com.github.rishabh9" 4 | 5 | name := "mdc-propagation-dispatcher" 6 | 7 | version := (version in ThisBuild).value 8 | 9 | scalaVersion := "2.13.1" 10 | 11 | exportJars := true 12 | 13 | retrieveManaged := true 14 | 15 | libraryDependencies ++= Seq( 16 | "com.typesafe" % "config" % "1.4.0" % "provided", 17 | "com.typesafe.play" %% "play" % "2.7.3" % "provided", 18 | "org.slf4j" % "slf4j-api" % "1.7.30" % "provided", 19 | "com.typesafe.akka" %% "akka-actor" % "2.6.4" % "provided" 20 | ) 21 | 22 | javacOptions ++= Seq("-source", "1.8", "-target", "1.8") 23 | 24 | initialize := { 25 | val _ = initialize.value 26 | if (sys.props("java.specification.version") != "1.8") 27 | sys.error("Java 8 is required for this project.") 28 | } 29 | 30 | sources in (Compile, doc) := Seq.empty 31 | 32 | // RELEASE -- 33 | 34 | // Maven publishing info 35 | publishMavenStyle := true 36 | 37 | publishTo := { 38 | val nexus = "https://oss.sonatype.org/" 39 | if (version.value.trim.endsWith("SNAPSHOT")) 40 | Some("snapshots" at nexus + "content/repositories/snapshots") 41 | else 42 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 43 | } 44 | 45 | publishArtifact in Test := false 46 | 47 | pomIncludeRepository := { _ => false } 48 | 49 | releasePublishArtifactsAction := PgpKeys.publishSigned.value 50 | 51 | // The Release configuration 52 | releaseProcess := Seq[ReleaseStep]( 53 | checkSnapshotDependencies, // : ReleaseStep 54 | inquireVersions, // : ReleaseStep 55 | runTest, // : ReleaseStep 56 | setReleaseVersion, // : ReleaseStep 57 | commitReleaseVersion, // : ReleaseStep, performs the initial git checks 58 | tagRelease, // : ReleaseStep 59 | publishArtifacts, // : ReleaseStep, checks whether `publishTo` is properly set up 60 | setNextVersion, // : ReleaseStep 61 | commitNextVersion, // : ReleaseStep 62 | pushChanges // : ReleaseStep, also checks that an upstream branch is properly configured 63 | ) 64 | 65 | pomExtra := ( 66 | https://github.com/rishabh9/mdc-propagation-dispatcher/ 67 | 68 | 69 | 70 | Apache 2 71 | http://www.apache.org/licenses/LICENSE-2.0.txt 72 | 73 | 74 | 75 | 76 | scm:git:git@github.com:rishabh9/mdc-propagation-dispatcher.git 77 | scm:git:git@github.com:rishabh9/mdc-propagation-dispatcher.git 78 | https://github.com/rishabh9/mdc-propagation-dispatcher/ 79 | 80 | 81 | 82 | 83 | rishabh9 84 | Rishabh Joshi 85 | https://github.com/rishabh9 86 | 87 | 88 | ) 89 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // SBT Release Plugin 2 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.12") 3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0") 4 | -------------------------------------------------------------------------------- /src/main/java/com/github/rishabh9/EnableMDC.java: -------------------------------------------------------------------------------- 1 | package com.github.rishabh9; 2 | 3 | import play.mvc.With; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * @author rishabh 12 | */ 13 | @With(MappedDiagnosticContextAction.class) 14 | @Target({ElementType.TYPE, ElementType.METHOD}) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface EnableMDC { 17 | boolean value() default true; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/rishabh9/MappedDiagnosticContextAction.java: -------------------------------------------------------------------------------- 1 | package com.github.rishabh9; 2 | 3 | 4 | import org.slf4j.MDC; 5 | import play.mvc.Action; 6 | import play.mvc.Http; 7 | import play.mvc.Result; 8 | 9 | import java.util.UUID; 10 | import java.util.concurrent.CompletionStage; 11 | 12 | /** 13 | * @author rishabh 14 | */ 15 | public class MappedDiagnosticContextAction extends Action { 16 | public CompletionStage call(Http.Context ctx) { 17 | if (configuration.value()) { 18 | MDC.put("X-UUID", UUID.randomUUID().toString()); 19 | } 20 | return delegate.call(ctx).whenComplete((result, throwable) -> { 21 | if (configuration.value()) { 22 | MDC.remove("X-UUID"); 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/rishabh9/MappedDiagnosticContextFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.rishabh9; 2 | 3 | import akka.stream.Materializer; 4 | import org.slf4j.MDC; 5 | import play.mvc.Filter; 6 | import play.mvc.Http; 7 | import play.mvc.Result; 8 | 9 | import javax.inject.Inject; 10 | import java.util.UUID; 11 | import java.util.concurrent.CompletionStage; 12 | import java.util.concurrent.Executor; 13 | import java.util.function.Function; 14 | 15 | /** 16 | * @author rishabh 17 | */ 18 | public class MappedDiagnosticContextFilter extends Filter { 19 | 20 | private final Executor exec; 21 | 22 | /** 23 | * @param mat This object is needed to handle streaming of requests 24 | * and responses. 25 | * @param exec This class is needed to execute code asynchronously. 26 | * It is used below by the thenAsyncApply method. 27 | */ 28 | @Inject 29 | public MappedDiagnosticContextFilter(Materializer mat, Executor exec) { 30 | super(mat); 31 | this.exec = exec; 32 | } 33 | 34 | @Override 35 | public CompletionStage apply(Function> next, 36 | Http.RequestHeader requestHeader) { 37 | 38 | MDC.put("X-UUID", UUID.randomUUID().toString()); 39 | return next.apply(requestHeader).thenApplyAsync( 40 | result -> { 41 | MDC.remove("X-UUID"); 42 | return result; 43 | }, 44 | exec 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/com/github/rishabh9/MDCPropagatingDispatcherConfigurator.scala: -------------------------------------------------------------------------------- 1 | package com.github.rishabh9 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import akka.dispatch._ 6 | import com.typesafe.config.Config 7 | import org.slf4j.MDC 8 | 9 | import scala.concurrent.ExecutionContext 10 | import scala.concurrent.duration.{Duration, FiniteDuration} 11 | 12 | /** 13 | * Configurator for a MDC propagating dispatcher. 14 | * 15 | * To use it, configure play like this: 16 | * {{{ 17 | * play { 18 | * akka { 19 | * actor { 20 | * default-dispatcher = { 21 | * type = "com.github.rishabh9.MDCPropagatingDispatcherConfigurator" 22 | * } 23 | * } 24 | * } 25 | * } 26 | * }}} 27 | * 28 | * Credits to James Roper for the [[https://github.com/jroper/thread-local-context-propagation/ initial implementation]] 29 | */ 30 | class MDCPropagatingDispatcherConfigurator(config: Config, prerequisites: DispatcherPrerequisites) 31 | extends MessageDispatcherConfigurator(config, prerequisites) { 32 | 33 | private val instance = new MDCPropagatingDispatcher( 34 | this, 35 | config.getString("id"), 36 | config.getInt("throughput"), 37 | FiniteDuration(config.getDuration("throughput-deadline-time", TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS), 38 | configureExecutor(), 39 | FiniteDuration(config.getDuration("shutdown-timeout", TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS)) 40 | 41 | override def dispatcher(): MessageDispatcher = instance 42 | } 43 | 44 | /** 45 | * A MDC propagating dispatcher. 46 | * 47 | * This dispatcher propagates the MDC current request context if it's set when it's executed. 48 | */ 49 | class MDCPropagatingDispatcher(_configurator: MessageDispatcherConfigurator, 50 | id: String, 51 | throughput: Int, 52 | throughputDeadlineTime: Duration, 53 | executorServiceFactoryProvider: ExecutorServiceFactoryProvider, 54 | shutdownTimeout: FiniteDuration) 55 | extends Dispatcher(_configurator, id, throughput, throughputDeadlineTime, executorServiceFactoryProvider, shutdownTimeout) { 56 | 57 | self => 58 | 59 | override def prepare(): ExecutionContext = new ExecutionContext { 60 | // capture the MDC 61 | val mdcContext = MDC.getCopyOfContextMap 62 | 63 | def execute(r: Runnable) = self.execute(new Runnable { 64 | def run() = { 65 | // backup the callee MDC context 66 | val oldMDCContext = MDC.getCopyOfContextMap 67 | 68 | // Run the runnable with the captured context 69 | setContextMap(mdcContext) 70 | try { 71 | r.run() 72 | } finally { 73 | // restore the callee MDC context 74 | setContextMap(oldMDCContext) 75 | } 76 | } 77 | }) 78 | 79 | def reportFailure(t: Throwable) = self.reportFailure(t) 80 | } 81 | 82 | private[this] def setContextMap(context: java.util.Map[String, String]) { 83 | if (context == null) { 84 | MDC.clear() 85 | } else { 86 | MDC.setContextMap(context) 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.0.9-SNAPSHOT" 2 | --------------------------------------------------------------------------------