blob: 210df597638ed5daa27e52cac545ed18e30fdd9c [file] [log] [blame]
// 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["X-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
}