Skip to content

Commit bad7570

Browse files
docd: add slog and JSON logging (#149)
1 parent 1937742 commit bad7570

14 files changed

Lines changed: 264 additions & 44 deletions

File tree

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
go: ["1.21", "1.20"]
14+
go: ["1.21"]
1515
steps:
1616
- uses: actions/checkout@v2
1717

README.md

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,6 @@ The `docd` tool runs as either:
6969
### Optional flags
7070
7171
- `addr` - the bind address for the HTTP server, default is ":8888"
72-
- `log-level`
73-
- 0: errors & critical info
74-
- 1: inclues 0 and logs each request as well
75-
- 2: include 1 and logs the response payloads
7672
- `readability-length-low` - sets the readability length low if the ?readability=1 parameter is set
7773
- `readability-length-high` - sets the readability length high if the ?readability=1 parameter is set
7874
- `readability-stopwords-low` - sets the readability stopwords low if the ?readability=1 parameter is set
@@ -83,11 +79,8 @@ The `docd` tool runs as either:
8379
8480
### How to start the service
8581
86-
$ # This will only log errors and critical info
87-
$ docd -log-level 0
88-
89-
$ # This will run on port 8000 and log each request
90-
$ docd -addr :8000 -log-level 1
82+
$ # This runs on port 8000
83+
$ docd -addr :8000
9184
9285
## Example usage (code)
9386
@@ -104,15 +97,14 @@ package main
10497
10598
import (
10699
"fmt"
107-
"log"
108100
109101
"code.sajari.com/docconv"
110102
)
111103
112104
func main() {
113105
res, err := docconv.ConvertPath("your-file.pdf")
114106
if err != nil {
115-
log.Fatal(err)
107+
// TODO: handle
116108
}
117109
fmt.Println(res)
118110
}
@@ -125,7 +117,6 @@ package main
125117

126118
import (
127119
"fmt"
128-
"log"
129120

130121
"code.sajari.com/docconv/client"
131122
)
@@ -136,7 +127,7 @@ func main() {
136127

137128
res, err := client.ConvertPath(c, "your-file.pdf")
138129
if err != nil {
139-
log.Fatal(err)
130+
// TODO: handle
140131
}
141132
fmt.Println(res)
142133
}

docd/appengine/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# of its runtime dependencies.
33
# https://cloud.google.com/appengine/docs/flexible/custom-runtimes/about-custom-runtimes
44
FROM sajari/docd:1.3.7
5-
CMD ["-addr", ":8080", "-error-reporting"]
5+
CMD ["-addr", ":8080", "-json-cloud-logging", "-error-reporting"]

docd/convert.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9-
"log"
9+
"log/slog"
1010
"net/http"
1111
"os"
1212

@@ -17,6 +17,7 @@ import (
1717
)
1818

1919
type convertServer struct {
20+
l *slog.Logger
2021
er internal.ErrorReporter
2122
}
2223

@@ -27,9 +28,7 @@ func (s *convertServer) convert(w http.ResponseWriter, r *http.Request) {
2728
var readability bool
2829
if r.FormValue("readability") == "1" {
2930
readability = true
30-
if *logLevel >= 2 {
31-
log.Println("Readability is on")
32-
}
31+
s.l.DebugContext(ctx, "Readability is on")
3332
}
3433

3534
path := r.FormValue("path")
@@ -73,9 +72,7 @@ func (s *convertServer) convert(w http.ResponseWriter, r *http.Request) {
7372
mimeType = docconv.MimeTypeByExtension(info.Filename)
7473
}
7574

76-
if *logLevel >= 1 {
77-
log.Printf("Received file: %v (%v)", info.Filename, mimeType)
78-
}
75+
s.l.InfoContext(ctx, "Received file", "filename", info.Filename, "mimeType", mimeType)
7976

8077
data, err := docconv.Convert(file, mimeType, readability)
8178
if err != nil {
@@ -91,7 +88,7 @@ func (s *convertServer) clientError(ctx context.Context, w http.ResponseWriter,
9188
Error: fmt.Sprintf(pattern, args...),
9289
})
9390

94-
log.Printf(pattern, args...)
91+
s.l.InfoContext(ctx, fmt.Sprintf(pattern, args...))
9592
}
9693

9794
func (s *convertServer) serverError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) {
@@ -104,7 +101,7 @@ func (s *convertServer) serverError(ctx context.Context, w http.ResponseWriter,
104101
}
105102
s.er.Report(e)
106103

107-
log.Printf("%v", err)
104+
s.l.ErrorContext(ctx, err.Error(), "error", err)
108105
}
109106

110107
func (s *convertServer) respond(ctx context.Context, w http.ResponseWriter, r *http.Request, code int, resp interface{}) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package cloudtrace
2+
3+
import (
4+
"context"
5+
)
6+
7+
type traceKey struct{}
8+
type spanKey struct{}
9+
10+
func contextWithTraceInfo(ctx context.Context, traceHeader string) context.Context {
11+
traceID, spanID := parseHeader(traceHeader)
12+
if traceID != "" {
13+
ctx = context.WithValue(ctx, traceKey{}, traceID)
14+
}
15+
if spanID != "" {
16+
ctx = context.WithValue(ctx, spanKey{}, spanID)
17+
}
18+
return ctx
19+
}
20+
21+
func traceInfoFromContext(ctx context.Context) (traceID, spanID string) {
22+
traceID, _ = ctx.Value(traceKey{}).(string)
23+
spanID, _ = ctx.Value(spanKey{}).(string)
24+
return
25+
}

docd/internal/cloudtrace/header.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package cloudtrace
2+
3+
import "strings"
4+
5+
// The header specification is:
6+
// "X-Cloud-Trace-Context: TRACE_ID/SPAN_ID;o=TRACE_TRUE"
7+
const CloudTraceContextHeader = "X-Cloud-Trace-Context"
8+
9+
func parseHeader(value string) (traceID, spanID string) {
10+
var found bool
11+
traceID, after, found := strings.Cut(value, "/")
12+
if found {
13+
spanID, _, _ = strings.Cut(after, ";")
14+
}
15+
return
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package cloudtrace
2+
3+
import "net/http"
4+
5+
type HTTPHandler struct {
6+
// Handler to wrap.
7+
Handler http.Handler
8+
}
9+
10+
func (h *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
11+
ctx := contextWithTraceInfo(r.Context(), r.Header.Get(CloudTraceContextHeader))
12+
13+
h.Handler.ServeHTTP(w, r.WithContext(ctx))
14+
}

docd/internal/cloudtrace/logger.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package cloudtrace
2+
3+
// Inspired by https://github.com/remko/cloudrun-slog
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"log/slog"
9+
"os"
10+
)
11+
12+
// Extra log level supported by Cloud Logging
13+
const LevelCritical = slog.Level(12)
14+
15+
// Handler that outputs JSON understood by the structured log agent.
16+
// See https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields
17+
type CloudLoggingHandler struct {
18+
handler slog.Handler
19+
projectID string
20+
}
21+
22+
var _ slog.Handler = (*CloudLoggingHandler)(nil)
23+
24+
func NewCloudLoggingHandler(projectID string, level slog.Level) *CloudLoggingHandler {
25+
return &CloudLoggingHandler{
26+
projectID: projectID,
27+
handler: slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
28+
AddSource: true,
29+
Level: level,
30+
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
31+
if a.Key == slog.MessageKey {
32+
a.Key = "message"
33+
} else if a.Key == slog.SourceKey {
34+
a.Key = "logging.googleapis.com/sourceLocation"
35+
} else if a.Key == slog.LevelKey {
36+
a.Key = "severity"
37+
level := a.Value.Any().(slog.Level)
38+
if level == LevelCritical {
39+
a.Value = slog.StringValue("CRITICAL")
40+
}
41+
}
42+
return a
43+
},
44+
}),
45+
}
46+
}
47+
48+
func (h *CloudLoggingHandler) Enabled(ctx context.Context, level slog.Level) bool {
49+
return h.handler.Enabled(ctx, level)
50+
}
51+
52+
func (h *CloudLoggingHandler) Handle(ctx context.Context, rec slog.Record) error {
53+
traceID, spanID := traceInfoFromContext(ctx)
54+
if traceID != "" {
55+
rec = rec.Clone()
56+
// https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields
57+
trace := fmt.Sprintf("projects/%s/traces/%s", h.projectID, traceID)
58+
rec.Add("logging.googleapis.com/trace", slog.StringValue(trace))
59+
if spanID != "" {
60+
rec.Add("logging.googleapis.com/spanId", slog.StringValue(spanID))
61+
}
62+
}
63+
return h.handler.Handle(ctx, rec)
64+
}
65+
66+
func (h *CloudLoggingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
67+
return &CloudLoggingHandler{handler: h.handler.WithAttrs(attrs)}
68+
}
69+
70+
func (h *CloudLoggingHandler) WithGroup(name string) slog.Handler {
71+
return &CloudLoggingHandler{handler: h.handler.WithGroup(name)}
72+
}

docd/internal/debug/context.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package debug
2+
3+
import (
4+
"context"
5+
)
6+
7+
type debugEnabledKey struct{}
8+
9+
func debugEnabledInContext(ctx context.Context) bool {
10+
enabled, ok := ctx.Value(debugEnabledKey{}).(bool)
11+
if !ok {
12+
return false
13+
}
14+
return enabled
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package debug
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"strconv"
7+
)
8+
9+
type HTTPHandler struct {
10+
// Handler to wrap.
11+
Handler http.Handler
12+
}
13+
14+
func (h *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
15+
ctx := r.Context()
16+
17+
if ok, _ := strconv.ParseBool(r.Header.Get(DebugHeader)); ok {
18+
ctx = context.WithValue(ctx, debugEnabledKey{}, true)
19+
}
20+
21+
h.Handler.ServeHTTP(w, r.WithContext(ctx))
22+
}

0 commit comments

Comments
 (0)