-
-
Notifications
You must be signed in to change notification settings - Fork 40
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
Curl Logging interceptor #141
Curl Logging interceptor #141
Conversation
*/ | ||
internal fun Request.buildCurlCommand(): String { | ||
var compressed = false | ||
//TODO: test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I write test method sample in RequestExtTest.kt.
But not yet write test in many case.
(I write test only about simple GET request case)
After I confirm whether this PR's direction is correct, I'll write test code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are on a right way!
Several improvements and this changes will be accepted.
@@ -345,14 +345,15 @@ val forkedClient = defaultHttpClient.fork { | |||
A Request Logging Interceptor. | |||
|
|||
Parameters: | |||
1. `log: (String) -> Unit = ::println`: function as a parameter to consume the log message. It defaults to `println`. Logs Request body when present. | |||
1. `outputCurlCommand: Boolean = false`: if it's true, output curl command on log message. | |||
2. `log: (String) -> Unit = ::println`: function as a parameter to consume the log message. It defaults to `println`. Logs Request body when present. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is better to change parameters order:
log: (String) -> Unit = ::println
outputCurlCommand: Boolean = false
First, because we do not break code of existing kohttp users (they will have compilation error with new library version)
Second, because 'log' parameter represents main logic of LoggingInterceptor and 'outputCurlCommand' is additional parameter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because, log
argument is function, this argument order cannot break code.
Below code is working correctly.
val client = defaultHttpClient.fork {
interceptors {
///////old version code (outputCurlCommand argument does not exist)//////
//no argument
+LoggingInterceptor()
//only log argument:
+LoggingInterceptor { log ->
println(log)
}
///////new version code (outputCurlCommand argument exists)//////
//only outputCurlCommand
+LoggingInterceptor(true)
//outputCurlCommand and log arguments:
+LoggingInterceptor(true) { log ->
println(log)
}
}
}
I think log
is function and need two or more line's code, so this order is better.
What do you think about it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are definitely right, I was wrong.
*/ | ||
internal fun Request.buildCurlCommand(): String { | ||
var compressed = false | ||
//TODO: test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are on a right way!
Several improvements and this changes will be accepted.
class LoggingInterceptor(private val log: (String) -> Unit = ::println) : Interceptor { | ||
class LoggingInterceptor( | ||
private val outputCurlCommand: Boolean = false, | ||
private val log: (String) -> Unit = ::println |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also one more time - change order of parameters
val command = request.buildCurlCommand() | ||
log("╭--- cURL command -------------------------------") | ||
log(command) | ||
log("╰--- (copy and paste the above line to a terminal)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is better to put it before kotlin chain.proceed(request)
because we logging here only request
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree! Fixed in 054f8f0
/** | ||
* @author doyaaaaaken | ||
*/ | ||
class RequestExtTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember that you will write more tests after our approval, but small comment - you should focus your attention on headers and also cookies
Good changes, you are on the right way, @doyaaaaaken |
Codecov Report
@@ Coverage Diff @@
## master #141 +/- ##
===========================================
+ Coverage 90.59% 90.9% +0.31%
- Complexity 120 130 +10
===========================================
Files 40 42 +2
Lines 372 396 +24
Branches 45 45
===========================================
+ Hits 337 360 +23
Misses 9 9
- Partials 26 27 +1
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@doyaaaaaken Thank you your feedback!
@@ -345,14 +345,15 @@ val forkedClient = defaultHttpClient.fork { | |||
A Request Logging Interceptor. | |||
|
|||
Parameters: | |||
1. `log: (String) -> Unit = ::println`: function as a parameter to consume the log message. It defaults to `println`. Logs Request body when present. | |||
1. `outputCurlCommand: Boolean = false`: if it's true, output curl command on log message. | |||
2. `log: (String) -> Unit = ::println`: function as a parameter to consume the log message. It defaults to `println`. Logs Request body when present. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because, log
argument is function, this argument order cannot break code.
Below code is working correctly.
val client = defaultHttpClient.fork {
interceptors {
///////old version code (outputCurlCommand argument does not exist)//////
//no argument
+LoggingInterceptor()
//only log argument:
+LoggingInterceptor { log ->
println(log)
}
///////new version code (outputCurlCommand argument exists)//////
//only outputCurlCommand
+LoggingInterceptor(true)
//outputCurlCommand and log arguments:
+LoggingInterceptor(true) { log ->
println(log)
}
}
}
I think log
is function and need two or more line's code, so this order is better.
What do you think about it?
val command = request.buildCurlCommand() | ||
log("╭--- cURL command -------------------------------") | ||
log(command) | ||
log("╰--- (copy and paste the above line to a terminal)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree! Fixed in 054f8f0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi all,
I had 10 mins to have a look at this PR. General direction is good! And I have several suggestions and questions to think about:
- Do we really want to mix Log and Curl interceptors?
- If we do -
outputCurlCommand: Boolean
seems to be not the best choice. Imagine we will decide to print pure HTTP request (not curl, or log) in the future
Yes, I think so, because the responsibility of
Yes, I agree. val client = defaultHttpClient.fork {
interceptors {
// old version code
+LoggingInterceptor()
+LoggingInterceptor { msg ->
println(msg)
}
// new version code
+LoggingInterceptor().setOutputCurlCommand(true)
+LoggingInterceptor { msg ->
println(msg)
}.setOutputCurlCommand(true)
} |
I would recommend to think about enum of output strategies: plain http, curl, m/b something else. Our top priority is to make a DSL. |
I see! So how about below LoggingInterceptor class signature?
|
It looks much more user friendly!
@DeviantBadge your opinion? |
I come up with the enum name candidates: DEFAULT, NATIVE, SUMMARY, ORIGINAL. |
@doyaaaaaken I take some time to think about. And come up with HTTP and CURL at the beginning. HTTP should be a default. Currently we don’t have a correct HTTP logging, we should implement it in a different issue, replacing the current default. So let’s
|
…utputCurlCommand`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rybalkinsd
I improved my code!
- Create an issue to implement HTTP
This issue should be created after this PR will be merged. So, I don't create it yet.
@@ -55,4 +55,16 @@ class LoggingInterceptorTest { | |||
assertEquals(200, response.code()) | |||
} | |||
|
|||
@Test | |||
fun `curl command logging happens without exceptions `() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test's output is below.
GET 200 - 1118ms https://postman-echo.com/get
╭--- cURL command -------------------------------
curl -X GET "https://postman-echo.com/get"
╰--- (copy and paste the above line to a terminal)
And a test result in the case of LoggingFormat.HTTP is below.
POST 200 - 2076ms http://postman-echo.com/post
--80d27159-868d-4252-8b65-392dd2596483
Content-Disposition: form-data; name="file"; filename="cat.gif"
Content-Length: 1046038
���"��#��+'!5+!95/=1"F;1H7%LB4LG@T��YL?ZH3]SE^YThUAh`Wm]HpbRqh`t��xaHxtp}nc}ys~jS~mZ�iK�|u�u`��7�{m�w^��~�z^��s��e�����{��g��F��r�����{�����������]������¸�Ʋ��þ�����|�ɡ���������������!��NETSCAPE2.0�����!���
��-"���Q�]t�ZB��C���x�q��w#�4�
qߋR9�
M!
�?��8���+[+���I�d"F�P��seiqSXa�b���=e�e(��c�/���z骎�j�*�6������e�O�v���a ������w����de�\��>YU:��A�4���z?�8� Z�ϸߓ�
�^�'+
���
-c�f"4��oG�ID�9[��\�9��� h� �07�%�HڥX堟� M]�n�p��P:t�r^�-�BI$�F��{
......... skip the rest
Great! That looks much better in terms of DevX. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@doyaaaaaken
We're close to making it production-ready. So, let's review the thing I mentioned.
I also suggest you to sign in our Gitter channel to get some discussions if you'll need
README.md
Outdated
|
||
Usage: | ||
|
||
```kotlin | ||
val client = defaultHttpClient.fork { | ||
interceptors { | ||
+LoggingInterceptor() | ||
+LoggingInterceptor(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this change looks outdated now
* @author doyaaaaaken | ||
*/ | ||
internal fun Request.buildCurlCommand(): String { | ||
return StringBuilder().apply { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I suggest to avoid a big-big builder scope here.
- we also have
buildString
to simplify
|
||
//headers | ||
val headers = headers() | ||
for (i in 0 until headers.size()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have asSequence()
method for headers
for (i in 0 until headers.size()) { | ||
val name = headers.name(i) | ||
var value = headers.value(i) | ||
if (value[0] == '"' && value[value.length - 1] == '"') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this segment of code looks rather complicated for me, probably it will be easier with the sequence, or will need further simplification
please check .last()
and .first()
methods
val name = headers.name(i) | ||
var value = headers.value(i) | ||
if (value[0] == '"' && value[value.length - 1] == '"') { | ||
value = "\\\"" + value.substring(1, value.length - 1) + "\\\"" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably, """ string"""
triple quoted string will look better here
} | ||
|
||
//body | ||
body()?.let { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please double-check LoggingInterceptor
. it looks like a code duplication
} | ||
} | ||
} | ||
|
||
private fun logAsHttp(request: Request) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to refactor as a strategy pattern.
|
||
private fun logAsCurl(request: Request) { | ||
val command = request.buildCurlCommand() | ||
log("╭--- cURL command -------------------------------") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's much better to keep the identical output style for both http and curl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here we don't really know the user's screen width, so ---- line m/b too long
* | ||
* @author doyaaaaaken | ||
*/ | ||
internal fun Request.buildCurlCommand(): String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After reading PR several times, I want to ask if we really need this ext function, or we simply can split it into several methods within strategy pattern, as I mentioned in another comment and put it close to the LogginInterceptor package?
Thank you!
Let me have time to look at this from next Sunday.
…On Thu, Aug 1, 2019, 20:14 Sergei Rybalkin ***@***.***> wrote:
***@***.**** requested changes on this pull request.
@doyaaaaaken <https://github.com/doyaaaaaken>
We're close to making it production-ready. So, let's review the thing I
mentioned.
I also suggest you to sign in our Gitter channel to get some discussions
if you'll need
------------------------------
In README.md
<#141 (comment)>:
>
Usage:
```kotlin
val client = defaultHttpClient.fork {
interceptors {
- +LoggingInterceptor()
+ +LoggingInterceptor(false)
this change looks outdated now
------------------------------
In src/main/kotlin/io/github/rybalkinsd/kohttp/ext/RequestExt.kt
<#141 (comment)>:
> @@ -0,0 +1,38 @@
+package io.github.rybalkinsd.kohttp.ext
+
+import okhttp3.Request
+import okio.Buffer
+import java.nio.charset.Charset
+
+/**
+ * Build curl command of the request
+ *
+ * @author doyaaaaaken
+ */
+internal fun Request.buildCurlCommand(): String {
+ return StringBuilder().apply {
1. I suggest to avoid a big-big builder scope here.
2. we also have buildString to simplify
------------------------------
In src/main/kotlin/io/github/rybalkinsd/kohttp/ext/RequestExt.kt
<#141 (comment)>:
> +import okhttp3.Request
+import okio.Buffer
+import java.nio.charset.Charset
+
+/**
+ * Build curl command of the request
+ *
+ * @author doyaaaaaken
+ */
+internal fun Request.buildCurlCommand(): String {
+ return StringBuilder().apply {
+ append("curl -X ${method()}")
+
+ //headers
+ val headers = headers()
+ for (i in 0 until headers.size()) {
we have asSequence() method for headers
------------------------------
In src/main/kotlin/io/github/rybalkinsd/kohttp/ext/RequestExt.kt
<#141 (comment)>:
> +
+/**
+ * Build curl command of the request
+ *
+ * @author doyaaaaaken
+ */
+internal fun Request.buildCurlCommand(): String {
+ return StringBuilder().apply {
+ append("curl -X ${method()}")
+
+ //headers
+ val headers = headers()
+ for (i in 0 until headers.size()) {
+ val name = headers.name(i)
+ var value = headers.value(i)
+ if (value[0] == '"' && value[value.length - 1] == '"') {
this segment of code looks rather complicated for me, probably it will be
easier with the sequence, or will need further simplification
please check .last() and .first() methods
------------------------------
In src/main/kotlin/io/github/rybalkinsd/kohttp/ext/RequestExt.kt
<#141 (comment)>:
> +/**
+ * Build curl command of the request
+ *
+ * @author doyaaaaaken
+ */
+internal fun Request.buildCurlCommand(): String {
+ return StringBuilder().apply {
+ append("curl -X ${method()}")
+
+ //headers
+ val headers = headers()
+ for (i in 0 until headers.size()) {
+ val name = headers.name(i)
+ var value = headers.value(i)
+ if (value[0] == '"' && value[value.length - 1] == '"') {
+ value = "\\\"" + value.substring(1, value.length - 1) + "\\\""
probably, """ string""" triple quoted string will look better here
------------------------------
In src/main/kotlin/io/github/rybalkinsd/kohttp/ext/RequestExt.kt
<#141 (comment)>:
> + return StringBuilder().apply {
+ append("curl -X ${method()}")
+
+ //headers
+ val headers = headers()
+ for (i in 0 until headers.size()) {
+ val name = headers.name(i)
+ var value = headers.value(i)
+ if (value[0] == '"' && value[value.length - 1] == '"') {
+ value = "\\\"" + value.substring(1, value.length - 1) + "\\\""
+ }
+ append(" -H \"$name: $value\"")
+ }
+
+ //body
+ body()?.let {
please double-check LoggingInterceptor. it looks like a code duplication
------------------------------
In
src/main/kotlin/io/github/rybalkinsd/kohttp/interceptors/LoggingInterceptor.kt
<#141 (comment)>:
> }
}
}
+
+ private fun logAsHttp(request: Request) {
I suggest to refactor as a strategy pattern.
------------------------------
In
src/main/kotlin/io/github/rybalkinsd/kohttp/interceptors/LoggingInterceptor.kt
<#141 (comment)>:
> }
}
}
+
+ private fun logAsHttp(request: Request) {
+ //TODO: output http request format log.
+ // see #141 (comment)
+ request.headers().asSequence().forEach { log("${it.name}: ${it.value}") }
+ Buffer().use {
+ request.body()?.writeTo(it)
+ log(it.readByteString().utf8())
+ }
+ }
+
+ private fun logAsCurl(request: Request) {
+ val command = request.buildCurlCommand()
+ log("╭--- cURL command -------------------------------")
It's much better to keep the identical output style for both http and curl
------------------------------
In
src/main/kotlin/io/github/rybalkinsd/kohttp/interceptors/LoggingInterceptor.kt
<#141 (comment)>:
> }
}
}
+
+ private fun logAsHttp(request: Request) {
+ //TODO: output http request format log.
+ // see #141 (comment)
+ request.headers().asSequence().forEach { log("${it.name}: ${it.value}") }
+ Buffer().use {
+ request.body()?.writeTo(it)
+ log(it.readByteString().utf8())
+ }
+ }
+
+ private fun logAsCurl(request: Request) {
+ val command = request.buildCurlCommand()
+ log("╭--- cURL command -------------------------------")
here we don't really know the user's screen width, so ---- line m/b too
long
------------------------------
In src/main/kotlin/io/github/rybalkinsd/kohttp/ext/RequestExt.kt
<#141 (comment)>:
> @@ -0,0 +1,38 @@
+package io.github.rybalkinsd.kohttp.ext
+
+import okhttp3.Request
+import okio.Buffer
+import java.nio.charset.Charset
+
+/**
+ * Build curl command of the request
+ *
+ * @author doyaaaaaken
+ */
+internal fun Request.buildCurlCommand(): String {
After reading PR several times, I want to ask if we really need this ext
function, or we simply can split it into several methods within strategy
pattern, as I mentioned in another comment and put it close to the
LogginInterceptor package?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#141?email_source=notifications&email_token=ABJNJMMIB5GCZNDKWPO6DXTQCLAPXA5CNFSM4IFNUCB2YY3PNVWWK3TUL52HS4DFWFIHK3DMKJSXC5LFON2FEZLWNFSXPKTDN5WW2ZLOORPWSZGOCAIOOLI#pullrequestreview-269543213>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABJNJMPVSKCZ2NKP2TZCYU3QCLAPXANCNFSM4IFNUCBQ>
.
|
I agree with all of your review comments! Not yet finish to fix, so please wait. |
private fun buildCurlHeaderOption(headers: Headers): String { | ||
return headers.asSequence().map { header -> | ||
val value = header.value.let { v -> | ||
if (v.startsWith('"') && v.endsWith('"')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest introducing private method String.isQuoted()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that's better.
And I suggest String.trimDoubleQuote()
method is more simpler? ( ecae2fd )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, agreed
} | ||
|
||
private fun buildCurlBodyOption(body: RequestBody?): String { | ||
return if (body == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kotlin has a smart casting and also nullability checks.
It's possible to simplify code like this
return if (body == null) { | |
if (body == null) return "" |
and it will be also possible to omit else
statement
val buffer = Buffer().apply { body.writeTo(this) } | ||
val utf8 = Charset.forName("UTF-8") | ||
val charset = body.contentType()?.charset(utf8) ?: utf8 | ||
" --data $'" + buffer.readString(charset).replace("\n", "\\n") + "'" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please share why we need to replace \n
here?
Also looks like utf-8
should be introduced as a const for both HTTP and CURL at least
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
utf-8 should be introduced as a const for both HTTP and CURL at least
In case of HTTP, ByteString.utf8()
method is used currently.
kohttp/src/main/kotlin/io/github/rybalkinsd/kohttp/interceptors/LoggingInterceptor.kt
Line 49 in 9333601
log(it.readByteString().utf8()) |
And in case of CURL, we can use Buffer.readUtf8()
method in stead of assigning Charset
class.
Which do you think better?
- Use
Charset
class. So, I fix logic of HTTP case. - Use
Buffer.readUtf8()
method. So, I fix logic of CURL case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please share why we need to replace \n here?
Because we can output curl command in one-liner.
For example, if we wouldn't replace \n
, we would output below curl command.
curl -X POST --data $'{
"aa": "bb",
"aa1": "bb2"
}' "https://postman-echo.com/post"
But , with replacing \n
, we could output command as one-liner.
curl -X POST --data $'{\n "aa": "bb",\n "aa1": "bb2"\n}' "https://postman-echo.com/post"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, make sense
|
||
@Test | ||
fun `build curl command of request with header`() { | ||
val request = Request.Builder() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably, we need to introduce Request
dsl builder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot find Request
dsl builder in existing code.
So, do you mean that we should implement new dsl method like this?
request {
url = "https://postman-echo.com/post"
post = requestBody {
contentType = "application/x-www-form-urlencoded"
body = ""
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, unfortunately, there is no one at the moment.
But I think it's better to introduce it as a separate issue later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree!
@doyaaaaaken |
Co-Authored-By: Sergei Rybalkin <yan.brikl@gmail.com>
@rybalkinsd |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGFM!
Implements #139
I don't know if this PR is correct direction. So, feel free to decline this PR.