blob: ba672f4ab63b0f0963c4e2b2bfeb962c0b5b0e3e [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package cloud
import (
cloudLogging ""
var (
// LogIDRegexp is a regular expression that can be used to validate a Log ID.
// From:
LogIDRegexp = regexp.MustCompile(`^[A-Za-z0-9/_\-\.]{0,512}$`)
// TraceIDLogLabel is the label used to express a request's trace ID in
// Stackdriver LogEntry messages. Attaching a consistent trace ID label to
// different logs allows them to be associated with the same trace.
TraceIDLogLabel = ""
consoleLoggerConfig = gologger.LoggerConfig{
Out: os.Stderr,
Format: gologger.StdFormat,
// InsertIDGenerator generates logging Insert IDs from a base key and an
// atomically-incrementing number. It is safe for concurrent usage.
// Insert IDs must be unique, so it's critical that Base is unique for any given
// InsertIDGenerator.
type InsertIDGenerator struct {
Counter int64
Base string
// Next returns the next InsertID in the InsertIDGenerator sequence.
// As a convenience, a nil InsertIDGenerator will return an empty string.
func (gen *InsertIDGenerator) Next() string {
if gen == nil {
return ""
nextValue := atomic.AddInt64(&gen.Counter, 1)
if nextValue == 0 {
// (This should be impossible)
panic("insert ID generator counter wrapped")
return fmt.Sprintf("%s_%d", gen.Base, nextValue-1)
// LogSeverityTracker tracks the highest observed log severity. It is safe for
// concurrent access.
type LogSeverityTracker struct {
observed bool
highestSeverity cloudLogging.Severity
lock sync.Mutex
// HighestSeverity returns the highest logging severity that is been observed.
// If no logging severity has been explicitly observed, Default severity will
// be returned.
func (lst *LogSeverityTracker) HighestSeverity() cloudLogging.Severity {
defer lst.lock.Unlock()
if !lst.observed {
return cloudLogging.Default
return lst.highestSeverity
// Observe updates the LogSeverityTracker's highest observed log severity.
// Observe is safe for concurrent usage.
func (lst *LogSeverityTracker) Observe(s cloudLogging.Severity) {
defer lst.lock.Unlock()
if !lst.observed || s > lst.highestSeverity {
lst.highestSeverity = s
lst.observed = true
// NewConsoleLogger instantiates a new Logger instance that logs to STDERR.
// STDERR is independently captured in Flex environments.
func NewConsoleLogger(c context.Context) logging.Logger {
return consoleLoggerConfig.NewLogger(c)
// LoggerConfig configures a Logger instance.
type LoggerConfig struct {
// SeverityTracker, if not nil, will be updated with the severity of log
// messages sent through the generated Logger(s).
SeverityTracker *LogSeverityTracker
// InsertIDGenerator, if not nil, will be used to generate InsertIDs for
// generated logs.
InsertIDGenerator *InsertIDGenerator
// Trace, if not empty, is the trace to associated with each log entry.
Trace string
// LogToSTDERR, if true, indicates that log messages should be tee'd to
// a simple STDERR logger prior to being written.
LogToSTDERR bool
// The given labels will be applied to each log entry. This allows to reuse
// Logger instance between requests, even if they have different default
// labels.
Labels map[string]string
// WithLogger installs an instance of a logging.Logger that forwards logs to an
// underlying Stackdriver Logger into the supplied Context.
// An alternative is to construct Logger per request, setting request labels via
// CommonLabels(...) option. But it is more heavy solution, and at the time of
// writing it leads to memory leaks:
func WithLogger(c context.Context, l *cloudLogging.Logger, cfg *LoggerConfig) context.Context {
if cfg == nil {
cfg = &LoggerConfig{}
return logging.SetFactory(c, func(c context.Context) logging.Logger {
bl := boundLogger{
Context: c,
LoggerConfig: *cfg,
cl: l,
if cfg.LogToSTDERR {
bl.stderrLogger = NewConsoleLogger(c)
return &bl
type boundLogger struct {
stderrLogger logging.Logger
cl *cloudLogging.Logger
func (bl *boundLogger) Debugf(format string, args ...interface{}) {
bl.LogCall(logging.Debug, 1, format, args)
func (bl *boundLogger) Infof(format string, args ...interface{}) {
bl.LogCall(logging.Info, 1, format, args)
func (bl *boundLogger) Warningf(format string, args ...interface{}) {
bl.LogCall(logging.Warning, 1, format, args)
func (bl *boundLogger) Errorf(format string, args ...interface{}) {
bl.LogCall(logging.Error, 1, format, args)
func (bl *boundLogger) LogCall(lvl logging.Level, calldepth int, format string, args []interface{}) {
if bl.stderrLogger != nil {
bl.stderrLogger.LogCall(lvl, calldepth+1, format, args)
severity := severityForLevel(lvl)
if bl.SeverityTracker != nil {
fields := logging.GetFields(bl)
line := fmt.Sprintf(format, args...)
if len(fields) > 0 {
line = fmt.Sprintf("%s :: %s", line, fields.String())
// Per docs, 'Log' takes ownership of Labels map, so make a copy.
var labels map[string]string
if len(bl.Labels) != 0 {
labels = make(map[string]string, len(bl.Labels))
for k, v := range bl.Labels {
labels[k] = v
// Generate a LogEntry for the supplied parameters.{
Severity: severity,
Payload: line,
Labels: labels,
InsertID: bl.InsertIDGenerator.Next(),
Trace: bl.Trace,
func severityForLevel(l logging.Level) cloudLogging.Severity {
switch l {
case logging.Debug:
return cloudLogging.Debug
case logging.Info:
return cloudLogging.Info
case logging.Warning:
return cloudLogging.Warning
case logging.Error:
return cloudLogging.Error
return cloudLogging.Default