blob: 9f3c22de0b4605c62373a6c67dbff645ce7bef11 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanskifb0fb6d2022-02-18 12:11:28 +01004package rpc
5
6import (
7 "context"
8 "fmt"
9 "strings"
10 "time"
11
12 "google.golang.org/grpc"
13 "google.golang.org/protobuf/encoding/prototext"
14 "google.golang.org/protobuf/proto"
15
Serge Bazanski3c5d0632024-09-12 10:49:12 +000016 "source.monogon.dev/go/logging"
Serge Bazanskifb0fb6d2022-02-18 12:11:28 +010017)
18
19// Span implements a compatible subset of
20// go.opentelemetry.io/otel/trace.Span.
21
Tim Windelschmidt8732d432024-04-18 23:20:05 +020022// Span is used in place of trace.Span until opentelemetry support
Serge Bazanskifb0fb6d2022-02-18 12:11:28 +010023// is fully implemented and thus the library is pulled in. Once
24// that happens, all relevant methods will be replace with an
25// embedding of the trace.Span interface.
26type Span interface {
27 // End() not implemented.
28
29 // AddEvent adds an event with the provided name.
30 //
31 // Changed from otel/trace.Span: no options.
32 AddEvent(name string)
33
34 // IsRecording returns the recording state of the Span. It will return true if
35 // the Span is active and events can be recorded.
36 IsRecording() bool
37
38 // RecordError() not implemented.
39
40 // SpanContext() not implemented.
41
42 // SetStatus() not implemented.
43
44 // SetName() not implemented.
45
46 // SetAttributes() not implemented.
47
48 // TraceProvider() not implemented.
49
50 // Monogon extensions follow. These call into standard otel.Span methods
51 // (and effectively underlying model), but provide tighter API for
52 // Metropolis.
53
54 // Printf adds an event via AddEvent after performing a string format expansion
55 // via fmt.Sprintf. The formatting is performed during the call if the span is
56 // recording, or never if it isn't.
57 Printf(format string, a ...interface{})
58}
59
60// logtreeSpan is an implementation of Span which just forwards events into a
61// local logtree LeveledLogger. All spans are always recording.
62//
63// This is a stop-gap implementation to introduce gRPC trace-based
64// logging/metrics into Metropolis which can then be evolved into a full-blown
65// opentelemetry implementation.
66type logtreeSpan struct {
67 // logger is the logtree LeveledLogger backing this span. All Events added into
68 // the Span will go straight into that logger. If the logger is nil, all events
69 // will be dropped instead.
Serge Bazanski3c5d0632024-09-12 10:49:12 +000070 logger logging.Leveled
Serge Bazanskifb0fb6d2022-02-18 12:11:28 +010071 // uid is the span ID of this logtreeSpan. Currently this is a monotonic counter
72 // based on the current nanosecond epoch, but this might change in the future.
73 // This field is ignored if logger is nil.
74 uid uint64
75}
76
Serge Bazanski3c5d0632024-09-12 10:49:12 +000077func newLogtreeSpan(l logging.Leveled) *logtreeSpan {
Serge Bazanskifb0fb6d2022-02-18 12:11:28 +010078 uid := uint64(time.Now().UnixNano())
79 return &logtreeSpan{
80 logger: l,
81 uid: uid,
82 }
83}
84
85func (l *logtreeSpan) AddEvent(name string) {
86 if l.logger == nil {
87 return
88 }
89 l.logger.WithAddedStackDepth(1).Infof("Span %x: %s", l.uid, name)
90}
91
92func (l *logtreeSpan) Printf(format string, a ...interface{}) {
93 if l.logger == nil {
94 return
95 }
96 msg := fmt.Sprintf(format, a...)
97 l.logger.WithAddedStackDepth(1).Infof("Span %x: %s", l.uid, msg)
98}
99
100func (l *logtreeSpan) IsRecording() bool {
101 return l.logger != nil
102}
103
104type spanKey string
105
106var spanKeyValue spanKey = "metropolis-trace-span"
107
108// contextWithSpan wraps a given context with a given logtreeSpan. This
109// logtreeSpan will be returned by Trace() calls on the returned context.
110func contextWithSpan(ctx context.Context, s *logtreeSpan) context.Context {
111 return context.WithValue(ctx, spanKeyValue, s)
112}
113
114// Trace returns the active Span for the current Go context. If no Span was set
115// up for this context, an inactive/empty span object is returned, on which
116// every operation is a no-op.
117func Trace(ctx context.Context) Span {
118 v := ctx.Value(spanKeyValue)
119 if v == nil {
120 return &logtreeSpan{}
121 }
122 if s, ok := v.(*logtreeSpan); ok {
123 return s
124 }
125 return &logtreeSpan{}
126}
127
128// spanServerStream is a grpc.ServerStream wrapper which contains some
129// logtreeSpan, and returns it as part of the Context() of the ServerStream. It
130// also intercepts SendMsg/RecvMsg and logs them to the same span.
131type spanServerStream struct {
132 grpc.ServerStream
133 span *logtreeSpan
134}
135
136func (s *spanServerStream) Context() context.Context {
137 return contextWithSpan(s.ServerStream.Context(), s.span)
138}
139
140func (s *spanServerStream) SendMsg(m interface{}) error {
141 s.span.Printf("RPC send: %s", protoMessagePretty(m))
142 return s.ServerStream.SendMsg(m)
143}
144
145func (s *spanServerStream) RecvMsg(m interface{}) error {
146 err := s.ServerStream.RecvMsg(m)
147 s.span.Printf("RPC recv: %s", protoMessagePretty(m))
148 return err
149}
150
151// protoMessagePretty attempts to pretty-print a given proto message into a
152// one-line string. The returned format is not guaranteed to be stable, and is
153// only intended to be used for debug purposes by operators.
154//
155// TODO(q3k): make this not print any confidential fields (once we have any),
156// eg. via extensions/annotations.
157func protoMessagePretty(m interface{}) string {
158 if m == nil {
159 return "nil"
160 }
161 v, ok := m.(proto.Message)
162 if !ok {
163 return "invalid"
164 }
165 name := string(v.ProtoReflect().Type().Descriptor().Name())
166 bytes, err := prototext.Marshal(v)
167 if err != nil {
168 return name
169 }
Serge Bazanski8b4a5ea2023-03-29 14:16:59 +0200170 pretty := strings.ReplaceAll(string(bytes), "\n", " ")
171 if len(pretty) > 100 {
172 pretty = pretty[:100] + "..."
173 }
174 return fmt.Sprintf("%s: %s", name, pretty)
Serge Bazanskifb0fb6d2022-02-18 12:11:28 +0100175}