funcframework/logging.go (155 lines of code) (raw):

package funcframework import ( "bufio" "context" "encoding/json" "fmt" "io" "math/rand" "net/http" "os" "regexp" "sync" "time" ) var ( loggingIDsContextKey contextKey = "loggingIDs" validXCloudTraceContext = regexp.MustCompile( // Matches on "TRACE_ID" `([a-f\d]+)?` + // Matches on "/SPAN_ID" `(?:/([a-f\d]+))?` + // Matches on ";0=TRACE_TRUE" `(?:;o=(\d))?`) ) type loggingIDs struct { trace string spanID string executionID string } type contextKey string func addLoggingIDsToRequest(r *http.Request) *http.Request { executionID := r.Header.Get("Function-Execution-Id") if executionID == "" { timestamp := time.Now().UnixNano() random := rand.Int63() executionID = fmt.Sprintf("%06x%06x", timestamp, random) } traceID, spanID, _ := deconstructXCloudTraceContext(r.Header.Get("X-Cloud-Trace-Context")) if executionID == "" && traceID == "" && spanID == "" { return r } r = r.WithContext(contextWithLoggingIDs(r.Context(), &loggingIDs{ trace: traceID, spanID: spanID, executionID: executionID, })) return r } func contextWithLoggingIDs(ctx context.Context, loggingIDs *loggingIDs) context.Context { return context.WithValue(ctx, loggingIDsContextKey, loggingIDs) } func loggingIDsFromContext(ctx context.Context) *loggingIDs { val := ctx.Value(loggingIDsContextKey) if val == nil { return nil } return val.(*loggingIDs) } func TraceIDFromContext(ctx context.Context) string { ids := loggingIDsFromContext(ctx) if ids == nil { return "" } return ids.trace } func ExecutionIDFromContext(ctx context.Context) string { ids := loggingIDsFromContext(ctx) if ids == nil { return "" } return ids.executionID } func SpanIDFromContext(ctx context.Context) string { ids := loggingIDsFromContext(ctx) if ids == nil { return "" } return ids.spanID } func deconstructXCloudTraceContext(s string) (traceID, spanID string, traceSampled bool) { // As per the format described at https://cloud.google.com/trace/docs/setup#force-trace // "X-Cloud-Trace-Context: TRACE_ID/SPAN_ID;o=TRACE_TRUE" // for example: // "X-Cloud-Trace-Context: 105445aa7843bc8bf206b120001000/1;o=1" matches := validXCloudTraceContext.FindStringSubmatch(s) if matches != nil { traceID, spanID, traceSampled = matches[1], matches[2], matches[3] == "1" } if spanID == "0" { spanID = "" } return } // structuredLogEvent declares a subset of the fields supported by cloudlogging structured log events. // See https://cloud.google.com/logging/docs/structured-logging. type structuredLogEvent struct { Message string `json:"message"` Trace string `json:"logging.googleapis.com/trace,omitempty"` SpanID string `json:"logging.googleapis.com/spanId,omitempty"` Labels map[string]string `json:"logging.googleapis.com/labels,omitempty"` } // structuredLogWriter writes structured logs type structuredLogWriter struct { mu sync.Mutex w io.Writer loggingIDs loggingIDs buf []byte } func (w *structuredLogWriter) writeStructuredLog(loggingIDs loggingIDs, message string) (int, error) { event := structuredLogEvent{ Message: message, Trace: loggingIDs.trace, SpanID: loggingIDs.spanID, } if loggingIDs.executionID != "" { event.Labels = map[string]string{ "execution_id": loggingIDs.executionID, } } marshalled, err := json.Marshal(event) if err != nil { return 0, err } marshalled = append(marshalled, '\n') return w.w.Write(marshalled) } func (w *structuredLogWriter) Write(output []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() w.buf = append(w.buf, output...) buf := w.buf wroteLines := 0 for { advance, token, err := bufio.ScanLines(buf, false) if token == nil || err != nil { break } buf = buf[advance:] if _, err := w.writeStructuredLog(w.loggingIDs, string(token)); err != nil { return 0, err } wroteLines += 1 } if wroteLines > 0 { // Compact the buffer by copying remaining bytes to the start. w.buf = append(w.buf[:0], buf...) } return len(output), nil } func (w *structuredLogWriter) Close() error { if len(w.buf) == 0 { return nil } _, err := w.writeStructuredLog(w.loggingIDs, string(w.buf)) return err } // LogWriter returns an io.Writer as a log sink for the request context. // One log event is generated for each new line terminated byte sequence // written to the io.Writer. // // This can be used with common logging frameworks, for example: // // import ( // "log" // "github.com/GoogleCloudPlatform/functions-framework-go/funcframework" // ) // ... // func helloWorld(w http.ResponseWriter, r *http.Request) { // l := log.New(funcframework.LogWriter(r.Context()), "", 0) // l.Println("hello world!") // } func LogWriter(ctx context.Context) io.WriteCloser { loggingIDs := loggingIDsFromContext(ctx) if loggingIDs == nil { return os.Stderr } return &structuredLogWriter{ w: os.Stderr, loggingIDs: *loggingIDs, } }