Skip to content

Conversation

@prodis
Copy link

@prodis prodis commented May 22, 2016

For some applications is valuable to have the option to log HTTP requests and responses. I have just added support to this.

Example using Rails.logger:

HTTP.log(Rails.logger).get('http://prodis.webstorelw.com.br/version.txt')

It will be logged request method, URL, headers and body, response status code, headers and body:

GET http://prodis.webstorelw.com.br/version.txt
Connection: close
Host: prodis.webstorelw.com.br
User-Agent: http.rb/2.0.1

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 22 May 2016 02:28:55 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 8
Last-Modified: Thu, 10 Mar 2016 01:04:48 GMT
Connection: close
Etag: "56e0c830-8"
Accept-Ranges: bytes

v4.24.0

If necessary, I can improve it with an optional range of options to log or not headers and body, for example.

What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

Why not just nil?

Copy link
Member

Choose a reason for hiding this comment

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

A agree. It should be either nil (I like this idea better), or at least some kind of cached null-logger.

Copy link
Author

Choose a reason for hiding this comment

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

@tarcieri @ixti
HTTP::Logger implicitly works like a null object. We can call HTTP::Logger#log even if its logger attribute is nil.
https://github.com/httprb/http/pull/348/files/9e0569ead67133f171b33582e515bade4185b970#diff-a32552e6c7aef4937f6b3148e3515abdR11

As an alternative we can use nil as default HTTP logger and to deal with this nil when HTTP::Logger#log is called in HTTP::Client class. 😉

Copy link
Member

Choose a reason for hiding this comment

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

Yeah but you will be allocating new instance everytime Options got initialized, which is not pretty awesome ;))

@ixti
Copy link
Member

ixti commented May 23, 2016

IMHO logic of request/response formatting is better to be extracted on Request/Response object:

class Request
  # @param [#<<] io
  def pretty_print(io: STDOUT, skip_headers: true, skip_body: true)
    message = "#{verb} #{uri}"
    headers.each { |k, v| message << "\n#{k}: #{v}" } unless skip_headers
    message << "\n\n#{body}" unless skip_headers || skip_body
    io << message 
  end
end

That way it will make this method possible to be used by #inspect for example:

class Request
  def inspect
    pretty_print(:io => "", :skip_headers => false, :skip_body => true)
  end
end

Also, please, beware that printing out body in response might not be a good idea by couple of reasons:

  • log will become pretty polluted with pretty useless info
  • log might become polluted with sensitive information
  • this might cause unexpected issues when you try to stream body

So, I propose to allow pass :with => [:headers, :body] to #log(logger) API to provide what should be logged. IMHO by default it should log ONLY headlines.

@prodis
Copy link
Author

prodis commented May 23, 2016

@ixti
I agree with you regarding to print body and headers not to be the default behavior. The interface of optional parameters that you proposed sounds good. I really expected to hear some opinions about printing the full log.

About to extract the formatting logic to Request and Response classes, HTTP::Response already has an implementation of inspect method. Can I reimplement it using the new pretty_print method?

@ixti
Copy link
Member

ixti commented May 23, 2016

@prodis Sure.

@prodis
Copy link
Author

prodis commented May 23, 2016

@ixti Great! I do all that things soon.

@prodis prodis force-pushed the log branch 4 times, most recently from 852377a to ff165dd Compare May 30, 2016 03:37
@prodis
Copy link
Author

prodis commented May 30, 2016

Done.

@prodis
Copy link
Author

prodis commented May 30, 2016

The build of Ruby 2.2.0 was breaking after run all tests green and Rubocop checks were OK. https://travis-ci.org/httprb/http/builds/133830598

I changed the RVM version from 2.2 to 2.2.5 in Travis configuration in order to fix it. If necessary I can checkout the modifications in .travis.yml file.

to_h.map do |name, value|
value = value.join("; ") if value.respond_to?(:join)
"#{name}: #{value}"
end.join(separator)
Copy link
Member

Choose a reason for hiding this comment

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

IMHO its' better to pretty print in the form we will send those headers to server.

Copy link
Author

Choose a reason for hiding this comment

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

@ixti
Sorry, I didn't get it.
Do you mean use "\n" separator or do you are talking about do not join multiples values of header with "; "?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, something like:

def pretty_print(separator: "\n")
  map { |k, v| "#{k}: #{v}" }.join(separator)
end

Copy link
Member

Choose a reason for hiding this comment

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

So, I was tlking about both :D

Copy link
Author

Choose a reason for hiding this comment

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

Hum, OK.
I used ", " separator to inspect method to return in a single line. Is it OK inspect methods return multiple lines?
Just confirming.

Same approach with inspect method in HTTP::Request and HTTP::Response classes?

Copy link
Author

Choose a reason for hiding this comment

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

Got it.
I am going to do this and the name issues in the other commentaries.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks!

Copy link
Author

Choose a reason for hiding this comment

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

Hey, @ixti.

