• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / go-lib-micro / 914585411

pending completion
914585411

Pull #200

gitlab-ci

alfrunes
feat: Configure logger from environment variables on startup

Changelog: Title
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #200: Fix logging caller for helper functions and expose configuration options

11 of 97 new or added lines in 2 files covered. (11.34%)

4 existing lines in 2 files now uncovered.

1369 of 1758 relevant lines covered (77.87%)

4.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

33.86
/log/log.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
// Package log provides a thin wrapper over logrus, with a definition
16
// of a global root logger, its setup functions and convenience wrappers.
17
//
18
// The wrappers are introduced to reduce verbosity:
19
// - logrus.Fields becomes log.Ctx
20
// - logrus.WithFields becomes log.F(), defined on a Logger type
21
//
22
// The usage scenario in a multilayer app is as follows:
23
// - a new Logger is created in the upper layer with an initial context (request id, api method...)
24
// - it is passed to lower layer which automatically includes the context, and can further enrich it
25
// - result - logs across layers are tied together with a common context
26
//
27
// Note on concurrency:
28
// - all Loggers in fact point to the single base log, which serializes logging with its mutexes
29
// - all context is copied - each layer operates on an independent copy
30

31
package log
32

33
import (
34
        "context"
35
        "fmt"
36
        "io"
37
        "os"
38
        "path"
39
        "runtime"
40
        "strconv"
41
        "strings"
42
        "time"
43

44
        "github.com/sirupsen/logrus"
45
)
46

47
var (
48
        // log is a global logger instance
49
        Log = logrus.New()
50
)
51

52
const (
53
        envLogFormat        = "LOG_FORMAT"
54
        envLogLevel         = "LOG_LEVEL"
55
        envLogDisableCaller = "LOG_DISABLE_CALLER"
56

57
        logFormatJSON    = "json"
58
        logFormatJSONAlt = "ndjson"
59
)
60

61
type loggerContextKeyType int
62

63
const (
64
        loggerContextKey loggerContextKeyType = 0
65
)
66

67
// ContextLogger interface for components which support
68
// logging with context, via setting a logger to an exisiting one,
69
// thereby inheriting its context.
70
type ContextLogger interface {
71
        UseLog(l *Logger)
72
}
73

74
// init initializes the global logger to sane defaults.
75
func init() {
1✔
76
        switch strings.ToLower(os.Getenv(envLogFormat)) {
1✔
NEW
77
        case logFormatJSON, logFormatJSONAlt:
×
NEW
78
                Log.Formatter = &logrus.JSONFormatter{
×
NEW
79
                        TimestampFormat: time.RFC3339,
×
NEW
80
                }
×
81
        default:
1✔
82
                Log.Formatter = &logrus.TextFormatter{
1✔
83
                        FullTimestamp:   true,
1✔
84
                        TimestampFormat: time.RFC3339,
1✔
85
                }
1✔
86
        }
87
        if lvl := os.Getenv(envLogLevel); lvl != "" {
1✔
NEW
88
                logLevel, err := logrus.ParseLevel(lvl)
×
NEW
89
                if err == nil {
×
NEW
90
                        Log.Level = logLevel
×
NEW
91
                } else {
×
NEW
92
                        Log.Level = logrus.InfoLevel
×
NEW
93
                }
×
94
        }
95
        disableCaller, _ := strconv.ParseBool(os.Getenv(envLogDisableCaller))
1✔
96
        if !disableCaller {
2✔
97
                Log.Hooks.Add(ContextHook{})
1✔
98
        }
1✔
99
        Log.ExitFunc = func(int) {}
1✔
100
}
101

102
type Level logrus.Level
103

104
const (
105
        LevelPanic = Level(logrus.PanicLevel)
106
        LevelFatal = Level(logrus.FatalLevel)
107
        LevelError = Level(logrus.ErrorLevel)
108
        LevelWarn  = Level(logrus.WarnLevel)
109
        LevelInfo  = Level(logrus.InfoLevel)
110
        LevelDebug = Level(logrus.DebugLevel)
111
        LevelTrace = Level(logrus.TraceLevel)
112
)
113

114
type Format int
115

116
const (
117
        FormatConsole Format = iota
118
        FormatJSON
119
)
120

121
type Options struct {
122
        TimestampFormat string
123

124
        Level Level
125

126
        DisableCaller bool
127

128
        Format Format
129

130
        Output io.Writer
131
}
132

NEW
133
func Configure(opts Options) {
×
NEW
134
        Log = logrus.New()
×
NEW
135

×
NEW
136
        Log.SetOutput(opts.Output)
×
NEW
137
        Log.SetLevel(logrus.Level(opts.Level))
×
NEW
138

×
NEW
139
        if !opts.DisableCaller {
×
NEW
140
                Log.AddHook(ContextHook{})
×
NEW
141
        }
×
142

NEW
143
        var formatter logrus.Formatter
×
NEW
144

×
NEW
145
        switch opts.Format {
×
NEW
146
        case FormatConsole:
×
NEW
147
                formatter = &logrus.TextFormatter{
×
NEW
148
                        FullTimestamp:   true,
×
NEW
149
                        TimestampFormat: opts.TimestampFormat,
×
NEW
150
                }
×
NEW
151
        case FormatJSON:
×
NEW
152
                formatter = &logrus.JSONFormatter{
×
NEW
153
                        TimestampFormat: opts.TimestampFormat,
×
NEW
154
                }
×
155
        }
NEW
156
        logrus.SetFormatter(formatter)
×
157
}
158

159
// Setup allows to override the global logger setup.
160
func Setup(debug bool) {
3✔
161
        if debug {
4✔
162
                Log.Level = logrus.DebugLevel
1✔
163
        }
1✔
164
}
165

