| // Copyright 2021 Google Inc. All rights reserved. |
| // Use of this source code is governed by the Apache 2.0 |
| // license that can be found in the LICENSE file. |
| |
| package internal |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "regexp" |
| "strings" |
| ) |
| |
| var ( |
| logLevelName = map[int64]string{ |
| 0: "DEBUG", |
| 1: "INFO", |
| 2: "WARNING", |
| 3: "ERROR", |
| 4: "CRITICAL", |
| } |
| traceContextRe = regexp.MustCompile(`^(\w+)/(\d+)(?:;o=[01])?$`) |
| |
| // maxLogMessage is the largest message that will be logged without chunking, reserving room for prefixes. |
| // See http://cloud/logging/quotas#log-limits |
| maxLogMessage = 255000 |
| ) |
| |
| func logf(c *context, level int64, format string, args ...interface{}) { |
| if c == nil { |
| panic("not an App Engine context") |
| } |
| |
| if !IsStandard() { |
| s := strings.TrimRight(fmt.Sprintf(format, args...), "\n") |
| now := timeNow().UTC() |
| timestamp := fmt.Sprintf("%d/%02d/%02d %02d:%02d:%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) |
| fmt.Fprintf(logStream, "%s %s: %s\n", timestamp, logLevelName[level], s) |
| return |
| } |
| |
| eol := func(s string) string { |
| if strings.HasSuffix(s, "\n") { |
| return "" |
| } |
| return "\n" |
| } |
| |
| msg := fmt.Sprintf(format, args...) |
| |
| if strings.HasPrefix(msg, "{") { |
| // Assume the message is already structured, leave as-is unless it is too long. |
| // Note: chunking destroys the structure; developers will have to ensure their structured log |
| // is small enough to fit in a single message. |
| for _, m := range chunkLog(msg) { |
| fmt.Fprint(logStream, m, eol(m)) |
| } |
| return |
| } |
| |
| // First chunk the message, then structure each chunk. |
| traceID, spanID := traceAndSpan(c) |
| for _, m := range chunkLog(msg) { |
| sl := structuredLog{ |
| Message: m, |
| Severity: logLevelName[level], |
| TraceID: traceID, |
| SpanID: spanID, |
| } |
| if b, err := json.Marshal(sl); err != nil { |
| // Write raw message if error. |
| fmt.Fprint(logStream, m, eol(m)) |
| } else { |
| s := string(b) |
| fmt.Fprint(logStream, s, eol(s)) |
| } |
| } |
| } |
| |
| type structuredLog struct { |
| Message string `json:"message"` |
| Severity string `json:"severity"` |
| TraceID string `json:"logging.googleapis.com/trace,omitempty"` |
| SpanID string `json:"logging.googleapis.com/spanId,omitempty"` |
| } |
| |
| func chunkLog(msg string) []string { |
| if len(msg) <= maxLogMessage { |
| return []string{msg} |
| } |
| var chunks []string |
| i := 0 |
| for { |
| if i == len(msg) { |
| break |
| } |
| if i+maxLogMessage > len(msg) { |
| chunks = append(chunks, msg[i:]) |
| break |
| } |
| chunks = append(chunks, msg[i:i+maxLogMessage]) |
| i += maxLogMessage |
| } |
| for i, c := range chunks { |
| chunks[i] = fmt.Sprintf("Part %d/%d: ", i+1, len(chunks)) + c |
| } |
| return chunks |
| } |
| |
| func traceAndSpan(c *context) (string, string) { |
| headers := c.req.Header["Cloud-Trace-Context"] |
| if len(headers) < 1 { |
| return "", "" |
| } |
| matches := traceContextRe.FindAllStringSubmatch(headers[0], -1) |
| if len(matches) < 1 || len(matches[0]) < 3 { |
| return "", "" |
| } |
| traceID := matches[0][1] |
| spanID := matches[0][2] |
| projectID := projectID() |
| return fmt.Sprintf("projects/%s/traces/%s", projectID, traceID), spanID |
| } |