summaryrefslogtreecommitdiff
path: root/internal/io/dlog/dlog.go
blob: e28b4420235c724c205d1b9c963390f3a48f54a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
package dlog

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/mimecast/dtail/internal/color/brush"
	"github.com/mimecast/dtail/internal/config"
	"github.com/mimecast/dtail/internal/io/dlog/loggers"
	"github.com/mimecast/dtail/internal/io/pool"
	"github.com/mimecast/dtail/internal/protocol"
	"github.com/mimecast/dtail/internal/source"
)

// Client is the log handler for the client packages.
var Client *DLog

// Server is the log handler for the server packages.
var Server *DLog

// Common is the log handler for all other packages.
var Common *DLog

var mutex sync.Mutex
var started bool

// DLog is the DTail logger.
type DLog struct {
	logger loggers.Logger
	// Is this a DTail server or client process logging?
	sourceProcess source.Source
	// Is this a DTail server or client package logging? In serverless mode
	// the client can also execute code from the server package.
	sourcePackage source.Source
	// Max log level to log.
	maxLevel level
	// Current hostname.
	hostname string
}

// new creates a new DTail logger.
func new(sourceProcess, sourcePackage source.Source) *DLog {
	hostname, err := config.Hostname()
	if err != nil {
		panic(err)
	}
	logRotation := loggers.NewStrategy(config.Common.LogRotation)
	loggerName := config.Common.Logger
	level := newLevel(config.Common.LogLevel)

	return &DLog{
		logger:        loggers.Factory(sourceProcess.String(), loggerName, logRotation),
		sourceProcess: sourceProcess,
		sourcePackage: sourcePackage,
		maxLevel:      level,
		hostname:      hostname,
	}
}

// Start logger(s).
func Start(ctx context.Context, wg *sync.WaitGroup, sourceProcess source.Source) {
	mutex.Lock()
	defer mutex.Unlock()

	if started {
		Common.FatalPanic("Logger already started")
	}

	Client = new(sourceProcess, source.Client)
	Server = new(sourceProcess, source.Server)
	Common = Client
	if sourceProcess == source.Server {
		Common = Server
	}

	var wg2 sync.WaitGroup
	wg2.Add(2)
	go Client.start(ctx, &wg2)
	go Server.start(ctx, &wg2)

	go rotation(ctx)
	go func() {
		wg2.Wait()
		wg.Done()
	}()

	started = true
}

func (d *DLog) start(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	var wg2 sync.WaitGroup
	wg2.Add(1)
	d.logger.Start(ctx, &wg2)
	<-ctx.Done()
	wg2.Wait()
}

// FatalPanic terminates the process with a fatal error.
func (d *DLog) FatalPanic(args ...interface{}) {
	d.log(Fatal, args)
	d.Flush()

	var sb strings.Builder
	d.writeArgStrings(&sb, args)
	panic(sb.String())
}

// Fatal logs a fatal error.
func (d *DLog) Fatal(args ...interface{}) string {
	return d.log(Fatal, args)
}

// Error logging.
func (d *DLog) Error(args ...interface{}) string {
	return d.log(Error, args)
}

// Warn logs a warning message.
func (d *DLog) Warn(args ...interface{}) string {
	return d.log(Warn, args)
}

// Info logging.
func (d *DLog) Info(args ...interface{}) string {
	return d.log(Info, args)
}

// Verbose logging.
func (d *DLog) Verbose(args ...interface{}) string {
	return d.log(Verbose, args)
}

// Debug logging.
func (d *DLog) Debug(args ...interface{}) string {
	return d.log(Debug, args)
}

// TraceEnabled reports whether trace-level logging is currently active.
//
// It performs exactly the same maxLevel comparison as Trace's internal
// early-return (see below), letting callers on per-line hot paths gate the
// whole trace call — the variadic []interface{} slice allocation plus the
// interface boxing of every non-pointer argument (uint64 line counts via
// runtime.convT64, strings via convTstring) — behind one cheap, inlinable,
// allocation-free branch. Without this guard those args are boxed at the call
// site before Trace even runs, so Trace's own early-return cannot save them.
//
// The receiver is nil-safe so call sites need no separate nil check on the
// package-level loggers (Server/Client/Common), which stay nil until Start.
// maxLevel is fixed at logger construction from config.Common.LogLevel, so the
// result mirrors whatever level Trace itself would observe.
func (d *DLog) TraceEnabled() bool {
	return d != nil && d.maxLevel >= Trace
}

// Trace logging.
func (d *DLog) Trace(args ...interface{}) string {
	// Early check to avoid expensive runtime.Caller when trace is disabled
	// This is a critical performance optimization for hot paths. Note that on
	// per-line hot paths callers should additionally gate with TraceEnabled()
	// so the argument boxing never happens; this check only saves runtime.Caller
	// and the log formatting, not the caller-side boxing of args.
	if d.maxLevel < Trace {
		return ""
	}
	_, file, line, _ := runtime.Caller(1)
	args = append(args, fmt.Sprintf("at %s:%d", file, line))
	return d.log(Trace, args)
}

// Devel used for development purpose only logging (e.g. "print" debugging).
func (d *DLog) Devel(args ...interface{}) string {
	// Early check to avoid expensive runtime.Caller when devel is disabled
	if d.maxLevel < Devel {
		return ""
	}
	_, file, line, _ := runtime.Caller(1)
	args = append(args, fmt.Sprintf("at %s:%d", file, line))
	return d.log(Devel, args)
}