One point about HTTP::Headers#pretty_print:
I used to_h.map {... instead of self.map {... to avoid duplicated headers with different values.

For example, for these headers:

headers.set :set_cookie, %w(hoo=ray woo=hoo)
headers.set :content_type, "application/json"

I believe we expected a printable version like this:
"Set-Cookie: hoo=ray; woo=hoo\nContent-Type: application/json"

And not like this:
"Set-Cookie: hoo=ray\nSet-Cookie: woo=hoo\nContent-Type: application/json"

What do you think?

Copy link
Member

@ixti ixti May 31, 2016

Choose a reason for hiding this comment

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

I'm not strongly against that. Although I thought it's better to print them just as they would be sent to server (we don't join them), but for pretty printing I think it worth to do so. Also, I think in this case it might worth to allow specify truncation length so that long lines would become truncated (have no opinion on this at all though). Anyway, if you think it worth join headers let' do that, but a bit more efficient way will be:

headers.group_by { |x| x[0] }.each do |key, values|
  io << "\n#{key}: #{values.join '; '}"
end

Copy link
Author

Choose a reason for hiding this comment

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

Great tip, @ixti!
I have never used group_by method.
It made my day!

@ixti ixti self-assigned this May 30, 2016
# "MyHttpRequest headline"
# end
# end
module Printer
Copy link
Member

Choose a reason for hiding this comment

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

Proabbly PrettyPrinter will be a bit better name? :D

@razum2um
Copy link

razum2um commented Jul 7, 2016

thanks for great suggestion @prodis, any updates? need help?
@ixti what's left to be done?

@prodis
Copy link
Author

prodis commented Jul 8, 2016

@razum2um @ixti
Really sorry for delay. Many changes are happening in my life and I didn't give more attention for this PR.

I need to finish the implementation of last comments. I promise to finish next week.

My apologies.

@prodis
Copy link
Author

prodis commented Jul 25, 2016

@razum2um @ixti
Finally done.

I didn't changed the original behaviour of Request#headline, so it will not log the entire request URI.
Travis is broken because ActiveSupport 5.0.0 requires Ruby version >= 2.2.2.

When possible, please give me a feedback.

private

def headline
"HTTP/#{@version} #{code} #{reason}"
Copy link
Member

Choose a reason for hiding this comment

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

It can be simply: "HTTP/#{@version} #{@status}"

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@ixti
Copy link
Member

ixti commented Jul 25, 2016

Thanks for all you hard work. I will try to walk through the PR and merge as soon as possible during this week.

@ixti
Copy link
Member

ixti commented Aug 1, 2016

After some consideration I think we should do this a bit different.
One of the main concerns I have is that in full format I would like to see request/response as is:

HTTP/1.1 200 OK
Content-Type: text/plain

Hello!

And in one-line format I would expect it to be something like:

HTTP/1.1 200 OK {"Content-Type" => "text/plain"}

It means that HTTP::Logger should receive not an options, but formatter instance.
Also that means we should provide 2 different formatters out of the box:

  • HTTP::Formatter::Pretty
  • HTTP::Formatter::Oneline

Both of those should respond to #call with single argument - Request or Response object. That will allow to create logger with formatter proc:

formatter = -> (obj) { "#{obj.class} | #{obj.headline}" }
logger = HTTP::Logger.new(STDOUT, :formatter => formatter)

By default HTTP::Formatter::Oneline for logger should be used, while Pretty with headers only for inspect.

And the last thing I would like to note, I think HTTP::Logger should also be an object that will respond to #call method, so that it will become possible to pass a simple proc for all the logging needs:

HTTP.log { |obj| puts obj }.get("http://example.com")

The above means that we will call logger separately for each req/res. Current PR does not handles logging upon redirects, which most likely should be handled too. I will try to

@prodis
Copy link
Author

prodis commented Aug 5, 2016

@ixti
I got it and I can do it. If I have some doubt during the development, I get in touch.
Maybe I will take a few weeks to finish it, because I am moving from my current country and I have a lot of things to deal.

Of course, if you want this feature early, feel free to delegate this task for another one. Really I would like to implement it.

Just one question: in the last line of your last comment, it seems you didn't finish the phrase "I will try to...". Am I correct?

@ixti
Copy link
Member

ixti commented Aug 5, 2016

@prodis heh, same here - lots of thing to handle in my life too. yeah, i haven't finished that phrase, but i forgot what i was going to say already :D

@ixti
Copy link
Member

ixti commented Aug 5, 2016

Oh. I was going to say that I will try to come up with better described API description later :D

@vassilevsky
Copy link
Contributor

Can I help with logging?

This was referenced Feb 11, 2017
@vjustov
Copy link

vjustov commented Oct 22, 2017

I dont want to be that guy but, any updates on this end?

@mikegee
Copy link
Contributor

mikegee commented Oct 23, 2017

@vassilevsky & @vjustov I think it would be acceptable to take this work, finish implementing @ixti's recommendations, and open another PR. (Retaining @prodis as the author of those commits somehow.)

@prodis
Copy link
Author

prodis commented Oct 23, 2017

Sorry for long time to have this PR stopped in my side. 🙇
If you might wait for more one week, by the end of the weekend I can finish it.

But @mikegee, @vassilevsky and @vjustov feel free to finish in some way before, just let me know. 😉

@tarcieri
Copy link
Member

Just wanted to note there’s similar functionality in the Reel web server, and I always liked color coding the log output:

https://github.com/celluloid/reel/wiki/Spy

reel log

@vassilevsky
Copy link
Contributor

vassilevsky commented Oct 23, 2017 via email

@tarcieri tarcieri mentioned this pull request May 28, 2018
@tarcieri
Copy link
Member

I think this was addressed by #499

@tarcieri tarcieri closed this Sep 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants