Skip to content

Commit e5b9545

Browse files
author
Tom Coupland
committed
Span to View converter
We have added open census span calls in interesting parts of the code, but without creating a jaeger or zipkin cluster they are not going anywhere. Here we add a SpanConverter that converts our Spans into OpenCensus distribution Views. It contains an in memory map of the Measures for these Views, recording values for the length of time reported to be spent in a Span. This list has a hard limit of 100, as there is a risk of a metric explosion should an element of the system include some ID value in it's Span names. Gin does this and those are discarded, but there might be others that we'll have to deal with in the future when discovered.
1 parent d454ff9 commit e5b9545

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

api/server/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,10 @@ func WithPrometheus() Option {
739739
s.promExporter = exporter
740740
view.RegisterExporter(exporter)
741741

742+
converter, _ := NewSpanConverter(Options{Namespace: "fn"})
743+
trace.RegisterExporter(converter)
744+
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
745+
742746
return nil
743747
}
744748
}

api/server/spanconverter.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"strings"
6+
"sync"
7+
"time"
8+
"unicode"
9+
10+
"go.opencensus.io/stats"
11+
view "go.opencensus.io/stats/view"
12+
"go.opencensus.io/trace"
13+
)
14+
15+
// SpanConverter registers as a opencensus Trace Exporter,
16+
// but it converts all the Spans in Views and registers them as such
17+
// A View exporter will then export them as normal.
18+
type SpanConverter struct {
19+
opts Options
20+
measures map[string]*stats.Float64Measure
21+
viewsMu sync.Mutex
22+
e view.Exporter
23+
}
24+
25+
// Options contains options for configuring the exporter.
26+
type Options struct {
27+
Namespace string
28+
}
29+
30+
func NewSpanConverter(o Options) (*SpanConverter, error) {
31+
c := &SpanConverter{
32+
opts: o,
33+
measures: make(map[string]*stats.Float64Measure),
34+
}
35+
return c, nil
36+
}
37+
38+
var maxViews = 100
39+
40+
// Spans are rejected if there are already maxViews (100) or they are
41+
// prefixed with '/', gin as been observed creating Span id specific
42+
// named Spans.
43+
func (c *SpanConverter) rejectSpan(sd *trace.SpanData) bool {
44+
return len(c.measures) > maxViews || urlName(sd)
45+
}
46+
47+
// ExportSpan creates a Measure and View once per Span.Name, registering
48+
// the View with the opencensus register. The length of time reported
49+
// by the span is then recorded using the measure.
50+
func (c *SpanConverter) ExportSpan(sd *trace.SpanData) {
51+
if c.rejectSpan(sd) {
52+
return
53+
}
54+
m := c.getMeasure(sd)
55+
56+
spanTimeNanos := sd.EndTime.Sub(sd.StartTime)
57+
spanTimeMillis := float64(int64(spanTimeNanos / time.Millisecond))
58+
59+
stats.Record(context.Background(), m.M(spanTimeMillis))
60+
}
61+
62+
func (c *SpanConverter) getMeasure(span *trace.SpanData) *stats.Float64Measure {
63+
sig := sanitize(span.Name)
64+
c.viewsMu.Lock()
65+
m, ok := c.measures[sig]
66+
c.viewsMu.Unlock()
67+
68+
if !ok {
69+
m = stats.Float64(sig+"_span_time", "The span length in milliseconds", "ms")
70+
v := &view.View{
71+
Name: sanitize(span.Name),
72+
Description: sanitize(span.Name),
73+
Measure: m,
74+
Aggregation: view.Distribution(1,
75+
10,
76+
50,
77+
100,
78+
250,
79+
500,
80+
1000,
81+
10000,
82+
60000,
83+
120000),
84+
}
85+
86+
c.viewsMu.Lock()
87+
c.measures[sig] = m
88+
view.Register(v)
89+
c.viewsMu.Unlock()
90+
}
91+
92+
return m
93+
}
94+
95+
const labelKeySizeLimit = 100
96+
97+
// sanitize returns a string that is trunacated to 100 characters if it's too
98+
// long, and replaces non-alphanumeric characters to underscores.
99+
func sanitize(s string) string {
100+
if len(s) == 0 {
101+
return s
102+
}
103+
if len(s) > labelKeySizeLimit {
104+
s = s[:labelKeySizeLimit]
105+
}
106+
s = strings.Map(sanitizeRune, s)
107+
if unicode.IsDigit(rune(s[0])) {
108+
s = "key_" + s
109+
}
110+
if s[0] == '_' {
111+
s = "key" + s
112+
}
113+
return s
114+
}
115+
116+
// converts anything that is not a letter or digit to an underscore
117+
func sanitizeRune(r rune) rune {
118+
if unicode.IsLetter(r) || unicode.IsDigit(r) {
119+
return r
120+
}
121+
// Everything else turns into an underscore
122+
return '_'
123+
}
124+
125+
//Gin creates spans for all paths, containing ID values.
126+
//We can safely discard these, as other histograms are being created for them.
127+
func urlName(sd *trace.SpanData) bool {
128+
return strings.HasPrefix(sd.Name, "/")
129+
}

0 commit comments

Comments
 (0)