// Raw message logging.
func (d *DLog) Raw(message string) string {
	if !config.Client.TermColorsEnable || !d.logger.SupportsColors() {
		d.logger.Raw(time.Now(), message)
		return message
	}
	d.logger.RawWithColors(time.Now(), message, brush.Colorfy(message))
	return message
}

// payloadFileTeer is the optional capability of a logger that can tee retrieved
// payload into its FILE sink without also writing it to stdout. Only the default
// fout logger implements it; stdout/none loggers have no file sink and are
// skipped via the type assertion in RawPayloadFileTee.
type payloadFileTeer interface {
	RawFileOnly(now time.Time, message string)
}

// RawPayloadFileTee writes retrieved payload to the logger's FILE sink only
// (never stdout), honoring the client's --log-payload / Client.LogPayload
// opt-in. It is used by the serverless direct-output path, which emits payload
// straight to stdout and therefore bypasses the fout logger's own Raw tee. When
// the active logger has no file sink (stdout/none) or payload teeing is
// disabled, this is a no-op. stdout output is unaffected: the caller writes the
// same payload bytes to stdout itself, and this method only adds the file tee.
func (d *DLog) RawPayloadFileTee(message string) {
	if teer, ok := d.logger.(payloadFileTeer); ok {
		teer.RawFileOnly(time.Now(), message)
	}
}

// RawLog writes a pre-formatted message through the DIAGNOSTIC (Log) sink, so it
// reaches both stdout and — in the default fout logger — the daily log file.
//
// It differs from Raw, which uses the PAYLOAD sink: with the default
// Client.LogPayload=false, Raw is gated out of the file (bulk dcat/dgrep/dtail
// output must not grow the log). RawLog is for audit-worthy, already-formatted
// lines such as server-error reports, which must always be kept in the file like
// other diagnostics rather than being treated as bulk payload. The message is
// written verbatim (no level/hostname prefix); callers pre-format it and must
// NOT append a trailing newline, since the Log sink appends one.
func (d *DLog) RawLog(message string) string {
	if !config.Client.TermColorsEnable || !d.logger.SupportsColors() {
		d.logger.Log(time.Now(), message)
		return message
	}
	d.logger.LogWithColors(time.Now(), message, brush.Colorfy(message))
	return message
}

// Mapreduce logging.
func (d *DLog) Mapreduce(table string, data map[string]interface{}) string {
	args := make([]interface{}, len(data)+1)

	if d.sourceProcess == source.Server {
		// level|date-time|process|caller|cpus|goroutines|cgocalls|loadavg|uptime|MAPREDUCE:TABLE|key=value|...

		var loadAvg string
		if loadAvgBytes, err := os.ReadFile("/proc/loadavg"); err == nil {
			tmp := string(loadAvgBytes)
			s := strings.SplitN(tmp, " ", 2)
			loadAvg = s[0]
		}

		var uptime string
		if uptimeBytes, err := os.ReadFile("/proc/uptime"); err == nil {
			tmp := string(uptimeBytes)
			s := strings.SplitN(tmp, ".", 2)
			i, _ := strconv.ParseInt(s[0], 10, 64)
			t := time.Duration(i) * time.Second
			uptime = fmt.Sprintf("%v", t)
		}

		_, file, line, _ := runtime.Caller(1)
		args[0] = fmt.Sprintf("%d|%s:%d|%d|%d|%d|%s|%s|MAPREDUCE:%s",
			os.Getpid(),
			filepath.Base(file), line,
			runtime.NumCPU(),
			runtime.NumGoroutine(),
			runtime.NumCgoCall(),
			loadAvg,
			uptime,
			strings.ToUpper(table))
	} else {
		args[0] = fmt.Sprintf("STATS:%s", strings.ToUpper(table))
	}

	i := 1
	for k, v := range data {
		args[i] = fmt.Sprintf("%s=%v", k, v)
		i++
	}
	return d.log(Info, args)
}

// Flush the log buffers.
func (d *DLog) Flush() { d.logger.Flush() }

// Pause the logging.
func (d *DLog) Pause() { d.logger.Pause() }

// Resume the logging.
func (d *DLog) Resume() { d.logger.Resume() }

func (d *DLog) log(level level, args []interface{}) string {
	if d.maxLevel < level {
		return ""
	}
	sb := pool.BuilderBuffer.Get().(*strings.Builder)
	defer pool.RecycleBuilderBuffer(sb)
	now := time.Now()

	switch d.sourceProcess {
	case source.Client:
		sb.WriteString(d.sourcePackage.String())
		sb.WriteString(protocol.FieldDelimiter)
		sb.WriteString(d.hostname)
		sb.WriteString(protocol.FieldDelimiter)
		sb.WriteString(level.String())
	default:
		sb.WriteString(level.String())
		sb.WriteString(protocol.FieldDelimiter)
		sb.WriteString(now.Format("0102-150405"))
	}
	sb.WriteString(protocol.FieldDelimiter)
	d.writeArgStrings(sb, args)

	message := sb.String()
	if !config.Client.TermColorsEnable || !d.logger.SupportsColors() {
		d.logger.Log(now, message)
		return message
	}

	d.logger.LogWithColors(now, message, brush.Colorfy(message))
	return message
}

func (d *DLog) writeArgStrings(sb *strings.Builder, args []interface{}) {
	for i, arg := range args {
		if i > 0 {
			sb.WriteString(protocol.FieldDelimiter)
		}
		switch v := arg.(type) {
		case string:
			sb.WriteString(v)
		case error:
			sb.WriteString(v.Error())
		default:
			sb.WriteString(fmt.Sprintf("%v", v))
		}
	}
}