Skip to content
This repository was archived by the owner on Oct 10, 2023. It is now read-only.

Commit 74de1c0

Browse files
committed
Version 1.1.0: Add support for custom logging context
1 parent 790489c commit 74de1c0

File tree

9 files changed

+343
-51
lines changed

9 files changed

+343
-51
lines changed

README.md

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ This is a wrapper around the akka-http-client that adds
1010
* AWS request signing
1111

1212
[![CircleCI](https://circleci.com/gh/moia-dev/scala-http-client/tree/master.svg?style=svg)](https://circleci.com/gh/moia-dev/scala-http-client/tree/master)
13+
[![Scala 2.13](https://img.shields.io/maven-central/v/io.moia/scala-http-client_2.13.svg)](https://search.maven.org/search?q=scala-http-client_2.13)
1314

1415
## Usage
1516

1617
```sbt
17-
libraryDependencies += "io.moia" % "scala-http-client" % "1.0.0"
18+
libraryDependencies += "io.moia" % "scala-http-client" % "1.1.0"
1819
```
1920

2021
```scala
@@ -23,7 +24,9 @@ implicit val executionContext: ExecutionContext = system.dispatcher
2324

2425
val httpClientConfig: HttpClientConfig = HttpClientConfig("http", isSecureConnection = false, "127.0.0.1", 8888)
2526
val clock: Clock = Clock.systemUTC()
26-
val httpMetrics: HttpMetrics = (_: HttpMethod, _: Uri.Path, _: HttpResponse) => ()
27+
val httpMetrics: HttpMetrics[String] = new HttpMetrics[String] {
28+
override def meterResponse(method: HttpMethod, path: Uri.Path, response: HttpResponse)(implicit ctx: String): Unit = ()
29+
}
2730
val retryConfig: RetryConfig =
2831
RetryConfig(
2932
retriesTooManyRequests = 2,
@@ -55,4 +58,37 @@ response.flatMap {
5558
case DomainError(content) => Unmarshal(content).to[DomainErrorObject].map(Left(_))
5659
case failure: HttpClientFailure => throw GatewayException(failure.toString)
5760
}
58-
```
61+
```
62+
63+
## Custom Logging
64+
65+
To use a custom logger (for correlation ids etc), you can use the typed `LoggingHttpClient`:
66+
67+
```scala
68+
// create a context-class
69+
case class LoggingContext(context: String)
70+
71+
// create a logger
72+
implicit val canLogString: CanLog[LoggingContext] = new CanLog[LoggingContext] // override logMessage here!
73+
implicit val theLogger: LoggerTakingImplicit[LoggingContext] = Logger.takingImplicit(LoggerFactory.getLogger(getClass.getName))
74+
75+
// create the client
76+
new LoggingHttpClient[LoggingContext](httpClientConfig, "TestGateway", typedHttpMetrics, retryConfig, clock, None)
77+
78+
// create an implicit logging context
79+
implicit val ctx: LoggingContext = LoggingContext("Logging Context")
80+
81+
// make a request
82+
testHttpClient.request(HttpMethods.POST, HttpEntity.Empty, "/test", immutable.Seq.empty, Deadline.now + 10.seconds)
83+
```
84+
85+
## Publishing
86+
87+
You need an account on https://oss.sonatype.org that can [access](https://issues.sonatype.org/browse/OSSRH-52948) the `io.moia` group.
88+
89+
Add your credentials to `~/.sbt/sonatype_credential` and run
90+
```sbt
91+
sbt:scala-http-client> +publishSigned
92+
```
93+
94+
Then head to https://oss.sonatype.org/#stagingRepositories, select the repository, `Close` and then `Release` it.

build.sbt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
21
lazy val root = (project in file("."))
32
.settings(
43
name := "scala-http-client",
54
organization := "io.moia",
6-
version := "1.0.0-akka2.5",
5+
version := "1.1.0-akka2.5",
76
licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0")),
87
scmInfo := Some(ScmInfo(url("https://github.com/moia-dev/scala-http-client"), "scm:git@github.com:moia-dev/scala-http-client.git")),
98
homepage := Some(url("https://github.com/moia-dev/scala-http-client")),
@@ -17,7 +16,8 @@ lazy val root = (project in file("."))
1716
}
1817
},
1918
libraryDependencies ++= akkaDependencies ++ awsDependencies ++ testDependencies ++ loggingDependencies ++ otherDependencies
20-
).settings(sonatypeSettings: _*)
19+
)
20+
.settings(sonatypeSettings: _*)
2121

2222
val akkaVersion = "2.5.29"
2323
val akkaHttpVersion = "10.1.11"
@@ -36,8 +36,9 @@ lazy val awsDependencies = Seq(
3636
)
3737

3838
lazy val testDependencies = Seq(
39-
"org.scalatest" %% "scalatest" % "3.1.0" % Test,
40-
"org.mockito" %% "mockito-scala" % "1.11.2" % Test
39+
"org.scalatest" %% "scalatest" % "3.1.0" % Test,
40+
"org.mockito" %% "mockito-scala" % "1.11.2" % Test,
41+
"org.mock-server" % "mockserver-netty" % "5.9.0" % Test
4142
)
4243

4344
lazy val loggingDependencies = Seq(
@@ -90,4 +91,4 @@ lazy val sonatypeSettings = {
9091
sonatypeProjectHosting := Some(GitHubHosting("moia-dev", "scala-http-client", "oss-support@moia.io")),
9192
credentials += Credentials(Path.userHome / ".sbt" / "sonatype_credential")
9293
)
93-
}
94+
}

src/main/scala/io/moia/scalaHttpClient/HttpClient.scala

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import akka.http.scaladsl.Http
77
import akka.http.scaladsl.model._
88
import akka.http.scaladsl.model.headers.{`Retry-After`, RetryAfterDateTime, RetryAfterDuration}
99
import akka.stream.ActorMaterializer
10-
import com.typesafe.scalalogging.StrictLogging
10+
import com.typesafe.scalalogging.{Logger, LoggerTakingImplicit}
11+
import org.slf4j.LoggerFactory
1112

1213
import scala.collection.immutable
1314
import scala.concurrent.duration._
@@ -18,25 +19,50 @@ import scala.util.{Failure, Success, Try}
1819
class HttpClient(
1920
config: HttpClientConfig,
2021
gatewayType: String,
21-
httpMetrics: HttpMetrics,
22+
httpMetrics: HttpMetrics[String],
2223
retryConfig: RetryConfig,
2324
clock: Clock,
2425
awsRequestSigner: Option[AwsRequestSigner]
25-
)(implicit system: ActorSystem)
26+
)(
27+
implicit system: ActorSystem
28+
) extends LoggingHttpClient[String](config, gatewayType, httpMetrics, retryConfig, clock, awsRequestSigner)(
29+
system,
30+
Logger.takingImplicit(LoggerFactory.getLogger(getClass.getName))((msg: String, _: String) => msg)
31+
) {
32+
override def request(
33+
method: HttpMethod,
34+
entity: MessageEntity,
35+
path: String,
36+
headers: immutable.Seq[HttpHeader],
37+
deadline: Deadline,
38+
queryString: Option[String]
39+
)(implicit executionContext: ExecutionContext, ctx: String = ""): Future[HttpClientResponse] =
40+
super.request(method, entity, path, headers, deadline, queryString)
41+
}
42+
43+
class LoggingHttpClient[LoggingContext](
44+
config: HttpClientConfig,
45+
gatewayType: String,
46+
httpMetrics: HttpMetrics[LoggingContext],
47+
retryConfig: RetryConfig,
48+
clock: Clock,
49+
awsRequestSigner: Option[AwsRequestSigner]
50+
)(implicit system: ActorSystem, logger: LoggerTakingImplicit[LoggingContext])
2651
extends HttpLayer(config, gatewayType, httpMetrics, retryConfig, clock, awsRequestSigner) {
2752
override protected def sendRequest: HttpRequest => Future[HttpResponse] = Http().singleRequest(_)
2853
}
2954

30-
abstract class HttpLayer(
55+
abstract class HttpLayer[LoggingContext](
3156
config: HttpClientConfig,
3257
gatewayType: String,
33-
httpMetrics: HttpMetrics,
58+
httpMetrics: HttpMetrics[LoggingContext],
3459
retryConfig: RetryConfig,
3560
clock: Clock,
3661
awsRequestSigner: Option[AwsRequestSigner] = None
3762
)(
38-
implicit system: ActorSystem
39-
) extends StrictLogging {
63+
implicit system: ActorSystem,
64+
logger: LoggerTakingImplicit[LoggingContext]
65+
) {
4066

4167
protected def sendRequest: HttpRequest => Future[HttpResponse]
4268

@@ -48,7 +74,8 @@ abstract class HttpLayer(
4874
deadline: Deadline,
4975
queryString: Option[String] = None
5076
)(
51-
implicit executionContext: ExecutionContext
77+
implicit executionContext: ExecutionContext,
78+
ctx: LoggingContext
5279
): Future[HttpClientResponse] =
5380
if (deadline.isOverdue()) {
5481
Future.successful(DeadlineExpired())
@@ -73,7 +100,8 @@ abstract class HttpLayer(
73100
}
74101

75102
private[this] def executeRequest(request: HttpRequest, tryNumber: Int, deadline: Deadline)(
76-
implicit ec: ExecutionContext
103+
implicit ec: ExecutionContext,
104+
ctx: LoggingContext
77105
): Future[HttpClientResponse] =
78106
Future
79107
.successful(request)
@@ -87,7 +115,7 @@ abstract class HttpLayer(
87115

88116
private[this] def handleResponse(tryNumber: Int, deadline: Deadline, httpRequest: HttpRequest)(
89117
response: HttpResponse
90-
)(implicit ec: ExecutionContext): Future[HttpClientResponse] = response match {
118+
)(implicit ec: ExecutionContext, ctx: LoggingContext): Future[HttpClientResponse] = response match {
91119
case response @ HttpResponse(StatusCodes.Success(_), _, _, _) => Future.successful(HttpClientSuccess(response))
92120
case response @ HttpResponse(StatusCodes.BadRequest, _, HttpEntity.Empty, _) => Future.successful(HttpClientError(response))
93121
case response @ HttpResponse(StatusCodes.BadRequest, _, _, _) => Future.successful(DomainError(response))
@@ -113,7 +141,8 @@ abstract class HttpLayer(
113141
}
114142

115143
private[this] def retryWithConfig(tryNum: Int, request: HttpRequest, response: HttpResponse, deadline: Deadline)(
116-
implicit ec: ExecutionContext
144+
implicit ec: ExecutionContext,
145+
ctx: LoggingContext
117146
): Future[HttpClientResponse] =
118147
if (deadline.isOverdue()) {
119148
logger.info(s"[$gatewayType] Try #$tryNum: Deadline has expired.")
@@ -145,7 +174,8 @@ abstract class HttpLayer(
145174
}
146175

147176
private[this] def handleErrors(tryNum: Int, deadline: Deadline, request: HttpRequest)(
148-
implicit ec: ExecutionContext
177+
implicit ec: ExecutionContext,
178+
ctx: LoggingContext
149179
): PartialFunction[Throwable, Future[HttpClientResponse]] = {
150180
case NonFatal(e) if tryNum <= retryConfig.retriesException =>
151181
val delay: FiniteDuration = calculateDelay(None, tryNum)
@@ -156,16 +186,16 @@ abstract class HttpLayer(
156186
Future.successful(DeadlineExpired(None))
157187
}
158188

159-
private[this] def logRequest[T]: PartialFunction[Try[HttpRequest], Unit] = {
189+
private[this] def logRequest[T](implicit ctx: LoggingContext): PartialFunction[Try[HttpRequest], Unit] = {
160190
case Success(request) => logger.debug(s"[$gatewayType] Sending request to ${request.method.value} ${request.uri}.")
161191
}
162192

163-
private[this] def logRetryAfter: PartialFunction[Try[HttpResponse], Unit] = {
193+
private[this] def logRetryAfter(implicit ctx: LoggingContext): PartialFunction[Try[HttpResponse], Unit] = {
164194
case Success(response) if response.header[`Retry-After`].isDefined =>
165195
logger.info(s"[$gatewayType] Received retry-after header with value ${response.header[`Retry-After`]}")
166196
}
167197

168-
private[this] def logResponse(request: HttpRequest): PartialFunction[Try[HttpResponse], Unit] = {
198+
private[this] def logResponse(request: HttpRequest)(implicit ctx: LoggingContext): PartialFunction[Try[HttpResponse], Unit] = {
169199
case Success(response) =>
170200
httpMetrics.meterResponse(request.method, request.uri.path, response)
171201
logger.debug(s"[$gatewayType] Received response ${response.status} from ${request.method.value} ${request.uri}.")

src/main/scala/io/moia/scalaHttpClient/HttpMetrics.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ package io.moia.scalaHttpClient
22

33
import akka.http.scaladsl.model.{HttpMethod, HttpResponse, Uri}
44

5-
trait HttpMetrics {
6-
def meterResponse(method: HttpMethod, path: Uri.Path, response: HttpResponse): Unit
5+
trait HttpMetrics[LoggingContext] {
6+
def meterResponse(method: HttpMethod, path: Uri.Path, response: HttpResponse)(implicit ctx: LoggingContext): Unit
77
}

src/test/scala/io/moia/scalaHttpClient/HttpClientTest.scala

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,17 @@
11
package io.moia.scalaHttpClient
22

3-
import java.time.Clock
4-
5-
import akka.actor.ActorSystem
63
import akka.http.scaladsl.model._
74
import akka.http.scaladsl.model.headers.{ModeledCustomHeader, ModeledCustomHeaderCompanion}
8-
import akka.stream.ActorMaterializer
5+
import com.typesafe.scalalogging.StrictLogging
96
import io.moia.scalaHttpClient.AwsRequestSigner.AwsRequestSignerConfig
10-
import org.scalatest.matchers.should.Matchers
11-
import org.scalatest.wordspec.AnyWordSpecLike
127
import org.scalatest.{Inside, Inspectors}
138

149
import scala.collection.immutable
1510
import scala.concurrent.duration._
16-
import scala.concurrent.{ExecutionContext, Future, Promise}
11+
import scala.concurrent.{Future, Promise}
1712
import scala.util.Try
1813

19-
class HttpClientTest extends AnyWordSpecLike with Matchers with FutureValues with Inside {
20-
21-
private implicit val system: ActorSystem = ActorSystem("test")
22-
private implicit val mat: ActorMaterializer = ActorMaterializer()
23-
private implicit val executionContext: ExecutionContext = system.dispatcher
24-
private val httpClientConfig: HttpClientConfig = HttpClientConfig("http", isSecureConnection = false, "127.0.0.1", 8888)
25-
private val clock: Clock = Clock.systemUTC()
26-
private val httpMetrics: HttpMetrics = (_: HttpMethod, _: Uri.Path, _: HttpResponse) => ()
27-
private val retryConfig: RetryConfig =
28-
RetryConfig(
29-
retriesTooManyRequests = 2,
30-
retriesServiceUnavailable = 0,
31-
retriesRequestTimeout = 0,
32-
retriesServerError = 0,
33-
retriesException = 3,
34-
initialBackoff = 10.millis,
35-
strictifyResponseTimeout = 1.second
36-
)
14+
class HttpClientTest extends TestSetup with Inside with StrictLogging {
3715

3816
classOf[HttpClient].getSimpleName should {
3917
"sign requests" in {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.moia.scalaHttpClient
2+
3+
import akka.http.scaladsl.model._
4+
import com.typesafe.scalalogging.{CanLog, Logger, LoggerTakingImplicit}
5+
import org.slf4j.{LoggerFactory, MDC}
6+
7+
import scala.collection.immutable
8+
import scala.concurrent.Future
9+
import scala.concurrent.duration._
10+
11+
class LoggingHttpClientTest extends TestSetup {
12+
13+
val typedHttpMetrics: HttpMetrics[LoggingContext] = new HttpMetrics[LoggingContext] {
14+
override def meterResponse(method: HttpMethod, path: Uri.Path, response: HttpResponse)(implicit ctx: LoggingContext): Unit = ()
15+
}
16+
17+
case class LoggingContext(context: String)
18+
19+
private implicit val canLogString: CanLog[LoggingContext] = new CanLog[LoggingContext] {
20+
val prevContext: String = MDC.get("context")
21+
22+
override def logMessage(originalMsg: String, ctx: LoggingContext): String = {
23+
// you can put things to the MDC here
24+
MDC.put("context", ctx.context)
25+
26+
// …or influence the string that is going to be logged:
27+
s"$originalMsg for (${ctx.context})"
28+
}
29+
override def afterLog(ctx: LoggingContext): Unit =
30+
if (prevContext != null) {
31+
MDC.put("context", prevContext)
32+
} else MDC.remove("context")
33+
}
34+
35+
private implicit val theLogger: LoggerTakingImplicit[LoggingContext] = Logger.takingImplicit(LoggerFactory.getLogger(getClass.getName))
36+
private implicit val ctx: LoggingContext = LoggingContext("Logging Context")
37+
38+
classOf[LoggingHttpClient[LoggingContext]].getSimpleName should {
39+
"take a customer logger" in {
40+
// given
41+
val testHttpClient =
42+
new LoggingHttpClient[LoggingContext](httpClientConfig, "TestGateway", typedHttpMetrics, retryConfig, clock, None) {
43+
override def sendRequest: HttpRequest => Future[HttpResponse] = (_: HttpRequest) => Future.successful(HttpResponse())
44+
}
45+
46+
// when
47+
val _ =
48+
testHttpClient.request(HttpMethods.POST, HttpEntity.Empty, "/test", immutable.Seq.empty, Deadline.now + 10.seconds).futureValue
49+
50+
// then succeed if it compiles
51+
succeed
52+
}
53+
}
54+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.moia.scalaHttpClient
2+
3+
import org.mockserver.integration.ClientAndServer
4+
import org.mockserver.integration.ClientAndServer.startClientAndServer
5+
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite}
6+
7+
trait MockServer extends BeforeAndAfterEach with BeforeAndAfterAll { self: Suite =>
8+
9+
val mockServerPort = 38080
10+
11+
val mockServerHttpClientConfig: HttpClientConfig =
12+
HttpClientConfig(
13+
scheme = "http",
14+
isSecureConnection = false,
15+
host = "127.0.0.1",
16+
port = mockServerPort
17+
)
18+
19+
private var clientAndServer: Option[ClientAndServer] = None
20+
21+
override def beforeAll(): Unit = {
22+
super.beforeAll()
23+
clientAndServer.getOrElse {
24+
val newServerMock = startClientAndServer(mockServerPort)
25+
clientAndServer = Some(newServerMock)
26+
newServerMock
27+
}
28+
()
29+
}
30+
31+
override def afterEach(): Unit = {
32+
super.afterEach()
33+
clientAndServer.foreach(_.reset())
34+
}
35+
36+
override def afterAll(): Unit = {
37+
super.afterAll()
38+
clientAndServer.foreach(_.stop())
39+
clientAndServer = None
40+
}
41+
42+
def getClientAndServer: ClientAndServer =
43+
clientAndServer.getOrElse(throw new IllegalStateException("Mock not started yet!"))
44+
}

0 commit comments

Comments
 (0)