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

added curl logger client aspect. #3285

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions zio-http/jvm/src/test/scala/zio/http/ZClientAspectSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,29 @@ object ZClientAspectSpec extends ZIOHttpSpec {
extractStatus(response) == Status.Ok,
),
),
test("curl request logger") {

for {
port <- Server.install(routes)
baseClient <- ZIO.service[Client]
client = baseClient
.url(
URL(Path.empty, Location.Absolute(Scheme.HTTP, "localhost", Some(port))),
)
.batched @@ ZClientAspect.curlLogger(logEffect = m => Console.printLine(m).orDie)
response <- client.request(Request.get(URL.empty / "hello"))
output <- TestConsole.output
} yield assertTrue(
output.mkString("") ==
s"""curl \\
| --verbose \\
| --request GET \\
| --header 'user-agent:${Client.defaultUAHeader.renderedValue}' \\
| 'http://localhost:${port}/hello'
|""".stripMargin,
extractStatus(response) == Status.Ok,
)
},
).provide(
ZLayer.succeed(Server.Config.default.onAnyOpenPort),
Server.customized,
Expand Down
100 changes: 100 additions & 0 deletions zio-http/shared/src/main/scala/zio/http/ZClientAspect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -492,4 +492,104 @@ object ZClientAspect {
},
)
}

/**
* Client aspect that logs details of web request as curl command
*/
final def curlLogger(
verbose: Boolean = true,
logEffect: String => UIO[Unit] = (m: String) => ZIO.log(m),
)(implicit trace: Trace): ZClientAspect[Nothing, Any, Nothing, Body, Nothing, Any, Nothing, Response] =
new ZClientAspect[Nothing, Any, Nothing, Body, Nothing, Any, Nothing, Response] {

def formatCurlCommand(
Copy link
Member

Choose a reason for hiding this comment

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

@landlockedsurfer Do you think we could put this capacity to transform a Request to a curl String as a method of Request and use this method here?

That'd allow people to "toString" their requests in a nice way if they need to (to log it at any moment, for example)

version: Version,
method: Method,
url: URL,
headers: Headers,
body: Body,
proxy: Option[Proxy],
): String = {
val versionOpt = version match {
case Version.Default => Chunk.empty
case Version.Http_1_0 => Chunk("--http1.0")
case Version.Http_1_1 => Chunk("--http1.1")
}
val verboseOpt = if (verbose) Chunk("--verbose") else Chunk.empty
val requestOpt = method match {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be simplified to

Suggested change
val requestOpt = method match {
val requestOpt = Chunk(s"--request ${method.name}")

case Method.GET => Chunk("--request GET")
case Method.POST => Chunk("--request POST")
case Method.PUT => Chunk("--request PUT")
case Method.DELETE => Chunk("--request DELETE")
case Method.PATCH => Chunk("--request PATCH")
case Method.HEAD => Chunk("--request HEAD")
case Method.OPTIONS => Chunk("--request OPTIONS")
case Method.CONNECT => Chunk("--request CONNECT")
case Method.TRACE => Chunk("--request TRACE")
case Method.CUSTOM(name) => Chunk(s"--request $name")
case Method.ANY => Chunk("--request GET")
}
val headerOpt = Chunk.fromIterable(headers.map(h => s"--header '${h.headerName}:${h.renderedValue}'"))
val bodyOpt = body match {
case Body.empty => Chunk.empty
case body => {
Chunk(s"--data '${body.asString.map(_.replace("'", "'\\''"))}'")
Copy link
Contributor

Choose a reason for hiding this comment

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

body.asString returns an effect. Printing an effect is not what you want.
Also, not all bodies can just be transformed to a string.
You need to check body.isCompleted to know if a it has data that you are allowed to read.
Else you might consume a stream of the body during logging, and it is not available anymore when we try to send it over the wire.
See the requestLogging client aspect for reference

}
}
val proxyOpt = proxy match {
case Some(proxy) =>
Chunk(
s"--proxy '${proxy.url}'" +
proxy.credentials.map(c => s" --proxy-user '${c.uname}:${c.upassword}'") +
proxy.headers.map(h => s" --proxy-header '${h.headerName}:${h.renderedValue}'").mkString(" "),
)
case None => Chunk.empty
}
(
Chunk("curl") ++
verboseOpt ++
requestOpt ++
headerOpt ++
versionOpt ++
proxyOpt ++
bodyOpt ++
Chunk.single("'" + url.encode + "'")
).mkString(" \\\n ")
}

override def apply[
ReqEnv,
Env >: Nothing <: Any,
In >: Nothing <: Body,
Err >: Nothing <: Any,
Out >: Nothing <: Response,
](
client: ZClient[Env, ReqEnv, In, Err, Out],
): ZClient[Env, ReqEnv, In, Err, Out] = {
val oldDriver = client.driver

val newDriver = new ZClient.Driver[Env, ReqEnv, Err] {
override def request(
version: Version,
method: Method,
url: URL,
headers: Headers,
body: Body,
sslConfig: Option[ClientSSLConfig],
proxy: Option[Proxy],
)(implicit trace: Trace): ZIO[Env & ReqEnv, Err, Response] =
logEffect(formatCurlCommand(version, method, url, headers, body, proxy)) *>
oldDriver.request(version, method, url, headers, body, sslConfig, proxy)

override def socket[Env1 <: Env](version: Version, url: URL, headers: Headers, app: WebSocketApp[Env1])(
implicit
trace: Trace,
ev: ReqEnv =:= Scope,
): ZIO[Env1 & ReqEnv, Err, Response] =
client.driver.socket(version, url, headers, app)
}

client.transform(client.bodyEncoder, client.bodyDecoder, newDriver)
}
}
}
Loading