This repository has been archived by the owner on May 7, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathgrouplogger.go
319 lines (288 loc) · 9.12 KB
/
grouplogger.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
// Package grouplogger wraps a Stackdriver logging client to facilitate writing
// groups of log entries, similar to the default behavior in Google App Engine
// Standard.
//
// var r *http.Request
//
// ctx := context.Background()
// cli, err := NewClient(ctx, "logging-parent")
// if err != nil {
// // Handle "failed to generate Stackdriver client."
// }
//
// logger := cli.Logger(r, "app_identifier", logging.CommonLabels(WithHostname(nil)))
//
// logger.Info("Info log entry body.")
// logger.Error("Error log entry body.")
//
// logger.Close()
package grouplogger
import (
"context"
"fmt"
"net/http"
"os"
"sync"
"cloud.google.com/go/compute/metadata"
"cloud.google.com/go/logging"
"github.com/google/uuid"
"google.golang.org/api/option"
)
const (
// outerFormat is a format string for a GroupLogger's outer log name.
outerFormat = "%v-request"
// innerFormat is a format string for a GroupLogger's inner log name.
innerFormat = "%v-app"
)
// Client adds different Logger generation to Stackdriver's logging.Client.
//
// It can be reused across multiple requests to generate a Logger for each one
// without repeating auth.
type Client struct {
innerClient *logging.Client
}
// NewClient generates a new Client associated with the provided parent.
//
// Options are documented here:
// https://godoc.org/google.golang.org/api/option#ClientOption
func NewClient(ctx context.Context, parent string, opts ...option.ClientOption) (*Client, error) {
client, err := logging.NewClient(ctx, parent, opts...)
if err != nil {
return &Client{}, err
}
return &Client{client}, nil
}
// Close waits for all opened GroupLoggers to be flushed and closes the client.
func (client *Client) Close() error {
return client.innerClient.Close()
}
// Logger constructs and returns a new GroupLogger object for a new group of log
// entries corresponding to a request R.
//
// Logger options (labels, resources, etc.) are documented here:
// https://godoc.org/cloud.google.com/go/logging#LoggerOption
func (client *Client) Logger(r *http.Request, name string, opts ...logging.LoggerOption) *GroupLogger {
outerLogger := client.innerClient.Logger(fmt.Sprintf(outerFormat, name), opts...)
innerLogger := client.innerClient.Logger(fmt.Sprintf(innerFormat, name), opts...)
// Use trace from request if available; otherwise generate a group ID.
gl := &GroupLogger{
Req: r,
GroupID: getGroupID(r, func() string {
return uuid.New().String()
}),
OuterLogger: outerLogger,
InnerLogger: innerLogger,
}
return gl
}
// Ping reports whether the client's connection to the logging service and the
// authentication configuration are valid. To accomplish this, Ping writes a log
// entry "ping" to a log named "ping".
func (client *Client) Ping(ctx context.Context) error {
return client.innerClient.Ping(ctx)
}
// SetOnError sets the function that is called when an error occurs in a call to
// Log. This function should be called only once, before any method of Client is
// called.
//
// Detailed OnError behavior is documented here:
// https://godoc.org/cloud.google.com/go/logging#Client
func (client *Client) SetOnError(f func(err error)) {
client.innerClient.OnError = f
}
// GroupLogger wraps two Stackdriver Logger clients. The OuterLogger is used to
// write the entries by which other entries are grouped: usually, these are
// requests. The InnerLogger is used to write the grouped (enclosed) entries.
//
// These groups are associated in the Stackdriver logging console by their
// GroupID.
//
// For the inner entries to appear grouped, either `LogOuterEntry` or
// `CloseWith` must be called.
type GroupLogger struct {
Req *http.Request
GroupID string
OuterLogger loggerLike
InnerLogger *logging.Logger
InnerEntries []logging.Entry
}
type loggerLike interface {
Log(logging.Entry)
}
type mockLogger struct {
LogFunc func(logging.Entry)
}
func (l *mockLogger) Log(e logging.Entry) {
l.LogFunc(e)
}
// Close calls CloseWith without specifying statistics. It does not close the
// client that generated the GroupLogger.
//
// Latency, status, response size, etc. are set to 0 or nil.
func (gl *GroupLogger) Close() {
gl.CloseWith(&logging.HTTPRequest{})
}
// CloseWith decorates the group's base request with the GroupID and with the
// maximum severity of the inner entries logged so far. It does not close the
// client that generated the GroupLogger.
//
// If LogOuterEntry is not called, nothing from this group will appear in
// the outer log.
func (gl *GroupLogger) CloseWith(stats *logging.HTTPRequest) {
if gl.Req != nil {
stats.Request = gl.Req
}
entry := logging.Entry{
Trace: gl.GroupID,
Severity: gl.getMaxSeverity(),
HTTPRequest: stats,
}
gl.LogOuterEntry(entry)
}
// LogInnerEntry pushes an inner log entry for the group, decorated with the
// GroupID.
func (gl *GroupLogger) LogInnerEntry(entry logging.Entry) {
entry.Trace = gl.GroupID
gl.InnerLogger.Log(entry)
gl.InnerEntries = append(gl.InnerEntries, entry)
}
// LogOuterEntry pushes the top-level log entry for the group, decorated
// with the GroupID.
//
// For the group to be grouped in the GCP logging console, ENTRY must have
// entry.HTTPRequest set.
func (gl *GroupLogger) LogOuterEntry(entry logging.Entry) {
entry.Trace = gl.GroupID
gl.OuterLogger.Log(entry)
}
// Log logs the payload as an inner entry with severity logging.Default.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Log(e logging.Entry) {
gl.LogInnerEntry(e)
}
// Default logs the payload as an inner entry with severity logging.Default.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Default(payload interface{}) {
gl.LogInnerEntry(logging.Entry{
Severity: logging.Default,
Payload: payload,
})
}
// Debug logs the payload as an inner entry with severity logging.Debug.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Debug(payload interface{}) {
gl.LogInnerEntry(logging.Entry{
Severity: logging.Debug,
Payload: payload,
})
}
// Info logs the payload as an inner entry with severity logging.Info.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Info(payload interface{}) {
gl.LogInnerEntry(logging.Entry{
Severity: logging.Info,
Payload: payload,
})
}
// Notice logs the payload as an inner entry with severity logging.Notice.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Notice(payload interface{}) {
gl.LogInnerEntry(logging.Entry{
Severity: logging.Notice,
Payload: payload,
})
}
// Warning logs the payload as an inner entry with severity logging.Warning.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Warning(payload interface{}) {
gl.LogInnerEntry(logging.Entry{
Severity: logging.Warning,
Payload: payload,
})
}
// Error logs the payload as an inner entry with severity logging.Error.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Error(payload interface{}) {
gl.LogInnerEntry(logging.Entry{
Severity: logging.Error,
Payload: payload,
})
}
// Critical logs the payload as an inner entry with severity logging.Critical.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Critical(payload interface{}) {
gl.LogInnerEntry(logging.Entry{
Severity: logging.Critical,
Payload: payload,
})
}
// Alert logs the payload as an inner entry with severity logging.Alert.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Alert(payload interface{}) {
gl.LogInnerEntry(logging.Entry{
Severity: logging.Alert,
Payload: payload,
})
}
// Emergency logs the payload as an inner entry with severity logging.Emergency.
// Payload must be JSON-marshalable.
func (gl *GroupLogger) Emergency(payload interface{}) {
gl.LogInnerEntry(logging.Entry{
Severity: logging.Emergency,
Payload: payload,
})
}
// getMaxSeverity returns the highest severity among the inner entries logged by
// a grouplogger.
// Logging severities are specified in Stackdriver documentation.
func (gl *GroupLogger) getMaxSeverity() logging.Severity {
max := logging.Default
for _, entry := range gl.InnerEntries {
if entry.Severity > max {
max = entry.Severity
}
}
return max
}
// getGroupID selects an ID by which the group will be grouped in the Google
// Cloud Logging console.
//
// If the `X-Cloud-Trace-Context` header is set in the request by GCP
// middleware, then that trace ID is used.
//
// Otherwise, a pseudorandom UUID is used.
func getGroupID(r *http.Request, UUIDFunc func() string) string {
// If the trace header exists, use the trace.
if r != nil {
if id := r.Header.Get("X-Cloud-Trace-Context"); id != "" {
return id
}
}
return UUIDFunc()
}
var detectedHost struct {
hostname string
once sync.Once
}
// WithHostname adds the hostname to a labels map. Useful for setting common
// labels: logging.CommonLabels(WithHostname(labels))
func WithHostname(labels map[string]string) map[string]string {
if labels == nil {
labels = make(map[string]string)
}
detectedHost.once.Do(func() {
if metadata.OnGCE() {
instanceName, err := metadata.InstanceName()
if err == nil {
detectedHost.hostname = instanceName
}
} else {
hostname, err := os.Hostname()
if err == nil {
detectedHost.hostname = hostname
}
}
})
labels["hostname"] = detectedHost.hostname
return labels
}