Skip to content

Commit 6826aa2

Browse files
committed
gracefully handle failing traces
Closes #1
1 parent 1622520 commit 6826aa2

File tree

5 files changed

+124
-7
lines changed

5 files changed

+124
-7
lines changed

__snapshots__/tracer_test.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,11 @@
527527
"size": 1
528528
}
529529
---
530+
531+
[TestSendReportError - 1]
532+
[]string{"failed to send report for operation \"id\": test fail send report"}
533+
---
534+
535+
[TestSendReportServerError - 1]
536+
[]string{"failed to send report for operation \"a0\": report sending failed with 400 Bad Request: test server error", "failed to send report for operation \"a1\": report sending failed with 500 Internal Server Error (no body)"}
537+
---

logger.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package gqlhive
2+
3+
import "log"
4+
5+
type Logger interface {
6+
Printf(format string, v ...any)
7+
}
8+
9+
// NewLogger creates a new Logger instance that writes to the standard log output
10+
// with the prefix "[gqlhive] " and includes the standard logging attribute such as timestamp.
11+
func NewLogger() Logger {
12+
return log.New(log.Writer(), "[gqlhive] ", log.LstdFlags|log.Lmsgprefix)
13+
}

tracer.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Tracer struct {
2323
generateID GenerateID
2424
sendReportTimeout time.Duration
2525
sendReport SendReport
26+
log Logger
2627
}
2728

2829
var _ interface {
@@ -44,6 +45,7 @@ func NewTracer(target, token string, opts ...TracerOption) *Tracer {
4445
generateID: defaultGenerateID,
4546
sendReportTimeout: defaultSendReportTimeout,
4647
sendReport: defaultSendReport,
48+
log: defaultLogger,
4749
}
4850
for _, opt := range opts {
4951
opt.set(tracer)
@@ -124,8 +126,8 @@ func (tracer Tracer) InterceptResponse(ctx context.Context, next graphql.Respons
124126

125127
err := queueOperation(operation)
126128
if err != nil {
127-
// TODO: report gracefully
128-
panic(err)
129+
tracer.log.Printf("failed to queue operation %q: %v", operation.ID, err)
130+
return
129131
}
130132

131133
// TODO: implement send retry
@@ -148,8 +150,7 @@ func (tracer Tracer) InterceptResponse(ctx context.Context, next graphql.Respons
148150
if tracer.sendReportTimeout == 0 {
149151
err := doSend(ctx)
150152
if err != nil {
151-
// TODO: report gracefully
152-
panic(err)
153+
tracer.log.Printf("failed to send report for operation %q: %v", operation.ID, err)
153154
}
154155
return
155156
}
@@ -165,8 +166,7 @@ func (tracer Tracer) InterceptResponse(ctx context.Context, next graphql.Respons
165166
context.TODO(),
166167
)
167168
if err != nil {
168-
// TODO: report gracefully
169-
panic(err)
169+
tracer.log.Printf("failed to send queued report for operation %q: %v", operation.ID, err)
170170
}
171171
}()
172172
}

tracer_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,80 @@ func TestSendingReportsOverHTTP(t *testing.T) {
298298
res := map[string]any{}
299299
client.New(srv).MustPost("{ todos { id } }", &res)
300300
}
301+
302+
//
303+
304+
type testLogger struct {
305+
logs []string
306+
}
307+
308+
func newTestLogger() *testLogger {
309+
return &testLogger{}
310+
}
311+
312+
func (l *testLogger) Printf(format string, v ...any) {
313+
l.logs = append(l.logs, fmt.Sprintf(format, v...))
314+
}
315+
316+
func TestSendReportError(t *testing.T) {
317+
srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}}))
318+
srv.AddTransport(transport.POST{})
319+
320+
testLogger := newTestLogger()
321+
322+
srv.Use(NewTracer(
323+
uu.IDv4().String(),
324+
"<token>",
325+
WithGenerateID(func(operation string, operationName nullable.TrimmedString) string {
326+
return "id"
327+
}),
328+
WithSendReportTimeout(0),
329+
WithSendReport(func(ctx context.Context, endpoint, target, token string, report *Report) error {
330+
return fmt.Errorf("test fail send report")
331+
}),
332+
WithLogger(testLogger),
333+
))
334+
335+
res := map[string]any{}
336+
client.New(srv).MustPost("{ todos { id } }", &res)
337+
338+
snaps.MatchSnapshot(t, testLogger.logs)
339+
}
340+
341+
func TestSendReportServerError(t *testing.T) {
342+
var attempt = 0
343+
tserver := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
344+
if attempt == 0 {
345+
// with body
346+
res.WriteHeader(http.StatusBadRequest)
347+
res.Write([]byte("test server error"))
348+
attempt++
349+
} else {
350+
// without body
351+
res.WriteHeader(http.StatusInternalServerError)
352+
}
353+
}))
354+
defer tserver.Close()
355+
356+
srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}}))
357+
srv.AddTransport(transport.POST{})
358+
359+
testLogger := newTestLogger()
360+
361+
srv.Use(NewTracer(
362+
uu.IDv4().String(),
363+
"<token>",
364+
WithEndpoint(tserver.URL),
365+
WithGenerateID(func(operation string, operationName nullable.TrimmedString) string {
366+
return fmt.Sprintf("a%d", attempt)
367+
}),
368+
WithSendReportTimeout(0),
369+
WithLogger(testLogger),
370+
))
371+
372+
res := map[string]any{}
373+
client.New(srv).MustPost("query a1 { todos { id } }", &res)
374+
client.New(srv).MustPost("query a2 { todos { id } }", &res)
375+
376+
snaps.MatchSnapshot(t, testLogger.logs)
377+
}

traceroptions.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"io"
89
"net/http"
910
"time"
1011

@@ -68,7 +69,14 @@ func defaultSendReport(ctx context.Context, endpoint, target, token string, repo
6869
}
6970

7071
if res.StatusCode != http.StatusOK {
71-
return fmt.Errorf("report sending failed with %d %s", res.StatusCode, res.Status)
72+
defer res.Body.Close()
73+
body, _ := io.ReadAll(res.Body)
74+
bodyStr := string(body)
75+
if bodyStr == "" {
76+
return fmt.Errorf("report sending failed with %s (no body)", res.Status)
77+
} else {
78+
return fmt.Errorf("report sending failed with %s: %s", res.Status, bodyStr)
79+
}
7280
}
7381

7482
return nil
@@ -84,6 +92,17 @@ func WithSendReport(fn SendReport) TracerOption {
8492
})
8593
}
8694

95+
// WithLogger sets the logger to be used by the tracer.
96+
// The logger is used for reporting errors during tracing. If set to nil, logging is disabled.
97+
// You can use the standard Go logger or provide a custom implementation (e.g., logrus, zap).
98+
func WithLogger(logger Logger) TracerOption {
99+
return tracerOptionFn(func(tracer *Tracer) {
100+
tracer.log = logger
101+
})
102+
}
103+
104+
var defaultLogger = NewLogger()
105+
87106
type TracerOption interface {
88107
set(*Tracer)
89108
}

0 commit comments

Comments
 (0)