Openstatus
www.openstatus.dev
1package otel
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8
9 "github.com/openstatushq/openstatus/apps/checker/checker"
10 "github.com/openstatushq/openstatus/apps/checker/request"
11 "github.com/rs/zerolog/log"
12
13 "go.opentelemetry.io/otel"
14 "go.opentelemetry.io/otel/attribute"
15 "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
16 "go.opentelemetry.io/otel/metric"
17 sdkMetrics "go.opentelemetry.io/otel/sdk/metric"
18 "go.opentelemetry.io/otel/sdk/resource"
19
20 semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
21)
22
23func SetupOTelSDK(
24 ctx context.Context, url string, probes string, headers map[string]string,
25) (shutdown func(context.Context) error, err error) {
26 var shutdownFuncs []func(context.Context) error
27
28 shutdown = func(ctx context.Context) error {
29 var err error
30
31 for _, fn := range shutdownFuncs {
32 err = errors.Join(err, fn(ctx))
33 }
34
35 shutdownFuncs = nil
36
37 return err
38 }
39
40 handleErr := func(inErr error) {
41 err = errors.Join(inErr, shutdown(ctx))
42 }
43
44 res, err := newResource()
45 if err != nil {
46 handleErr(err)
47
48 return nil, err
49 }
50
51 meterProvider, err := newMeterProvider(ctx, res, url, headers)
52 if err != nil {
53 handleErr(err)
54
55 return nil, err
56 }
57
58 shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
59
60 otel.SetMeterProvider(meterProvider)
61
62 return shutdown, nil
63}
64
65func newResource() (*resource.Resource, error) {
66 return resource.Merge(resource.Default(),
67 resource.NewWithAttributes(semconv.SchemaURL,
68 semconv.ServiceName("openstatus-synthetic-check"),
69 semconv.ServiceVersion("0.1.0"),
70 ))
71}
72
73func newMeterProvider(
74 ctx context.Context,
75 res *resource.Resource,
76 url string,
77 headers map[string]string,
78) (*sdkMetrics.MeterProvider, error) {
79
80 grafanaExporter, err := otlpmetrichttp.New(ctx,
81 otlpmetrichttp.WithEndpointURL(url),
82 // otlpmetrichttp.WithInsecure(),
83 otlpmetrichttp.WithHeaders(headers),
84 )
85 if err != nil {
86 return nil, err
87 }
88
89 meterProvider := sdkMetrics.NewMeterProvider(
90 sdkMetrics.WithResource(res),
91
92 sdkMetrics.WithReader(sdkMetrics.NewPeriodicReader(grafanaExporter,
93 sdkMetrics.WithInterval(3*time.Second))),
94 )
95
96 return meterProvider, nil
97}
98
99func RecordHTTPMetrics(ctx context.Context, req request.HttpCheckerRequest, result checker.Response, region string) {
100
101 otelShutdown, err := SetupOTelSDK(ctx, req.OtelConfig.Endpoint, region, req.OtelConfig.Headers)
102
103 if err != nil {
104 log.Ctx(ctx).Error().Err(err).Msg("Error setting up otel")
105 }
106
107 defer func() {
108 err = errors.Join(err, otelShutdown(ctx))
109 if err != nil {
110 log.Ctx(ctx).Error().Err(err).Msg("Error sending the data")
111 }
112 }()
113
114 meter := otel.Meter("OpenStatus")
115
116 if result.Error != "" {
117 att := metric.WithAttributes(
118 attribute.String("openstatus.probes", region),
119 attribute.String("openstatus.target", req.URL),
120 semconv.HTTPResponseStatusCode(result.Status),
121 )
122 statusError, err := meter.Int64Counter("openstatus.error", metric.WithDescription("Status of the check"))
123
124 if err != nil {
125 log.Ctx(ctx).Error().Err(err).Msg("Error setting up counter")
126 }
127
128 statusError.Add(ctx, (1), att)
129
130 return
131 }
132
133 att := metric.WithAttributes(
134 attribute.String("openstatus.probes", region),
135 attribute.String("openstatus.target", req.URL),
136 semconv.HTTPResponseStatusCode(result.Status),
137 )
138
139 status, err := meter.Int64Counter("openstatus.status", metric.WithDescription("Status of the check"))
140
141 if err != nil {
142 log.Ctx(ctx).Error().Err(err).Msg("Error setting up conunter")
143 }
144
145 status.Add(ctx, 1, att)
146
147 gauge, err := meter.Float64Gauge("openstatus.http.request.duration",
148 metric.WithDescription("Duration of the check"), metric.WithUnit("ms"))
149
150 if err != nil {
151 fmt.Println("Error creating gauge", err)
152 }
153
154 gauge.Record(ctx, float64(result.Latency), att)
155
156 gaugeDns, err := meter.Float64Gauge("openstatus.http.dns.duration",
157 metric.WithDescription("Duration of the dns lookup"), metric.WithUnit("ms"))
158
159 if err != nil {
160 fmt.Println("Error creating gauge", err)
161 }
162
163 gaugeDns.Record(ctx, float64(result.Timing.DnsDone-result.Timing.DnsStart), att)
164
165 gaugeConnect, err := meter.Float64Gauge("openstatus.http.connection.duration",
166 metric.WithDescription("Duration of the connection"), metric.WithUnit("ms"))
167
168 if err != nil {
169 fmt.Println("Error creating gauge", err)
170 }
171
172 gaugeConnect.Record(ctx, float64(result.Timing.ConnectDone-result.Timing.ConnectStart), att)
173
174 gaugeTLS, err := meter.Float64Gauge("openstatus.http.tls.duration",
175 metric.WithDescription("Duration of the tls handshake"), metric.WithUnit("ms"))
176
177 if err != nil {
178 fmt.Println("Error creating gauge", err)
179 }
180
181 gaugeTLS.Record(ctx, float64(result.Timing.TlsHandshakeDone-result.Timing.TlsHandshakeStart), att)
182
183 gaugeTTFB, err := meter.Float64Gauge("openstatus.http.ttfb.duration",
184 metric.WithDescription("Duration of the ttfb"), metric.WithUnit("ms"))
185
186 if err != nil {
187 fmt.Println("Error creating gauge", err)
188 }
189
190 gaugeTTFB.Record(ctx, float64(result.Timing.FirstByteDone-result.Timing.FirstByteStart), att)
191
192 gaugeTransfer, err := meter.Float64Gauge("openstatus.http.transfer.duration",
193 metric.WithDescription("Duration of the transfer"), metric.WithUnit("ms"))
194
195 if err != nil {
196 fmt.Println("Error creating gauge", err)
197 }
198
199 gaugeTransfer.Record(ctx, float64(result.Timing.TransferDone-result.Timing.TransferStart), att)
200}
201
202func RecordTCPMetrics(ctx context.Context, req request.TCPCheckerRequest, result checker.TCPResponse, region string) {
203
204 otelShutdown, err := SetupOTelSDK(ctx, req.OtelConfig.Endpoint, region, req.OtelConfig.Headers)
205
206 if err != nil {
207 log.Ctx(ctx).Error().Err(err).Msg("Error setting up otel")
208 }
209
210 defer func() {
211 err = errors.Join(err, otelShutdown(ctx))
212 if err != nil {
213 log.Ctx(ctx).Error().Err(err).Msg("Error sending the data")
214 }
215 }()
216
217 meter := otel.Meter("OpenStatus")
218
219 if result.Error == 1 {
220 att := metric.WithAttributes(
221 attribute.String("openstatus.probes", region),
222 attribute.String("openstatus.target", req.URI),
223 )
224 statusError, err := meter.Int64Counter("openstatus.error", metric.WithDescription("Status of the check"))
225
226 if err != nil {
227 log.Ctx(ctx).Error().Err(err).Msg("Error setting up counter")
228 }
229
230 statusError.Add(ctx, (1), att)
231
232 return
233 }
234
235 att := metric.WithAttributes(
236 attribute.String("openstatus.probes", region),
237 attribute.String("openstatus.target", req.URI),
238 )
239
240 gauge, err := meter.Float64Gauge("openstatus.tcp.request.duration",
241 metric.WithDescription("Duration of the check"), metric.WithUnit("ms"))
242
243 if err != nil {
244 fmt.Println("Error creating gauge", err)
245 }
246
247 gauge.Record(ctx, float64(result.Latency), att)
248
249 gaugeTCP, err := meter.Float64Gauge("openstatus.tcp.tcp.duration",
250 metric.WithDescription("Duration of the dns lookup"), metric.WithUnit("ms"))
251
252 if err != nil {
253 fmt.Println("Error creating gauge", err)
254 }
255
256 gaugeTCP.Record(ctx, float64(result.Timing.TCPDone-result.Timing.TCPStart), att)
257
258}