1package log
2
3import (
4	"fmt"
5	"io"
6	"os"
7	"time"
8
9	"github.com/sirupsen/logrus"
10)
11
12const (
13	iso8601TimestampFormat = "2006-01-02T15:04:05.000Z07:00"
14	iso8601TimestampEnvKey = "GITLAB_ISO8601_LOG_TIMESTAMP"
15)
16
17type loggerConfig struct {
18	logger     *logrus.Logger
19	level      logrus.Level
20	formatter  logrus.Formatter
21	location   *time.Location
22	outputPath string
23	writer     io.Writer
24
25	// A list of warnings that will be emitted once the logger is configured
26	warnings []string
27}
28
29type timezoneFormatter struct {
30	formatter logrus.Formatter
31	location  *time.Location
32}
33
34// LoggerOption will configure a new logrus Logger.
35type LoggerOption func(*loggerConfig)
36
37// We default to time.RFC3339 (to match the Logrus default) for the timestamp format for backward compatibility reasons.
38// Optionally, users can opt in for ISO8601 with millisecond precision by setting (to any value) the
39// iso8601TimestampEnvKey environment variable.
40func timestampFormat() string {
41	if _, exists := os.LookupEnv(iso8601TimestampEnvKey); exists {
42		return iso8601TimestampFormat
43	}
44	return time.RFC3339
45}
46
47func applyLoggerOptions(opts []LoggerOption) *loggerConfig {
48	conf := loggerConfig{
49		logger:    logger,
50		level:     logrus.InfoLevel,
51		formatter: &logrus.TextFormatter{TimestampFormat: timestampFormat()},
52		writer:    os.Stdout,
53	}
54
55	for _, v := range opts {
56		v(&conf)
57	}
58
59	return &conf
60}
61
62func (l *loggerConfig) buildFormatter() logrus.Formatter {
63	out := l.formatter
64
65	if l.location != nil {
66		out = &timezoneFormatter{formatter: out, location: l.location}
67	}
68
69	return out
70}
71
72func (f *timezoneFormatter) Format(entry *logrus.Entry) ([]byte, error) {
73	// Avoid mutating the passed-in entry as callers may retain a reference to
74	// it. Since we don't change the `Data` field, a shallow copy is sufficient.
75	if entry != nil {
76		entryCopy := *entry
77		entryCopy.Time = entryCopy.Time.In(f.location)
78
79		entry = &entryCopy
80	}
81
82	return f.formatter.Format(entry)
83}
84
85// WithFormatter allows setting the format to `text`, `json`, `color` or `combined`. In case
86// the input is not recognized it defaults to text with a warning.
87// More details of these formats:
88// * `text` - human readable.
89// * `json` - computer readable, new-line delimited JSON.
90// * `color` - human readable, in color. Useful for development.
91// * `combined` - httpd access logs. Good for legacy access log parsers.
92func WithFormatter(format string) LoggerOption {
93	return func(conf *loggerConfig) {
94		timestampFormat := timestampFormat()
95		switch format {
96		case "text":
97			conf.formatter = &logrus.TextFormatter{TimestampFormat: timestampFormat}
98		case "color":
99			conf.formatter = &logrus.TextFormatter{TimestampFormat: timestampFormat, ForceColors: true, EnvironmentOverrideColors: true}
100		case "json":
101			conf.formatter = &logrus.JSONFormatter{TimestampFormat: timestampFormat}
102		case "combined":
103			conf.formatter = newCombinedcombinedAccessLogFormatter()
104		default:
105			conf.warnings = append(conf.warnings, fmt.Sprintf("unknown logging format %s, ignoring option", format))
106		}
107	}
108}
109
110// WithTimezone allows setting the timezone that will be used for log messages.
111// The default behaviour is to use the local timezone, but a specific timezone
112// (such as time.UTC) may be preferred.
113func WithTimezone(location *time.Location) LoggerOption {
114	return func(conf *loggerConfig) {
115		conf.location = location
116	}
117}
118
119// WithLogLevel is used to set the log level when defaulting to `info` is not
120// wanted. Other options are: `debug`, `warn`, `error`, `fatal`, and `panic`.
121func WithLogLevel(level string) LoggerOption {
122	return func(conf *loggerConfig) {
123		logrusLevel, err := logrus.ParseLevel(level)
124		if err != nil {
125			conf.warnings = append(conf.warnings, fmt.Sprintf("unknown log level, ignoring option: %v", err))
126		} else {
127			conf.level = logrusLevel
128		}
129	}
130}
131
132// WithOutputName allows customization of the sink of the logger. Output is either:
133// `stdout`, `stderr`, or a path to a file.
134func WithOutputName(outputName string) LoggerOption {
135	return func(conf *loggerConfig) {
136		switch outputName {
137		case "stdout":
138			conf.writer = os.Stdout
139		case "stderr":
140			conf.writer = os.Stderr
141		default:
142			conf.writer = nil
143			conf.outputPath = outputName
144		}
145	}
146}
147
148// WithWriter allows the writer to be customized. The application is responsible for closing the writer manually.
149func WithWriter(writer io.Writer) LoggerOption {
150	return func(conf *loggerConfig) {
151		conf.writer = writer
152	}
153}
154
155// WithLogger allows you to configure a proprietary logger using the `Initialize` method.
156func WithLogger(logger *logrus.Logger) LoggerOption {
157	return func(conf *loggerConfig) {
158		conf.logger = logger
159	}
160}
161