| Tim Windelschmidt | 6d33a43 | 2025-02-04 14:34:25 +0100 | [diff] [blame] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| Serge Bazanski | fb0fb6d | 2022-02-18 12:11:28 +0100 | [diff] [blame] | 4 | package rpc |
| 5 | |
| 6 | import ( |
| 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 Bazanski | 3c5d063 | 2024-09-12 10:49:12 +0000 | [diff] [blame] | 16 | "source.monogon.dev/go/logging" |
| Serge Bazanski | fb0fb6d | 2022-02-18 12:11:28 +0100 | [diff] [blame] | 17 | ) |
| 18 | |
| 19 | // Span implements a compatible subset of |
| 20 | // go.opentelemetry.io/otel/trace.Span. |
| 21 | |
| Tim Windelschmidt | 8732d43 | 2024-04-18 23:20:05 +0200 | [diff] [blame] | 22 | // Span is used in place of trace.Span until opentelemetry support |
| Serge Bazanski | fb0fb6d | 2022-02-18 12:11:28 +0100 | [diff] [blame] | 23 | // 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. |
| 26 | type 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. |
| 66 | type 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 Bazanski | 3c5d063 | 2024-09-12 10:49:12 +0000 | [diff] [blame] | 70 | logger logging.Leveled |
| Serge Bazanski | fb0fb6d | 2022-02-18 12:11:28 +0100 | [diff] [blame] | 71 | // 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 Bazanski | 3c5d063 | 2024-09-12 10:49:12 +0000 | [diff] [blame] | 77 | func newLogtreeSpan(l logging.Leveled) *logtreeSpan { |
| Serge Bazanski | fb0fb6d | 2022-02-18 12:11:28 +0100 | [diff] [blame] | 78 | uid := uint64(time.Now().UnixNano()) |
| 79 | return &logtreeSpan{ |
| 80 | logger: l, |
| 81 | uid: uid, |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | func (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 | |
| 92 | func (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 | |
| 100 | func (l *logtreeSpan) IsRecording() bool { |
| 101 | return l.logger != nil |
| 102 | } |
| 103 | |
| 104 | type spanKey string |
| 105 | |
| 106 | var 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. |
| 110 | func 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. |
| 117 | func 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. |
| 131 | type spanServerStream struct { |
| 132 | grpc.ServerStream |
| 133 | span *logtreeSpan |
| 134 | } |
| 135 | |
| 136 | func (s *spanServerStream) Context() context.Context { |
| 137 | return contextWithSpan(s.ServerStream.Context(), s.span) |
| 138 | } |
| 139 | |
| 140 | func (s *spanServerStream) SendMsg(m interface{}) error { |
| 141 | s.span.Printf("RPC send: %s", protoMessagePretty(m)) |
| 142 | return s.ServerStream.SendMsg(m) |
| 143 | } |
| 144 | |
| 145 | func (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. |
| 157 | func 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 Bazanski | 8b4a5ea | 2023-03-29 14:16:59 +0200 | [diff] [blame] | 170 | 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 Bazanski | fb0fb6d | 2022-02-18 12:11:28 +0100 | [diff] [blame] | 175 | } |