Skip to content

Commit 8d971b7

Browse files
authored
Merge pull request #1338 from circleci/put-back-honeycomb
Revert "Remove honeycomb otel provider"
2 parents c535750 + f65ace2 commit 8d971b7

File tree

21 files changed

+1726
-71
lines changed

21 files changed

+1726
-71
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ See example / template repo [here](https://github.com/circleci/ex-service-templa
2020
- `httpserver/ginrouter` A common base for configuring a Gin router instance.
2121
- `httpserver/healthcheck` A healthcheck HTTP server that can accept all the checks from a `system`.
2222
- `mongoex` **Experimental** Common patterns using when talking to MongoDB.
23-
- `o11y` Observability that is currently backed by Otel. It also supports outputting
23+
- `o11y` Observability that is currently backed by Honeycomb. It also supports outputting
2424
trace data as JSON and plain or colored text output.
25+
- `o11y/honeycomb` The honeycomb-backed implementation of `o11y`.
2526
- `o11y/wrappers/o11ygin` `o11y` middleware for the Gin router.
2627
- `o11y/wrappers/o11ynethttp` `o11y` middleware for the standard Go HTTP server.
2728
- `rabbit` **Experimental** RabbitMQ publishing client.

config/o11y/doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/*
2+
Package o11y is the primary entrypoint for wiring up a standard configuration of the o11y
3+
observability system.
4+
*/
5+
package o11y

config/o11y/o11y.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package o11y
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"math"
8+
"os"
9+
"time"
10+
11+
"github.com/DataDog/datadog-go/statsd"
12+
"github.com/cenkalti/backoff/v5"
13+
"github.com/honeycombio/libhoney-go/transmission"
14+
"github.com/rollbar/rollbar-go"
15+
16+
"github.com/circleci/ex/config/secret"
17+
"github.com/circleci/ex/o11y"
18+
"github.com/circleci/ex/o11y/honeycomb"
19+
)
20+
21+
type Config struct {
22+
Statsd string
23+
RollbarToken secret.String
24+
RollbarEnv string
25+
RollbarServerRoot string
26+
HoneycombEnabled bool
27+
HoneycombDataset string
28+
HoneycombHost string
29+
HoneycombKey secret.String
30+
SampleTraces bool
31+
SampleKeyFunc func(map[string]any) string
32+
SampleRates map[string]int
33+
Format string
34+
Version string
35+
Service string
36+
StatsNamespace string
37+
38+
// Optional
39+
Mode string
40+
Debug bool
41+
RollbarDisabled bool
42+
StatsdTelemetryDisabled bool
43+
Writer io.Writer
44+
45+
// Sender allows setting a custom honeycomb sender, Typically the build-in one is preferred.
46+
Sender transmission.Sender
47+
48+
// Metrics allows setting a custom metrics client. Typically setting Statsd/StatsNamespace is preferred
49+
Metrics o11y.ClosableMetricsProvider
50+
}
51+
52+
// Setup is the primary entrypoint to initialize the o11y system.
53+
//
54+
//nolint:funlen
55+
func Setup(ctx context.Context, o Config) (context.Context, func(context.Context), error) {
56+
honeyConfig, err := honeyComb(o)
57+
if err != nil {
58+
return nil, nil, err
59+
}
60+
61+
hostname, _ := os.Hostname()
62+
63+
if o.Metrics != nil {
64+
honeyConfig.Metrics = o.Metrics
65+
} else if o.Statsd == "" {
66+
honeyConfig.Metrics = &statsd.NoOpClient{}
67+
} else {
68+
tags := []string{
69+
"service:" + o.Service,
70+
"version:" + o.Version,
71+
"hostname:" + hostname,
72+
}
73+
if o.Mode != "" {
74+
tags = append(tags, "mode:"+o.Mode)
75+
}
76+
77+
statsdOpts := []statsd.Option{
78+
statsd.WithNamespace(o.StatsNamespace),
79+
statsd.WithTags(tags),
80+
}
81+
if o.StatsdTelemetryDisabled {
82+
statsdOpts = append(statsdOpts, statsd.WithoutTelemetry())
83+
}
84+
85+
stats, err := backoff.Retry(ctx, func() (*statsd.Client, error) {
86+
return statsd.New(o.Statsd, statsdOpts...)
87+
}, backoff.WithBackOff(backoff.NewConstantBackOff(time.Second)), backoff.WithMaxTries(31))
88+
if err != nil {
89+
return ctx, nil, err
90+
}
91+
92+
honeyConfig.Metrics = stats
93+
}
94+
95+
o11yProvider := honeycomb.New(honeyConfig)
96+
o11yProvider.AddGlobalField("service", o.Service)
97+
o11yProvider.AddGlobalField("version", o.Version)
98+
if o.Mode != "" {
99+
o11yProvider.AddGlobalField("mode", o.Mode)
100+
}
101+
102+
if o.RollbarToken != "" {
103+
client := rollbar.NewAsync(o.RollbarToken.Raw(), o.RollbarEnv, o.Version, hostname, o.RollbarServerRoot)
104+
client.SetEnabled(!o.RollbarDisabled)
105+
client.Message(rollbar.INFO, "Deployment")
106+
o11yProvider = rollBarHoneycombProvider{
107+
Provider: o11yProvider,
108+
rollBarClient: client,
109+
}
110+
}
111+
112+
ctx = o11y.WithProvider(ctx, o11yProvider)
113+
114+
return ctx, o11yProvider.Close, nil
115+
}
116+
117+
type rollBarHoneycombProvider struct {
118+
o11y.Provider
119+
rollBarClient *rollbar.Client
120+
}
121+
122+
func (p rollBarHoneycombProvider) Close(ctx context.Context) {
123+
p.Provider.Close(ctx)
124+
_ = p.rollBarClient.Close()
125+
}
126+
127+
func (p rollBarHoneycombProvider) RollBarClient() *rollbar.Client {
128+
return p.rollBarClient
129+
}
130+
131+
func honeyComb(o Config) (honeycomb.Config, error) {
132+
if o.SampleKeyFunc == nil {
133+
o.SampleKeyFunc = func(fields map[string]interface{}) string {
134+
// defaults for gin server
135+
return fmt.Sprintf("%s %s %v",
136+
fields["http.server_name"],
137+
fields["http.route"],
138+
fields["http.status_code"],
139+
)
140+
}
141+
}
142+
143+
conf := honeycomb.Config{
144+
Host: o.HoneycombHost,
145+
Dataset: o.HoneycombDataset,
146+
Key: string(o.HoneycombKey),
147+
Format: o.Format,
148+
SendTraces: o.HoneycombEnabled,
149+
SampleTraces: o.SampleTraces,
150+
SampleKeyFunc: o.SampleKeyFunc,
151+
SampleRates: o.SampleRates,
152+
ServiceName: o.Service,
153+
Debug: o.Debug,
154+
Writer: o.Writer,
155+
Sender: o.Sender,
156+
}
157+
return conf, conf.Validate()
158+
}
159+
160+
func clampToUintMax(v int) uint {
161+
if int64(v) >= math.MaxUint32 {
162+
return math.MaxUint32 - 1
163+
}
164+
165+
//nolint gosec:G115 // This overflow is handled above
166+
return uint(v)
167+
}
168+
169+
// OtelSampleRates adapts the root o11y package configured map[string]int
170+
// sample rates to the Otel-required map[string]uint
171+
func (c *Config) OtelSampleRates() map[string]uint {
172+
adapted := make(map[string]uint, len(c.SampleRates))
173+
for k, v := range c.SampleRates {
174+
adapted[k] = clampToUintMax(v)
175+
}
176+
return adapted
177+
}