166
// Ctx short for log context, alias for the more verbose logrus.Fields.
167
type Ctx map[string]interface{}
168

169
// Logger is a wrapper for logrus.Entry.
170
type Logger struct {
171
        *logrus.Entry
172
}
173

174
// New returns a new Logger with a given context, derived from the global Log.
175
func New(ctx Ctx) *Logger {
6✔
176
        return NewFromLogger(Log, ctx)
6✔
177
}
6✔
178

179
// NewEmpty returns a new logger with empty context
180
func NewEmpty() *Logger {
×
181
        return New(Ctx{})
×
182
}
×
183

184
// NewFromLogger returns a new Logger derived from a given logrus.Logger,
185
// instead of the global one.
186
func NewFromLogger(log *logrus.Logger, ctx Ctx) *Logger {
7✔
187
        return &Logger{log.WithFields(logrus.Fields(ctx))}
7✔
188
}
7✔
189

190
// NewFromLogger returns a new Logger derived from a given logrus.Logger,
191
// instead of the global one.
192
func NewFromEntry(log *logrus.Entry, ctx Ctx) *Logger {
×
193
        return &Logger{log.WithFields(logrus.Fields(ctx))}
×
194
}
×
195

196
// F returns a new Logger enriched with new context fields.
197
// It's a less verbose wrapper over logrus.WithFields.
198
func (l *Logger) F(ctx Ctx) *Logger {
1✔
199
        return &Logger{l.Entry.WithFields(logrus.Fields(ctx))}
1✔
200
}
1✔
201

202
func (l *Logger) Level() logrus.Level {
2✔
203
        return l.Entry.Logger.Level
2✔
204
}
2✔
205

206
type ContextHook struct {
207
}
208

209
func (hook ContextHook) Levels() []logrus.Level {
1✔
210
        return logrus.AllLevels
1✔
211
}
1✔
212

NEW
213
func fmtCaller(frame runtime.Frame) string {
×
NEW
214
        return fmt.Sprintf(
×
NEW
215
                "%s:%d>%s",
×
NEW
216
                path.Base(frame.File),
×
NEW
217
                frame.Line,
×
NEW
218
                path.Base(frame.Function),
×
NEW
219
        )
×
NEW
220
}
×
221

222
func (hook ContextHook) Fire(entry *logrus.Entry) error {
×
NEW
223
        const (
×
NEW
224
                minCallDepth = 6 // logrus.Logger.Log
×
NEW
225
                maxCallDepth = 8 // logrus.Logger.<Level>f
×
NEW
226
        )
×
NEW
227
        var pcs [1 + maxCallDepth - minCallDepth]uintptr
×
NEW
228
        if _, ok := entry.Data["caller"]; !ok {
×
NEW
229
                // We don't know how deep we are in the callstack since the hook can be fired
×
NEW
230
                // at different levels. Search between depth 6 -> 8.
×
NEW
231
                i := runtime.Callers(minCallDepth, pcs[:])
×
NEW
232
                frames := runtime.CallersFrames(pcs[:i])
×
NEW
233
                var caller *runtime.Frame
×
NEW
234
                for frame, ok := frames.Next(); ok; frame, ok = frames.Next() {
×
NEW
235
                        if !strings.HasPrefix(frame.Function, "github.com/sirupsen/logrus") {
×
NEW
236
                                caller = &frame
×
UNCOV
237
                                break
×
238
                        }
239
                }
NEW
240
                if caller != nil {
×
NEW
241
                        entry.Data["caller"] = fmtCaller(*caller)
×
NEW
242
                }
×
243
        }
UNCOV
244
        return nil
×
245
}
246

247
// WithCallerContext returns a new logger with caller set to the parent caller
248
// context. The skipParents select how many caller contexts to skip, a value of
249
// 0 sets the context to the caller of this function.
NEW
250
func (l *Logger) WithCallerContext(skipParents int) *Logger {
×
NEW
251
        const calleeDepth = 2
×
NEW
252
        var pc [1]uintptr
×
NEW
253
        newEntry := l
×
NEW
254
        i := runtime.Callers(calleeDepth+skipParents, pc[:])
×
NEW
255
        frame, _ := runtime.CallersFrames(pc[:i]).
×
NEW
256
                Next()
×
NEW
257
        if frame.Func != nil {
×
NEW
258
                newEntry = &Logger{Entry: l.Dup()}
×
NEW
259
                newEntry.Data["caller"] = fmtCaller(frame)
×
NEW
260
        }
×
NEW
261
        return newEntry
×
262
}
263

264
// Grab an instance of Logger that may have been passed in context.Context.
265
// Returns the logger or creates a new instance if none was found in ctx. Since
266
// Logger is based on logrus.Entry, if logger instance from context is any of
267
// logrus.Logger, logrus.Entry, necessary adaption will be applied.
268
func FromContext(ctx context.Context) *Logger {
2✔
269
        l := ctx.Value(loggerContextKey)
2✔
270
        if l == nil {
3✔
271
                return New(Ctx{})
1✔
272
        }
1✔
273

274
        switch v := l.(type) {
1✔
275
        case *Logger:
1✔
276
                return v
1✔
277
        case *logrus.Entry:
×
278
                return NewFromEntry(v, Ctx{})
×
279
        case *logrus.Logger:
×
280
                return NewFromLogger(v, Ctx{})
×
281
        default:
×
282
                return New(Ctx{})
×
283
        }
284
}
285

286
// WithContext adds logger to context `ctx` and returns the resulting context.
287
func WithContext(ctx context.Context, log *Logger) context.Context {
2✔
288
        return context.WithValue(ctx, loggerContextKey, log)
2✔
289
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc