Skip to content

Commit 40403ae

Browse files
committed
log: create initial trace2 logger
Create a general 'TraceLogger' interface and (for now) a sole implementation in the 'Trace2' struct. There are two main components of this commit: the Trace2 logger itself, and how it's used in the CLIs built in this repository. The Trace2 logger is built around the 'zap' structured logger. As of right now, it does not log anything other than 'start', 'exit', and 'atexit' (and only through the 'WithLogger()' wrapper described later). The logs include: * 'event': the event type; it takes over the 'message' field of the 'zap' logging functions, mostly for the sake of simplicity (those functions require a message, and every trace2 log has an 'event'). * 'sid': a UUID generated by the 'uuid.NewUUID()' function, propagated through the call stack via the 'context.Context'. * 'time': the UTC timestamp of the log generation; this is automatically provided by a 'zap' logger, but requires a custom formatter to provide the timestamp in UTC. * 't_abs': the time (in seconds) since the start of the program (technically, when the 'log' package is loaded and the 'globalStartTime' is initialized). * 'file' and 'line': the filename and line number where the relevant trace2 log function was called. * 'thread': hardcoded to 'main', but may be updated in the future to give unique names to goroutines. From a user's perspective, the logger is configured by the 'GIT_TRACE2_EVENT' environment variable. Its behavior matches what is described in Git [1], but it is currently missing support for open file handles 2-9 and Unix Domain Sockets. To use the trace2 logger, create the 'main()' function wrapper 'WithLogger()'. This function creates an instance of a 'TraceLogger', logs the program start, defers a "cleanup" logging function to write out the 'exit'/'atexit' logs, then runs the wrapped function. [1] https://git-scm.com/docs/git-config#Documentation/git-config.txt-trace2eventTarget Signed-off-by: Victoria Dye <[email protected]>
1 parent 7f309c0 commit 40403ae

File tree

6 files changed

+260
-35
lines changed

6 files changed

+260
-35
lines changed

cmd/git-bundle-server/main.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77

88
"github.com/github/git-bundle-server/internal/argparse"
9+
tracelog "github.com/github/git-bundle-server/internal/log"
910
)
1011

1112
func all() []argparse.Subcommand {
@@ -21,19 +22,19 @@ func all() []argparse.Subcommand {
2122
}
2223

2324
func main() {
24-
ctx := context.Background()
25+
tracelog.WithTraceLogger(context.Background(), func(ctx context.Context, logger tracelog.TraceLogger) {
26+
cmds := all()
2527

26-
cmds := all()
28+
parser := argparse.NewArgParser("git-bundle-server <command> [<options>]")
29+
parser.SetIsTopLevel(true)
30+
for _, cmd := range cmds {
31+
parser.Subcommand(cmd)
32+
}
33+
parser.Parse(ctx, os.Args[1:])
2734

28-
parser := argparse.NewArgParser("git-bundle-server <command> [<options>]")
29-
parser.SetIsTopLevel(true)
30-
for _, cmd := range cmds {
31-
parser.Subcommand(cmd)
32-
}
33-
parser.Parse(ctx, os.Args[1:])
34-
35-
err := parser.InvokeSubcommand(ctx)
36-
if err != nil {
37-
log.Fatal("Failed with error: ", err)
38-
}
35+
err := parser.InvokeSubcommand(ctx)
36+
if err != nil {
37+
log.Fatalf("Failed with error: %s", err)
38+
}
39+
})
3940
}

cmd/git-bundle-web-server/main.go

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,37 @@ import (
88

99
"github.com/github/git-bundle-server/cmd/utils"
1010
"github.com/github/git-bundle-server/internal/argparse"
11+
"github.com/github/git-bundle-server/internal/log"
1112
)
1213

1314
func main() {
14-
ctx := context.Background()
15+
log.WithTraceLogger(context.Background(), func(ctx context.Context, logger log.TraceLogger) {
16+
parser := argparse.NewArgParser("git-bundle-web-server [--port <port>] [--cert <filename> --key <filename>]")
17+
flags, validate := utils.WebServerFlags(parser)
18+
flags.VisitAll(func(f *flag.Flag) {
19+
parser.Var(f.Value, f.Name, f.Usage)
20+
})
1521

16-
parser := argparse.NewArgParser("git-bundle-web-server [--port <port>] [--cert <filename> --key <filename>]")
17-
flags, validate := utils.WebServerFlags(parser)
18-
flags.VisitAll(func(f *flag.Flag) {
19-
parser.Var(f.Value, f.Name, f.Usage)
20-
})
21-
parser.Parse(ctx, os.Args[1:])
22-
validate(ctx)
22+
parser.Parse(ctx, os.Args[1:])
23+
validate(ctx)
2324

24-
// Get the flag values
25-
port := utils.GetFlagValue[string](parser, "port")
26-
cert := utils.GetFlagValue[string](parser, "cert")
27-
key := utils.GetFlagValue[string](parser, "key")
25+
// Get the flag values
26+
port := utils.GetFlagValue[string](parser, "port")
27+
cert := utils.GetFlagValue[string](parser, "cert")
28+
key := utils.GetFlagValue[string](parser, "key")
2829

29-
// Configure the server
30-
bundleServer := NewBundleWebServer(port, cert, key)
30+
// Configure the server
31+
bundleServer := NewBundleWebServer(port, cert, key)
3132

32-
// Start the server asynchronously
33-
bundleServer.StartServerAsync(ctx)
33+
// Start the server asynchronously
34+
bundleServer.StartServerAsync(ctx)
3435

35-
// Intercept interrupt signals
36-
bundleServer.HandleSignalsAsync(ctx)
36+
// Intercept interrupt signals
37+
bundleServer.HandleSignalsAsync(ctx)
3738

38-
// Wait for server to shut down
39-
bundleServer.Wait()
39+
// Wait for server to shut down
40+
bundleServer.Wait()
4041

41-
fmt.Println("Shutdown complete")
42+
fmt.Println("Shutdown complete")
43+
})
4244
}

go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ module github.com/github/git-bundle-server
22

33
go 1.19
44

5-
require github.com/stretchr/testify v1.8.1
5+
require (
6+
github.com/google/uuid v1.3.0
7+
github.com/stretchr/testify v1.8.1
8+
go.uber.org/zap v1.24.0
9+
)
610

711
require (
812
github.com/davecgh/go-spew v1.1.1 // indirect
913
github.com/pmezard/go-difflib v1.0.0 // indirect
1014
github.com/stretchr/objx v0.5.0 // indirect
15+
go.uber.org/atomic v1.10.0 // indirect
16+
go.uber.org/multierr v1.9.0 // indirect
1117
gopkg.in/yaml.v3 v3.0.1 // indirect
1218
)

go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
12
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
23
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
34
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
6+
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7+
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
48
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
59
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
610
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -11,6 +15,13 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
1115
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
1216
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
1317
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
18+
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
19+
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
20+
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
21+
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
22+
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
23+
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
24+
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
1425
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1526
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1627
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/log/logger.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package log
2+
3+
import (
4+
"context"
5+
"os"
6+
)
7+
8+
type TraceLogger interface{}
9+
10+
type traceLoggerInternal interface {
11+
// Internal setup/teardown functions
12+
logStart(ctx context.Context) context.Context
13+
logExit(ctx context.Context, exitCode int)
14+
15+
TraceLogger
16+
}
17+
18+
func WithTraceLogger(
19+
ctx context.Context,
20+
mainFunc func(context.Context, TraceLogger),
21+
) {
22+
logger := NewTrace2()
23+
24+
// Set up the program-level context
25+
ctx = logger.logStart(ctx)
26+
defer func() {
27+
if recover() != nil {
28+
// Panicking - exit with error
29+
logger.logExit(ctx, 1)
30+
os.Exit(1)
31+
} else {
32+
// Just log the exit (but don't os.Exit()) so we can exit normally
33+
logger.logExit(ctx, 0)
34+
}
35+
}()
36+
37+
mainFunc(ctx, logger)
38+
}

internal/log/trace2.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package log
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path"
8+
"path/filepath"
9+
"runtime"
10+
"strconv"
11+
"time"
12+
13+
"github.com/google/uuid"
14+
"go.uber.org/zap"
15+
"go.uber.org/zap/zapcore"
16+
)
17+
18+
// Trace2 environment variables
19+
const (
20+
// TODO: handle GIT_TRACE2 by adding a separate output config (see zapcore
21+
// "AdvancedConfiguration" example:
22+
// https://pkg.go.dev/go.uber.org/zap#example-package-AdvancedConfiguration)
23+
trace2Event string = "GIT_TRACE2_EVENT"
24+
)
25+
26+
// Global start time
27+
var globalStart = time.Now().UTC()
28+
29+
const trace2TimeFormat string = "2006-01-02T15:04:05.999999Z"
30+
31+
type ctxKey int
32+
33+
const (
34+
sidId ctxKey = iota
35+
)
36+
37+
type Trace2 struct {
38+
logger *zap.Logger
39+
}
40+
41+
func getTrace2OutputPaths(envKey string) []string {
42+
tr2Output := os.Getenv(envKey)
43+
44+
// Configure the output
45+
if tr2, err := strconv.Atoi(tr2Output); err == nil {
46+
// Handle numeric values
47+
if tr2 == 1 {
48+
return []string{"stderr"}
49+
}
50+
// TODO: handle file handles 2-9 and unix sockets
51+
} else if tr2Output != "" {
52+
// Assume we received a path
53+
fileInfo, err := os.Stat(tr2Output)
54+
if err == nil && fileInfo.IsDir() {
55+
// If the path is an existing directory, generate a filename
56+
return []string{
57+
filepath.Join(tr2Output, fmt.Sprintf("trace2_%s.txt", globalStart.Format(trace2TimeFormat))),
58+
}
59+
} else {
60+
// Create leading directories
61+
parentDir := path.Dir(tr2Output)
62+
os.MkdirAll(parentDir, 0o755)
63+
return []string{tr2Output}
64+
}
65+
}
66+
67+
return []string{}
68+
}
69+
70+
func createTrace2ZapLogger() *zap.Logger {
71+
loggerConfig := zap.NewProductionConfig()
72+
73+
// Configure the output for GIT_TRACE2_EVENT
74+
loggerConfig.OutputPaths = getTrace2OutputPaths(trace2Event)
75+
loggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
76+
77+
// Encode UTC time
78+
loggerConfig.EncoderConfig.TimeKey = "time"
79+
loggerConfig.EncoderConfig.EncodeTime = zapcore.TimeEncoder(
80+
func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
81+
enc.AppendString(t.Format(trace2TimeFormat))
82+
},
83+
)
84+
85+
// Re-purpose the "message" to represent the (always-present) "event" key
86+
loggerConfig.EncoderConfig.MessageKey = "event"
87+
88+
// Don't print the log level
89+
loggerConfig.EncoderConfig.LevelKey = ""
90+
91+
// Disable caller info, we'll customize those fields manually
92+
logger, _ := loggerConfig.Build(zap.WithCaller(false))
93+
return logger
94+
}
95+
96+
func NewTrace2() traceLoggerInternal {
97+
return &Trace2{
98+
logger: createTrace2ZapLogger(),
99+
}
100+
}
101+
102+
type fieldList []zap.Field
103+
104+
func (l fieldList) withTime() fieldList {
105+
return append(l, zap.Float64("t_abs", time.Since(globalStart).Seconds()))
106+
}
107+
108+
func (l fieldList) with(f ...zap.Field) fieldList {
109+
return append(l, f...)
110+
}
111+
112+
func (t *Trace2) sharedFields(ctx context.Context) (context.Context, fieldList) {
113+
fields := fieldList{}
114+
115+
// Get the session ID
116+
var sid uuid.UUID
117+
haveSid := false
118+
sidAny := ctx.Value(sidId)
119+
if sidAny != nil {
120+
sid, haveSid = sidAny.(uuid.UUID)
121+
}
122+
if !haveSid {
123+
sid = uuid.New()
124+
ctx = context.WithValue(ctx, sidId, sid)
125+
}
126+
fields = append(fields, zap.String("sid", sid.String()))
127+
128+
// Hardcode the thread to "main" because Go doesn't like to share its
129+
// internal info about threading.
130+
fields = append(fields, zap.String("thread", "main"))
131+
132+
// Get the caller of the function in trace2.go
133+
// Skip up two levels:
134+
// 0: this function
135+
// 1: the caller of this function (StartTrace, LogEvent, etc.)
136+
// 2: the function calling this trace2 library
137+
_, fileName, lineNum, ok := runtime.Caller(2)
138+
if ok {
139+
fields = append(fields,
140+
zap.String("file", filepath.Base(fileName)),
141+
zap.Int("line", lineNum),
142+
)
143+
}
144+
145+
return ctx, fields
146+
}
147+
148+
func (t *Trace2) logStart(ctx context.Context) context.Context {
149+
ctx, sharedFields := t.sharedFields(ctx)
150+
151+
t.logger.Info("start", sharedFields.withTime().with(
152+
zap.Strings("argv", os.Args),
153+
)...)
154+
155+
return ctx
156+
}
157+
158+
func (t *Trace2) logExit(ctx context.Context, exitCode int) {
159+
_, sharedFields := t.sharedFields(ctx)
160+
fields := sharedFields.with(
161+
zap.Int("code", exitCode),
162+
)
163+
t.logger.Info("exit", fields.withTime()...)
164+
t.logger.Info("atexit", fields.withTime()...)
165+
166+
t.logger.Sync()
167+
}

0 commit comments

Comments
 (0)