| // Copyright 2017 The Go Authors. All rights reserved. |
| // |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file or at |
| // https://developers.google.com/open-source/licenses/bsd. |
| |
| package requestlog |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "io" |
| "strconv" |
| "sync" |
| "time" |
| ) |
| |
| // A FluentdLogger writes log entries in the Fluentd forward JSON |
| // format. The record's fields are suitable for consumption by |
| // Stackdriver Logging. |
| type FluentdLogger struct { |
| tag string |
| onErr func(error) |
| |
| mu sync.Mutex |
| w io.Writer |
| buf bytes.Buffer |
| enc *json.Encoder |
| } |
| |
| // NewFluentdLogger returns a new logger that writes to w. |
| func NewFluentdLogger(w io.Writer, tag string, onErr func(error)) *FluentdLogger { |
| l := &FluentdLogger{ |
| tag: tag, |
| w: w, |
| onErr: onErr, |
| } |
| l.enc = json.NewEncoder(&l.buf) |
| return l |
| } |
| |
| // Log writes a record to its writer. Multiple concurrent calls will |
| // produce sequential writes to its writer. |
| func (l *FluentdLogger) Log(ent *Entry) { |
| if err := l.log(ent); err != nil { |
| l.onErr(err) |
| } |
| } |
| |
| func (l *FluentdLogger) log(ent *Entry) error { |
| defer l.mu.Unlock() |
| l.mu.Lock() |
| |
| l.buf.Reset() |
| l.buf.WriteByte('[') |
| if err := l.enc.Encode(l.tag); err != nil { |
| return err |
| } |
| l.buf.WriteByte(',') |
| t := ent.ReceivedTime.Add(ent.Latency) |
| if err := l.enc.Encode(t.Unix()); err != nil { |
| return err |
| } |
| l.buf.WriteByte(',') |
| |
| var r struct { |
| HTTPRequest struct { |
| RequestMethod string `json:"requestMethod"` |
| RequestURL string `json:"requestUrl"` |
| RequestSize int64 `json:"requestSize,string"` |
| Status int `json:"status"` |
| ResponseSize int64 `json:"responseSize,string"` |
| UserAgent string `json:"userAgent"` |
| RemoteIP string `json:"remoteIp"` |
| Referer string `json:"referer"` |
| Latency string `json:"latency"` |
| } `json:"httpRequest"` |
| Timestamp struct { |
| Seconds int64 `json:"seconds"` |
| Nanos int `json:"nanos"` |
| } `json:"timestamp"` |
| } |
| r.HTTPRequest.RequestMethod = ent.RequestMethod |
| r.HTTPRequest.RequestURL = ent.RequestURL |
| // TODO(light): determine whether this is the formula LogEntry expects. |
| r.HTTPRequest.RequestSize = ent.RequestHeaderSize + ent.RequestBodySize |
| r.HTTPRequest.Status = ent.Status |
| // TODO(light): determine whether this is the formula LogEntry expects. |
| r.HTTPRequest.ResponseSize = ent.ResponseHeaderSize + ent.ResponseBodySize |
| r.HTTPRequest.UserAgent = ent.UserAgent |
| r.HTTPRequest.RemoteIP = ent.RemoteIP |
| r.HTTPRequest.Referer = ent.Referer |
| r.HTTPRequest.Latency = string(appendLatency(nil, ent.Latency)) |
| r.Timestamp.Seconds = t.Unix() |
| r.Timestamp.Nanos = t.Nanosecond() |
| if err := l.enc.Encode(r); err != nil { |
| return err |
| } |
| l.buf.WriteByte(']') |
| _, err := l.w.Write(l.buf.Bytes()) |
| return err |
| } |
| |
| func appendLatency(b []byte, d time.Duration) []byte { |
| // Parses format understood by google-fluentd (which is looser than the documented LogEntry format). |
| // See the comment at https://github.com/GoogleCloudPlatform/fluent-plugin-google-cloud/blob/e2f60cdd1d97e79ffe4e91bdbf6bd84837f27fa5/lib/fluent/plugin/out_google_cloud.rb#L1539 |
| b = strconv.AppendFloat(b, d.Seconds(), 'f', 9, 64) |
| b = append(b, 's') |
| return b |
| } |