Skip to content

Commit 91d68d3

Browse files
Adrian Fernandez De La Torreadri1197
authored andcommitted
Setup OTEL initial implementation
Signed-off-by: Adrian Fernandez De La Torre <adri1197@gmail.com>
1 parent 8f4c7b2 commit 91d68d3

3 files changed

Lines changed: 225 additions & 2 deletions

File tree

go.mod

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ require (
4141
github.com/spf13/pflag v1.0.6
4242
github.com/stretchr/testify v1.10.0
4343
gitlab.com/gitlab-org/api/client-go v0.134.0
44+
go.opentelemetry.io/otel v1.37.0
45+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0
46+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0
47+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0
48+
go.opentelemetry.io/otel/sdk v1.36.0
49+
go.opentelemetry.io/otel/trace v1.37.0
4450
golang.org/x/oauth2 v0.30.0
4551
golang.org/x/text v0.27.0
4652
google.golang.org/api v0.241.0
@@ -78,6 +84,7 @@ require (
7884
github.com/blang/semver/v4 v4.0.0 // indirect
7985
github.com/bradleyfalzon/ghinstallation/v2 v2.16.0 // indirect
8086
github.com/carapace-sh/carapace-shlex v1.0.1 // indirect
87+
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
8188
github.com/cespare/xxhash/v2 v2.3.0 // indirect
8289
github.com/chai2010/gettext-go v1.0.2 // indirect
8390
github.com/cloudevents/sdk-go/v2 v2.15.2 // indirect
@@ -122,6 +129,7 @@ require (
122129
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
123130
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
124131
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
132+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
125133
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
126134
github.com/hashicorp/go-version v1.7.0 // indirect
127135
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -167,9 +175,8 @@ require (
167175
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
168176
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
169177
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
170-
go.opentelemetry.io/otel v1.37.0 // indirect
171178
go.opentelemetry.io/otel/metric v1.37.0 // indirect
172-
go.opentelemetry.io/otel/trace v1.37.0 // indirect
179+
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
173180
go.uber.org/multierr v1.11.0 // indirect
174181
go.uber.org/zap v1.27.0 // indirect
175182
go.yaml.in/yaml/v2 v2.4.2 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ github.com/carapace-sh/carapace-shlex v1.0.1 h1:ww0JCgWpOVuqWG7k3724pJ18Lq8gh5pH
7474
github.com/carapace-sh/carapace-shlex v1.0.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
7575
github.com/cdevents/sdk-go v0.4.1 h1:Cr/iH/I51Z+slxKRx9AV7stn6hr2pjRHQ5wpPJhRLTU=
7676
github.com/cdevents/sdk-go v0.4.1/go.mod h1:3IhWLoY4vsyUEzv7XJbyr0BRQ0KPgvNx+wiD2hQGFNU=
77+
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
78+
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
7779
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
7880
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
7981
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -260,6 +262,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
260262
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
261263
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
262264
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
265+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
266+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
263267
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
264268
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
265269
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
@@ -418,6 +422,12 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6h
418422
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
419423
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
420424
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
425+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
426+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
427+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
428+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
429+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
430+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
421431
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
422432
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
423433
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
@@ -426,6 +436,8 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw
426436
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
427437
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
428438
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
439+
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
440+
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
429441
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
430442
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
431443
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=

internal/server/event_handlers.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package server
1818

1919
import (
2020
"context"
21+
"crypto/sha256"
22+
"encoding/base64"
23+
"encoding/hex"
2124
"errors"
2225
"fmt"
2326
"net/http"
@@ -27,6 +30,15 @@ import (
2730
"strings"
2831
"time"
2932

33+
"go.opentelemetry.io/otel"
34+
"go.opentelemetry.io/otel/attribute"
35+
"go.opentelemetry.io/otel/codes"
36+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
37+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
38+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
39+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
40+
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
41+
"go.opentelemetry.io/otel/trace"
3042
corev1 "k8s.io/api/core/v1"
3143
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3244
"k8s.io/apimachinery/pkg/labels"
@@ -88,11 +100,24 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request)
88100
"providerName": alert.Spec.ProviderRef.Name,
89101
})
90102
ctx := log.IntoContext(ctx, alertLogger)
103+
// OTEL processing
104+
var provider apiv1beta3.Provider
105+
providerName := types.NamespacedName{
106+
Namespace: alert.Namespace,
107+
Name: alert.Spec.ProviderRef.Name,
108+
}
109+
if err := s.kubeClient.Get(ctx, providerName, &provider); err == nil {
110+
s.processTracing(ctx, event, alert, &provider)
111+
}
112+
91113
if err := s.dispatchNotification(ctx, event, alert); err != nil {
92114
alertLogger.Error(err, "failed to dispatch notification")
93115
s.Eventf(alert, corev1.EventTypeWarning, "NotificationDispatchFailed",
94116
"failed to dispatch notification for %s: %s", involvedObjectString(event.InvolvedObject), err)
95117
}
118+
// else {
119+
120+
// }
96121
}
97122

98123
w.WriteHeader(http.StatusAccepted)
@@ -609,3 +634,182 @@ func excludeInternalMetadata(event *eventv1.Event) {
609634
delete(event.Metadata, key)
610635
}
611636
}
637+
638+
// Add this function to generate root span ID
639+
func generateRootSpanID(alertUID, sourceRevision string) string {
640+
input := fmt.Sprintf("%s:%s", alertUID, sourceRevision)
641+
hash := sha256.Sum256([]byte(input))
642+
return hex.EncodeToString(hash[:])
643+
}
644+
645+
// Add this function to check if provider supports OTLP
646+
func isOTLPProvider(providerType string) bool {
647+
otlpProviders := []string{"jaeger", "tempo", "otlp", "generic"}
648+
return slices.Contains(otlpProviders, providerType)
649+
}
650+
651+
// Add this function to process tracing
652+
func (s *EventServer) processTracing(ctx context.Context, event *eventv1.Event, alert *apiv1beta3.Alert, provider *apiv1beta3.Provider) {
653+
654+
if isOTLPProvider(provider.Spec.Type) {
655+
s.setupOTLPExporter(ctx, provider)
656+
s.sendOTLPTrace(ctx, event, alert, provider)
657+
} else {
658+
s.logTraceWarning(ctx, event, alert)
659+
}
660+
}
661+
662+
// Add this function to send OTLP traces
663+
func (s *EventServer) sendOTLPTrace(ctx context.Context, event *eventv1.Event, alert *apiv1beta3.Alert, provider *apiv1beta3.Provider) {
664+
revision, hasRevision := event.GetRevision()
665+
if !hasRevision {
666+
return
667+
}
668+
669+
var spanCtx context.Context = ctx
670+
spanID := generateRootSpanID(string(alert.UID), revision)
671+
tracer := otel.Tracer("flux-notification-controller")
672+
673+
// If a source kind is considered a potential root span
674+
if isSourceKind(event.InvolvedObject.Kind) {
675+
if !s.spanExists(ctx, spanID, provider) {
676+
spanCtx = context.Background() // Create root span
677+
}
678+
}
679+
680+
_, span := tracer.Start(spanCtx, event.InvolvedObject.Kind)
681+
defer span.End()
682+
683+
span.SetAttributes(
684+
semconv.ServiceName("flux-notification-controller"),
685+
attribute.String("flux.trace.id", spanID),
686+
attribute.String("flux.object.kind", event.InvolvedObject.Kind),
687+
attribute.String("flux.object.name", event.InvolvedObject.Name),
688+
attribute.String("flux.object.namespace", event.InvolvedObject.Namespace),
689+
attribute.String("flux.event.reason", event.Reason),
690+
)
691+
692+
span.AddEvent(event.Message, trace.WithTimestamp(event.Timestamp.Time))
693+
if event.Severity == "error" {
694+
span.SetStatus(codes.Error, event.Message)
695+
}
696+
}
697+
698+
func (s *EventServer) setupOTLPExporter(ctx context.Context, provider *apiv1beta3.Provider) {
699+
httpOptions := []otlptracehttp.Option{otlptracehttp.WithEndpoint(provider.Spec.Address)}
700+
grpcOptions := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(provider.Spec.Address)}
701+
702+
// NOTE: Posibly reuse extractAuthFromSecret()
703+
// Add authentication if secretRef is set
704+
if provider.Spec.SecretRef != nil {
705+
var secret corev1.Secret
706+
secretName := types.NamespacedName{
707+
Namespace: provider.Namespace,
708+
Name: provider.Spec.SecretRef.Name,
709+
}
710+
if err := s.kubeClient.Get(ctx, secretName, &secret); err == nil {
711+
var headers map[string]string
712+
if token, ok := secret.Data["token"]; ok {
713+
headers = map[string]string{"Authorization": "Bearer " + string(token)}
714+
715+
} else {
716+
user, userOk := secret.Data["username"]
717+
pass, passOk := secret.Data["password"]
718+
if userOk && passOk {
719+
auth := base64.StdEncoding.EncodeToString([]byte(string(user) + ":" + string(pass)))
720+
headers = map[string]string{"Authorization": "Basic " + auth}
721+
}
722+
}
723+
httpOptions = append(httpOptions, otlptracehttp.WithHeaders(headers))
724+
grpcOptions = append(grpcOptions, otlptracegrpc.WithHeaders(headers))
725+
}
726+
}
727+
728+
var exporter *otlptrace.Exporter
729+
var err error
730+
731+
if err != nil {
732+
return
733+
}
734+
735+
if strings.HasPrefix(provider.Spec.Address, "http") {
736+
httpOptions = append(httpOptions, otlptracehttp.WithInsecure())
737+
exporter, err = otlptracehttp.New(ctx, httpOptions...)
738+
} else {
739+
grpcOptions = append(grpcOptions, otlptracegrpc.WithInsecure())
740+
exporter, err = otlptracegrpc.New(ctx, grpcOptions...)
741+
}
742+
743+
if err != nil {
744+
return
745+
}
746+
747+
defer exporter.Shutdown(ctx)
748+
749+
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
750+
otel.SetTracerProvider(tp)
751+
defer tp.Shutdown(ctx)
752+
}
753+
754+
// Query to get the spanID
755+
func (s *EventServer) spanExists(ctx context.Context, spanID string, provider *apiv1beta3.Provider) bool {
756+
if !isOTLPProvider(provider.Spec.Type) {
757+
return false
758+
}
759+
760+
queryURL := fmt.Sprintf("%s/api/traces/%s", provider.Spec.Address, spanID)
761+
req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil)
762+
if err != nil {
763+
return false
764+
}
765+
766+
if provider.Spec.SecretRef != nil {
767+
var secret corev1.Secret
768+
secretName := types.NamespacedName{
769+
Namespace: provider.Namespace,
770+
Name: provider.Spec.SecretRef.Name,
771+
}
772+
if err := s.kubeClient.Get(ctx, secretName, &secret); err == nil {
773+
if token, ok := secret.Data["token"]; ok {
774+
req.Header.Set("Authorization", "Bearer "+string(token))
775+
} else {
776+
user, userOk := secret.Data["username"]
777+
pass, passOk := secret.Data["password"]
778+
if userOk && passOk {
779+
req.SetBasicAuth(string(user), string(pass))
780+
}
781+
}
782+
}
783+
}
784+
785+
resp, err := http.DefaultClient.Do(req)
786+
if err != nil {
787+
return false
788+
}
789+
defer resp.Body.Close()
790+
791+
return resp.StatusCode == http.StatusOK
792+
}
793+
794+
// Add this function to log trace warnings for non-OTLP providers
795+
func (s *EventServer) logTraceWarning(ctx context.Context, event *eventv1.Event, alert *apiv1beta3.Alert) {
796+
logger := log.FromContext(ctx)
797+
spanType := "child"
798+
if isSourceKind(event.InvolvedObject.Kind) {
799+
spanType = "root"
800+
}
801+
802+
logger.Info("trace information (provider does not support OTLP)",
803+
"spanType", spanType,
804+
"alertUID", string(alert.UID),
805+
"eventReason", event.Reason,
806+
"objectKind", event.InvolvedObject.Kind,
807+
"objectName", event.InvolvedObject.Name,
808+
"objectNamespace", event.InvolvedObject.Namespace,
809+
)
810+
}
811+
812+
func isSourceKind(kind string) bool {
813+
sourceKinds := []string{"GitRepository", "OCIRepository", "HelmRepository", "Bucket"}
814+
return slices.Contains(sourceKinds, kind)
815+
}

0 commit comments

Comments
 (0)