config/o11y/o11y_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package o11y_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"strings"
7+
"testing"
8+
9+
"gotest.tools/v3/assert"
10+
"gotest.tools/v3/assert/cmp"
11+
"gotest.tools/v3/poll"
12+
13+
o11yconfig "github.com/circleci/ex/config/o11y"
14+
"github.com/circleci/ex/config/secret"
15+
"github.com/circleci/ex/o11y"
16+
"github.com/circleci/ex/o11y/honeycomb"
17+
"github.com/circleci/ex/testing/fakestatsd"
18+
)
19+
20+
func TestO11Y_SecretRedacted(t *testing.T) {
21+
// confirm that honeycomb uses the json marshaller and that we dont see secrets
22+
buf := bytes.Buffer{}
23+
provider := honeycomb.New(honeycomb.Config{
24+
Writer: &buf,
25+
})
26+
ctx := context.Background()
27+
ctx, span := provider.StartSpan(ctx, "secret test")
28+
s := secret.String("super-secret")
29+
span.AddField("secret", s)
30+
span.End()
31+
provider.Close(ctx)
32+
assert.Check(t, !strings.Contains(buf.String(), "super-secret"), buf.String())
33+
assert.Check(t, cmp.Contains(buf.String(), "REDACTED"))
34+
}
35+
36+
func TestO11Y_SecretRedactedColor(t *testing.T) {
37+
// This test can be run to *VISUALLY* confirm that color honeycomb
38+
// formatter uses the json marshaller under the hood and that we dont see secrets.
39+
// Once you provide a writer the Format is ignored so this test does
40+
// not assert on anything since we cant catch the output
41+
//
42+
// The test is left in so that it can be eyeballed with -v
43+
// The other buffer based test does assert and it is very likely if secrets
44+
// leak in this test then the above test will fail in any case
45+
provider := honeycomb.New(honeycomb.Config{
46+
Format: "color",
47+
})
48+
ctx := context.Background()
49+
ctx, span := provider.StartSpan(ctx, "secret test")
50+
s := secret.String("super-secret")
51+
span.AddField("secret", s)
52+
span.End()
53+
provider.Close(ctx)
54+
}
55+
56+
func TestSetup_Wiring(t *testing.T) {
57+
s := fakestatsd.New(t)
58+
59+
ctx := context.Background()
60+
ctx, cleanup, err := o11yconfig.Setup(ctx, o11yconfig.Config{
61+
Statsd: s.Addr(),
62+
RollbarToken: "qwertyuiop",
63+
RollbarDisabled: true,
64+
RollbarEnv: "production",
65+
RollbarServerRoot: "github.com/circleci/ex",
66+
HoneycombEnabled: false,
67+
HoneycombDataset: "does-not-exist",
68+
HoneycombKey: "1234567890",
69+
SampleTraces: false,
70+
Format: "color",
71+
Version: "1.2.3",
72+
Service: "test-service",
73+
StatsNamespace: "test.service",
74+
Mode: "banana",
75+
Debug: true,
76+
})
77+
assert.Assert(t, err)
78+
79+
t.Run("Send metric", func(t *testing.T) {
80+
p := o11y.FromContext(ctx)
81+
err = p.MetricsProvider().Count("my_count", 1, []string{"mytag:myvalue"}, 1)
82+
assert.Check(t, err)
83+
})
84+
85+
t.Run("Cleanup provider", func(t *testing.T) {
86+
cleanup(ctx)
87+
})
88+
89+
t.Run("Check metrics received", func(t *testing.T) {
90+
poll.WaitOn(t, func(t poll.LogT) poll.Result {
91+
metrics := s.Metrics()
92+
if len(metrics) == 0 {
93+
return poll.Continue("no metrics found yet")
94+
}
95+
return poll.Success()
96+
})
97+
98+
metrics := s.Metrics()
99+
assert.Assert(t, cmp.Len(metrics, 1))
100+
metric := metrics[0]
101+
assert.Check(t, cmp.Equal("test.service.my_count", metric.Name))
102+
assert.Check(t, cmp.Equal("1|c|", metric.Value))
103+
assert.Check(t, cmp.Contains(metric.Tags, "service:test-service"))
104+
assert.Check(t, cmp.Contains(metric.Tags, "version:1.2.3"))
105+
assert.Check(t, cmp.Contains(metric.Tags, "mode:banana"))
106+
assert.Check(t, cmp.Contains(metric.Tags, "mytag:myvalue"))
107+
})
108+
}
109+
110+
func TestSetup_WithWriter(t *testing.T) {
111+
buf := bytes.Buffer{}
112+
ctx := context.Background()
113+
ctx, cleanup, err := o11yconfig.Setup(ctx, o11yconfig.Config{
114+
Writer: &buf,
115+
})
116+
assert.Assert(t, err)
117+
defer cleanup(ctx)
118+
119+
o11y.Log(ctx, "some log output")
120+
121+
assert.Check(t, cmp.Contains(buf.String(), "some log output"))
122+
}
123+
124+
func TestConfig_OTelSampleRates(t *testing.T) {
125+
conf := o11yconfig.Config{
126+
SampleRates: map[string]int{
127+
"foo": 128,
128+
},
129+
}
130+
otelSampleRates := conf.OtelSampleRates()
131+
132+
assert.Check(t, cmp.DeepEqual(otelSampleRates, map[string]uint{
133+
"foo": uint(128),
134+
}))
135+
}

config/o11y/otel.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
/*
2-
Package o11y is the primary entrypoint for wiring up a standard configuration of the o11y
3-
observability system.
4-
*/
51
package o11y
62

73
import (
84
"context"
95
"fmt"
106
"io"
11-
"math"
127
"os"
138
"time"
149

@@ -23,9 +18,6 @@ import (
2318
"github.com/circleci/ex/o11y/otel"
2419
)
2520

26-
// SampleOut is the maximum value the sample rate can be
27-
const SampleOut = math.MaxUint32
28-
2921
// OtelConfig contains all the things we need to configure for otel based instrumentation.
3022
type OtelConfig struct {
3123
GrpcHostAndPort string

0 commit comments

Comments
 (0)