-
Notifications
You must be signed in to change notification settings - Fork 226
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
As an operator of Cloud Foundry, I want to enable logging of specified headers for all applications. #92
Conversation
Hey simonjohansson! Thanks for submitting this pull request! I'm here to inform the recipients of the pull request that you've already signed the CLA. |
We have created an issue in Pivotal Tracker to manage this. You can view the current status of your issue at: https://www.pivotaltracker.com/story/show/99988708. |
@@ -261,5 +265,7 @@ func InitConfigFromFile(path string) *Config { | |||
|
|||
c.Process() | |||
|
|||
ExtraHeadersToLog = c.ExtraHeadersToLog |
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 are globally exposing a list of headers we want to log. It works but is a bit ugly.
This is because we don't have access to the config struct when calling MakeRecord() and we did not want to refactor the world without guidance.
Hi Thanks for the PR, reviewed and here are our comments:
This new configuration entry will have to be optional and operators will need Instead of using a global variable in config.go, we'd suggest
For example: type AccessLogRecord struct {
Request *http.Request
StatusCode int
RouteEndpoint *route.Endpoint
StartedAt time.Time
FirstByteAt time.Time
FinishedAt time.Time
BodyBytesSent int
RequestBytesReceived int
OptionalHeaders map[string]string
}
func NewAccessLogRecord(request *http.Request, startedAt time.Time, logHeaders []string) *AccessLogRecord {
accessLog := &AccessLogRecord{
Request: request,
StartedAt: startedAt,
}
accessLog.setOptionalHeaders(logHeaders)
return accessLog
}
func (r *AccessLogRecord) setOptionalHeaders(logHeaders []string) {
if len(logHeaders) > 0 {
r.OptionalHeaders = make(map[string]string)
for _, header := range logHeaders {
r.OptionalHeaders[header] = r.FormatRequestHeader(header)
}
}
}
if r.OptionalHeaders != nil {
marshalledExtraHeadersToLog, _ := json.Marshal(r.OptionalHeaders)
fmt.Fprintf(b, `headers:%s`, string(marshalledExtraHeadersToLog))
} @markstgodard and @mmb Thanks for the PR, let us know if you have any questions. |
+1 for the @mmb idea. |
I took a slightly different approach, rather than passing in a list of headers I want to (a new func) NewAccessLogRecord and adding the lookup results to AccessLogRecord.OptionalHeaders I added AccessLogRecord.ExtraHeadersToLog which contains the list of the headers I want, and populate it ServeHTTP, as this require less refactoring of the codebase/tests. I have yet to write tests cases for when marshalling fails, but will look into that. I will also write some gatling tests to verify the performance impact of using the new feature. |
@simonjohansson That approach looks fine to us. Let us known when you have everything ready to go. |
@shinji62 @mmb @crhino @mrdavidlaing So, some JMeter tests. Running these on a VM with 8gigs of ram, 4 cpus in the same network as the CF installation.
The app Im targeting
JMeter tests.
Ive run two test cases, one with 10 threads and 1000 loops, and one with 10 threads and 10000 loops. Ie 10k vs 100k requests in each test case and I get slightly different results. 10k requests
100k requests
Some takeaways
All in all, Im happy with the performance. |
Nice seems no different when not enabled. Just a question did you try to compile with go 1.5 ? Envoyé de mon iPhone
|
Nope, compiled with go1.4.2 darwin/amd64, gorouter compiled with 1.5 in CF release? Edit: had a peak, 1.4.2 in CF Release. |
No no I was just wondering if perf improve ;) Envoyé de mon iPhone
|
} | ||
marshalledExtraHeadersToLog, err := json.Marshal(extraHeadersToLog) | ||
if err != nil { | ||
return "{}" |
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.
@simonjohansson I think we might want to return something to the effect of "UnableToMarshalJson" instead of an empty hash here, in keeping with some of the other fields. That way a developer/operator would no something is wrong with their logging instead of being confused as to why the extra headers did not show up.
Commented inline, if you could make that change and add a test case for it that would be great. Otherwise looks good to go. We have this PR prioritized in our backlog here. |
Also might want to rebase / squash the commits Cheers |
@crhino Ive made the returned value from ExtraHeaders() a bit more clear. But I have a hard time creating a test case as ExtraHeadersToLog[1] is typed as []string, FormatRequestHeader[2] returns a string, extraHeadersToLog[3](which we marshal) is typed map[string]string, http.Headers used by the tests[4] is typed map[string][]string so everywhere where we deal with the headers we are dealing with strings, and thus would not compile if I try to add a int somewhere for instance. Do you have any pointers as how to create a failing test case? [1] https://github.com/simonjohansson/gorouter/blob/add-extra-headers-to-log/access_log/access_log_record.go#L23 |
680b899
to
ca044ce
Compare
@markstgodard rebased and squashed. |
for _, header := range r.ExtraHeadersToLog { | ||
extraHeadersToLog[header] = r.FormatRequestHeader(header) | ||
} | ||
marshalledExtraHeadersToLog, err := json.Marshal(extraHeadersToLog) |
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.
Why are you using JSON here? The rest of the log line uses the header_name: "Header value"
format, why not reusing the same format?
(I wouldn't mind using JSON for the whole log, mind you... it's just that having JSON embedded in a text log means you need not one but two parsers!)
@CAFxX Maybe you should read the discussion here http://cf-dev.70369.x6.nabble.com/cf-dev-Allow-gorouter-to-log-random-headers-td800.html#a912 "The reason for a stringified JSON is to make it easy to parse with logstash or other loganalysis tools" |
@shinji62 yup I read that discussion before posting, and the fact is that my point still stands: you need two parsers (custom text format, JSON) instead of one (custom text format) :) |
I would ask that we keep the logging format consistent. If that format is not easily consumable, let's address that holistically as an independent issue. |
I think that the json format is the easiest and least error prone to deal with. Take these two examples Say for instance that we care about two headers for all our application, Header-A, Header-B. With json
With normal fields
A Grok pattern for json
A grok pattern for normal fields.
I see two issues with a grok pattern for normal fields.
In https://github.com/logsearch/logsearch-for-cloudfoundry which is a generic release for random peoples CF installations we don't know upfront what headers they want, if any. And I don't think the users are to keen to themselves have to create the Grok patterns, the beauty of the project is that you basically do a "bosh install" and magically gets all the logs from CF/Apps parsed and ready to go for free. I hope this clears up the rational behind the json vs fields. |
@shalako; on the issue of consistancy; as you can see we've thought about this carefully, and decided that in this case usability of the logging the "user configured extra fields" as JSON outweighs the apparent difference in format. Also, if you squint, the log format is consistent 😄 :
Note how the "value" of the |
Not ok to merge yet. The introduction of a new format for admin-managed logged headers is a concern. We are considering and will reply. |
In the interests of getting this moving again, if we updated this PR to remove the JSON from the logging format, i.e.:
would that get us back on track to getting this merged? Thanks David |
@mrdavidlaing I was just re-reviewing this PR and we will finalize decision w/ @shalako... stay tuned. |
Thank you, @mrdavidlaing. Yes, this should get us on back on track. We'll likely review your latest changes tomorrow and merge if it looks good. |
We'll need the new property Please confirm that:
|
PR updated to revert to the current logging format.
Yes, as soon as this is merged I will send a PR for cf-release spec/template/relevant spiff-templates
Extra headers to be logged is derived from the gorouter config. So if the config changes, and the goprocess is restarted all of the above will be true.
This will be the case. The details on how this is to be done is a discussion better kept in the PR from cf-release I think |
for _, header := range r.ExtraHeadersToLog { | ||
// X-Something-Cool -> x_something_cool | ||
formatted_header_name := strings.Replace(strings.ToLower(header), "-", "_", -1) | ||
headerString := fmt.Sprintf("%s:%s", formatted_header_name, r.FormatRequestHeader(header)) |
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 we should double quote the header values when we format to ensure we handle lists and so parsing of log files should be predictable (key:"value" or key:"value1, value2")
For example: If we wanted to log the X-Forwarded-Proto and X-Forwarded-For headers
X-Forwarded-Proto: https, http
X-Forwarded-For: 10.0.0.1, 10.0.0.2
We would want the access log elements to look like this:
x_forwarded_proto:"https, http" x_forwarded_for:"10.0.0.1, 10.0.0.2"
vs.
x_forwarded_proto:https, http x_forwarded_for:10.0.0.1, 10.0.0.2
Cheers
@shalako @simonjohansson Reviewed and provided my comments on last commit in PR Cheers |
Good spot, altough now we break the original layout again... :P Ill get cracking at it first thing monday, |
Header values are now quoted. Updated test to take into when we want a header that doesnt exist, when a header has a quoted value in it and when the header have a list of values in it. |
@simonjohansson Everything looks good, but we would prefer that the PR to cf-release at least be opened. Otherwise, we are not able to validate the feature from a user-facing perspective. We could of course change the config on the router VM, but without the property being exposed in the manifest this PR has no real value. Sorry for the run around here, we also want to get this PR merged! If you have thoughts about why merging this PR first is better, I would love to hear them. |
@crhino sure thang, Ill get cracking on it right away. The reason why I would prefer this to be merged beforehand is because we would expose configuration in the release that does nothing, as the relevant code is not merged for the component it controls. Doesn't matter at the end of the day. :) |
@@ -69,6 +71,10 @@ func (r *AccessLogRecord) makeRecord() *bytes.Buffer { | |||
fmt.Fprintf(b, `app_id:%s`, r.RouteEndpoint.ApplicationId) | |||
} | |||
|
|||
if r.ExtraHeadersToLog != nil { |
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 check for empty slice. We do not want to log an space in case ExtraHeadersToLog is an empty slice, which is the default value.
Otherwise we will log a uneccecary seperator space.
@leochu ah, good catch, thanks! Pushed a fix. |
Could someone retrigger travis? |
These changes seems to have been merged/cherry-picked into master, so I will close the PR. |
Thank you for working with us to get this in, @simonjohansson |
This PR implements the use case
It is based on the discussion on the mailing list.
If you specify the following in your gorouter config
your access log line will look like