From 177521953972b21fd514ae4ee6fef7258a759949 Mon Sep 17 00:00:00 2001 From: William Dumont Date: Mon, 3 Feb 2025 17:56:56 +0100 Subject: [PATCH 01/11] add live graph backend --- go.mod | 5 +- go.sum | 8 -- internal/component/discovery/discovery.go | 7 +- .../component/discovery/relabel/relabel.go | 7 +- internal/component/loki/process/process.go | 18 ++- .../component/loki/process/process_test.go | 2 +- internal/component/loki/relabel/relabel.go | 13 +- .../loki/secretfilter/secretfilter.go | 9 +- .../component/otelcol/connector/connector.go | 2 +- .../otelcol/connector/spanlogs/spanlogs.go | 2 +- .../component/otelcol/exporter/exporter.go | 2 +- .../component/otelcol/exporter/loki/loki.go | 2 +- .../otelcol/exporter/prometheus/prometheus.go | 2 +- .../internal/lazyconsumer/lazyconsumer.go | 15 ++- .../lazyconsumer/lazyconsumer_test.go | 10 +- .../livedebuggingconsumer.go | 55 ++++++-- .../otelcol/processor/discovery/discovery.go | 2 +- .../component/otelcol/processor/processor.go | 2 +- .../component/prometheus/relabel/relabel.go | 13 +- .../prometheus/remotewrite/remote_write.go | 52 ++++++-- internal/service/livedebugging/feed.go | 50 +++++++ .../service/livedebugging/livedebugging.go | 88 ++++++++++-- .../livedebugging/livedebugging_test.go | 125 +++++++++++++++--- .../testlivedebugging/testlivedebugging.go | 22 +++ internal/web/api/api.go | 114 +++++++++++++++- internal/web/api/data.go | 15 +++ 26 files changed, 558 insertions(+), 84 deletions(-) create mode 100644 internal/service/livedebugging/feed.go create mode 100644 internal/web/api/data.go diff --git a/go.mod b/go.mod index d072c0656d..edc3dc93f8 100644 --- a/go.mod +++ b/go.mod @@ -499,7 +499,7 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect - github.com/go-kit/kit v0.13.0 // indirect + github.com/go-kit/kit v0.13.0 github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -883,9 +883,6 @@ require ( github.com/containers/common v0.61.0 // indirect github.com/deneonet/benc v1.1.2 // indirect github.com/itchyny/timefmt-go v0.1.6 // indirect - github.com/onsi/ginkgo/v2 v2.21.0 // indirect - github.com/onsi/gomega v1.35.1 // indirect - go.etcd.io/bbolt v1.3.11 // indirect ) // NOTE: replace directives below must always be *temporary*. diff --git a/go.sum b/go.sum index 129499e92f..a5562f8eb4 100644 --- a/go.sum +++ b/go.sum @@ -1852,17 +1852,11 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gosnmp/gosnmp v1.37.0 h1:/Tf8D3b9wrnNuf/SfbvO+44mPrjVphBhRtcGg22V07Y= -github.com/gosnmp/gosnmp v1.37.0/go.mod h1:GDH9vNqpsD7f2HvZhKs5dlqSEcAS6s6Qp099oZRCR+M= github.com/gosnmp/gosnmp v1.38.0 h1:I5ZOMR8kb0DXAFg/88ACurnuwGwYkXWq3eLpJPHMEYc= github.com/gosnmp/gosnmp v1.38.0/go.mod h1:FE+PEZvKrFz9afP9ii1W3cprXuVZ17ypCcyyfYuu5LY= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/grafana/alloy-remote-config v0.0.9 h1:gy34SxZ8Iq/HrDTIFZi80+8BlT+FnJhKiP9mryHNEUE= github.com/grafana/alloy-remote-config v0.0.9/go.mod h1:kHE1usYo2WAVCikQkIXuoG1Clz8BSdiz3kF+DZSCQ4k= -github.com/grafana/beyla v0.0.0-20250108110233-3f1b9b55c6dc h1:oY8yQB8IG0dBo1UrLlLC2CspxbiVtSWWExMxXOnfWgk= -github.com/grafana/beyla v0.0.0-20250108110233-3f1b9b55c6dc/go.mod h1:hpk185gTeIQXjxV/so9vAxhZtSEgm8ODanWXZNVnH2M= -github.com/grafana/beyla v1.9.1-0.20250122195759-1117708def46 h1:/aw+Ze9lUluE1hNZ0fAtwhmf2CKP0VbsLFumpN8xztY= -github.com/grafana/beyla v1.9.1-0.20250122195759-1117708def46/go.mod h1:CRWu15fkScScSYBlYUtdJu2Ak8ojGvnuwHToGGkaOXY= github.com/grafana/beyla v1.10.0-alloy h1:kGyZtBSS/Br2qdhbvzu8sVYZHuE9a3OzWpbp6gN55EY= github.com/grafana/beyla v1.10.0-alloy/go.mod h1:CRWu15fkScScSYBlYUtdJu2Ak8ojGvnuwHToGGkaOXY= github.com/grafana/cadvisor v0.0.0-20240729082359-1f04a91701e2 h1:ju6EcY2aEobeBg185ETtFCKj5WzaQ48qfkbsSRRQrF4= @@ -2126,8 +2120,6 @@ github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= -github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd h1:EVX1s+XNss9jkRW9K6XGJn2jL2lB1h5H804oKPsxOec= github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/illumos/go-kstat v0.0.0-20210513183136-173c9b0a9973 h1:hk4LPqXIY/c9XzRbe7dA6qQxaT6Axcbny0L/G5a4owQ= diff --git a/internal/component/discovery/discovery.go b/internal/component/discovery/discovery.go index 03670814e1..79c4009e78 100644 --- a/internal/component/discovery/discovery.go +++ b/internal/component/discovery/discovery.go @@ -224,7 +224,12 @@ func (c *Component) runDiscovery(ctx context.Context, d DiscovererWithMetrics) { allTargets := toAlloyTargets(cache) componentID := livedebugging.ComponentID(c.opts.ID) if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, fmt.Sprintf("%s", allTargets)) + c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.Target, + uint64(len(allTargets)), + func() string { return fmt.Sprintf("%s", allTargets) }, + )) } c.opts.OnStateChange(Exports{Targets: allTargets}) } diff --git a/internal/component/discovery/relabel/relabel.go b/internal/component/discovery/relabel/relabel.go index 6613c6c801..2fdd42d620 100644 --- a/internal/component/discovery/relabel/relabel.go +++ b/internal/component/discovery/relabel/relabel.go @@ -99,7 +99,12 @@ func (c *Component) Update(args component.Arguments) error { } componentID := livedebugging.ComponentID(c.opts.ID) if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, fmt.Sprintf("%s => %s", lset.String(), relabelled.String())) + c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.Target, + 1, + func() string { return fmt.Sprintf("%s => %s", lset.String(), relabelled.String()) }, + )) } } diff --git a/internal/component/loki/process/process.go b/internal/component/loki/process/process.go index fb1fc01fde..e7af0a75ed 100644 --- a/internal/component/loki/process/process.go +++ b/internal/component/loki/process/process.go @@ -164,7 +164,14 @@ func (c *Component) handleIn(ctx context.Context, wg *sync.WaitGroup) { case entry := <-c.receiver.Chan(): c.mut.RLock() if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, fmt.Sprintf("[IN]: timestamp: %s, entry: %s, labels: %s", entry.Timestamp.Format(time.RFC3339Nano), entry.Line, entry.Labels.String())) + c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.LokiLog, + 0, // does not count because we count only the data that exists + func() string { + return fmt.Sprintf("[IN]: timestamp: %s, entry: %s, labels: %s", entry.Timestamp.Format(time.RFC3339Nano), entry.Line, entry.Labels.String()) + }, + )) } select { case <-ctx.Done(): @@ -195,7 +202,14 @@ func (c *Component) handleOut(shutdownCh chan struct{}, wg *sync.WaitGroup) { // The log entry is the same for every fanout, // so we can publish it only once. if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, fmt.Sprintf("[OUT]: timestamp: %s, entry: %s, labels: %s", entry.Timestamp.Format(time.RFC3339Nano), entry.Line, entry.Labels.String())) + c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.LokiLog, + 1, + func() string { + return fmt.Sprintf("[OUT]: timestamp: %s, entry: %s, labels: %s", entry.Timestamp.Format(time.RFC3339Nano), entry.Line, entry.Labels.String()) + }, + )) } for _, f := range fanout { diff --git a/internal/component/loki/process/process_test.go b/internal/component/loki/process/process_test.go index 7558b0fc6c..35810d9f09 100644 --- a/internal/component/loki/process/process_test.go +++ b/internal/component/loki/process/process_test.go @@ -651,7 +651,7 @@ func getServiceDataWithLiveDebugging(log *testlivedebugging.Log) func(string) (i ld.AddCallback( "callback1", "", - func(data string) { log.Append(data) }, + func(data *livedebugging.Feed) { log.Append(data.DataFunc()) }, ) return func(name string) (interface{}, error) { diff --git a/internal/component/loki/relabel/relabel.go b/internal/component/loki/relabel/relabel.go index 8ee9eb20b7..fb3caf7f92 100644 --- a/internal/component/loki/relabel/relabel.go +++ b/internal/component/loki/relabel/relabel.go @@ -126,7 +126,18 @@ func (c *Component) Run(ctx context.Context) error { lbls := c.relabel(entry) if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, fmt.Sprintf("entry: %s, labels: %s => %s", entry.Line, entry.Labels.String(), lbls.String())) + count := uint64(1) + if len(lbls) == 0 { + count = 0 // if no labels are left, the count is not incremented because the log will be filtered out + } + c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.LokiLog, + count, + func() string { + return fmt.Sprintf("entry: %s, labels: %s => %s", entry.Line, entry.Labels.String(), lbls.String()) + }, + )) } if len(lbls) == 0 { diff --git a/internal/component/loki/secretfilter/secretfilter.go b/internal/component/loki/secretfilter/secretfilter.go index 071ba481c4..05fe5227a4 100644 --- a/internal/component/loki/secretfilter/secretfilter.go +++ b/internal/component/loki/secretfilter/secretfilter.go @@ -153,7 +153,14 @@ func (c *Component) Run(ctx context.Context) error { // Start processing the log entry to redact secrets newEntry := c.processEntry(entry) if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, fmt.Sprintf("%s => %s", entry.Line, newEntry.Line)) + c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.LokiLog, + 1, + func() string { + return fmt.Sprintf("%s => %s", entry.Line, newEntry.Line) + }, + )) } for _, f := range c.fanout { diff --git a/internal/component/otelcol/connector/connector.go b/internal/component/otelcol/connector/connector.go index 57059f1b09..135bce14d0 100644 --- a/internal/component/otelcol/connector/connector.go +++ b/internal/component/otelcol/connector/connector.go @@ -105,7 +105,7 @@ func New(opts component.Options, f otelconnector.Factory, args Arguments) (*Conn ctx, cancel := context.WithCancel(context.Background()) - consumer := lazyconsumer.NewPaused(ctx) + consumer := lazyconsumer.NewPaused(ctx, opts.ID) // Create a lazy collector where metrics from the upstream component will be // forwarded. diff --git a/internal/component/otelcol/connector/spanlogs/spanlogs.go b/internal/component/otelcol/connector/spanlogs/spanlogs.go index 2a9d9891d6..c7e5bb9069 100644 --- a/internal/component/otelcol/connector/spanlogs/spanlogs.go +++ b/internal/component/otelcol/connector/spanlogs/spanlogs.go @@ -123,7 +123,7 @@ func New(o component.Options, c Arguments) (*Component, error) { // Export the consumer. // This will remain the same throughout the component's lifetime, // so we do this during component construction. - export := lazyconsumer.New(context.Background()) + export := lazyconsumer.New(context.Background(), o.ID) export.SetConsumers(res.consumer, nil, nil) o.OnStateChange(otelcol.ConsumerExports{Input: export}) diff --git a/internal/component/otelcol/exporter/exporter.go b/internal/component/otelcol/exporter/exporter.go index 51fde0f315..8b21d06bed 100644 --- a/internal/component/otelcol/exporter/exporter.go +++ b/internal/component/otelcol/exporter/exporter.go @@ -115,7 +115,7 @@ var ( func New(opts component.Options, f otelexporter.Factory, args Arguments, supportedSignals TypeSignalFunc) (*Exporter, error) { ctx, cancel := context.WithCancel(context.Background()) - consumer := lazyconsumer.NewPaused(ctx) + consumer := lazyconsumer.NewPaused(ctx, opts.ID) // Create a lazy collector where metrics from the upstream component will be // forwarded. diff --git a/internal/component/otelcol/exporter/loki/loki.go b/internal/component/otelcol/exporter/loki/loki.go index c9ae300468..0c112438f4 100644 --- a/internal/component/otelcol/exporter/loki/loki.go +++ b/internal/component/otelcol/exporter/loki/loki.go @@ -58,7 +58,7 @@ func New(o component.Options, c Arguments) (*Component, error) { // Construct a consumer based on our converter and export it. This will // remain the same throughout the component's lifetime, so we do this // during component construction. - export := lazyconsumer.New(context.Background()) + export := lazyconsumer.New(context.Background(), o.ID) export.SetConsumers(nil, nil, converter) o.OnStateChange(otelcol.ConsumerExports{Input: export}) diff --git a/internal/component/otelcol/exporter/prometheus/prometheus.go b/internal/component/otelcol/exporter/prometheus/prometheus.go index d36803b8fa..c838afd499 100644 --- a/internal/component/otelcol/exporter/prometheus/prometheus.go +++ b/internal/component/otelcol/exporter/prometheus/prometheus.go @@ -105,7 +105,7 @@ func New(o component.Options, c Arguments) (*Component, error) { // Construct a consumer based on our converter and export it. This will // remain the same throughout the component's lifetime, so we do this during // component construction. - export := lazyconsumer.New(context.Background()) + export := lazyconsumer.New(context.Background(), o.ID) export.SetConsumers(nil, converter, nil) o.OnStateChange(otelcol.ConsumerExports{Input: export}) diff --git a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go index a5813e8469..0eb613c0d2 100644 --- a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go +++ b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go @@ -17,6 +17,8 @@ import ( type Consumer struct { ctx context.Context + componentID string + // pauseMut and pausedWg are used to implement Pause & Resume semantics. See Pause method for more info. pauseMut sync.RWMutex pausedWg *sync.WaitGroup @@ -36,17 +38,22 @@ var ( // New creates a new Consumer. The provided ctx is used to determine when the // Consumer should stop accepting data; if the ctx is closed, no further data // will be accepted. -func New(ctx context.Context) *Consumer { - return &Consumer{ctx: ctx} +func New(ctx context.Context, componentID string) *Consumer { + return &Consumer{ctx: ctx, componentID: componentID} } // NewPaused is like New, but returns a Consumer that is paused by calling Pause method. -func NewPaused(ctx context.Context) *Consumer { - c := New(ctx) +func NewPaused(ctx context.Context, componentID string) *Consumer { + c := New(ctx, componentID) c.Pause() return c } +// ComponentID returns the componentID associated with the consumer. +func (c *Consumer) ComponentID() string { + return c.componentID +} + // Capabilities implements otelconsumer.baseConsumer. func (c *Consumer) Capabilities() otelconsumer.Capabilities { return otelconsumer.Capabilities{ diff --git a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer_test.go b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer_test.go index 1980029d5e..225a77d69c 100644 --- a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer_test.go +++ b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer_test.go @@ -16,7 +16,7 @@ import ( ) func Test_PauseAndResume(t *testing.T) { - c := New(componenttest.TestContext(t)) + c := New(componenttest.TestContext(t), "") require.False(t, c.IsPaused()) c.Pause() require.True(t, c.IsPaused()) @@ -25,14 +25,14 @@ func Test_PauseAndResume(t *testing.T) { } func Test_NewPaused(t *testing.T) { - c := NewPaused(componenttest.TestContext(t)) + c := NewPaused(componenttest.TestContext(t), "") require.True(t, c.IsPaused()) c.Resume() require.False(t, c.IsPaused()) } func Test_PauseResume_MultipleCalls(t *testing.T) { - c := New(componenttest.TestContext(t)) + c := New(componenttest.TestContext(t), "") require.False(t, c.IsPaused()) c.Pause() c.Pause() @@ -46,7 +46,7 @@ func Test_PauseResume_MultipleCalls(t *testing.T) { func Test_ConsumeWaitsForResume(t *testing.T) { goleak.VerifyNone(t, goleak.IgnoreCurrent()) - c := NewPaused(componenttest.TestContext(t)) + c := NewPaused(componenttest.TestContext(t), "") require.True(t, c.IsPaused()) method := map[string]func(){ @@ -109,7 +109,7 @@ func Test_PauseResume_Multithreaded(t *testing.T) { routines := 5 allDone := sync.WaitGroup{} - c := NewPaused(componenttest.TestContext(t)) + c := NewPaused(componenttest.TestContext(t), "") require.True(t, c.IsPaused()) // Run goroutines that constantly try to call Consume* methods diff --git a/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go b/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go index 591a4b9886..c6bb81772b 100644 --- a/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go +++ b/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go @@ -6,6 +6,7 @@ import ( "context" "github.com/grafana/alloy/internal/component/otelcol" + "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" "github.com/grafana/alloy/internal/service/livedebugging" otelconsumer "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/pdata/plog" @@ -14,11 +15,14 @@ import ( ) type Consumer struct { - debugDataPublisher livedebugging.DebugDataPublisher - componentID livedebugging.ComponentID - logsMarshaler plog.Marshaler - metricsMarshaler pmetric.Marshaler - tracesMarshaler ptrace.Marshaler + debugDataPublisher livedebugging.DebugDataPublisher + componentID livedebugging.ComponentID + logsMarshaler plog.Marshaler + metricsMarshaler pmetric.Marshaler + tracesMarshaler ptrace.Marshaler + targetComponentIDsMetric []string + targetComponentIDsLog []string + targetComponentIDsTraces []string } var _ otelcol.Consumer = (*Consumer)(nil) @@ -33,6 +37,23 @@ func New(debugDataPublisher livedebugging.DebugDataPublisher, componentID string } } +// SetTargetConsumers stores the componentIDs of the next consumers +func (c *Consumer) SetTargetConsumers(metric, log, trace []otelcol.Consumer) { + c.targetComponentIDsMetric = extractIds(metric) + c.targetComponentIDsLog = extractIds(log) + c.targetComponentIDsTraces = extractIds(trace) +} + +func extractIds(consumers []otelcol.Consumer) []string { + ids := make([]string, 0) + for _, cons := range consumers { + if lazy, ok := cons.(*lazyconsumer.Consumer); ok { + ids = append(ids, lazy.ComponentID()) + } + } + return ids +} + // Capabilities implements otelcol.Consumer. func (c *Consumer) Capabilities() otelconsumer.Capabilities { // streaming data should not modify the value @@ -43,7 +64,13 @@ func (c *Consumer) Capabilities() otelconsumer.Capabilities { func (c *Consumer) ConsumeTraces(ctx context.Context, td ptrace.Traces) error { if c.debugDataPublisher.IsActive(c.componentID) { data, _ := c.tracesMarshaler.MarshalTraces(td) - c.debugDataPublisher.Publish(c.componentID, string(data)) + c.debugDataPublisher.Publish(c.componentID, livedebugging.NewFeed( + c.componentID, + livedebugging.OtelTrace, + uint64(td.SpanCount()), + func() string { return string(data) }, + livedebugging.WithTargetComponentIDs(c.targetComponentIDsTraces), + )) } return nil } @@ -52,7 +79,13 @@ func (c *Consumer) ConsumeTraces(ctx context.Context, td ptrace.Traces) error { func (c *Consumer) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error { if c.debugDataPublisher.IsActive(c.componentID) { data, _ := c.metricsMarshaler.MarshalMetrics(md) - c.debugDataPublisher.Publish(c.componentID, string(data)) + c.debugDataPublisher.Publish(c.componentID, livedebugging.NewFeed( + c.componentID, + livedebugging.OtelMetric, + uint64(md.MetricCount()), + func() string { return string(data) }, + livedebugging.WithTargetComponentIDs(c.targetComponentIDsMetric), + )) } return nil } @@ -61,7 +94,13 @@ func (c *Consumer) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error func (c *Consumer) ConsumeLogs(ctx context.Context, ld plog.Logs) error { if c.debugDataPublisher.IsActive(c.componentID) { data, _ := c.logsMarshaler.MarshalLogs(ld) - c.debugDataPublisher.Publish(c.componentID, string(data)) + c.debugDataPublisher.Publish(c.componentID, livedebugging.NewFeed( + c.componentID, + livedebugging.OtelLog, + uint64(ld.LogRecordCount()), + func() string { return string(data) }, + livedebugging.WithTargetComponentIDs(c.targetComponentIDsLog), + )) } return nil } diff --git a/internal/component/otelcol/processor/discovery/discovery.go b/internal/component/otelcol/processor/discovery/discovery.go index ada84731e2..8e6183fc24 100644 --- a/internal/component/otelcol/processor/discovery/discovery.go +++ b/internal/component/otelcol/processor/discovery/discovery.go @@ -137,7 +137,7 @@ func New(o component.Options, c Arguments) (*Component, error) { // Export the consumer. // This will remain the same throughout the component's lifetime, // so we do this during component construction. - export := lazyconsumer.New(context.Background()) + export := lazyconsumer.New(context.Background(), o.ID) export.SetConsumers(res.consumer, nil, nil) o.OnStateChange(otelcol.ConsumerExports{Input: export}) diff --git a/internal/component/otelcol/processor/processor.go b/internal/component/otelcol/processor/processor.go index 73bcd96938..9e4c24aed4 100644 --- a/internal/component/otelcol/processor/processor.go +++ b/internal/component/otelcol/processor/processor.go @@ -92,7 +92,7 @@ func New(opts component.Options, f otelprocessor.Factory, args Arguments) (*Proc ctx, cancel := context.WithCancel(context.Background()) - consumer := lazyconsumer.NewPaused(ctx) + consumer := lazyconsumer.NewPaused(ctx, opts.ID) // Create a lazy collector where metrics from the upstream component will be // forwarded. diff --git a/internal/component/prometheus/relabel/relabel.go b/internal/component/prometheus/relabel/relabel.go index 425440845b..a0eeb8ff9a 100644 --- a/internal/component/prometheus/relabel/relabel.go +++ b/internal/component/prometheus/relabel/relabel.go @@ -277,7 +277,18 @@ func (c *Component) relabel(val float64, lbls labels.Labels) labels.Labels { componentID := livedebugging.ComponentID(c.opts.ID) if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, fmt.Sprintf("%s => %s", lbls.String(), relabelled.String())) + count := uint64(1) + if relabelled.Len() == 0 { + count = 0 // if no labels are left, the count is not incremented because the metric will be filtered out + } + c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.PrometheusMetric, + count, + func() string { + return fmt.Sprintf("%s => %s", lbls.String(), relabelled.String()) + }, + )) } return relabelled diff --git a/internal/component/prometheus/remotewrite/remote_write.go b/internal/component/prometheus/remotewrite/remote_write.go index 144a742c6d..d54e081aac 100644 --- a/internal/component/prometheus/remotewrite/remote_write.go +++ b/internal/component/prometheus/remotewrite/remote_write.go @@ -130,7 +130,14 @@ func New(o component.Options, c Arguments) (*Component, error) { ls.GetOrAddLink(res.opts.ID, uint64(newRef), l) } if res.debugDataPublisher.IsActive(componentID) { - res.debugDataPublisher.Publish(componentID, fmt.Sprintf("ts=%d, labels=%s, value=%f", t, l, v)) + res.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.PrometheusMetric, + 1, + func() string { + return fmt.Sprintf("ts=%d, labels=%s, value=%f", t, l, v) + }, + )) } return globalRef, nextErr }), @@ -145,15 +152,22 @@ func New(o component.Options, c Arguments) (*Component, error) { ls.GetOrAddLink(res.opts.ID, uint64(newRef), l) } if res.debugDataPublisher.IsActive(componentID) { - var data string - if h != nil { - data = fmt.Sprintf("ts=%d, labels=%s, histogram=%s", t, l, h.String()) - } else if fh != nil { - data = fmt.Sprintf("ts=%d, labels=%s, float_histogram=%s", t, l, fh.String()) - } else { - data = fmt.Sprintf("ts=%d, labels=%s, no_value", t, l) - } - res.debugDataPublisher.Publish(componentID, data) + res.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.PrometheusMetric, + 1, + func() string { + var data string + if h != nil { + data = fmt.Sprintf("ts=%d, labels=%s, histogram=%s", t, l, h.String()) + } else if fh != nil { + data = fmt.Sprintf("ts=%d, labels=%s, float_histogram=%s", t, l, fh.String()) + } else { + data = fmt.Sprintf("ts=%d, labels=%s, no_value", t, l) + } + return data + }, + )) } return globalRef, nextErr }), @@ -168,7 +182,14 @@ func New(o component.Options, c Arguments) (*Component, error) { ls.GetOrAddLink(res.opts.ID, uint64(newRef), l) } if res.debugDataPublisher.IsActive(componentID) { - res.debugDataPublisher.Publish(componentID, fmt.Sprintf("labels=%s, type=%s, unit=%s, help=%s", l, m.Type, m.Unit, m.Help)) + res.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.PrometheusMetric, + 1, + func() string { + return fmt.Sprintf("labels=%s, type=%s, unit=%s, help=%s", l, m.Type, m.Unit, m.Help) + }, + )) } return globalRef, nextErr }), @@ -183,7 +204,14 @@ func New(o component.Options, c Arguments) (*Component, error) { ls.GetOrAddLink(res.opts.ID, uint64(newRef), l) } if res.debugDataPublisher.IsActive(componentID) { - res.debugDataPublisher.Publish(componentID, fmt.Sprintf("ts=%d, labels=%s, exemplar_labels=%s, value=%f", e.Ts, l, e.Labels, e.Value)) + res.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.PrometheusMetric, + 1, + func() string { + return fmt.Sprintf("ts=%d, labels=%s, exemplar_labels=%s, value=%f", e.Ts, l, e.Labels, e.Value) + }, + )) } return globalRef, nextErr }), diff --git a/internal/service/livedebugging/feed.go b/internal/service/livedebugging/feed.go new file mode 100644 index 0000000000..f9d5f27951 --- /dev/null +++ b/internal/service/livedebugging/feed.go @@ -0,0 +1,50 @@ +package livedebugging + +type FeedType string + +const ( + Target FeedType = "target" + PrometheusMetric FeedType = "prometheus_metric" + LokiLog FeedType = "loki_log" + OtelMetric FeedType = "otel_metric" + OtelLog FeedType = "otel_log" + OtelTrace FeedType = "otel_trace" +) + +type FeedOption func(*Feed) + +func WithTargetComponentIDs(ids []string) FeedOption { + return func(f *Feed) { + f.TargetComponentIDs = ids + } +} + +type Feed struct { + // ID of the component that created the feed. + ComponentID ComponentID + // Ids of the components which will consume the Feed data. + // This is needed for components that can export different types of data (most Otel components) to know + // where the Feed should go. When left empty, the Feed is expected to be sent to all components consuming data + // from the component that created it. + TargetComponentIDs []string + Type FeedType + // Count is the number of spans, metrics, logs that the Feed represent. + Count uint64 + // The data string is passed as a function to only compute the string if needed. + DataFunc func() string +} + +func NewFeed(componentID ComponentID, feedType FeedType, count uint64, dataFunc func() string, opts ...FeedOption) *Feed { + feed := &Feed{ + ComponentID: componentID, + Type: feedType, + Count: count, + DataFunc: dataFunc, + } + + for _, opt := range opts { + opt(feed) + } + + return feed +} diff --git a/internal/service/livedebugging/livedebugging.go b/internal/service/livedebugging/livedebugging.go index 39bcec0805..76fea6bffb 100644 --- a/internal/service/livedebugging/livedebugging.go +++ b/internal/service/livedebugging/livedebugging.go @@ -9,28 +9,34 @@ import ( ) type ComponentID string +type ModuleID string type CallbackID string // CallbackManager is used to manage live debugging callbacks. type CallbackManager interface { // AddCallback sets a callback for a given componentID. // The callback is used to send debugging data to live debugging consumers. - AddCallback(callbackID CallbackID, componentID ComponentID, callback func(string)) error + AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Feed)) error // DeleteCallback deletes a callback for a given componentID. DeleteCallback(callbackID CallbackID, componentID ComponentID) + // AddCallbackMulti sets a callback to all components. + // The callbacks are used to send debugging data to live debugging consumers. + AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Feed)) error + // DeleteCallbackMulti deletes callbacks for all components. + DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) } // DebugDataPublisher is used by components to push information to live debugging consumers. type DebugDataPublisher interface { // Publish sends debugging data for a given componentID. - Publish(componentID ComponentID, data string) + Publish(componentID ComponentID, data *Feed) // IsActive returns true when at least one consumer is listening for debugging data for the given componentID. IsActive(componentID ComponentID) bool } type liveDebugging struct { loadMut sync.RWMutex - callbacks map[ComponentID]map[CallbackID]func(string) + callbacks map[ComponentID]map[CallbackID]func(*Feed) host service.Host enabled bool } @@ -41,11 +47,11 @@ var _ DebugDataPublisher = &liveDebugging{} // NewLiveDebugging creates a new instance of liveDebugging. func NewLiveDebugging() *liveDebugging { return &liveDebugging{ - callbacks: make(map[ComponentID]map[CallbackID]func(string)), + callbacks: make(map[ComponentID]map[CallbackID]func(*Feed)), } } -func (s *liveDebugging) Publish(componentID ComponentID, data string) { +func (s *liveDebugging) Publish(componentID ComponentID, data *Feed) { s.loadMut.RLock() defer s.loadMut.RUnlock() if s.enabled { @@ -62,7 +68,7 @@ func (s *liveDebugging) IsActive(componentID ComponentID) bool { return exist && len(callbacks) > 0 } -func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID ComponentID, callback func(string)) error { +func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Feed)) error { err := s.addCallback(callbackID, componentID, callback) if err != nil { return err @@ -71,6 +77,15 @@ func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID Component return nil } +func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Feed)) error { + err := s.addCallbackMulti(callbackID, moduleID, callback) + if err != nil { + return err + } + s.notifyComponents(moduleID) + return nil +} + func (s *liveDebugging) DeleteCallback(callbackID CallbackID, componentID ComponentID) { defer s.notifyComponent(componentID) s.loadMut.Lock() @@ -78,7 +93,18 @@ func (s *liveDebugging) DeleteCallback(callbackID CallbackID, componentID Compon delete(s.callbacks[componentID], callbackID) } -func (s *liveDebugging) addCallback(callbackID CallbackID, componentID ComponentID, callback func(string)) error { +func (s *liveDebugging) DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) { + defer s.notifyComponents(moduleID) + s.loadMut.Lock() + defer s.loadMut.Unlock() + // ignore errors on delete + components, _ := s.host.ListComponents(string(moduleID), component.InfoOptions{}) + for _, cp := range components { + delete(s.callbacks[ComponentID(cp.ID.String())], callbackID) + } +} + +func (s *liveDebugging) addCallback(callbackID CallbackID, componentID ComponentID, callback func(*Feed)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -100,12 +126,42 @@ func (s *liveDebugging) addCallback(callbackID CallbackID, componentID Component } if _, ok := s.callbacks[componentID]; !ok { - s.callbacks[componentID] = make(map[CallbackID]func(string)) + s.callbacks[componentID] = make(map[CallbackID]func(*Feed)) } s.callbacks[componentID][callbackID] = callback return nil } +func (s *liveDebugging) addCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Feed)) error { + s.loadMut.Lock() + defer s.loadMut.Unlock() + + if !s.enabled { + return fmt.Errorf("the live debugging service is disabled. Check the documentation to find out how to enable it") + } + + if s.host == nil { + return fmt.Errorf("the live debugging service is not ready yet") + } + + components, err := s.host.ListComponents(string(moduleID), component.InfoOptions{GetHealth: true}) + if err != nil { + return err + } + + for _, cp := range components { + if _, ok := cp.Component.(component.LiveDebugging); !ok { + continue // Ignore components that don't support live debugging + } + + if _, ok := s.callbacks[ComponentID(cp.ID.String())]; !ok { + s.callbacks[ComponentID(cp.ID.String())] = make(map[CallbackID]func(*Feed)) + } + s.callbacks[ComponentID(cp.ID.String())][callbackID] = callback + } + return nil +} + func (s *liveDebugging) notifyComponent(componentID ComponentID) { s.loadMut.RLock() defer s.loadMut.RUnlock() @@ -120,6 +176,22 @@ func (s *liveDebugging) notifyComponent(componentID ComponentID) { } } +func (s *liveDebugging) notifyComponents(moduleID ModuleID) { + s.loadMut.RLock() + defer s.loadMut.RUnlock() + + components, err := s.host.ListComponents(string(moduleID), component.InfoOptions{}) + if err != nil { + return + } + for _, cp := range components { + if c, ok := cp.Component.(component.LiveDebugging); ok { + // notify the component of the change + c.LiveDebugging(len(s.callbacks[ComponentID(cp.ID.String())])) + } + } +} + func (s *liveDebugging) SetServiceHost(h service.Host) { s.loadMut.Lock() defer s.loadMut.Unlock() diff --git a/internal/service/livedebugging/livedebugging_test.go b/internal/service/livedebugging/livedebugging_test.go index a851416f10..f4a62b997d 100644 --- a/internal/service/livedebugging/livedebugging_test.go +++ b/internal/service/livedebugging/livedebugging_test.go @@ -11,7 +11,7 @@ import ( func TestAddCallback(t *testing.T) { livedebugging := NewLiveDebugging() callbackID := CallbackID("callback1") - callback := func(data string) {} + callback := func(data *Feed) {} err := livedebugging.AddCallback(callbackID, "fake.liveDebugging", callback) require.ErrorContains(t, err, "the live debugging service is disabled. Check the documentation to find out how to enable it") @@ -46,8 +46,8 @@ func TestStream(t *testing.T) { componentID := ComponentID("fake.liveDebugging") callbackID := CallbackID("callback1") - var receivedData string - callback := func(data string) { + var receivedData *Feed + callback := func(data *Feed) { receivedData = data } require.False(t, livedebugging.IsActive(componentID)) @@ -55,19 +55,23 @@ func TestStream(t *testing.T) { require.True(t, livedebugging.IsActive(componentID)) require.Len(t, livedebugging.callbacks[componentID], 1) - livedebugging.Publish(componentID, "test data") - require.Equal(t, "test data", receivedData) - + livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) + require.Equal(t, componentID, receivedData.ComponentID) + require.Equal(t, []string{"component1"}, receivedData.TargetComponentIDs) + require.Equal(t, uint64(3), receivedData.Count) + require.Equal(t, "test data", receivedData.DataFunc()) livedebugging.SetEnabled(false) - livedebugging.Publish(componentID, "new test data") - require.Equal(t, "test data", receivedData) // not updated because the feature is disabled + livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "new test data" }, WithTargetComponentIDs([]string{"component1"}))) + require.Equal(t, "test data", receivedData.DataFunc()) // not updated because the feature is disabled } func TestStreamEmpty(t *testing.T) { livedebugging := NewLiveDebugging() setupServiceHost(livedebugging) componentID := ComponentID("fake.liveDebugging") - require.NotPanics(t, func() { livedebugging.Publish(componentID, "test data") }) + require.NotPanics(t, func() { + livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) + }) } func TestMultipleStreams(t *testing.T) { @@ -77,13 +81,13 @@ func TestMultipleStreams(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - var receivedData1 string - callback1 := func(data string) { + var receivedData1 *Feed + callback1 := func(data *Feed) { receivedData1 = data } - var receivedData2 string - callback2 := func(data string) { + var receivedData2 *Feed + callback2 := func(data *Feed) { receivedData2 = data } @@ -91,9 +95,9 @@ func TestMultipleStreams(t *testing.T) { require.NoError(t, livedebugging.AddCallback(callbackID2, componentID, callback2)) require.Len(t, livedebugging.callbacks[componentID], 2) - livedebugging.Publish(componentID, "test data") - require.Equal(t, "test data", receivedData1) - require.Equal(t, "test data", receivedData2) + livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "test data" })) + require.Equal(t, "test data", receivedData1.DataFunc()) + require.Equal(t, "test data", receivedData2.DataFunc()) } func TestDeleteCallback(t *testing.T) { @@ -103,8 +107,8 @@ func TestDeleteCallback(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - callback1 := func(data string) {} - callback2 := func(data string) {} + callback1 := func(data *Feed) {} + callback2 := func(data *Feed) {} component, _ := livedebugging.host.GetComponent(component.ParseID("fake.liveDebugging"), component.InfoOptions{}) @@ -141,3 +145,88 @@ func setupServiceHost(liveDebugging *liveDebugging) { liveDebugging.SetServiceHost(host) liveDebugging.SetEnabled(true) } + +func TestAddCallbackMulti(t *testing.T) { + livedebugging := NewLiveDebugging() + callbackID := CallbackID("callback1") + callback := func(data *Feed) {} + + err := livedebugging.AddCallbackMulti(callbackID, "", callback) + require.ErrorContains(t, err, "the live debugging service is disabled. Check the documentation to find out how to enable it") + + livedebugging.SetEnabled(true) + + err = livedebugging.AddCallbackMulti(callbackID, "", callback) + require.ErrorContains(t, err, "the live debugging service is not ready yet") + + setupServiceHost(livedebugging) + + err = livedebugging.AddCallbackMulti(callbackID, "not found", callback) + require.ErrorContains(t, err, "module not found") + + require.NoError(t, livedebugging.AddCallbackMulti(callbackID, "", callback)) + + component, _ := livedebugging.host.GetComponent(component.ParseID("fake.liveDebugging"), component.InfoOptions{}) + require.Equal(t, 1, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) + + require.NoError(t, livedebugging.AddCallbackMulti(callbackID, "declared.cmp", callback)) +} + +func TestDeleteCallbackMulti(t *testing.T) { + livedebugging := NewLiveDebugging() + setupServiceHost(livedebugging) + componentID := ComponentID("fake.liveDebugging") + callbackID1 := CallbackID("callback1") + callbackID2 := CallbackID("callback2") + + callback1 := func(data *Feed) {} + callback2 := func(data *Feed) {} + + component, _ := livedebugging.host.GetComponent(component.ParseID("fake.liveDebugging"), component.InfoOptions{}) + + require.NoError(t, livedebugging.AddCallbackMulti(callbackID1, "", callback1)) + require.Equal(t, 1, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) + require.NoError(t, livedebugging.AddCallbackMulti(callbackID2, "", callback2)) + require.Equal(t, 2, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) + require.Len(t, livedebugging.callbacks[componentID], 2) + + // Deleting callbacks that don't exist should not panic + require.NotPanics(t, func() { livedebugging.DeleteCallbackMulti(callbackID1, "fakeComponentID") }) + require.NotPanics(t, func() { livedebugging.DeleteCallbackMulti("fakeCallbackID", "") }) + + livedebugging.DeleteCallbackMulti(callbackID1, "") + require.Len(t, livedebugging.callbacks[componentID], 1) + require.Equal(t, 1, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) + + livedebugging.DeleteCallbackMulti(callbackID2, "") + require.Empty(t, livedebugging.callbacks[componentID]) + require.Equal(t, 0, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) + + require.False(t, livedebugging.IsActive(ComponentID("fake.liveDebugging"))) +} + +func TestMultiCallbacksMultipleStreams(t *testing.T) { + livedebugging := NewLiveDebugging() + setupServiceHost(livedebugging) + componentID := ComponentID("fake.liveDebugging") + callbackID1 := CallbackID("callback1") + callbackID2 := CallbackID("callback2") + + var receivedData1 *Feed + callback1 := func(data *Feed) { + receivedData1 = data + } + + var receivedData2 *Feed + callback2 := func(data *Feed) { + receivedData2 = data + } + + require.NoError(t, livedebugging.AddCallbackMulti(callbackID1, "", callback1)) + require.NoError(t, livedebugging.AddCallbackMulti(callbackID2, "", callback2)) + require.Len(t, livedebugging.callbacks[componentID], 2) + + livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "test data" })) + require.Equal(t, "test data", receivedData1.DataFunc()) + require.Equal(t, "test data", receivedData2.DataFunc()) +} diff --git a/internal/util/testlivedebugging/testlivedebugging.go b/internal/util/testlivedebugging/testlivedebugging.go index faf9fa14bb..a866cf2b00 100644 --- a/internal/util/testlivedebugging/testlivedebugging.go +++ b/internal/util/testlivedebugging/testlivedebugging.go @@ -27,6 +27,28 @@ func (h *FakeServiceHost) GetComponent(id component.ID, opts component.InfoOptio return nil, component.ErrComponentNotFound } +func (h *FakeServiceHost) ListComponents(moduleID string, opts component.InfoOptions) ([]*component.Info, error) { + if moduleID != "" { + for key, _ := range h.ComponentsInfo { + if key.ModuleID == moduleID { + return h.getComponentsInModule(moduleID), nil + } + } + return nil, component.ErrModuleNotFound + } + return h.getComponentsInModule(""), nil +} + +func (h *FakeServiceHost) getComponentsInModule(module string) []*component.Info { + detail := make([]*component.Info, 0, len(h.ComponentsInfo)) + for key, cp := range h.ComponentsInfo { + if key.ModuleID == module { + detail = append(detail, &component.Info{ID: key, ComponentName: cp.ComponentName, Component: cp.Component}) + } + } + return detail +} + type FakeComponentLiveDebugging struct { ConsumersCount int } diff --git a/internal/web/api/api.go b/internal/web/api/api.go index 5164f91854..321fe52193 100644 --- a/internal/web/api/api.go +++ b/internal/web/api/api.go @@ -51,6 +51,9 @@ func (a *AlloyAPI) RegisterRoutes(urlPrefix string, r *mux.Router) { r.Handle(path.Join(urlPrefix, "/peers"), httputil.CompressionHandler{Handler: getClusteringPeersHandler(a.alloy)}) r.Handle(path.Join(urlPrefix, "/debug/{id:.+}"), liveDebugging(a.alloy, a.CallbackManager)) + + r.Handle(path.Join(urlPrefix, "/graph"), graph(a.alloy, a.CallbackManager)) + r.Handle(path.Join(urlPrefix, "/graph/{moduleID:.+}"), graph(a.alloy, a.CallbackManager)) } func listComponentsHandler(host service.Host) http.HandlerFunc { @@ -166,6 +169,100 @@ func getClusteringPeersHandler(host service.Host) http.HandlerFunc { } } +type feedKey struct { + ComponentID livedebugging.ComponentID + Type livedebugging.FeedType +} + +func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var moduleID livedebugging.ModuleID + if vars := mux.Vars(r); vars != nil { + moduleID = livedebugging.ModuleID(vars["moduleID"]) + } + + window := setWindow(w, r.URL.Query().Get("window")) + + dataCh := make(chan *livedebugging.Feed, 1000) + feedDataMap := make(map[feedKey]feed) + + ctx := r.Context() + id := livedebugging.CallbackID(uuid.New().String()) + + err := callbackManager.AddCallbackMulti(id, moduleID, func(data *livedebugging.Feed) { + select { + case <-ctx.Done(): + return + default: + select { + case dataCh <- data: + default: + } + } + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { + close(dataCh) + callbackManager.DeleteCallbackMulti(id, moduleID) + }() + + ticker := time.NewTicker(time.Duration(window) * time.Second) + defer ticker.Stop() + + for { + select { + case data := <-dataCh: + // Aggregate incoming data + key := feedKey{ComponentID: data.ComponentID, Type: data.Type} + if existing, exists := feedDataMap[key]; exists { + existing.Count += data.Count + } else { + // The data is ignored for the graph. + feedDataMap[key] = feed{ + ComponentID: string(data.ComponentID), + Count: data.Count, + Type: string(data.Type), + TargetComponentIDs: data.TargetComponentIDs, + } + } + + case <-ticker.C: + // Flush aggregated data + var builder strings.Builder + for _, data := range feedDataMap { + jsonData, err := json.Marshal(data) + if err != nil { + continue + } + builder.Write(jsonData) + builder.WriteString("|;|") + } + + // Add an empty limiter to show the lack of data + if builder.Len() == 0 { + builder.WriteString("|;|") + } + + _, writeErr := w.Write([]byte(builder.String())) + if writeErr != nil { + return + } + w.(http.Flusher).Flush() + + feedDataMap = make(map[feedKey]feed) + + case <-ctx.Done(): + return + } + } + } +} + func liveDebugging(_ service.Host, callbackManager livedebugging.CallbackManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -178,7 +275,7 @@ func liveDebugging(_ service.Host, callbackManager livedebugging.CallbackManager id := livedebugging.CallbackID(uuid.New().String()) - err := callbackManager.AddCallback(id, componentID, func(data string) { + err := callbackManager.AddCallback(id, componentID, func(data *livedebugging.Feed) { select { case <-ctx.Done(): return @@ -188,7 +285,7 @@ func liveDebugging(_ service.Host, callbackManager livedebugging.CallbackManager } // Avoid blocking the channel when the channel is full select { - case dataCh <- data: + case dataCh <- data.DataFunc(): default: } } @@ -239,3 +336,16 @@ func setSampleProb(w http.ResponseWriter, sampleProbParam string) (sampleProb fl } return sampleProb } + +func setWindow(w http.ResponseWriter, windowParam string) (window int64) { + window = 5 + if windowParam != "" { + var err error + window, err = strconv.ParseInt(windowParam, 10, 64) + if err != nil || window < 1 || window > 60 { + http.Error(w, "Invalid window", http.StatusBadRequest) + return 5 + } + } + return window +} diff --git a/internal/web/api/data.go b/internal/web/api/data.go new file mode 100644 index 0000000000..efd366ac08 --- /dev/null +++ b/internal/web/api/data.go @@ -0,0 +1,15 @@ +package api + +type feed struct { + // ID of the component that created the feed. + ComponentID string `json:"componentID"` + // Ids of the components which will consume the Feed data. + // This is needed for components that can export different types of data (most Otel components) to know + // where the Feed should go. When left empty, the Feed is expected to be sent to all components consuming data + // from the component that created it. + TargetComponentIDs []string `json:"targetComponentIDs"` + // Type specifies the category of data represented by the count (otel_metric, loki_log, target...). + Type string `json:"type"` + // Count is the number of spans, metrics, logs that the Feed represent. + Count uint64 `json:"count"` +} From e18e0ca5e8922cf655eb37be7cc5b969697cd442 Mon Sep 17 00:00:00 2001 From: William Dumont Date: Fri, 7 Feb 2025 12:27:06 +0100 Subject: [PATCH 02/11] update discovery process --- internal/component/discovery/process/process.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/component/discovery/process/process.go b/internal/component/discovery/process/process.go index 4760543af3..71408d28cf 100644 --- a/internal/component/discovery/process/process.go +++ b/internal/component/discovery/process/process.go @@ -70,7 +70,12 @@ func (c *Component) Run(ctx context.Context) error { componentID := livedebugging.ComponentID(c.opts.ID) if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, fmt.Sprintf("%s", c.processes)) + c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + componentID, + livedebugging.Target, + uint64(len(c.processes)), + func() string { return fmt.Sprintf("%s", c.processes) }, + )) } return nil From 9ae5904edd6d1dbe5edbdfea9382d097881496f5 Mon Sep 17 00:00:00 2001 From: William Dumont Date: Tue, 11 Feb 2025 17:34:44 +0100 Subject: [PATCH 03/11] review feedback1 --- internal/component/discovery/discovery.go | 14 ++- .../component/discovery/process/process.go | 2 +- .../component/discovery/relabel/relabel.go | 14 ++- internal/component/loki/process/process.go | 36 ++++---- .../component/loki/process/process_test.go | 2 +- internal/component/loki/relabel/relabel.go | 24 +++-- .../loki/secretfilter/secretfilter.go | 19 ++-- .../component/otelcol/connector/connector.go | 2 + .../internal/lazyconsumer/lazyconsumer.go | 1 + .../lazyconsumer/lazyconsumer_test.go | 10 +- .../livedebuggingconsumer.go | 92 +++++++++++-------- .../component/otelcol/processor/processor.go | 2 + .../component/otelcol/receiver/receiver.go | 2 + .../component/prometheus/relabel/relabel.go | 2 +- .../prometheus/remotewrite/remote_write.go | 88 ++++++++---------- internal/service/livedebugging/data.go | 50 ++++++++++ internal/service/livedebugging/feed.go | 50 ---------- .../service/livedebugging/livedebugging.go | 41 +++++---- .../livedebugging/livedebugging_test.go | 51 +++++----- internal/web/api/api.go | 22 ++--- internal/web/api/data.go | 10 +- 21 files changed, 273 insertions(+), 261 deletions(-) create mode 100644 internal/service/livedebugging/data.go delete mode 100644 internal/service/livedebugging/feed.go diff --git a/internal/component/discovery/discovery.go b/internal/component/discovery/discovery.go index 79c4009e78..0450df6e00 100644 --- a/internal/component/discovery/discovery.go +++ b/internal/component/discovery/discovery.go @@ -223,14 +223,12 @@ func (c *Component) runDiscovery(ctx context.Context, d DiscovererWithMetrics) { send := func() { allTargets := toAlloyTargets(cache) componentID := livedebugging.ComponentID(c.opts.ID) - if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.Target, - uint64(len(allTargets)), - func() string { return fmt.Sprintf("%s", allTargets) }, - )) - } + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.Target, + uint64(len(allTargets)), + func() string { return fmt.Sprintf("%s", allTargets) }, + )) c.opts.OnStateChange(Exports{Targets: allTargets}) } diff --git a/internal/component/discovery/process/process.go b/internal/component/discovery/process/process.go index 71408d28cf..81dd652d77 100644 --- a/internal/component/discovery/process/process.go +++ b/internal/component/discovery/process/process.go @@ -70,7 +70,7 @@ func (c *Component) Run(ctx context.Context) error { componentID := livedebugging.ComponentID(c.opts.ID) if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( componentID, livedebugging.Target, uint64(len(c.processes)), diff --git a/internal/component/discovery/relabel/relabel.go b/internal/component/discovery/relabel/relabel.go index 2fdd42d620..662c258c0c 100644 --- a/internal/component/discovery/relabel/relabel.go +++ b/internal/component/discovery/relabel/relabel.go @@ -98,14 +98,12 @@ func (c *Component) Update(args component.Arguments) error { targets = append(targets, promLabelsToComponent(relabelled)) } componentID := livedebugging.ComponentID(c.opts.ID) - if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.Target, - 1, - func() string { return fmt.Sprintf("%s => %s", lset.String(), relabelled.String()) }, - )) - } + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.Target, + 1, + func() string { return fmt.Sprintf("%s => %s", lset.String(), relabelled.String()) }, + )) } c.opts.OnStateChange(Exports{ diff --git a/internal/component/loki/process/process.go b/internal/component/loki/process/process.go index e7af0a75ed..a51192ec45 100644 --- a/internal/component/loki/process/process.go +++ b/internal/component/loki/process/process.go @@ -163,16 +163,14 @@ func (c *Component) handleIn(ctx context.Context, wg *sync.WaitGroup) { return case entry := <-c.receiver.Chan(): c.mut.RLock() - if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.LokiLog, - 0, // does not count because we count only the data that exists - func() string { - return fmt.Sprintf("[IN]: timestamp: %s, entry: %s, labels: %s", entry.Timestamp.Format(time.RFC3339Nano), entry.Line, entry.Labels.String()) - }, - )) - } + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.LokiLog, + 0, // does not count because we count only the data that exists + func() string { + return fmt.Sprintf("[IN]: timestamp: %s, entry: %s, labels: %s", entry.Timestamp.Format(time.RFC3339Nano), entry.Line, entry.Labels.String()) + }, + )) select { case <-ctx.Done(): return @@ -201,16 +199,14 @@ func (c *Component) handleOut(shutdownCh chan struct{}, wg *sync.WaitGroup) { // The log entry is the same for every fanout, // so we can publish it only once. - if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.LokiLog, - 1, - func() string { - return fmt.Sprintf("[OUT]: timestamp: %s, entry: %s, labels: %s", entry.Timestamp.Format(time.RFC3339Nano), entry.Line, entry.Labels.String()) - }, - )) - } + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.LokiLog, + 1, + func() string { + return fmt.Sprintf("[OUT]: timestamp: %s, entry: %s, labels: %s", entry.Timestamp.Format(time.RFC3339Nano), entry.Line, entry.Labels.String()) + }, + )) for _, f := range fanout { select { diff --git a/internal/component/loki/process/process_test.go b/internal/component/loki/process/process_test.go index 35810d9f09..e958af6700 100644 --- a/internal/component/loki/process/process_test.go +++ b/internal/component/loki/process/process_test.go @@ -651,7 +651,7 @@ func getServiceDataWithLiveDebugging(log *testlivedebugging.Log) func(string) (i ld.AddCallback( "callback1", "", - func(data *livedebugging.Feed) { log.Append(data.DataFunc()) }, + func(data *livedebugging.Data) { log.Append(data.DataFunc()) }, ) return func(name string) (interface{}, error) { diff --git a/internal/component/loki/relabel/relabel.go b/internal/component/loki/relabel/relabel.go index fb3caf7f92..9b288ed0a1 100644 --- a/internal/component/loki/relabel/relabel.go +++ b/internal/component/loki/relabel/relabel.go @@ -125,20 +125,18 @@ func (c *Component) Run(ctx context.Context) error { c.metrics.entriesProcessed.Inc() lbls := c.relabel(entry) - if c.debugDataPublisher.IsActive(componentID) { - count := uint64(1) - if len(lbls) == 0 { - count = 0 // if no labels are left, the count is not incremented because the log will be filtered out - } - c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.LokiLog, - count, - func() string { - return fmt.Sprintf("entry: %s, labels: %s => %s", entry.Line, entry.Labels.String(), lbls.String()) - }, - )) + count := uint64(1) + if len(lbls) == 0 { + count = 0 // if no labels are left, the count is not incremented because the log will be filtered out } + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.LokiLog, + count, + func() string { + return fmt.Sprintf("entry: %s, labels: %s => %s", entry.Line, entry.Labels.String(), lbls.String()) + }, + )) if len(lbls) == 0 { level.Debug(c.opts.Logger).Log("msg", "dropping entry after relabeling", "labels", entry.Labels.String()) diff --git a/internal/component/loki/secretfilter/secretfilter.go b/internal/component/loki/secretfilter/secretfilter.go index 05fe5227a4..e766f117ba 100644 --- a/internal/component/loki/secretfilter/secretfilter.go +++ b/internal/component/loki/secretfilter/secretfilter.go @@ -152,16 +152,15 @@ func (c *Component) Run(ctx context.Context) error { c.mut.RLock() // Start processing the log entry to redact secrets newEntry := c.processEntry(entry) - if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.LokiLog, - 1, - func() string { - return fmt.Sprintf("%s => %s", entry.Line, newEntry.Line) - }, - )) - } + + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.LokiLog, + 1, + func() string { + return fmt.Sprintf("%s => %s", entry.Line, newEntry.Line) + }, + )) for _, f := range c.fanout { select { diff --git a/internal/component/otelcol/connector/connector.go b/internal/component/otelcol/connector/connector.go index 135bce14d0..8828971310 100644 --- a/internal/component/otelcol/connector/connector.go +++ b/internal/component/otelcol/connector/connector.go @@ -226,6 +226,8 @@ func (p *Connector) Update(args component.Arguments) error { return errors.New("unsupported connector type") } + p.liveDebuggingConsumer.SetTargetConsumers(next.Metrics, next.Logs, next.Traces) + updateConsumersFunc := func() { p.consumer.SetConsumers(tracesConnector, metricsConnector, logsConnector) } diff --git a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go index 0eb613c0d2..732d818ee3 100644 --- a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go +++ b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go @@ -50,6 +50,7 @@ func NewPaused(ctx context.Context, componentID string) *Consumer { } // ComponentID returns the componentID associated with the consumer. +// TODO: find a way to decouple the lazyconsumer from the component for better abstraction. func (c *Consumer) ComponentID() string { return c.componentID } diff --git a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer_test.go b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer_test.go index 225a77d69c..b81ecda0c5 100644 --- a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer_test.go +++ b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer_test.go @@ -16,7 +16,7 @@ import ( ) func Test_PauseAndResume(t *testing.T) { - c := New(componenttest.TestContext(t), "") + c := New(componenttest.TestContext(t), "test_component") require.False(t, c.IsPaused()) c.Pause() require.True(t, c.IsPaused()) @@ -25,14 +25,14 @@ func Test_PauseAndResume(t *testing.T) { } func Test_NewPaused(t *testing.T) { - c := NewPaused(componenttest.TestContext(t), "") + c := NewPaused(componenttest.TestContext(t), "test_component") require.True(t, c.IsPaused()) c.Resume() require.False(t, c.IsPaused()) } func Test_PauseResume_MultipleCalls(t *testing.T) { - c := New(componenttest.TestContext(t), "") + c := New(componenttest.TestContext(t), "test_component") require.False(t, c.IsPaused()) c.Pause() c.Pause() @@ -46,7 +46,7 @@ func Test_PauseResume_MultipleCalls(t *testing.T) { func Test_ConsumeWaitsForResume(t *testing.T) { goleak.VerifyNone(t, goleak.IgnoreCurrent()) - c := NewPaused(componenttest.TestContext(t), "") + c := NewPaused(componenttest.TestContext(t), "test_component") require.True(t, c.IsPaused()) method := map[string]func(){ @@ -109,7 +109,7 @@ func Test_PauseResume_Multithreaded(t *testing.T) { routines := 5 allDone := sync.WaitGroup{} - c := NewPaused(componenttest.TestContext(t), "") + c := NewPaused(componenttest.TestContext(t), "test_component") require.True(t, c.IsPaused()) // Run goroutines that constantly try to call Consume* methods diff --git a/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go b/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go index c6bb81772b..9242991d9f 100644 --- a/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go +++ b/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go @@ -4,6 +4,7 @@ package livedebuggingconsumer import ( "context" + "sync" "github.com/grafana/alloy/internal/component/otelcol" "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" @@ -15,11 +16,13 @@ import ( ) type Consumer struct { - debugDataPublisher livedebugging.DebugDataPublisher - componentID livedebugging.ComponentID - logsMarshaler plog.Marshaler - metricsMarshaler pmetric.Marshaler - tracesMarshaler ptrace.Marshaler + debugDataPublisher livedebugging.DebugDataPublisher + componentID livedebugging.ComponentID + logsMarshaler plog.Marshaler + metricsMarshaler pmetric.Marshaler + tracesMarshaler ptrace.Marshaler + + mut sync.RWMutex targetComponentIDsMetric []string targetComponentIDsLog []string targetComponentIDsTraces []string @@ -39,13 +42,15 @@ func New(debugDataPublisher livedebugging.DebugDataPublisher, componentID string // SetTargetConsumers stores the componentIDs of the next consumers func (c *Consumer) SetTargetConsumers(metric, log, trace []otelcol.Consumer) { + c.mut.Lock() + defer c.mut.Unlock() c.targetComponentIDsMetric = extractIds(metric) c.targetComponentIDsLog = extractIds(log) c.targetComponentIDsTraces = extractIds(trace) } func extractIds(consumers []otelcol.Consumer) []string { - ids := make([]string, 0) + ids := make([]string, 0, len(consumers)) for _, cons := range consumers { if lazy, ok := cons.(*lazyconsumer.Consumer); ok { ids = append(ids, lazy.ComponentID()) @@ -62,45 +67,60 @@ func (c *Consumer) Capabilities() otelconsumer.Capabilities { // ConsumeTraces implements otelcol.ConsumeTraces. func (c *Consumer) ConsumeTraces(ctx context.Context, td ptrace.Traces) error { - if c.debugDataPublisher.IsActive(c.componentID) { - data, _ := c.tracesMarshaler.MarshalTraces(td) - c.debugDataPublisher.Publish(c.componentID, livedebugging.NewFeed( - c.componentID, - livedebugging.OtelTrace, - uint64(td.SpanCount()), - func() string { return string(data) }, - livedebugging.WithTargetComponentIDs(c.targetComponentIDsTraces), - )) - } + c.mut.RLock() + defer c.mut.RUnlock() + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + c.componentID, + livedebugging.OtelTrace, + uint64(td.SpanCount()), + func() string { + data, err := c.tracesMarshaler.MarshalTraces(td) + if err != nil { + return "" + } + return string(data) + }, + livedebugging.WithTargetComponentIDs(c.targetComponentIDsTraces), + )) return nil } // ConsumeMetrics implements otelcol.ConsumeMetrics. func (c *Consumer) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error { - if c.debugDataPublisher.IsActive(c.componentID) { - data, _ := c.metricsMarshaler.MarshalMetrics(md) - c.debugDataPublisher.Publish(c.componentID, livedebugging.NewFeed( - c.componentID, - livedebugging.OtelMetric, - uint64(md.MetricCount()), - func() string { return string(data) }, - livedebugging.WithTargetComponentIDs(c.targetComponentIDsMetric), - )) - } + c.mut.RLock() + defer c.mut.RUnlock() + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + c.componentID, + livedebugging.OtelMetric, + uint64(md.MetricCount()), + func() string { + data, err := c.metricsMarshaler.MarshalMetrics(md) + if err != nil { + return "" + } + return string(data) + }, + livedebugging.WithTargetComponentIDs(c.targetComponentIDsMetric), + )) return nil } // ConsumeLogs implements otelcol.ConsumeLogs. func (c *Consumer) ConsumeLogs(ctx context.Context, ld plog.Logs) error { - if c.debugDataPublisher.IsActive(c.componentID) { - data, _ := c.logsMarshaler.MarshalLogs(ld) - c.debugDataPublisher.Publish(c.componentID, livedebugging.NewFeed( - c.componentID, - livedebugging.OtelLog, - uint64(ld.LogRecordCount()), - func() string { return string(data) }, - livedebugging.WithTargetComponentIDs(c.targetComponentIDsLog), - )) - } + c.mut.RLock() + defer c.mut.RUnlock() + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + c.componentID, + livedebugging.OtelLog, + uint64(ld.LogRecordCount()), + func() string { + data, err := c.logsMarshaler.MarshalLogs(ld) + if err != nil { + return "" + } + return string(data) + }, + livedebugging.WithTargetComponentIDs(c.targetComponentIDsLog), + )) return nil } diff --git a/internal/component/otelcol/processor/processor.go b/internal/component/otelcol/processor/processor.go index 9e4c24aed4..23df790384 100644 --- a/internal/component/otelcol/processor/processor.go +++ b/internal/component/otelcol/processor/processor.go @@ -228,6 +228,8 @@ func (p *Processor) Update(args component.Arguments) error { } } + p.liveDebuggingConsumer.SetTargetConsumers(next.Metrics, next.Logs, next.Traces) + updateConsumersFunc := func() { p.consumer.SetConsumers(tracesProcessor, metricsProcessor, logsProcessor) } diff --git a/internal/component/otelcol/receiver/receiver.go b/internal/component/otelcol/receiver/receiver.go index 8abb62ea83..23c30ac689 100644 --- a/internal/component/otelcol/receiver/receiver.go +++ b/internal/component/otelcol/receiver/receiver.go @@ -223,6 +223,8 @@ func (r *Receiver) Update(args component.Arguments) error { } } + r.liveDebuggingConsumer.SetTargetConsumers(next.Metrics, next.Logs, next.Traces) + // Schedule the components to run once our component is running. r.sched.Schedule(r.ctx, func() {}, host, components...) return nil diff --git a/internal/component/prometheus/relabel/relabel.go b/internal/component/prometheus/relabel/relabel.go index a0eeb8ff9a..65c7fc0dc1 100644 --- a/internal/component/prometheus/relabel/relabel.go +++ b/internal/component/prometheus/relabel/relabel.go @@ -281,7 +281,7 @@ func (c *Component) relabel(val float64, lbls labels.Labels) labels.Labels { if relabelled.Len() == 0 { count = 0 // if no labels are left, the count is not incremented because the metric will be filtered out } - c.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( componentID, livedebugging.PrometheusMetric, count, diff --git a/internal/component/prometheus/remotewrite/remote_write.go b/internal/component/prometheus/remotewrite/remote_write.go index d54e081aac..57c923f535 100644 --- a/internal/component/prometheus/remotewrite/remote_write.go +++ b/internal/component/prometheus/remotewrite/remote_write.go @@ -129,16 +129,14 @@ func New(o component.Options, c Arguments) (*Component, error) { if localID == 0 { ls.GetOrAddLink(res.opts.ID, uint64(newRef), l) } - if res.debugDataPublisher.IsActive(componentID) { - res.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.PrometheusMetric, - 1, - func() string { - return fmt.Sprintf("ts=%d, labels=%s, value=%f", t, l, v) - }, - )) - } + res.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.PrometheusMetric, + 1, + func() string { + return fmt.Sprintf("ts=%d, labels=%s, value=%f", t, l, v) + }, + )) return globalRef, nextErr }), prometheus.WithHistogramHook(func(globalRef storage.SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram, next storage.Appender) (storage.SeriesRef, error) { @@ -151,24 +149,22 @@ func New(o component.Options, c Arguments) (*Component, error) { if localID == 0 { ls.GetOrAddLink(res.opts.ID, uint64(newRef), l) } - if res.debugDataPublisher.IsActive(componentID) { - res.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.PrometheusMetric, - 1, - func() string { - var data string - if h != nil { - data = fmt.Sprintf("ts=%d, labels=%s, histogram=%s", t, l, h.String()) - } else if fh != nil { - data = fmt.Sprintf("ts=%d, labels=%s, float_histogram=%s", t, l, fh.String()) - } else { - data = fmt.Sprintf("ts=%d, labels=%s, no_value", t, l) - } - return data - }, - )) - } + res.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.PrometheusMetric, + 1, + func() string { + var data string + if h != nil { + data = fmt.Sprintf("ts=%d, labels=%s, histogram=%s", t, l, h.String()) + } else if fh != nil { + data = fmt.Sprintf("ts=%d, labels=%s, float_histogram=%s", t, l, fh.String()) + } else { + data = fmt.Sprintf("ts=%d, labels=%s, no_value", t, l) + } + return data + }, + )) return globalRef, nextErr }), prometheus.WithMetadataHook(func(globalRef storage.SeriesRef, l labels.Labels, m metadata.Metadata, next storage.Appender) (storage.SeriesRef, error) { @@ -181,16 +177,14 @@ func New(o component.Options, c Arguments) (*Component, error) { if localID == 0 { ls.GetOrAddLink(res.opts.ID, uint64(newRef), l) } - if res.debugDataPublisher.IsActive(componentID) { - res.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.PrometheusMetric, - 1, - func() string { - return fmt.Sprintf("labels=%s, type=%s, unit=%s, help=%s", l, m.Type, m.Unit, m.Help) - }, - )) - } + res.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.PrometheusMetric, + 1, + func() string { + return fmt.Sprintf("labels=%s, type=%s, unit=%s, help=%s", l, m.Type, m.Unit, m.Help) + }, + )) return globalRef, nextErr }), prometheus.WithExemplarHook(func(globalRef storage.SeriesRef, l labels.Labels, e exemplar.Exemplar, next storage.Appender) (storage.SeriesRef, error) { @@ -203,16 +197,14 @@ func New(o component.Options, c Arguments) (*Component, error) { if localID == 0 { ls.GetOrAddLink(res.opts.ID, uint64(newRef), l) } - if res.debugDataPublisher.IsActive(componentID) { - res.debugDataPublisher.Publish(componentID, livedebugging.NewFeed( - componentID, - livedebugging.PrometheusMetric, - 1, - func() string { - return fmt.Sprintf("ts=%d, labels=%s, exemplar_labels=%s, value=%f", e.Ts, l, e.Labels, e.Value) - }, - )) - } + res.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.PrometheusMetric, + 1, + func() string { + return fmt.Sprintf("ts=%d, labels=%s, exemplar_labels=%s, value=%f", e.Ts, l, e.Labels, e.Value) + }, + )) return globalRef, nextErr }), ) diff --git a/internal/service/livedebugging/data.go b/internal/service/livedebugging/data.go new file mode 100644 index 0000000000..dcae46c9c8 --- /dev/null +++ b/internal/service/livedebugging/data.go @@ -0,0 +1,50 @@ +package livedebugging + +type DataType string + +const ( + Target DataType = "target" + PrometheusMetric DataType = "prometheus_metric" + LokiLog DataType = "loki_log" + OtelMetric DataType = "otel_metric" + OtelLog DataType = "otel_log" + OtelTrace DataType = "otel_trace" +) + +type DataOption func(*Data) + +func WithTargetComponentIDs(ids []string) DataOption { + return func(f *Data) { + f.TargetComponentIDs = ids + } +} + +type Data struct { + // ID of the component that created the data. + ComponentID ComponentID + // Ids of the components which will consume the data. + // This is needed for components that can export different types of data (most Otel components) to know + // where the data should go. When left empty, the data is expected to be sent to all components consuming data + // from the component that created it. + TargetComponentIDs []string + Type DataType + // Count is the number of spans, metrics, logs that the data represent. + Count uint64 + // The data string is passed as a function to only compute the string if needed. + DataFunc func() string +} + +func NewData(componentID ComponentID, dataType DataType, count uint64, dataFunc func() string, opts ...DataOption) *Data { + data := &Data{ + ComponentID: componentID, + Type: dataType, + Count: count, + DataFunc: dataFunc, + } + + for _, opt := range opts { + opt(data) + } + + return data +} diff --git a/internal/service/livedebugging/feed.go b/internal/service/livedebugging/feed.go deleted file mode 100644 index f9d5f27951..0000000000 --- a/internal/service/livedebugging/feed.go +++ /dev/null @@ -1,50 +0,0 @@ -package livedebugging - -type FeedType string - -const ( - Target FeedType = "target" - PrometheusMetric FeedType = "prometheus_metric" - LokiLog FeedType = "loki_log" - OtelMetric FeedType = "otel_metric" - OtelLog FeedType = "otel_log" - OtelTrace FeedType = "otel_trace" -) - -type FeedOption func(*Feed) - -func WithTargetComponentIDs(ids []string) FeedOption { - return func(f *Feed) { - f.TargetComponentIDs = ids - } -} - -type Feed struct { - // ID of the component that created the feed. - ComponentID ComponentID - // Ids of the components which will consume the Feed data. - // This is needed for components that can export different types of data (most Otel components) to know - // where the Feed should go. When left empty, the Feed is expected to be sent to all components consuming data - // from the component that created it. - TargetComponentIDs []string - Type FeedType - // Count is the number of spans, metrics, logs that the Feed represent. - Count uint64 - // The data string is passed as a function to only compute the string if needed. - DataFunc func() string -} - -func NewFeed(componentID ComponentID, feedType FeedType, count uint64, dataFunc func() string, opts ...FeedOption) *Feed { - feed := &Feed{ - ComponentID: componentID, - Type: feedType, - Count: count, - DataFunc: dataFunc, - } - - for _, opt := range opts { - opt(feed) - } - - return feed -} diff --git a/internal/service/livedebugging/livedebugging.go b/internal/service/livedebugging/livedebugging.go index 76fea6bffb..6024381cb5 100644 --- a/internal/service/livedebugging/livedebugging.go +++ b/internal/service/livedebugging/livedebugging.go @@ -16,27 +16,27 @@ type CallbackID string type CallbackManager interface { // AddCallback sets a callback for a given componentID. // The callback is used to send debugging data to live debugging consumers. - AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Feed)) error + AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error // DeleteCallback deletes a callback for a given componentID. DeleteCallback(callbackID CallbackID, componentID ComponentID) // AddCallbackMulti sets a callback to all components. // The callbacks are used to send debugging data to live debugging consumers. - AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Feed)) error + AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error // DeleteCallbackMulti deletes callbacks for all components. DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) } // DebugDataPublisher is used by components to push information to live debugging consumers. type DebugDataPublisher interface { - // Publish sends debugging data for a given componentID. - Publish(componentID ComponentID, data *Feed) + // Publish sends debugging data for a given componentID if a least one consumer is listening for debugging data for the given componentID. + PublishIfActive(data *Data) // IsActive returns true when at least one consumer is listening for debugging data for the given componentID. IsActive(componentID ComponentID) bool } type liveDebugging struct { loadMut sync.RWMutex - callbacks map[ComponentID]map[CallbackID]func(*Feed) + callbacks map[ComponentID]map[CallbackID]func(*Data) host service.Host enabled bool } @@ -47,17 +47,24 @@ var _ DebugDataPublisher = &liveDebugging{} // NewLiveDebugging creates a new instance of liveDebugging. func NewLiveDebugging() *liveDebugging { return &liveDebugging{ - callbacks: make(map[ComponentID]map[CallbackID]func(*Feed)), + callbacks: make(map[ComponentID]map[CallbackID]func(*Data)), } } -func (s *liveDebugging) Publish(componentID ComponentID, data *Feed) { +func (s *liveDebugging) PublishIfActive(data *Data) { s.loadMut.RLock() defer s.loadMut.RUnlock() - if s.enabled { - for _, callback := range s.callbacks[componentID] { - callback(data) - } + + if !s.enabled { + return + } + + if callbacks, exist := s.callbacks[data.ComponentID]; !exist || len(callbacks) == 0 { + return + } + + for _, callback := range s.callbacks[data.ComponentID] { + callback(data) } } @@ -68,7 +75,7 @@ func (s *liveDebugging) IsActive(componentID ComponentID) bool { return exist && len(callbacks) > 0 } -func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Feed)) error { +func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { err := s.addCallback(callbackID, componentID, callback) if err != nil { return err @@ -77,7 +84,7 @@ func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID Component return nil } -func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Feed)) error { +func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { err := s.addCallbackMulti(callbackID, moduleID, callback) if err != nil { return err @@ -104,7 +111,7 @@ func (s *liveDebugging) DeleteCallbackMulti(callbackID CallbackID, moduleID Modu } } -func (s *liveDebugging) addCallback(callbackID CallbackID, componentID ComponentID, callback func(*Feed)) error { +func (s *liveDebugging) addCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -126,13 +133,13 @@ func (s *liveDebugging) addCallback(callbackID CallbackID, componentID Component } if _, ok := s.callbacks[componentID]; !ok { - s.callbacks[componentID] = make(map[CallbackID]func(*Feed)) + s.callbacks[componentID] = make(map[CallbackID]func(*Data)) } s.callbacks[componentID][callbackID] = callback return nil } -func (s *liveDebugging) addCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Feed)) error { +func (s *liveDebugging) addCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -155,7 +162,7 @@ func (s *liveDebugging) addCallbackMulti(callbackID CallbackID, moduleID ModuleI } if _, ok := s.callbacks[ComponentID(cp.ID.String())]; !ok { - s.callbacks[ComponentID(cp.ID.String())] = make(map[CallbackID]func(*Feed)) + s.callbacks[ComponentID(cp.ID.String())] = make(map[CallbackID]func(*Data)) } s.callbacks[ComponentID(cp.ID.String())][callbackID] = callback } diff --git a/internal/service/livedebugging/livedebugging_test.go b/internal/service/livedebugging/livedebugging_test.go index f4a62b997d..b25657d66d 100644 --- a/internal/service/livedebugging/livedebugging_test.go +++ b/internal/service/livedebugging/livedebugging_test.go @@ -11,7 +11,7 @@ import ( func TestAddCallback(t *testing.T) { livedebugging := NewLiveDebugging() callbackID := CallbackID("callback1") - callback := func(data *Feed) {} + callback := func(data *Data) {} err := livedebugging.AddCallback(callbackID, "fake.liveDebugging", callback) require.ErrorContains(t, err, "the live debugging service is disabled. Check the documentation to find out how to enable it") @@ -46,22 +46,23 @@ func TestStream(t *testing.T) { componentID := ComponentID("fake.liveDebugging") callbackID := CallbackID("callback1") - var receivedData *Feed - callback := func(data *Feed) { + var receivedData *Data + callback := func(data *Data) { receivedData = data } require.False(t, livedebugging.IsActive(componentID)) + livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) + require.Nil(t, receivedData) // nil because there are no active callbacks for it + livedebugging.AddCallback(callbackID, componentID, callback) - require.True(t, livedebugging.IsActive(componentID)) - require.Len(t, livedebugging.callbacks[componentID], 1) - livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) + livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) require.Equal(t, componentID, receivedData.ComponentID) require.Equal(t, []string{"component1"}, receivedData.TargetComponentIDs) require.Equal(t, uint64(3), receivedData.Count) require.Equal(t, "test data", receivedData.DataFunc()) livedebugging.SetEnabled(false) - livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "new test data" }, WithTargetComponentIDs([]string{"component1"}))) + livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "new test data" }, WithTargetComponentIDs([]string{"component1"}))) require.Equal(t, "test data", receivedData.DataFunc()) // not updated because the feature is disabled } @@ -70,7 +71,7 @@ func TestStreamEmpty(t *testing.T) { setupServiceHost(livedebugging) componentID := ComponentID("fake.liveDebugging") require.NotPanics(t, func() { - livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) + livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) }) } @@ -81,13 +82,13 @@ func TestMultipleStreams(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - var receivedData1 *Feed - callback1 := func(data *Feed) { + var receivedData1 *Data + callback1 := func(data *Data) { receivedData1 = data } - var receivedData2 *Feed - callback2 := func(data *Feed) { + var receivedData2 *Data + callback2 := func(data *Data) { receivedData2 = data } @@ -95,7 +96,7 @@ func TestMultipleStreams(t *testing.T) { require.NoError(t, livedebugging.AddCallback(callbackID2, componentID, callback2)) require.Len(t, livedebugging.callbacks[componentID], 2) - livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "test data" })) + livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "test data" })) require.Equal(t, "test data", receivedData1.DataFunc()) require.Equal(t, "test data", receivedData2.DataFunc()) } @@ -107,8 +108,8 @@ func TestDeleteCallback(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - callback1 := func(data *Feed) {} - callback2 := func(data *Feed) {} + callback1 := func(data *Data) {} + callback2 := func(data *Data) {} component, _ := livedebugging.host.GetComponent(component.ParseID("fake.liveDebugging"), component.InfoOptions{}) @@ -129,8 +130,6 @@ func TestDeleteCallback(t *testing.T) { livedebugging.DeleteCallback(callbackID2, componentID) require.Empty(t, livedebugging.callbacks[componentID]) require.Equal(t, 0, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) - - require.False(t, livedebugging.IsActive(ComponentID("fake.liveDebugging"))) } func setupServiceHost(liveDebugging *liveDebugging) { @@ -149,7 +148,7 @@ func setupServiceHost(liveDebugging *liveDebugging) { func TestAddCallbackMulti(t *testing.T) { livedebugging := NewLiveDebugging() callbackID := CallbackID("callback1") - callback := func(data *Feed) {} + callback := func(data *Data) {} err := livedebugging.AddCallbackMulti(callbackID, "", callback) require.ErrorContains(t, err, "the live debugging service is disabled. Check the documentation to find out how to enable it") @@ -179,8 +178,8 @@ func TestDeleteCallbackMulti(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - callback1 := func(data *Feed) {} - callback2 := func(data *Feed) {} + callback1 := func(data *Data) {} + callback2 := func(data *Data) {} component, _ := livedebugging.host.GetComponent(component.ParseID("fake.liveDebugging"), component.InfoOptions{}) @@ -201,8 +200,6 @@ func TestDeleteCallbackMulti(t *testing.T) { livedebugging.DeleteCallbackMulti(callbackID2, "") require.Empty(t, livedebugging.callbacks[componentID]) require.Equal(t, 0, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) - - require.False(t, livedebugging.IsActive(ComponentID("fake.liveDebugging"))) } func TestMultiCallbacksMultipleStreams(t *testing.T) { @@ -212,13 +209,13 @@ func TestMultiCallbacksMultipleStreams(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - var receivedData1 *Feed - callback1 := func(data *Feed) { + var receivedData1 *Data + callback1 := func(data *Data) { receivedData1 = data } - var receivedData2 *Feed - callback2 := func(data *Feed) { + var receivedData2 *Data + callback2 := func(data *Data) { receivedData2 = data } @@ -226,7 +223,7 @@ func TestMultiCallbacksMultipleStreams(t *testing.T) { require.NoError(t, livedebugging.AddCallbackMulti(callbackID2, "", callback2)) require.Len(t, livedebugging.callbacks[componentID], 2) - livedebugging.Publish(componentID, NewFeed(componentID, PrometheusMetric, 3, func() string { return "test data" })) + livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "test data" })) require.Equal(t, "test data", receivedData1.DataFunc()) require.Equal(t, "test data", receivedData2.DataFunc()) } diff --git a/internal/web/api/api.go b/internal/web/api/api.go index 321fe52193..1c6f5f005d 100644 --- a/internal/web/api/api.go +++ b/internal/web/api/api.go @@ -169,9 +169,9 @@ func getClusteringPeersHandler(host service.Host) http.HandlerFunc { } } -type feedKey struct { +type dataKey struct { ComponentID livedebugging.ComponentID - Type livedebugging.FeedType + Type livedebugging.DataType } func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.HandlerFunc { @@ -183,13 +183,13 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.H window := setWindow(w, r.URL.Query().Get("window")) - dataCh := make(chan *livedebugging.Feed, 1000) - feedDataMap := make(map[feedKey]feed) + dataCh := make(chan *livedebugging.Data, 1000) + dataMap := make(map[dataKey]liveDebuggingData) ctx := r.Context() id := livedebugging.CallbackID(uuid.New().String()) - err := callbackManager.AddCallbackMulti(id, moduleID, func(data *livedebugging.Feed) { + err := callbackManager.AddCallbackMulti(id, moduleID, func(data *livedebugging.Data) { select { case <-ctx.Done(): return @@ -218,12 +218,12 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.H select { case data := <-dataCh: // Aggregate incoming data - key := feedKey{ComponentID: data.ComponentID, Type: data.Type} - if existing, exists := feedDataMap[key]; exists { + key := dataKey{ComponentID: data.ComponentID, Type: data.Type} + if existing, exists := dataMap[key]; exists { existing.Count += data.Count } else { // The data is ignored for the graph. - feedDataMap[key] = feed{ + dataMap[key] = liveDebuggingData{ ComponentID: string(data.ComponentID), Count: data.Count, Type: string(data.Type), @@ -234,7 +234,7 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.H case <-ticker.C: // Flush aggregated data var builder strings.Builder - for _, data := range feedDataMap { + for _, data := range dataMap { jsonData, err := json.Marshal(data) if err != nil { continue @@ -254,7 +254,7 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.H } w.(http.Flusher).Flush() - feedDataMap = make(map[feedKey]feed) + dataMap = make(map[dataKey]liveDebuggingData) case <-ctx.Done(): return @@ -275,7 +275,7 @@ func liveDebugging(_ service.Host, callbackManager livedebugging.CallbackManager id := livedebugging.CallbackID(uuid.New().String()) - err := callbackManager.AddCallback(id, componentID, func(data *livedebugging.Feed) { + err := callbackManager.AddCallback(id, componentID, func(data *livedebugging.Data) { select { case <-ctx.Done(): return diff --git a/internal/web/api/data.go b/internal/web/api/data.go index efd366ac08..364231615f 100644 --- a/internal/web/api/data.go +++ b/internal/web/api/data.go @@ -1,15 +1,15 @@ package api -type feed struct { - // ID of the component that created the feed. +type liveDebuggingData struct { + // ID of the component that created the data. ComponentID string `json:"componentID"` - // Ids of the components which will consume the Feed data. + // Ids of the components which will consume the data. // This is needed for components that can export different types of data (most Otel components) to know - // where the Feed should go. When left empty, the Feed is expected to be sent to all components consuming data + // where the data should go. When left empty, the data is expected to be sent to all components consuming data // from the component that created it. TargetComponentIDs []string `json:"targetComponentIDs"` // Type specifies the category of data represented by the count (otel_metric, loki_log, target...). Type string `json:"type"` - // Count is the number of spans, metrics, logs that the Feed represent. + // Count is the number of spans, metrics, logs that the data represent. Count uint64 `json:"count"` } From 5be010233221874d0e08c9edbc47bb72f9a7d873 Mon Sep 17 00:00:00 2001 From: William Dumont Date: Wed, 12 Feb 2025 12:20:59 +0100 Subject: [PATCH 04/11] review feedback 2 --- internal/alloycli/cmd_run.go | 1 + .../component/otelcol/connector/connector.go | 5 ++ .../otelcol/connector/spanlogs/spanlogs.go | 5 ++ .../otelcol/processor/discovery/discovery.go | 5 ++ .../component/otelcol/processor/processor.go | 5 ++ .../component/otelcol/receiver/receiver.go | 5 ++ .../service/livedebugging/livedebugging.go | 73 +++++++------------ internal/service/ui/ui.go | 4 +- internal/web/api/api.go | 33 +++++++-- internal/web/api/data.go | 4 +- 10 files changed, 85 insertions(+), 55 deletions(-) diff --git a/internal/alloycli/cmd_run.go b/internal/alloycli/cmd_run.go index 7ef521ed19..b928f7fcf7 100644 --- a/internal/alloycli/cmd_run.go +++ b/internal/alloycli/cmd_run.go @@ -318,6 +318,7 @@ func (fr *alloyRun) Run(cmd *cobra.Command, configPath string) error { uiService := uiservice.New(uiservice.Options{ UIPrefix: fr.uiPrefix, CallbackManager: liveDebuggingService.Data().(livedebugging.CallbackManager), + Logger: log.With(l, "service", "ui"), }) otelService := otel_service.New(l) diff --git a/internal/component/otelcol/connector/connector.go b/internal/component/otelcol/connector/connector.go index 8828971310..e13c36f265 100644 --- a/internal/component/otelcol/connector/connector.go +++ b/internal/component/otelcol/connector/connector.go @@ -6,6 +6,7 @@ import ( "context" "errors" "os" + "sync" "github.com/prometheus/client_golang/prometheus" otelcomponent "go.opentelemetry.io/collector/component" @@ -83,6 +84,8 @@ type Connector struct { debugDataPublisher livedebugging.DebugDataPublisher args Arguments + + updateMut sync.Mutex } var ( @@ -148,6 +151,8 @@ func (p *Connector) Run(ctx context.Context) error { // configuration for OpenTelemetry Collector connector configuration and manage // the underlying OpenTelemetry Collector connector. func (p *Connector) Update(args component.Arguments) error { + p.updateMut.Lock() + defer p.updateMut.Unlock() p.args = args.(Arguments) host := scheduler.NewHost( diff --git a/internal/component/otelcol/connector/spanlogs/spanlogs.go b/internal/component/otelcol/connector/spanlogs/spanlogs.go index c7e5bb9069..bf79159bff 100644 --- a/internal/component/otelcol/connector/spanlogs/spanlogs.go +++ b/internal/component/otelcol/connector/spanlogs/spanlogs.go @@ -4,6 +4,7 @@ package spanlogs import ( "context" "fmt" + "sync" "github.com/grafana/alloy/internal/component" "github.com/grafana/alloy/internal/component/otelcol" @@ -85,6 +86,8 @@ type Component struct { debugDataPublisher livedebugging.DebugDataPublisher args Arguments + + updateMut sync.Mutex } var ( @@ -140,6 +143,8 @@ func (c *Component) Run(ctx context.Context) error { // Update implements Component. func (c *Component) Update(newConfig component.Arguments) error { + c.updateMut.Lock() + defer c.updateMut.Unlock() c.args = newConfig.(Arguments) fanoutConsumer := c.args.Output.Logs diff --git a/internal/component/otelcol/processor/discovery/discovery.go b/internal/component/otelcol/processor/discovery/discovery.go index 8e6183fc24..9031adce3f 100644 --- a/internal/component/otelcol/processor/discovery/discovery.go +++ b/internal/component/otelcol/processor/discovery/discovery.go @@ -4,6 +4,7 @@ package discovery import ( "context" "fmt" + "sync" "github.com/go-kit/log" "github.com/grafana/alloy/internal/component" @@ -85,6 +86,8 @@ type Component struct { opts component.Options args Arguments + + updateMut sync.Mutex } var ( @@ -154,6 +157,8 @@ func (c *Component) Run(ctx context.Context) error { // Update implements Component. func (c *Component) Update(newConfig component.Arguments) error { + c.updateMut.Lock() + defer c.updateMut.Unlock() c.args = newConfig.(Arguments) hostLabels := make(map[string]discovery.Target) diff --git a/internal/component/otelcol/processor/processor.go b/internal/component/otelcol/processor/processor.go index 23df790384..597a9b9db9 100644 --- a/internal/component/otelcol/processor/processor.go +++ b/internal/component/otelcol/processor/processor.go @@ -6,6 +6,7 @@ import ( "context" "errors" "os" + "sync" "github.com/prometheus/client_golang/prometheus" otelcomponent "go.opentelemetry.io/collector/component" @@ -69,6 +70,8 @@ type Processor struct { debugDataPublisher livedebugging.DebugDataPublisher args Arguments + + updateMut sync.Mutex } var ( @@ -136,6 +139,8 @@ func (p *Processor) Run(ctx context.Context) error { // configuration for OpenTelemetry Collector processor configuration and manage // the underlying OpenTelemetry Collector processor. func (p *Processor) Update(args component.Arguments) error { + p.updateMut.Lock() + defer p.updateMut.Unlock() p.args = args.(Arguments) host := scheduler.NewHost( diff --git a/internal/component/otelcol/receiver/receiver.go b/internal/component/otelcol/receiver/receiver.go index 23c30ac689..d27e8a584a 100644 --- a/internal/component/otelcol/receiver/receiver.go +++ b/internal/component/otelcol/receiver/receiver.go @@ -6,6 +6,7 @@ import ( "context" "errors" "os" + "sync" "github.com/grafana/alloy/internal/build" "github.com/grafana/alloy/internal/component" @@ -67,6 +68,8 @@ type Receiver struct { debugDataPublisher livedebugging.DebugDataPublisher args Arguments + + updateMut sync.Mutex } var ( @@ -124,6 +127,8 @@ func (r *Receiver) Run(ctx context.Context) error { // configuration for OpenTelemetry Collector receiver configuration and manage // the underlying OpenTelemetry Collector receiver. func (r *Receiver) Update(args component.Arguments) error { + r.updateMut.Lock() + defer r.updateMut.Unlock() r.args = args.(Arguments) host := scheduler.NewHost( diff --git a/internal/service/livedebugging/livedebugging.go b/internal/service/livedebugging/livedebugging.go index 6024381cb5..0efbb233af 100644 --- a/internal/service/livedebugging/livedebugging.go +++ b/internal/service/livedebugging/livedebugging.go @@ -76,42 +76,6 @@ func (s *liveDebugging) IsActive(componentID ComponentID) bool { } func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { - err := s.addCallback(callbackID, componentID, callback) - if err != nil { - return err - } - s.notifyComponent(componentID) - return nil -} - -func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { - err := s.addCallbackMulti(callbackID, moduleID, callback) - if err != nil { - return err - } - s.notifyComponents(moduleID) - return nil -} - -func (s *liveDebugging) DeleteCallback(callbackID CallbackID, componentID ComponentID) { - defer s.notifyComponent(componentID) - s.loadMut.Lock() - defer s.loadMut.Unlock() - delete(s.callbacks[componentID], callbackID) -} - -func (s *liveDebugging) DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) { - defer s.notifyComponents(moduleID) - s.loadMut.Lock() - defer s.loadMut.Unlock() - // ignore errors on delete - components, _ := s.host.ListComponents(string(moduleID), component.InfoOptions{}) - for _, cp := range components { - delete(s.callbacks[ComponentID(cp.ID.String())], callbackID) - } -} - -func (s *liveDebugging) addCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -135,11 +99,13 @@ func (s *liveDebugging) addCallback(callbackID CallbackID, componentID Component if _, ok := s.callbacks[componentID]; !ok { s.callbacks[componentID] = make(map[CallbackID]func(*Data)) } + s.callbacks[componentID][callbackID] = callback + s.notifyComponent(componentID) return nil } -func (s *liveDebugging) addCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { +func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -166,27 +132,44 @@ func (s *liveDebugging) addCallbackMulti(callbackID CallbackID, moduleID ModuleI } s.callbacks[ComponentID(cp.ID.String())][callbackID] = callback } + + s.notifyComponents(moduleID) return nil } -func (s *liveDebugging) notifyComponent(componentID ComponentID) { - s.loadMut.RLock() - defer s.loadMut.RUnlock() +func (s *liveDebugging) DeleteCallback(callbackID CallbackID, componentID ComponentID) { + s.loadMut.Lock() + defer s.loadMut.Unlock() + delete(s.callbacks[componentID], callbackID) + s.notifyComponent(componentID) +} + +func (s *liveDebugging) DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) { + s.loadMut.Lock() + defer s.loadMut.Unlock() + // ignore errors on delete + components, _ := s.host.ListComponents(string(moduleID), component.InfoOptions{}) + for _, cp := range components { + delete(s.callbacks[ComponentID(cp.ID.String())], callbackID) + } + s.notifyComponents(moduleID) +} + +// expect mut to be locked +func (s *liveDebugging) notifyComponent(componentID ComponentID) { info, err := s.host.GetComponent(component.ParseID(string(componentID)), component.InfoOptions{}) if err != nil { return } if component, ok := info.Component.(component.LiveDebugging); ok { // notify the component of the change - component.LiveDebugging(len(s.callbacks[componentID])) + go component.LiveDebugging(len(s.callbacks[componentID])) } } +// expect mut to be locked func (s *liveDebugging) notifyComponents(moduleID ModuleID) { - s.loadMut.RLock() - defer s.loadMut.RUnlock() - components, err := s.host.ListComponents(string(moduleID), component.InfoOptions{}) if err != nil { return @@ -194,7 +177,7 @@ func (s *liveDebugging) notifyComponents(moduleID ModuleID) { for _, cp := range components { if c, ok := cp.Component.(component.LiveDebugging); ok { // notify the component of the change - c.LiveDebugging(len(s.callbacks[ComponentID(cp.ID.String())])) + go c.LiveDebugging(len(s.callbacks[ComponentID(cp.ID.String())])) } } } diff --git a/internal/service/ui/ui.go b/internal/service/ui/ui.go index ea69448a82..f0b0b2b813 100644 --- a/internal/service/ui/ui.go +++ b/internal/service/ui/ui.go @@ -7,6 +7,7 @@ import ( "net/http" "path" + "github.com/go-kit/log" "github.com/gorilla/mux" "github.com/grafana/alloy/internal/featuregate" "github.com/grafana/alloy/internal/service" @@ -25,6 +26,7 @@ const ServiceName = "ui" type Options struct { UIPrefix string // Path prefix to host the UI at. CallbackManager livedebugging.CallbackManager // CallbackManager is used for live debugging in the UI. + Logger log.Logger } // Service implements the UI service. @@ -78,7 +80,7 @@ func (s *Service) Data() any { func (s *Service) ServiceHandler(host service.Host) (base string, handler http.Handler) { r := mux.NewRouter() - fa := api.NewAlloyAPI(host, s.opts.CallbackManager) + fa := api.NewAlloyAPI(host, s.opts.CallbackManager, s.opts.Logger) fa.RegisterRoutes(path.Join(s.opts.UIPrefix, "/api/v0/web"), r) ui.RegisterRoutes(s.opts.UIPrefix, r) diff --git a/internal/web/api/api.go b/internal/web/api/api.go index 1c6f5f005d..a6c26c4501 100644 --- a/internal/web/api/api.go +++ b/internal/web/api/api.go @@ -13,9 +13,11 @@ import ( "strings" "time" + "github.com/go-kit/log" "github.com/google/uuid" "github.com/gorilla/mux" "github.com/grafana/alloy/internal/component" + "github.com/grafana/alloy/internal/runtime/logging/level" "github.com/grafana/alloy/internal/service" "github.com/grafana/alloy/internal/service/cluster" "github.com/grafana/alloy/internal/service/livedebugging" @@ -27,11 +29,12 @@ import ( type AlloyAPI struct { alloy service.Host CallbackManager livedebugging.CallbackManager + logger log.Logger } // NewAlloyAPI instantiates a new Alloy API. -func NewAlloyAPI(alloy service.Host, CallbackManager livedebugging.CallbackManager) *AlloyAPI { - return &AlloyAPI{alloy: alloy, CallbackManager: CallbackManager} +func NewAlloyAPI(alloy service.Host, CallbackManager livedebugging.CallbackManager, l log.Logger) *AlloyAPI { + return &AlloyAPI{alloy: alloy, CallbackManager: CallbackManager, logger: l} } // RegisterRoutes registers all the API's routes. @@ -50,10 +53,10 @@ func (a *AlloyAPI) RegisterRoutes(urlPrefix string, r *mux.Router) { r.Handle(path.Join(urlPrefix, "/remotecfg/components/{id:.+}"), httputil.CompressionHandler{Handler: getComponentHandlerRemoteCfg(a.alloy)}) r.Handle(path.Join(urlPrefix, "/peers"), httputil.CompressionHandler{Handler: getClusteringPeersHandler(a.alloy)}) - r.Handle(path.Join(urlPrefix, "/debug/{id:.+}"), liveDebugging(a.alloy, a.CallbackManager)) + r.Handle(path.Join(urlPrefix, "/debug/{id:.+}"), liveDebugging(a.alloy, a.CallbackManager, a.logger)) - r.Handle(path.Join(urlPrefix, "/graph"), graph(a.alloy, a.CallbackManager)) - r.Handle(path.Join(urlPrefix, "/graph/{moduleID:.+}"), graph(a.alloy, a.CallbackManager)) + r.Handle(path.Join(urlPrefix, "/graph"), graph(a.alloy, a.CallbackManager, a.logger)) + r.Handle(path.Join(urlPrefix, "/graph/{moduleID:.+}"), graph(a.alloy, a.CallbackManager, a.logger)) } func listComponentsHandler(host service.Host) http.HandlerFunc { @@ -174,7 +177,7 @@ type dataKey struct { Type livedebugging.DataType } -func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.HandlerFunc { +func graph(_ service.Host, callbackManager livedebugging.CallbackManager, logger log.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var moduleID livedebugging.ModuleID if vars := mux.Vars(r); vars != nil { @@ -189,6 +192,7 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.H ctx := r.Context() id := livedebugging.CallbackID(uuid.New().String()) + droppedData := false err := callbackManager.AddCallbackMulti(id, moduleID, func(data *livedebugging.Data) { select { case <-ctx.Done(): @@ -197,6 +201,10 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.H select { case dataCh <- data: default: + if !droppedData { + level.Warn(logger).Log("msg", "data throughput is very high, not all debugging data can be sent to the graph") + droppedData = true + } } } }) @@ -235,6 +243,7 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.H // Flush aggregated data var builder strings.Builder for _, data := range dataMap { + data.Rate = float64(data.Count) / float64(window) jsonData, err := json.Marshal(data) if err != nil { continue @@ -254,7 +263,9 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.H } w.(http.Flusher).Flush() - dataMap = make(map[dataKey]liveDebuggingData) + for k := range dataMap { + delete(dataMap, k) + } case <-ctx.Done(): return @@ -263,7 +274,7 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager) http.H } } -func liveDebugging(_ service.Host, callbackManager livedebugging.CallbackManager) http.HandlerFunc { +func liveDebugging(_ service.Host, callbackManager livedebugging.CallbackManager, logger log.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) componentID := livedebugging.ComponentID(vars["id"]) @@ -275,6 +286,7 @@ func liveDebugging(_ service.Host, callbackManager livedebugging.CallbackManager id := livedebugging.CallbackID(uuid.New().String()) + droppedData := false err := callbackManager.AddCallback(id, componentID, func(data *livedebugging.Data) { select { case <-ctx.Done(): @@ -287,6 +299,10 @@ func liveDebugging(_ service.Host, callbackManager livedebugging.CallbackManager select { case dataCh <- data.DataFunc(): default: + if !droppedData { + level.Warn(logger).Log("msg", "data throughput is very high, not all debugging data can be sent the live debugging stream") + droppedData = true + } } } }) @@ -337,6 +353,7 @@ func setSampleProb(w http.ResponseWriter, sampleProbParam string) (sampleProb fl return sampleProb } +// window is expected to be in seconds, between 1 and 60. func setWindow(w http.ResponseWriter, windowParam string) (window int64) { window = 5 if windowParam != "" { diff --git a/internal/web/api/data.go b/internal/web/api/data.go index 364231615f..cb20151a90 100644 --- a/internal/web/api/data.go +++ b/internal/web/api/data.go @@ -10,6 +10,8 @@ type liveDebuggingData struct { TargetComponentIDs []string `json:"targetComponentIDs"` // Type specifies the category of data represented by the count (otel_metric, loki_log, target...). Type string `json:"type"` + // Rate represents the number of events of the given Type sent per second by the component. + Rate float64 `json:"rate"` // Count is the number of spans, metrics, logs that the data represent. - Count uint64 `json:"count"` + Count uint64 `json:"-"` } From 4cf6bc53e60f349fff96a4f6d5e685bb1cfc4fd6 Mon Sep 17 00:00:00 2001 From: William Dumont Date: Wed, 12 Feb 2025 15:34:11 +0100 Subject: [PATCH 05/11] change back to splitting functions in livedebugging service --- .../service/livedebugging/livedebugging.go | 79 ++++++++++++------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/internal/service/livedebugging/livedebugging.go b/internal/service/livedebugging/livedebugging.go index 0efbb233af..5b103fc890 100644 --- a/internal/service/livedebugging/livedebugging.go +++ b/internal/service/livedebugging/livedebugging.go @@ -33,7 +33,6 @@ type DebugDataPublisher interface { // IsActive returns true when at least one consumer is listening for debugging data for the given componentID. IsActive(componentID ComponentID) bool } - type liveDebugging struct { loadMut sync.RWMutex callbacks map[ComponentID]map[CallbackID]func(*Data) @@ -76,6 +75,48 @@ func (s *liveDebugging) IsActive(componentID ComponentID) bool { } func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { + // Split in two functions because this one needs write lock while notifyComponent needs read lock only. + // Using write lock on notifyComponent is not possible because the notified component might call IsActive or + // PublishIfActive which would create a deadlock. + err := s.addCallback(callbackID, componentID, callback) + if err != nil { + return err + } + s.notifyComponent(componentID) + return nil +} + +func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { + // Split in two functions because this one needs write lock while notifyComponents needs read lock only. + // Using write lock on notifyComponents is not possible because notified components might call IsActive or + // PublishIfActive which would create a deadlock. + err := s.addCallbackMulti(callbackID, moduleID, callback) + if err != nil { + return err + } + s.notifyComponents(moduleID) + return nil +} + +func (s *liveDebugging) DeleteCallback(callbackID CallbackID, componentID ComponentID) { + defer s.notifyComponent(componentID) + s.loadMut.Lock() + defer s.loadMut.Unlock() + delete(s.callbacks[componentID], callbackID) +} + +func (s *liveDebugging) DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) { + defer s.notifyComponents(moduleID) + s.loadMut.Lock() + defer s.loadMut.Unlock() + // ignore errors on delete + components, _ := s.host.ListComponents(string(moduleID), component.InfoOptions{}) + for _, cp := range components { + delete(s.callbacks[ComponentID(cp.ID.String())], callbackID) + } +} + +func (s *liveDebugging) addCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -101,11 +142,10 @@ func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID Component } s.callbacks[componentID][callbackID] = callback - s.notifyComponent(componentID) return nil } -func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { +func (s *liveDebugging) addCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -132,44 +172,27 @@ func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleI } s.callbacks[ComponentID(cp.ID.String())][callbackID] = callback } - - s.notifyComponents(moduleID) return nil } -func (s *liveDebugging) DeleteCallback(callbackID CallbackID, componentID ComponentID) { - s.loadMut.Lock() - defer s.loadMut.Unlock() - delete(s.callbacks[componentID], callbackID) - s.notifyComponent(componentID) -} - -func (s *liveDebugging) DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) { - s.loadMut.Lock() - defer s.loadMut.Unlock() - // ignore errors on delete - components, _ := s.host.ListComponents(string(moduleID), component.InfoOptions{}) - for _, cp := range components { - delete(s.callbacks[ComponentID(cp.ID.String())], callbackID) - } - - s.notifyComponents(moduleID) -} - -// expect mut to be locked func (s *liveDebugging) notifyComponent(componentID ComponentID) { + s.loadMut.RLock() + defer s.loadMut.RUnlock() + info, err := s.host.GetComponent(component.ParseID(string(componentID)), component.InfoOptions{}) if err != nil { return } if component, ok := info.Component.(component.LiveDebugging); ok { // notify the component of the change - go component.LiveDebugging(len(s.callbacks[componentID])) + component.LiveDebugging(len(s.callbacks[componentID])) } } -// expect mut to be locked func (s *liveDebugging) notifyComponents(moduleID ModuleID) { + s.loadMut.RLock() + defer s.loadMut.RUnlock() + components, err := s.host.ListComponents(string(moduleID), component.InfoOptions{}) if err != nil { return @@ -177,7 +200,7 @@ func (s *liveDebugging) notifyComponents(moduleID ModuleID) { for _, cp := range components { if c, ok := cp.Component.(component.LiveDebugging); ok { // notify the component of the change - go c.LiveDebugging(len(s.callbacks[ComponentID(cp.ID.String())])) + c.LiveDebugging(len(s.callbacks[ComponentID(cp.ID.String())])) } } } From 0dd571d6a8f63a56ce75c6009b73c0c2023d7c38 Mon Sep 17 00:00:00 2001 From: William Dumont Date: Wed, 12 Feb 2025 15:37:49 +0100 Subject: [PATCH 06/11] fix race condition in otel receiver --- .../component/otelcol/receiver/receiver.go | 12 +++++++-- .../otelcol/receiver/receiver_test.go | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/internal/component/otelcol/receiver/receiver.go b/internal/component/otelcol/receiver/receiver.go index d27e8a584a..0abb2d049f 100644 --- a/internal/component/otelcol/receiver/receiver.go +++ b/internal/component/otelcol/receiver/receiver.go @@ -69,6 +69,8 @@ type Receiver struct { args Arguments + // The mutex is needed because the live debugging service can trigger an update + // concurrently to add/remove the live debugging consumer. updateMut sync.Mutex } @@ -128,8 +130,14 @@ func (r *Receiver) Run(ctx context.Context) error { // the underlying OpenTelemetry Collector receiver. func (r *Receiver) Update(args component.Arguments) error { r.updateMut.Lock() - defer r.updateMut.Unlock() r.args = args.(Arguments) + r.updateMut.Unlock() + return r.update() +} + +func (r *Receiver) update() error { + r.updateMut.Lock() + defer r.updateMut.Unlock() host := scheduler.NewHost( r.opts.Logger, @@ -241,5 +249,5 @@ func (r *Receiver) CurrentHealth() component.Health { } func (p *Receiver) LiveDebugging(_ int) { - p.Update(p.args) + p.update() } diff --git a/internal/component/otelcol/receiver/receiver_test.go b/internal/component/otelcol/receiver/receiver_test.go index 29d263c9a4..19ce33c53a 100644 --- a/internal/component/otelcol/receiver/receiver_test.go +++ b/internal/component/otelcol/receiver/receiver_test.go @@ -127,6 +127,31 @@ func TestReceiverUpdate(t *testing.T) { require.ErrorContains(t, waitConsumerTrigger.Wait(time.Second), "context deadline exceeded") } +// This test will trigger a race error if the update function is not properly protected with a mutex +// because the live debugging service can call the update function concurrently. +func TestReceiverUpdateLiveDebugging(t *testing.T) { + te := newTestEnvironment(t, func(t otelconsumer.Traces) {}) + te.Start(fakeReceiverArgs{ + Output: &otelcol.ConsumerArguments{}, + }) + + require.NoError(t, te.Controller.WaitRunning(50*time.Millisecond)) + + go func() { + for i := 0; i < 100; i++ { + te.Controller.Update(fakeReceiverArgs{ + Output: &otelcol.ConsumerArguments{}, + }) + } + }() + + for i := 0; i < 100; i++ { + cp, err := te.Controller.GetComponent() + require.NoError(t, err) + cp.(*receiver.Receiver).LiveDebugging(1) + } +} + type testEnvironment struct { t *testing.T From ec53660169aa2fbc92f343dea87588b12ee613a6 Mon Sep 17 00:00:00 2001 From: William Dumont Date: Wed, 12 Feb 2025 15:51:19 +0100 Subject: [PATCH 07/11] add comment about memory leak --- internal/service/livedebugging/livedebugging.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/service/livedebugging/livedebugging.go b/internal/service/livedebugging/livedebugging.go index 5b103fc890..1e25e3555e 100644 --- a/internal/service/livedebugging/livedebugging.go +++ b/internal/service/livedebugging/livedebugging.go @@ -114,6 +114,9 @@ func (s *liveDebugging) DeleteCallbackMulti(callbackID CallbackID, moduleID Modu for _, cp := range components { delete(s.callbacks[ComponentID(cp.ID.String())], callbackID) } + // The s.callbacks[componentID] is not deleted. This is a very small memory leak which could only become significant if a user + // has a lot of components and reload the config with always different component labels while having the graph open. + // If this ever become a realistic scenario we should cleanup the map here. } func (s *liveDebugging) addCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { From c2c9bf4d7814e8671a570cbeac9a6de409daa5ae Mon Sep 17 00:00:00 2001 From: William Dumont Date: Fri, 14 Feb 2025 14:53:09 +0100 Subject: [PATCH 08/11] rework live debugging for otel components --- internal/component/component.go | 5 +- internal/component/discovery/discovery.go | 2 +- .../component/discovery/process/process.go | 16 +-- .../component/discovery/relabel/relabel.go | 2 +- internal/component/loki/process/process.go | 2 +- internal/component/loki/relabel/relabel.go | 2 +- .../loki/secretfilter/secretfilter.go | 2 +- .../component/otelcol/connector/connector.go | 38 +++--- .../otelcol/connector/spanlogs/spanlogs.go | 34 ++--- internal/component/otelcol/consumer.go | 5 + .../internal/interceptorconsumer/logs.go | 37 +++++ .../internal/interceptorconsumer/metrics.go | 37 +++++ .../internal/interceptorconsumer/traces.go | 37 +++++ .../internal/lazyconsumer/lazyconsumer.go | 8 +- .../livedebuggingconsumer.go | 126 ------------------ .../livedebuggingpublisher.go | 68 ++++++++++ .../databuffer.go | 2 +- .../logs.go | 13 +- .../metrics.go | 13 +- .../traces.go | 13 +- .../otelcol/processor/discovery/discovery.go | 46 +++---- .../component/otelcol/processor/processor.go | 58 ++++---- .../component/otelcol/receiver/loki/loki.go | 31 ++--- .../otelcol/receiver/prometheus/prometheus.go | 32 ++--- .../component/otelcol/receiver/receiver.go | 76 +++++------ .../otelcol/receiver/receiver_test.go | 25 ---- .../component/prometheus/relabel/relabel.go | 26 ++-- .../prometheus/remotewrite/remote_write.go | 2 +- .../service/livedebugging/livedebugging.go | 93 ++----------- .../livedebugging/livedebugging_test.go | 20 --- .../testlivedebugging/testlivedebugging.go | 8 +- 31 files changed, 392 insertions(+), 487 deletions(-) create mode 100644 internal/component/otelcol/internal/interceptorconsumer/logs.go create mode 100644 internal/component/otelcol/internal/interceptorconsumer/metrics.go create mode 100644 internal/component/otelcol/internal/interceptorconsumer/traces.go delete mode 100644 internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go create mode 100644 internal/component/otelcol/internal/livedebuggingpublisher/livedebuggingpublisher.go rename internal/component/otelcol/internal/{livedebuggingconsumer => textmarshaler}/databuffer.go (99%) rename internal/component/otelcol/internal/{livedebuggingconsumer => textmarshaler}/logs.go (79%) rename internal/component/otelcol/internal/{livedebuggingconsumer => textmarshaler}/metrics.go (71%) rename internal/component/otelcol/internal/{livedebuggingconsumer => textmarshaler}/traces.go (79%) diff --git a/internal/component/component.go b/internal/component/component.go index 45641ca25b..80fe187389 100644 --- a/internal/component/component.go +++ b/internal/component/component.go @@ -114,8 +114,7 @@ type DebugComponent interface { DebugInfo() interface{} } -// LiveDebugging is an interface used by the components that support the live debugging feature. +// LiveDebugging is a marker interface to check if a component supports live debugging. type LiveDebugging interface { - // LiveDebugging is invoked when the number of consumers changes. - LiveDebugging(consumers int) + LiveDebugging() // This function is never called. } diff --git a/internal/component/discovery/discovery.go b/internal/component/discovery/discovery.go index 0450df6e00..a50528c559 100644 --- a/internal/component/discovery/discovery.go +++ b/internal/component/discovery/discovery.go @@ -288,4 +288,4 @@ func toAlloyTargets(cache map[string]*targetgroup.Group) []Target { return allTargets } -func (c *Component) LiveDebugging(_ int) {} +func (c *Component) LiveDebugging() {} diff --git a/internal/component/discovery/process/process.go b/internal/component/discovery/process/process.go index 81dd652d77..11e1d40d44 100644 --- a/internal/component/discovery/process/process.go +++ b/internal/component/discovery/process/process.go @@ -69,14 +69,12 @@ func (c *Component) Run(ctx context.Context) error { c.changed() componentID := livedebugging.ComponentID(c.opts.ID) - if c.debugDataPublisher.IsActive(componentID) { - c.debugDataPublisher.PublishIfActive(livedebugging.NewData( - componentID, - livedebugging.Target, - uint64(len(c.processes)), - func() string { return fmt.Sprintf("%s", c.processes) }, - )) - } + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.Target, + uint64(len(c.processes)), + func() string { return fmt.Sprintf("%s", c.processes) }, + )) return nil } @@ -114,4 +112,4 @@ func (c *Component) changed() { }) } -func (c *Component) LiveDebugging(_ int) {} +func (c *Component) LiveDebugging() {} diff --git a/internal/component/discovery/relabel/relabel.go b/internal/component/discovery/relabel/relabel.go index 662c258c0c..d0187ba295 100644 --- a/internal/component/discovery/relabel/relabel.go +++ b/internal/component/discovery/relabel/relabel.go @@ -114,7 +114,7 @@ func (c *Component) Update(args component.Arguments) error { return nil } -func (c *Component) LiveDebugging(_ int) {} +func (c *Component) LiveDebugging() {} func componentMapToPromLabels(ls discovery.Target) labels.Labels { res := make([]labels.Label, 0, len(ls)) diff --git a/internal/component/loki/process/process.go b/internal/component/loki/process/process.go index a51192ec45..335fe1bce4 100644 --- a/internal/component/loki/process/process.go +++ b/internal/component/loki/process/process.go @@ -231,4 +231,4 @@ func stagesChanged(prev, next []stages.StageConfig) bool { return false } -func (c *Component) LiveDebugging(_ int) {} +func (c *Component) LiveDebugging() {} diff --git a/internal/component/loki/relabel/relabel.go b/internal/component/loki/relabel/relabel.go index 9b288ed0a1..6369343ac3 100644 --- a/internal/component/loki/relabel/relabel.go +++ b/internal/component/loki/relabel/relabel.go @@ -255,4 +255,4 @@ func (c *Component) process(e loki.Entry) model.LabelSet { return relabeled } -func (c *Component) LiveDebugging(_ int) {} +func (c *Component) LiveDebugging() {} diff --git a/internal/component/loki/secretfilter/secretfilter.go b/internal/component/loki/secretfilter/secretfilter.go index e766f117ba..fbfda7e903 100644 --- a/internal/component/loki/secretfilter/secretfilter.go +++ b/internal/component/loki/secretfilter/secretfilter.go @@ -382,4 +382,4 @@ func (c *Component) Update(args component.Arguments) error { return nil } -func (c *Component) LiveDebugging(_ int) {} +func (c *Component) LiveDebugging() {} diff --git a/internal/component/otelcol/connector/connector.go b/internal/component/otelcol/connector/connector.go index e13c36f265..0cf115a2c9 100644 --- a/internal/component/otelcol/connector/connector.go +++ b/internal/component/otelcol/connector/connector.go @@ -12,6 +12,7 @@ import ( otelcomponent "go.opentelemetry.io/collector/component" otelconnector "go.opentelemetry.io/collector/connector" otelextension "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/pipeline" sdkprometheus "go.opentelemetry.io/otel/exporters/prometheus" "go.opentelemetry.io/otel/sdk/metric" @@ -21,9 +22,10 @@ import ( "github.com/grafana/alloy/internal/component/otelcol" otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazycollector" "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/component/otelcol/internal/scheduler" "github.com/grafana/alloy/internal/service/livedebugging" "github.com/grafana/alloy/internal/util/zapadapter" @@ -80,8 +82,7 @@ type Connector struct { sched *scheduler.Scheduler collector *lazycollector.Collector - liveDebuggingConsumer *livedebuggingconsumer.Consumer - debugDataPublisher livedebugging.DebugDataPublisher + debugDataPublisher livedebugging.DebugDataPublisher args Arguments @@ -130,10 +131,9 @@ func New(opts component.Options, f otelconnector.Factory, args Arguments) (*Conn factory: f, consumer: consumer, - liveDebuggingConsumer: livedebuggingconsumer.New(debugDataPublisher.(livedebugging.DebugDataPublisher), opts.ID), - debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), - sched: scheduler.NewWithPauseCallbacks(opts.Logger, consumer.Pause, consumer.Resume), - collector: collector, + debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), + sched: scheduler.NewWithPauseCallbacks(opts.Logger, consumer.Pause, consumer.Resume), + collector: collector, } if err := p.Update(args); err != nil { return nil, err @@ -151,8 +151,6 @@ func (p *Connector) Run(ctx context.Context) error { // configuration for OpenTelemetry Collector connector configuration and manage // the underlying OpenTelemetry Collector connector. func (p *Connector) Update(args component.Arguments) error { - p.updateMut.Lock() - defer p.updateMut.Unlock() p.args = args.(Arguments) host := scheduler.NewHost( @@ -198,8 +196,6 @@ func (p *Connector) Update(args component.Arguments) error { next := p.args.NextConsumers() - liveDebuggingActive := p.debugDataPublisher.IsActive(livedebugging.ComponentID(p.opts.ID)) - // Create instances of the connector from our factory for each of our // supported telemetry signals. var components []otelcomponent.Component @@ -215,12 +211,14 @@ func (p *Connector) Update(args component.Arguments) error { } if len(next.Metrics) > 0 { - metrics := next.Metrics - if liveDebuggingActive { - metrics = append(metrics, p.liveDebuggingConsumer) - } - nextMetrics := fanoutconsumer.Metrics(metrics) - tracesConnector, err = p.factory.CreateTracesToMetrics(p.ctx, settings, connectorConfig, nextMetrics) + fanout := fanoutconsumer.Metrics(next.Metrics) + metricsInterceptor := interceptorconsumer.Metrics(fanout, false, + func(ctx context.Context, md pmetric.Metrics) error { + livedebuggingpublisher.PublishMetricsIfActive(p.debugDataPublisher, p.opts.ID, md, next.Metrics) + return fanout.ConsumeMetrics(ctx, md) + }, + ) + tracesConnector, err = p.factory.CreateTracesToMetrics(p.ctx, settings, connectorConfig, metricsInterceptor) if err != nil && !errors.Is(err, pipeline.ErrSignalNotSupported) { return err } else if tracesConnector != nil { @@ -231,8 +229,6 @@ func (p *Connector) Update(args component.Arguments) error { return errors.New("unsupported connector type") } - p.liveDebuggingConsumer.SetTargetConsumers(next.Metrics, next.Logs, next.Traces) - updateConsumersFunc := func() { p.consumer.SetConsumers(tracesConnector, metricsConnector, logsConnector) } @@ -247,6 +243,4 @@ func (p *Connector) CurrentHealth() component.Health { return p.sched.CurrentHealth() } -func (p *Connector) LiveDebugging(_ int) { - p.Update(p.args) -} +func (p *Connector) LiveDebugging() {} diff --git a/internal/component/otelcol/connector/spanlogs/spanlogs.go b/internal/component/otelcol/connector/spanlogs/spanlogs.go index bf79159bff..6376dbb9e2 100644 --- a/internal/component/otelcol/connector/spanlogs/spanlogs.go +++ b/internal/component/otelcol/connector/spanlogs/spanlogs.go @@ -9,12 +9,14 @@ import ( "github.com/grafana/alloy/internal/component" "github.com/grafana/alloy/internal/component/otelcol" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/featuregate" "github.com/grafana/alloy/internal/runtime/logging/level" "github.com/grafana/alloy/internal/service/livedebugging" "github.com/grafana/alloy/syntax" + "go.opentelemetry.io/collector/pdata/plog" ) func init() { @@ -82,8 +84,7 @@ type Component struct { opts component.Options - liveDebuggingConsumer *livedebuggingconsumer.Consumer - debugDataPublisher livedebugging.DebugDataPublisher + debugDataPublisher livedebugging.DebugDataPublisher args Arguments @@ -113,10 +114,9 @@ func New(o component.Options, c Arguments) (*Component, error) { } res := &Component{ - opts: o, - consumer: consumer, - liveDebuggingConsumer: livedebuggingconsumer.New(debugDataPublisher.(livedebugging.DebugDataPublisher), o.ID), - debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), + opts: o, + consumer: consumer, + debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), } if err := res.Update(c); err != nil { @@ -147,15 +147,17 @@ func (c *Component) Update(newConfig component.Arguments) error { defer c.updateMut.Unlock() c.args = newConfig.(Arguments) - fanoutConsumer := c.args.Output.Logs + nextLogs := c.args.Output.Logs - if c.debugDataPublisher.IsActive(livedebugging.ComponentID(c.opts.ID)) { - fanoutConsumer = append(fanoutConsumer, c.liveDebuggingConsumer) - } - - nextLogs := fanoutconsumer.Logs(fanoutConsumer) + fanout := fanoutconsumer.Logs(nextLogs) + logsInterceptor := interceptorconsumer.Logs(fanout, false, + func(ctx context.Context, ld plog.Logs) error { + livedebuggingpublisher.PublishLogsIfActive(c.debugDataPublisher, c.opts.ID, ld, nextLogs) + return fanout.ConsumeLogs(ctx, ld) + }, + ) - err := c.consumer.UpdateOptions(c.args, nextLogs) + err := c.consumer.UpdateOptions(c.args, logsInterceptor) if err != nil { return fmt.Errorf("failed to update traces consumer due to error: %w", err) } @@ -163,6 +165,4 @@ func (c *Component) Update(newConfig component.Arguments) error { return nil } -func (c *Component) LiveDebugging(_ int) { - c.Update(c.args) -} +func (c *Component) LiveDebugging() {} diff --git a/internal/component/otelcol/consumer.go b/internal/component/otelcol/consumer.go index e538b5a5ba..cfc4dcfa3d 100644 --- a/internal/component/otelcol/consumer.go +++ b/internal/component/otelcol/consumer.go @@ -12,6 +12,11 @@ type Consumer interface { otelconsumer.Logs } +type ConsumerWithComponentID interface { + Consumer + ComponentID() string +} + // ConsumerArguments is a common Arguments type for Alloy components which can // send data to otelcol consumers. // diff --git a/internal/component/otelcol/internal/interceptorconsumer/logs.go b/internal/component/otelcol/internal/interceptorconsumer/logs.go new file mode 100644 index 0000000000..34905b6048 --- /dev/null +++ b/internal/component/otelcol/internal/interceptorconsumer/logs.go @@ -0,0 +1,37 @@ +package interceptorconsumer + +import ( + "context" + + otelconsumer "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/plog" +) + +type LogsInterceptorFunc func(context.Context, plog.Logs) error + +type LogsInterceptor struct { + onConsumeLogs LogsInterceptorFunc + nextLogs otelconsumer.Logs + mutatesData bool // must be set to true if the provided opts modifies the data +} + +func Logs(nextLogs otelconsumer.Logs, mutatesData bool, f LogsInterceptorFunc) otelconsumer.Logs { + return &LogsInterceptor{ + nextLogs: nextLogs, + mutatesData: mutatesData, + onConsumeLogs: f, + } +} + +func (i *LogsInterceptor) Capabilities() otelconsumer.Capabilities { + return otelconsumer.Capabilities{MutatesData: i.mutatesData} +} + +func (i *LogsInterceptor) ConsumeLogs(ctx context.Context, ld plog.Logs) error { + + if i.onConsumeLogs != nil { + return i.onConsumeLogs(ctx, ld) + } + + return i.nextLogs.ConsumeLogs(ctx, ld) +} diff --git a/internal/component/otelcol/internal/interceptorconsumer/metrics.go b/internal/component/otelcol/internal/interceptorconsumer/metrics.go new file mode 100644 index 0000000000..921ba7a8e9 --- /dev/null +++ b/internal/component/otelcol/internal/interceptorconsumer/metrics.go @@ -0,0 +1,37 @@ +package interceptorconsumer + +import ( + "context" + + otelconsumer "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/pmetric" +) + +type MetricsInterceptorFunc func(context.Context, pmetric.Metrics) error + +type MetricsInterceptor struct { + onConsumeMetrics MetricsInterceptorFunc + nextMetrics otelconsumer.Metrics + mutatesData bool // must be set to true if the provided opts modifies the data +} + +func Metrics(nextMetrics otelconsumer.Metrics, mutatesData bool, f MetricsInterceptorFunc) otelconsumer.Metrics { + return &MetricsInterceptor{ + nextMetrics: nextMetrics, + mutatesData: mutatesData, + onConsumeMetrics: f, + } +} + +func (i *MetricsInterceptor) Capabilities() otelconsumer.Capabilities { + return otelconsumer.Capabilities{MutatesData: i.mutatesData} +} + +func (i *MetricsInterceptor) ConsumeMetrics(ctx context.Context, ld pmetric.Metrics) error { + + if i.onConsumeMetrics != nil { + return i.onConsumeMetrics(ctx, ld) + } + + return i.nextMetrics.ConsumeMetrics(ctx, ld) +} diff --git a/internal/component/otelcol/internal/interceptorconsumer/traces.go b/internal/component/otelcol/internal/interceptorconsumer/traces.go new file mode 100644 index 0000000000..30c72a5079 --- /dev/null +++ b/internal/component/otelcol/internal/interceptorconsumer/traces.go @@ -0,0 +1,37 @@ +package interceptorconsumer + +import ( + "context" + + otelconsumer "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +type TracesInterceptorFunc func(context.Context, ptrace.Traces) error + +type TracesInterceptor struct { + onConsumeTraces TracesInterceptorFunc + nextTraces otelconsumer.Traces + mutatesData bool // must be set to true if the provided opts modifies the data +} + +func Traces(nextTraces otelconsumer.Traces, mutatesData bool, f TracesInterceptorFunc) otelconsumer.Traces { + return &TracesInterceptor{ + nextTraces: nextTraces, + mutatesData: mutatesData, + onConsumeTraces: f, + } +} + +func (i *TracesInterceptor) Capabilities() otelconsumer.Capabilities { + return otelconsumer.Capabilities{MutatesData: i.mutatesData} +} + +func (i *TracesInterceptor) ConsumeTraces(ctx context.Context, ld ptrace.Traces) error { + + if i.onConsumeTraces != nil { + return i.onConsumeTraces(ctx, ld) + } + + return i.nextTraces.ConsumeTraces(ctx, ld) +} diff --git a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go index 732d818ee3..b0df5f7223 100644 --- a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go +++ b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go @@ -6,6 +6,7 @@ import ( "context" "sync" + "github.com/grafana/alloy/internal/component/otelcol" otelconsumer "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/pdata/plog" "go.opentelemetry.io/collector/pdata/pmetric" @@ -30,9 +31,10 @@ type Consumer struct { } var ( - _ otelconsumer.Traces = (*Consumer)(nil) - _ otelconsumer.Metrics = (*Consumer)(nil) - _ otelconsumer.Logs = (*Consumer)(nil) + _ otelconsumer.Traces = (*Consumer)(nil) + _ otelconsumer.Metrics = (*Consumer)(nil) + _ otelconsumer.Logs = (*Consumer)(nil) + _ otelcol.ConsumerWithComponentID = (*Consumer)(nil) ) // New creates a new Consumer. The provided ctx is used to determine when the diff --git a/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go b/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go deleted file mode 100644 index 9242991d9f..0000000000 --- a/internal/component/otelcol/internal/livedebuggingconsumer/livedebuggingconsumer.go +++ /dev/null @@ -1,126 +0,0 @@ -// Package livedebuggingconsumer implements an OpenTelemetry Collector consumer -// which can be used to send live debugging data to Alloy UI. -package livedebuggingconsumer - -import ( - "context" - "sync" - - "github.com/grafana/alloy/internal/component/otelcol" - "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" - "github.com/grafana/alloy/internal/service/livedebugging" - otelconsumer "go.opentelemetry.io/collector/consumer" - "go.opentelemetry.io/collector/pdata/plog" - "go.opentelemetry.io/collector/pdata/pmetric" - "go.opentelemetry.io/collector/pdata/ptrace" -) - -type Consumer struct { - debugDataPublisher livedebugging.DebugDataPublisher - componentID livedebugging.ComponentID - logsMarshaler plog.Marshaler - metricsMarshaler pmetric.Marshaler - tracesMarshaler ptrace.Marshaler - - mut sync.RWMutex - targetComponentIDsMetric []string - targetComponentIDsLog []string - targetComponentIDsTraces []string -} - -var _ otelcol.Consumer = (*Consumer)(nil) - -func New(debugDataPublisher livedebugging.DebugDataPublisher, componentID string) *Consumer { - return &Consumer{ - debugDataPublisher: debugDataPublisher, - componentID: livedebugging.ComponentID(componentID), - logsMarshaler: NewTextLogsMarshaler(), - metricsMarshaler: NewTextMetricsMarshaler(), - tracesMarshaler: NewTextTracesMarshaler(), - } -} - -// SetTargetConsumers stores the componentIDs of the next consumers -func (c *Consumer) SetTargetConsumers(metric, log, trace []otelcol.Consumer) { - c.mut.Lock() - defer c.mut.Unlock() - c.targetComponentIDsMetric = extractIds(metric) - c.targetComponentIDsLog = extractIds(log) - c.targetComponentIDsTraces = extractIds(trace) -} - -func extractIds(consumers []otelcol.Consumer) []string { - ids := make([]string, 0, len(consumers)) - for _, cons := range consumers { - if lazy, ok := cons.(*lazyconsumer.Consumer); ok { - ids = append(ids, lazy.ComponentID()) - } - } - return ids -} - -// Capabilities implements otelcol.Consumer. -func (c *Consumer) Capabilities() otelconsumer.Capabilities { - // streaming data should not modify the value - return otelconsumer.Capabilities{MutatesData: false} -} - -// ConsumeTraces implements otelcol.ConsumeTraces. -func (c *Consumer) ConsumeTraces(ctx context.Context, td ptrace.Traces) error { - c.mut.RLock() - defer c.mut.RUnlock() - c.debugDataPublisher.PublishIfActive(livedebugging.NewData( - c.componentID, - livedebugging.OtelTrace, - uint64(td.SpanCount()), - func() string { - data, err := c.tracesMarshaler.MarshalTraces(td) - if err != nil { - return "" - } - return string(data) - }, - livedebugging.WithTargetComponentIDs(c.targetComponentIDsTraces), - )) - return nil -} - -// ConsumeMetrics implements otelcol.ConsumeMetrics. -func (c *Consumer) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error { - c.mut.RLock() - defer c.mut.RUnlock() - c.debugDataPublisher.PublishIfActive(livedebugging.NewData( - c.componentID, - livedebugging.OtelMetric, - uint64(md.MetricCount()), - func() string { - data, err := c.metricsMarshaler.MarshalMetrics(md) - if err != nil { - return "" - } - return string(data) - }, - livedebugging.WithTargetComponentIDs(c.targetComponentIDsMetric), - )) - return nil -} - -// ConsumeLogs implements otelcol.ConsumeLogs. -func (c *Consumer) ConsumeLogs(ctx context.Context, ld plog.Logs) error { - c.mut.RLock() - defer c.mut.RUnlock() - c.debugDataPublisher.PublishIfActive(livedebugging.NewData( - c.componentID, - livedebugging.OtelLog, - uint64(ld.LogRecordCount()), - func() string { - data, err := c.logsMarshaler.MarshalLogs(ld) - if err != nil { - return "" - } - return string(data) - }, - livedebugging.WithTargetComponentIDs(c.targetComponentIDsLog), - )) - return nil -} diff --git a/internal/component/otelcol/internal/livedebuggingpublisher/livedebuggingpublisher.go b/internal/component/otelcol/internal/livedebuggingpublisher/livedebuggingpublisher.go new file mode 100644 index 0000000000..d078248c0b --- /dev/null +++ b/internal/component/otelcol/internal/livedebuggingpublisher/livedebuggingpublisher.go @@ -0,0 +1,68 @@ +package livedebuggingpublisher + +import ( + "github.com/grafana/alloy/internal/component/otelcol" + "github.com/grafana/alloy/internal/component/otelcol/internal/textmarshaler" + "github.com/grafana/alloy/internal/service/livedebugging" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +func extractIds(consumers []otelcol.Consumer) []string { + ids := make([]string, 0, len(consumers)) + for _, cons := range consumers { + if consWithID, ok := cons.(otelcol.ConsumerWithComponentID); ok { + ids = append(ids, consWithID.ComponentID()) + } + } + return ids +} + +func PublishLogsIfActive(debugDataPublisher livedebugging.DebugDataPublisher, componentID string, ld plog.Logs, nextLogs []otelcol.Consumer) { + debugDataPublisher.PublishIfActive(livedebugging.NewData( + livedebugging.ComponentID(componentID), + livedebugging.OtelLog, + uint64(ld.LogRecordCount()), + func() string { + data, err := textmarshaler.MarshalLogs(ld) + if err != nil { + return "" + } + return string(data) + }, + livedebugging.WithTargetComponentIDs(extractIds(nextLogs)), + )) +} + +func PublishTracesIfActive(debugDataPublisher livedebugging.DebugDataPublisher, componentID string, td ptrace.Traces, nextTraces []otelcol.Consumer) { + debugDataPublisher.PublishIfActive(livedebugging.NewData( + livedebugging.ComponentID(componentID), + livedebugging.OtelTrace, + uint64(td.SpanCount()), + func() string { + data, err := textmarshaler.MarshalTraces(td) + if err != nil { + return "" + } + return string(data) + }, + livedebugging.WithTargetComponentIDs(extractIds(nextTraces)), + )) +} + +func PublishMetricsIfActive(debugDataPublisher livedebugging.DebugDataPublisher, componentID string, md pmetric.Metrics, nextMetrics []otelcol.Consumer) { + debugDataPublisher.PublishIfActive(livedebugging.NewData( + livedebugging.ComponentID(componentID), + livedebugging.OtelMetric, + uint64(md.MetricCount()), + func() string { + data, err := textmarshaler.MarshalMetrics(md) + if err != nil { + return "" + } + return string(data) + }, + livedebugging.WithTargetComponentIDs(extractIds(nextMetrics)), + )) +} diff --git a/internal/component/otelcol/internal/livedebuggingconsumer/databuffer.go b/internal/component/otelcol/internal/textmarshaler/databuffer.go similarity index 99% rename from internal/component/otelcol/internal/livedebuggingconsumer/databuffer.go rename to internal/component/otelcol/internal/textmarshaler/databuffer.go index 7ba5d4918a..b347e98f38 100644 --- a/internal/component/otelcol/internal/livedebuggingconsumer/databuffer.go +++ b/internal/component/otelcol/internal/textmarshaler/databuffer.go @@ -3,7 +3,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package livedebuggingconsumer +package textmarshaler import ( "bytes" diff --git a/internal/component/otelcol/internal/livedebuggingconsumer/logs.go b/internal/component/otelcol/internal/textmarshaler/logs.go similarity index 79% rename from internal/component/otelcol/internal/livedebuggingconsumer/logs.go rename to internal/component/otelcol/internal/textmarshaler/logs.go index de3c326e2d..fab7f850e8 100644 --- a/internal/component/otelcol/internal/livedebuggingconsumer/logs.go +++ b/internal/component/otelcol/internal/textmarshaler/logs.go @@ -1,23 +1,16 @@ -// Copy from the OTLP text in the Opentelemetry collector +// Adapted copy from the OTLP text in the Opentelemetry collector // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package livedebuggingconsumer +package textmarshaler import ( "go.opentelemetry.io/collector/pdata/plog" ) -// NewTextLogsMarshaler returns a plog.Marshaler to encode to OTLP text bytes. -func NewTextLogsMarshaler() plog.Marshaler { - return textLogsMarshaler{} -} - -type textLogsMarshaler struct{} - // MarshalLogs plog.Logs to OTLP text. -func (textLogsMarshaler) MarshalLogs(ld plog.Logs) ([]byte, error) { +func MarshalLogs(ld plog.Logs) ([]byte, error) { buf := dataBuffer{} rls := ld.ResourceLogs() for i := 0; i < rls.Len(); i++ { diff --git a/internal/component/otelcol/internal/livedebuggingconsumer/metrics.go b/internal/component/otelcol/internal/textmarshaler/metrics.go similarity index 71% rename from internal/component/otelcol/internal/livedebuggingconsumer/metrics.go rename to internal/component/otelcol/internal/textmarshaler/metrics.go index 507f96e4b0..68b35d3ec6 100644 --- a/internal/component/otelcol/internal/livedebuggingconsumer/metrics.go +++ b/internal/component/otelcol/internal/textmarshaler/metrics.go @@ -1,21 +1,14 @@ -// Copy from the OTLP text in the Opentelemetry collector +// Adapted copy from the OTLP text in the Opentelemetry collector // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package livedebuggingconsumer +package textmarshaler import "go.opentelemetry.io/collector/pdata/pmetric" -// NewTextMetricsMarshaler returns a pmetric.Marshaler to encode to OTLP text bytes. -func NewTextMetricsMarshaler() pmetric.Marshaler { - return textMetricsMarshaler{} -} - -type textMetricsMarshaler struct{} - // MarshalMetrics pmetric.Metrics to OTLP text. -func (textMetricsMarshaler) MarshalMetrics(md pmetric.Metrics) ([]byte, error) { +func MarshalMetrics(md pmetric.Metrics) ([]byte, error) { buf := dataBuffer{} rms := md.ResourceMetrics() for i := 0; i < rms.Len(); i++ { diff --git a/internal/component/otelcol/internal/livedebuggingconsumer/traces.go b/internal/component/otelcol/internal/textmarshaler/traces.go similarity index 79% rename from internal/component/otelcol/internal/livedebuggingconsumer/traces.go rename to internal/component/otelcol/internal/textmarshaler/traces.go index 7ffedd0659..1c94372815 100644 --- a/internal/component/otelcol/internal/livedebuggingconsumer/traces.go +++ b/internal/component/otelcol/internal/textmarshaler/traces.go @@ -1,23 +1,16 @@ -// Copy from the OTLP text in the Opentelemetry collector +// Adapted copy from the OTLP text in the Opentelemetry collector // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package livedebuggingconsumer +package textmarshaler import ( "go.opentelemetry.io/collector/pdata/ptrace" ) -// NewTextTracesMarshaler returns a ptrace.Marshaler to encode to OTLP text bytes. -func NewTextTracesMarshaler() ptrace.Marshaler { - return textTracesMarshaler{} -} - -type textTracesMarshaler struct{} - // MarshalTraces ptrace.Traces to OTLP text. -func (textTracesMarshaler) MarshalTraces(td ptrace.Traces) ([]byte, error) { +func MarshalTraces(td ptrace.Traces) ([]byte, error) { buf := dataBuffer{} rss := td.ResourceSpans() for i := 0; i < rss.Len(); i++ { diff --git a/internal/component/otelcol/processor/discovery/discovery.go b/internal/component/otelcol/processor/discovery/discovery.go index 9031adce3f..64428a26ee 100644 --- a/internal/component/otelcol/processor/discovery/discovery.go +++ b/internal/component/otelcol/processor/discovery/discovery.go @@ -11,13 +11,15 @@ import ( "github.com/grafana/alloy/internal/component/discovery" "github.com/grafana/alloy/internal/component/otelcol" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/featuregate" "github.com/grafana/alloy/internal/runtime/logging/level" "github.com/grafana/alloy/internal/service/livedebugging" promsdconsumer "github.com/grafana/alloy/internal/static/traces/promsdprocessor/consumer" "github.com/grafana/alloy/syntax" + "go.opentelemetry.io/collector/pdata/ptrace" ) func init() { @@ -79,10 +81,9 @@ func (args *Arguments) Validate() error { // Component is the otelcol.exporter.discovery component. type Component struct { - consumer *promsdconsumer.Consumer - logger log.Logger - liveDebuggingConsumer *livedebuggingconsumer.Consumer - debugDataPublisher livedebugging.DebugDataPublisher + consumer *promsdconsumer.Consumer + logger log.Logger + debugDataPublisher livedebugging.DebugDataPublisher opts component.Options args Arguments @@ -106,19 +107,11 @@ func New(o component.Options, c Arguments) (*Component, error) { return nil, err } - liveDebuggingConsumer := livedebuggingconsumer.New(debugDataPublisher.(livedebugging.DebugDataPublisher), o.ID) - - traces := c.Output.Traces - if debugDataPublisher.(livedebugging.DebugDataPublisher).IsActive(livedebugging.ComponentID(o.ID)) { - traces = append(traces, liveDebuggingConsumer) - } - consumerOpts := promsdconsumer.Options{ // Don't bother setting up labels - this will be done by the Update() function. HostLabels: map[string]discovery.Target{}, OperationType: c.OperationType, PodAssociations: c.PodAssociations, - NextConsumer: fanoutconsumer.Traces(traces), } consumer, err := promsdconsumer.NewConsumer(consumerOpts, o.Logger) if err != nil { @@ -126,11 +119,10 @@ func New(o component.Options, c Arguments) (*Component, error) { } res := &Component{ - consumer: consumer, - logger: o.Logger, - opts: o, - debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), - liveDebuggingConsumer: liveDebuggingConsumer, + consumer: consumer, + logger: o.Logger, + opts: o, + debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), } if err := res.Update(c); err != nil { @@ -172,16 +164,20 @@ func (c *Component) Update(newConfig component.Arguments) error { hostLabels[host] = promsdconsumer.NewTargetsWithNonInternalLabels(labels) } - traces := c.args.Output.Traces - if c.debugDataPublisher.IsActive(livedebugging.ComponentID(c.opts.ID)) { - traces = append(traces, c.liveDebuggingConsumer) - } + nextTraces := c.args.Output.Traces + fanout := fanoutconsumer.Traces(nextTraces) + tracesInterceptor := interceptorconsumer.Traces(fanout, false, + func(ctx context.Context, td ptrace.Traces) error { + livedebuggingpublisher.PublishTracesIfActive(c.debugDataPublisher.(livedebugging.DebugDataPublisher), c.opts.ID, td, nextTraces) + return fanout.ConsumeTraces(ctx, td) + }, + ) err := c.consumer.UpdateOptions(promsdconsumer.Options{ HostLabels: hostLabels, OperationType: c.args.OperationType, PodAssociations: c.args.PodAssociations, - NextConsumer: fanoutconsumer.Traces(traces), + NextConsumer: tracesInterceptor, }) if err != nil { @@ -191,6 +187,4 @@ func (c *Component) Update(newConfig component.Arguments) error { return nil } -func (c *Component) LiveDebugging(_ int) { - c.Update(c.args) -} +func (c *Component) LiveDebugging() {} diff --git a/internal/component/otelcol/processor/processor.go b/internal/component/otelcol/processor/processor.go index 597a9b9db9..925e588b88 100644 --- a/internal/component/otelcol/processor/processor.go +++ b/internal/component/otelcol/processor/processor.go @@ -11,6 +11,9 @@ import ( "github.com/prometheus/client_golang/prometheus" otelcomponent "go.opentelemetry.io/collector/component" otelextension "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pipeline" otelprocessor "go.opentelemetry.io/collector/processor" sdkprometheus "go.opentelemetry.io/otel/exporters/prometheus" @@ -21,9 +24,10 @@ import ( "github.com/grafana/alloy/internal/component/otelcol" otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazycollector" "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/component/otelcol/internal/scheduler" "github.com/grafana/alloy/internal/service/livedebugging" "github.com/grafana/alloy/internal/util/zapadapter" @@ -66,8 +70,7 @@ type Processor struct { sched *scheduler.Scheduler collector *lazycollector.Collector - liveDebuggingConsumer *livedebuggingconsumer.Consumer - debugDataPublisher livedebugging.DebugDataPublisher + debugDataPublisher livedebugging.DebugDataPublisher args Arguments @@ -120,8 +123,7 @@ func New(opts component.Options, f otelprocessor.Factory, args Arguments) (*Proc sched: scheduler.NewWithPauseCallbacks(opts.Logger, consumer.Pause, consumer.Resume), collector: collector, - liveDebuggingConsumer: livedebuggingconsumer.New(debugDataPublisher.(livedebugging.DebugDataPublisher), opts.ID), - debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), + debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), } if err := p.Update(args); err != nil { return nil, err @@ -185,19 +187,6 @@ func (p *Processor) Update(args component.Arguments) error { } next := p.args.NextConsumers() - traces, metrics, logs := next.Traces, next.Metrics, next.Logs - - if p.debugDataPublisher.IsActive(livedebugging.ComponentID(p.opts.ID)) { - traces = append(traces, p.liveDebuggingConsumer) - metrics = append(metrics, p.liveDebuggingConsumer) - logs = append(logs, p.liveDebuggingConsumer) - } - - var ( - nextTraces = fanoutconsumer.Traces(traces) - nextMetrics = fanoutconsumer.Metrics(metrics) - nextLogs = fanoutconsumer.Logs(logs) - ) // Create instances of the processor from our factory for each of our // supported telemetry signals. @@ -205,7 +194,14 @@ func (p *Processor) Update(args component.Arguments) error { var tracesProcessor otelprocessor.Traces if len(next.Traces) > 0 { - tracesProcessor, err = p.factory.CreateTraces(p.ctx, settings, processorConfig, nextTraces) + fanout := fanoutconsumer.Traces(next.Traces) + tracesInterceptor := interceptorconsumer.Traces(fanout, false, + func(ctx context.Context, td ptrace.Traces) error { + livedebuggingpublisher.PublishTracesIfActive(p.debugDataPublisher, p.opts.ID, td, next.Traces) + return fanout.ConsumeTraces(ctx, td) + }, + ) + tracesProcessor, err = p.factory.CreateTraces(p.ctx, settings, processorConfig, tracesInterceptor) if err != nil && !errors.Is(err, pipeline.ErrSignalNotSupported) { return err } else if tracesProcessor != nil { @@ -215,7 +211,14 @@ func (p *Processor) Update(args component.Arguments) error { var metricsProcessor otelprocessor.Metrics if len(next.Metrics) > 0 { - metricsProcessor, err = p.factory.CreateMetrics(p.ctx, settings, processorConfig, nextMetrics) + fanout := fanoutconsumer.Metrics(next.Metrics) + metricsInterceptor := interceptorconsumer.Metrics(fanout, false, + func(ctx context.Context, md pmetric.Metrics) error { + livedebuggingpublisher.PublishMetricsIfActive(p.debugDataPublisher, p.opts.ID, md, next.Metrics) + return fanout.ConsumeMetrics(ctx, md) + }, + ) + metricsProcessor, err = p.factory.CreateMetrics(p.ctx, settings, processorConfig, metricsInterceptor) if err != nil && !errors.Is(err, pipeline.ErrSignalNotSupported) { return err } else if metricsProcessor != nil { @@ -225,7 +228,14 @@ func (p *Processor) Update(args component.Arguments) error { var logsProcessor otelprocessor.Logs if len(next.Logs) > 0 { - logsProcessor, err = p.factory.CreateLogs(p.ctx, settings, processorConfig, nextLogs) + fanout := fanoutconsumer.Logs(next.Logs) + logsInterceptor := interceptorconsumer.Logs(fanout, false, + func(ctx context.Context, ld plog.Logs) error { + livedebuggingpublisher.PublishLogsIfActive(p.debugDataPublisher, p.opts.ID, ld, next.Logs) + return fanout.ConsumeLogs(ctx, ld) + }, + ) + logsProcessor, err = p.factory.CreateLogs(p.ctx, settings, processorConfig, logsInterceptor) if err != nil && !errors.Is(err, pipeline.ErrSignalNotSupported) { return err } else if logsProcessor != nil { @@ -233,8 +243,6 @@ func (p *Processor) Update(args component.Arguments) error { } } - p.liveDebuggingConsumer.SetTargetConsumers(next.Metrics, next.Logs, next.Traces) - updateConsumersFunc := func() { p.consumer.SetConsumers(tracesProcessor, metricsProcessor, logsProcessor) } @@ -250,6 +258,4 @@ func (p *Processor) CurrentHealth() component.Health { return p.sched.CurrentHealth() } -func (p *Processor) LiveDebugging(_ int) { - p.Update(p.args) -} +func (p *Processor) LiveDebugging() {} diff --git a/internal/component/otelcol/receiver/loki/loki.go b/internal/component/otelcol/receiver/loki/loki.go index c75f462d4b..282f64d2be 100644 --- a/internal/component/otelcol/receiver/loki/loki.go +++ b/internal/component/otelcol/receiver/loki/loki.go @@ -12,7 +12,8 @@ import ( "github.com/grafana/alloy/internal/component/common/loki" "github.com/grafana/alloy/internal/component/otelcol" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/featuregate" "github.com/grafana/alloy/internal/runtime/logging/level" "github.com/grafana/alloy/internal/service/livedebugging" @@ -57,8 +58,7 @@ type Component struct { receiver loki.LogsReceiver logsSink consumer.Logs - liveDebuggingConsumer *livedebuggingconsumer.Consumer - debugDataPublisher livedebugging.DebugDataPublisher + debugDataPublisher livedebugging.DebugDataPublisher args Arguments } @@ -78,10 +78,9 @@ func New(o component.Options, c Arguments) (*Component, error) { // TODO(@tpaschalis) Create a metrics struct to count // total/successful/errored log entries? res := &Component{ - log: o.Logger, - opts: o, - liveDebuggingConsumer: livedebuggingconsumer.New(debugDataPublisher.(livedebugging.DebugDataPublisher), o.ID), - debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), + log: o.Logger, + opts: o, + debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), } // Create and immediately export the receiver which remains the same for @@ -120,11 +119,15 @@ func (c *Component) Update(newConfig component.Arguments) error { defer c.mut.Unlock() c.args = newConfig.(Arguments) - logs := c.args.Output.Logs - if c.debugDataPublisher.IsActive(livedebugging.ComponentID(c.opts.ID)) { - logs = append(logs, c.liveDebuggingConsumer) - } - c.logsSink = fanoutconsumer.Logs(logs) + nextLogs := c.args.Output.Logs + fanout := fanoutconsumer.Logs(nextLogs) + logsInterceptor := interceptorconsumer.Logs(fanout, false, + func(ctx context.Context, ld plog.Logs) error { + livedebuggingpublisher.PublishLogsIfActive(c.debugDataPublisher, c.opts.ID, ld, nextLogs) + return fanout.ConsumeLogs(ctx, ld) + }, + ) + c.logsSink = logsInterceptor return nil } @@ -171,6 +174,4 @@ func convertLokiEntryToPlog(lokiEntry loki.Entry) plog.Logs { return logs } -func (c *Component) LiveDebugging(_ int) { - c.Update(c.args) -} +func (c *Component) LiveDebugging() {} diff --git a/internal/component/otelcol/receiver/prometheus/prometheus.go b/internal/component/otelcol/receiver/prometheus/prometheus.go index 8058f1a99d..f09ec8cd19 100644 --- a/internal/component/otelcol/receiver/prometheus/prometheus.go +++ b/internal/component/otelcol/receiver/prometheus/prometheus.go @@ -14,7 +14,8 @@ import ( "github.com/grafana/alloy/internal/component/otelcol" otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/component/otelcol/receiver/prometheus/internal" "github.com/grafana/alloy/internal/featuregate" "github.com/grafana/alloy/internal/service/livedebugging" @@ -22,6 +23,7 @@ import ( "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/storage" otelcomponent "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pmetric" otelreceiver "go.opentelemetry.io/collector/receiver" metricNoop "go.opentelemetry.io/otel/metric/noop" traceNoop "go.opentelemetry.io/otel/trace/noop" @@ -68,8 +70,7 @@ type Component struct { cfg Arguments appendable storage.Appendable - liveDebuggingConsumer *livedebuggingconsumer.Consumer - debugDataPublisher livedebugging.DebugDataPublisher + debugDataPublisher livedebugging.DebugDataPublisher } var ( @@ -85,10 +86,9 @@ func New(o component.Options, c Arguments) (*Component, error) { } res := &Component{ - log: o.Logger, - opts: o, - liveDebuggingConsumer: livedebuggingconsumer.New(debugDataPublisher.(livedebugging.DebugDataPublisher), o.ID), - debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), + log: o.Logger, + opts: o, + debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), } if err := res.Update(c); err != nil { @@ -160,11 +160,15 @@ func (c *Component) Update(newConfig component.Arguments) error { Version: build.Version, }, } - metrics := cfg.Output.Metrics - if c.debugDataPublisher.IsActive(livedebugging.ComponentID(c.opts.ID)) { - metrics = append(metrics, c.liveDebuggingConsumer) - } - metricsSink := fanoutconsumer.Metrics(metrics) + nextMetrics := cfg.Output.Metrics + fanout := fanoutconsumer.Metrics(nextMetrics) + metricsInterceptor := interceptorconsumer.Metrics(fanout, false, + func(ctx context.Context, md pmetric.Metrics) error { + livedebuggingpublisher.PublishMetricsIfActive(c.debugDataPublisher, c.opts.ID, md, nextMetrics) + return fanout.ConsumeMetrics(ctx, md) + }, + ) + metricsSink := metricsInterceptor appendable, err := internal.NewAppendable( metricsSink, @@ -188,6 +192,4 @@ func (c *Component) Update(newConfig component.Arguments) error { return nil } -func (c *Component) LiveDebugging(_ int) { - c.Update(c.cfg) -} +func (c *Component) LiveDebugging() {} diff --git a/internal/component/otelcol/receiver/receiver.go b/internal/component/otelcol/receiver/receiver.go index 0abb2d049f..d5c0f4ff4c 100644 --- a/internal/component/otelcol/receiver/receiver.go +++ b/internal/component/otelcol/receiver/receiver.go @@ -6,15 +6,15 @@ import ( "context" "errors" "os" - "sync" "github.com/grafana/alloy/internal/build" "github.com/grafana/alloy/internal/component" "github.com/grafana/alloy/internal/component/otelcol" otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazycollector" - "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/component/otelcol/internal/scheduler" "github.com/grafana/alloy/internal/component/otelcol/internal/views" "github.com/grafana/alloy/internal/service/livedebugging" @@ -22,6 +22,9 @@ import ( "github.com/prometheus/client_golang/prometheus" otelcomponent "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pipeline" otelreceiver "go.opentelemetry.io/collector/receiver" sdkprometheus "go.opentelemetry.io/otel/exporters/prometheus" @@ -64,14 +67,9 @@ type Receiver struct { sched *scheduler.Scheduler collector *lazycollector.Collector - liveDebuggingConsumer *livedebuggingconsumer.Consumer - debugDataPublisher livedebugging.DebugDataPublisher + debugDataPublisher livedebugging.DebugDataPublisher args Arguments - - // The mutex is needed because the live debugging service can trigger an update - // concurrently to add/remove the live debugging consumer. - updateMut sync.Mutex } var ( @@ -110,8 +108,7 @@ func New(opts component.Options, f otelreceiver.Factory, args Arguments) (*Recei sched: scheduler.New(opts.Logger), collector: collector, - liveDebuggingConsumer: livedebuggingconsumer.New(debugDataPublisher.(livedebugging.DebugDataPublisher), opts.ID), - debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), + debugDataPublisher: debugDataPublisher.(livedebugging.DebugDataPublisher), } if err := r.Update(args); err != nil { return nil, err @@ -129,16 +126,7 @@ func (r *Receiver) Run(ctx context.Context) error { // configuration for OpenTelemetry Collector receiver configuration and manage // the underlying OpenTelemetry Collector receiver. func (r *Receiver) Update(args component.Arguments) error { - r.updateMut.Lock() r.args = args.(Arguments) - r.updateMut.Unlock() - return r.update() -} - -func (r *Receiver) update() error { - r.updateMut.Lock() - defer r.updateMut.Unlock() - host := scheduler.NewHost( r.opts.Logger, scheduler.WithHostExtensions(r.args.Extensions()), @@ -192,15 +180,15 @@ func (r *Receiver) update() error { // supported telemetry signals. var components []otelcomponent.Component - liveDebuggingActive := r.debugDataPublisher.IsActive(livedebugging.ComponentID(r.opts.ID)) - if len(next.Traces) > 0 { - traces := next.Traces - if liveDebuggingActive { - traces = append(traces, r.liveDebuggingConsumer) - } - nextTraces := fanoutconsumer.Traces(traces) - tracesReceiver, err := r.factory.CreateTraces(r.ctx, settings, receiverConfig, nextTraces) + fanout := fanoutconsumer.Traces(next.Traces) + tracesInterceptor := interceptorconsumer.Traces(fanout, false, + func(ctx context.Context, td ptrace.Traces) error { + livedebuggingpublisher.PublishTracesIfActive(r.debugDataPublisher, r.opts.ID, td, next.Traces) + return fanout.ConsumeTraces(ctx, td) + }, + ) + tracesReceiver, err := r.factory.CreateTraces(r.ctx, settings, receiverConfig, tracesInterceptor) if err != nil && !errors.Is(err, pipeline.ErrSignalNotSupported) { return err } else if tracesReceiver != nil { @@ -209,12 +197,14 @@ func (r *Receiver) update() error { } if len(next.Metrics) > 0 { - metrics := next.Metrics - if liveDebuggingActive { - metrics = append(metrics, r.liveDebuggingConsumer) - } - nextMetrics := fanoutconsumer.Metrics(metrics) - metricsReceiver, err := r.factory.CreateMetrics(r.ctx, settings, receiverConfig, nextMetrics) + fanout := fanoutconsumer.Metrics(next.Metrics) + metricsInterceptor := interceptorconsumer.Metrics(fanout, false, + func(ctx context.Context, md pmetric.Metrics) error { + livedebuggingpublisher.PublishMetricsIfActive(r.debugDataPublisher, r.opts.ID, md, next.Metrics) + return fanout.ConsumeMetrics(ctx, md) + }, + ) + metricsReceiver, err := r.factory.CreateMetrics(r.ctx, settings, receiverConfig, metricsInterceptor) if err != nil && !errors.Is(err, pipeline.ErrSignalNotSupported) { return err } else if metricsReceiver != nil { @@ -223,12 +213,14 @@ func (r *Receiver) update() error { } if len(next.Logs) > 0 { - logs := next.Logs - if liveDebuggingActive { - logs = append(logs, r.liveDebuggingConsumer) - } - nextLogs := fanoutconsumer.Logs(logs) - logsReceiver, err := r.factory.CreateLogs(r.ctx, settings, receiverConfig, nextLogs) + fanout := fanoutconsumer.Logs(next.Logs) + logsInterceptor := interceptorconsumer.Logs(fanout, false, + func(ctx context.Context, ld plog.Logs) error { + livedebuggingpublisher.PublishLogsIfActive(r.debugDataPublisher, r.opts.ID, ld, next.Logs) + return fanout.ConsumeLogs(ctx, ld) + }, + ) + logsReceiver, err := r.factory.CreateLogs(r.ctx, settings, receiverConfig, logsInterceptor) if err != nil && !errors.Is(err, pipeline.ErrSignalNotSupported) { return err } else if logsReceiver != nil { @@ -236,8 +228,6 @@ func (r *Receiver) update() error { } } - r.liveDebuggingConsumer.SetTargetConsumers(next.Metrics, next.Logs, next.Traces) - // Schedule the components to run once our component is running. r.sched.Schedule(r.ctx, func() {}, host, components...) return nil @@ -248,6 +238,4 @@ func (r *Receiver) CurrentHealth() component.Health { return r.sched.CurrentHealth() } -func (p *Receiver) LiveDebugging(_ int) { - p.update() -} +func (p *Receiver) LiveDebugging() {} diff --git a/internal/component/otelcol/receiver/receiver_test.go b/internal/component/otelcol/receiver/receiver_test.go index 19ce33c53a..29d263c9a4 100644 --- a/internal/component/otelcol/receiver/receiver_test.go +++ b/internal/component/otelcol/receiver/receiver_test.go @@ -127,31 +127,6 @@ func TestReceiverUpdate(t *testing.T) { require.ErrorContains(t, waitConsumerTrigger.Wait(time.Second), "context deadline exceeded") } -// This test will trigger a race error if the update function is not properly protected with a mutex -// because the live debugging service can call the update function concurrently. -func TestReceiverUpdateLiveDebugging(t *testing.T) { - te := newTestEnvironment(t, func(t otelconsumer.Traces) {}) - te.Start(fakeReceiverArgs{ - Output: &otelcol.ConsumerArguments{}, - }) - - require.NoError(t, te.Controller.WaitRunning(50*time.Millisecond)) - - go func() { - for i := 0; i < 100; i++ { - te.Controller.Update(fakeReceiverArgs{ - Output: &otelcol.ConsumerArguments{}, - }) - } - }() - - for i := 0; i < 100; i++ { - cp, err := te.Controller.GetComponent() - require.NoError(t, err) - cp.(*receiver.Receiver).LiveDebugging(1) - } -} - type testEnvironment struct { t *testing.T diff --git a/internal/component/prometheus/relabel/relabel.go b/internal/component/prometheus/relabel/relabel.go index 65c7fc0dc1..e3d7412d97 100644 --- a/internal/component/prometheus/relabel/relabel.go +++ b/internal/component/prometheus/relabel/relabel.go @@ -276,20 +276,18 @@ func (c *Component) relabel(val float64, lbls labels.Labels) labels.Labels { c.cacheSize.Set(float64(c.cache.Len())) componentID := livedebugging.ComponentID(c.opts.ID) - if c.debugDataPublisher.IsActive(componentID) { - count := uint64(1) - if relabelled.Len() == 0 { - count = 0 // if no labels are left, the count is not incremented because the metric will be filtered out - } - c.debugDataPublisher.PublishIfActive(livedebugging.NewData( - componentID, - livedebugging.PrometheusMetric, - count, - func() string { - return fmt.Sprintf("%s => %s", lbls.String(), relabelled.String()) - }, - )) + count := uint64(1) + if relabelled.Len() == 0 { + count = 0 // if no labels are left, the count is not incremented because the metric will be filtered out } + c.debugDataPublisher.PublishIfActive(livedebugging.NewData( + componentID, + livedebugging.PrometheusMetric, + count, + func() string { + return fmt.Sprintf("%s => %s", lbls.String(), relabelled.String()) + }, + )) return relabelled } @@ -331,7 +329,7 @@ func (c *Component) addToCache(originalID uint64, lbls labels.Labels, keep bool) }) } -func (c *Component) LiveDebugging(_ int) {} +func (c *Component) LiveDebugging() {} // labelAndID stores both the globalrefid for the label and the id itself. We store the id so that it doesn't have // to be recalculated again. diff --git a/internal/component/prometheus/remotewrite/remote_write.go b/internal/component/prometheus/remotewrite/remote_write.go index 57c923f535..dd78d9eae0 100644 --- a/internal/component/prometheus/remotewrite/remote_write.go +++ b/internal/component/prometheus/remotewrite/remote_write.go @@ -327,4 +327,4 @@ func (c *Component) Update(newConfig component.Arguments) error { return nil } -func (c *Component) LiveDebugging(_ int) {} +func (c *Component) LiveDebugging() {} diff --git a/internal/service/livedebugging/livedebugging.go b/internal/service/livedebugging/livedebugging.go index 1e25e3555e..a346a34fa9 100644 --- a/internal/service/livedebugging/livedebugging.go +++ b/internal/service/livedebugging/livedebugging.go @@ -30,8 +30,6 @@ type CallbackManager interface { type DebugDataPublisher interface { // Publish sends debugging data for a given componentID if a least one consumer is listening for debugging data for the given componentID. PublishIfActive(data *Data) - // IsActive returns true when at least one consumer is listening for debugging data for the given componentID. - IsActive(componentID ComponentID) bool } type liveDebugging struct { loadMut sync.RWMutex @@ -67,59 +65,7 @@ func (s *liveDebugging) PublishIfActive(data *Data) { } } -func (s *liveDebugging) IsActive(componentID ComponentID) bool { - s.loadMut.RLock() - defer s.loadMut.RUnlock() - callbacks, exist := s.callbacks[componentID] - return exist && len(callbacks) > 0 -} - func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { - // Split in two functions because this one needs write lock while notifyComponent needs read lock only. - // Using write lock on notifyComponent is not possible because the notified component might call IsActive or - // PublishIfActive which would create a deadlock. - err := s.addCallback(callbackID, componentID, callback) - if err != nil { - return err - } - s.notifyComponent(componentID) - return nil -} - -func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { - // Split in two functions because this one needs write lock while notifyComponents needs read lock only. - // Using write lock on notifyComponents is not possible because notified components might call IsActive or - // PublishIfActive which would create a deadlock. - err := s.addCallbackMulti(callbackID, moduleID, callback) - if err != nil { - return err - } - s.notifyComponents(moduleID) - return nil -} - -func (s *liveDebugging) DeleteCallback(callbackID CallbackID, componentID ComponentID) { - defer s.notifyComponent(componentID) - s.loadMut.Lock() - defer s.loadMut.Unlock() - delete(s.callbacks[componentID], callbackID) -} - -func (s *liveDebugging) DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) { - defer s.notifyComponents(moduleID) - s.loadMut.Lock() - defer s.loadMut.Unlock() - // ignore errors on delete - components, _ := s.host.ListComponents(string(moduleID), component.InfoOptions{}) - for _, cp := range components { - delete(s.callbacks[ComponentID(cp.ID.String())], callbackID) - } - // The s.callbacks[componentID] is not deleted. This is a very small memory leak which could only become significant if a user - // has a lot of components and reload the config with always different component labels while having the graph open. - // If this ever become a realistic scenario we should cleanup the map here. -} - -func (s *liveDebugging) addCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -148,7 +94,7 @@ func (s *liveDebugging) addCallback(callbackID CallbackID, componentID Component return nil } -func (s *liveDebugging) addCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { +func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -178,34 +124,23 @@ func (s *liveDebugging) addCallbackMulti(callbackID CallbackID, moduleID ModuleI return nil } -func (s *liveDebugging) notifyComponent(componentID ComponentID) { - s.loadMut.RLock() - defer s.loadMut.RUnlock() - - info, err := s.host.GetComponent(component.ParseID(string(componentID)), component.InfoOptions{}) - if err != nil { - return - } - if component, ok := info.Component.(component.LiveDebugging); ok { - // notify the component of the change - component.LiveDebugging(len(s.callbacks[componentID])) - } +func (s *liveDebugging) DeleteCallback(callbackID CallbackID, componentID ComponentID) { + s.loadMut.Lock() + defer s.loadMut.Unlock() + delete(s.callbacks[componentID], callbackID) } -func (s *liveDebugging) notifyComponents(moduleID ModuleID) { - s.loadMut.RLock() - defer s.loadMut.RUnlock() - - components, err := s.host.ListComponents(string(moduleID), component.InfoOptions{}) - if err != nil { - return - } +func (s *liveDebugging) DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) { + s.loadMut.Lock() + defer s.loadMut.Unlock() + // ignore errors on delete + components, _ := s.host.ListComponents(string(moduleID), component.InfoOptions{}) for _, cp := range components { - if c, ok := cp.Component.(component.LiveDebugging); ok { - // notify the component of the change - c.LiveDebugging(len(s.callbacks[ComponentID(cp.ID.String())])) - } + delete(s.callbacks[ComponentID(cp.ID.String())], callbackID) } + // The s.callbacks[componentID] is not deleted. This is a very small memory leak which could only become significant if a user + // has a lot of components and reload the config with always different component labels while having the graph open. + // If this ever become a realistic scenario we should cleanup the map here. } func (s *liveDebugging) SetServiceHost(h service.Host) { diff --git a/internal/service/livedebugging/livedebugging_test.go b/internal/service/livedebugging/livedebugging_test.go index b25657d66d..082b9b74c1 100644 --- a/internal/service/livedebugging/livedebugging_test.go +++ b/internal/service/livedebugging/livedebugging_test.go @@ -28,9 +28,6 @@ func TestAddCallback(t *testing.T) { require.NoError(t, livedebugging.AddCallback(callbackID, "fake.liveDebugging", callback)) - component, _ := livedebugging.host.GetComponent(component.ParseID("fake.liveDebugging"), component.InfoOptions{}) - require.Equal(t, 1, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) - err = livedebugging.AddCallback(callbackID, "fake.noLiveDebugging", callback) require.ErrorContains(t, err, "the component \"fake.noLiveDebugging\" does not support live debugging") @@ -50,7 +47,6 @@ func TestStream(t *testing.T) { callback := func(data *Data) { receivedData = data } - require.False(t, livedebugging.IsActive(componentID)) livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) require.Nil(t, receivedData) // nil because there are no active callbacks for it @@ -111,13 +107,8 @@ func TestDeleteCallback(t *testing.T) { callback1 := func(data *Data) {} callback2 := func(data *Data) {} - component, _ := livedebugging.host.GetComponent(component.ParseID("fake.liveDebugging"), component.InfoOptions{}) - require.NoError(t, livedebugging.AddCallback(callbackID1, componentID, callback1)) - require.Equal(t, 1, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) require.NoError(t, livedebugging.AddCallback(callbackID2, componentID, callback2)) - require.Equal(t, 2, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) - require.Len(t, livedebugging.callbacks[componentID], 2) // Deleting callbacks that don't exist should not panic require.NotPanics(t, func() { livedebugging.DeleteCallback(callbackID1, "fakeComponentID") }) @@ -125,11 +116,9 @@ func TestDeleteCallback(t *testing.T) { livedebugging.DeleteCallback(callbackID1, componentID) require.Len(t, livedebugging.callbacks[componentID], 1) - require.Equal(t, 1, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) livedebugging.DeleteCallback(callbackID2, componentID) require.Empty(t, livedebugging.callbacks[componentID]) - require.Equal(t, 0, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) } func setupServiceHost(liveDebugging *liveDebugging) { @@ -165,9 +154,6 @@ func TestAddCallbackMulti(t *testing.T) { require.NoError(t, livedebugging.AddCallbackMulti(callbackID, "", callback)) - component, _ := livedebugging.host.GetComponent(component.ParseID("fake.liveDebugging"), component.InfoOptions{}) - require.Equal(t, 1, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) - require.NoError(t, livedebugging.AddCallbackMulti(callbackID, "declared.cmp", callback)) } @@ -181,12 +167,8 @@ func TestDeleteCallbackMulti(t *testing.T) { callback1 := func(data *Data) {} callback2 := func(data *Data) {} - component, _ := livedebugging.host.GetComponent(component.ParseID("fake.liveDebugging"), component.InfoOptions{}) - require.NoError(t, livedebugging.AddCallbackMulti(callbackID1, "", callback1)) - require.Equal(t, 1, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) require.NoError(t, livedebugging.AddCallbackMulti(callbackID2, "", callback2)) - require.Equal(t, 2, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) require.Len(t, livedebugging.callbacks[componentID], 2) // Deleting callbacks that don't exist should not panic @@ -195,11 +177,9 @@ func TestDeleteCallbackMulti(t *testing.T) { livedebugging.DeleteCallbackMulti(callbackID1, "") require.Len(t, livedebugging.callbacks[componentID], 1) - require.Equal(t, 1, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) livedebugging.DeleteCallbackMulti(callbackID2, "") require.Empty(t, livedebugging.callbacks[componentID]) - require.Equal(t, 0, component.Component.(*testlivedebugging.FakeComponentLiveDebugging).ConsumersCount) } func TestMultiCallbacksMultipleStreams(t *testing.T) { diff --git a/internal/util/testlivedebugging/testlivedebugging.go b/internal/util/testlivedebugging/testlivedebugging.go index a866cf2b00..3b0258d352 100644 --- a/internal/util/testlivedebugging/testlivedebugging.go +++ b/internal/util/testlivedebugging/testlivedebugging.go @@ -49,13 +49,9 @@ func (h *FakeServiceHost) getComponentsInModule(module string) []*component.Info return detail } -type FakeComponentLiveDebugging struct { - ConsumersCount int -} +type FakeComponentLiveDebugging struct{} -func (f *FakeComponentLiveDebugging) LiveDebugging(consumers int) { - f.ConsumersCount = consumers -} +func (f *FakeComponentLiveDebugging) LiveDebugging() {} func (f *FakeComponentLiveDebugging) Run(ctx context.Context) error { <-ctx.Done() From c36ebefea7fddc6ae5351db91a61fad902d2a3d5 Mon Sep 17 00:00:00 2001 From: William Dumont Date: Fri, 14 Feb 2025 14:54:36 +0100 Subject: [PATCH 09/11] add comment about window unit --- internal/web/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/web/api/api.go b/internal/web/api/api.go index a6c26c4501..3868905c67 100644 --- a/internal/web/api/api.go +++ b/internal/web/api/api.go @@ -184,7 +184,7 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager, logger moduleID = livedebugging.ModuleID(vars["moduleID"]) } - window := setWindow(w, r.URL.Query().Get("window")) + window := setWindow(w, r.URL.Query().Get("window")) // in seconds dataCh := make(chan *livedebugging.Data, 1000) dataMap := make(map[dataKey]liveDebuggingData) From ea5a5e2c1b5b10dfce9536b0b7fbc7007f0841de Mon Sep 17 00:00:00 2001 From: William Dumont Date: Thu, 20 Feb 2025 13:54:19 +0100 Subject: [PATCH 10/11] review feedback --- .../component/loki/process/process_test.go | 2 +- .../component/otelcol/connector/connector.go | 4 +-- .../otelcol/connector/spanlogs/spanlogs.go | 4 +-- internal/component/otelcol/consumer.go | 4 +-- .../logs.go | 17 ++++++--- .../metrics.go | 17 ++++++--- .../traces.go | 17 ++++++--- .../internal/lazyconsumer/lazyconsumer.go | 8 ++--- .../livedebuggingpublisher.go | 20 +++++------ .../otelcol/processor/discovery/discovery.go | 4 +-- .../component/otelcol/processor/processor.go | 8 ++--- .../component/otelcol/receiver/loki/loki.go | 4 +-- .../otelcol/receiver/prometheus/prometheus.go | 4 +-- .../component/otelcol/receiver/receiver.go | 8 ++--- internal/service/livedebugging/data.go | 13 +++---- .../service/livedebugging/livedebugging.go | 20 +++++------ .../livedebugging/livedebugging_test.go | 32 ++++++++--------- internal/web/api/api.go | 35 ++++++++++--------- 18 files changed, 126 insertions(+), 95 deletions(-) rename internal/component/otelcol/internal/{interceptorconsumer => interceptconsumer}/logs.go (62%) rename internal/component/otelcol/internal/{interceptorconsumer => interceptconsumer}/metrics.go (62%) rename internal/component/otelcol/internal/{interceptorconsumer => interceptconsumer}/traces.go (62%) diff --git a/internal/component/loki/process/process_test.go b/internal/component/loki/process/process_test.go index e958af6700..712b01d068 100644 --- a/internal/component/loki/process/process_test.go +++ b/internal/component/loki/process/process_test.go @@ -651,7 +651,7 @@ func getServiceDataWithLiveDebugging(log *testlivedebugging.Log) func(string) (i ld.AddCallback( "callback1", "", - func(data *livedebugging.Data) { log.Append(data.DataFunc()) }, + func(data livedebugging.Data) { log.Append(data.DataFunc()) }, ) return func(name string) (interface{}, error) { diff --git a/internal/component/otelcol/connector/connector.go b/internal/component/otelcol/connector/connector.go index 0cf115a2c9..4ca848bedf 100644 --- a/internal/component/otelcol/connector/connector.go +++ b/internal/component/otelcol/connector/connector.go @@ -22,7 +22,7 @@ import ( "github.com/grafana/alloy/internal/component/otelcol" otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazycollector" "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" @@ -212,7 +212,7 @@ func (p *Connector) Update(args component.Arguments) error { if len(next.Metrics) > 0 { fanout := fanoutconsumer.Metrics(next.Metrics) - metricsInterceptor := interceptorconsumer.Metrics(fanout, false, + metricsInterceptor := interceptconsumer.Metrics(fanout, func(ctx context.Context, md pmetric.Metrics) error { livedebuggingpublisher.PublishMetricsIfActive(p.debugDataPublisher, p.opts.ID, md, next.Metrics) return fanout.ConsumeMetrics(ctx, md) diff --git a/internal/component/otelcol/connector/spanlogs/spanlogs.go b/internal/component/otelcol/connector/spanlogs/spanlogs.go index 6376dbb9e2..bddd2d89c6 100644 --- a/internal/component/otelcol/connector/spanlogs/spanlogs.go +++ b/internal/component/otelcol/connector/spanlogs/spanlogs.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/alloy/internal/component" "github.com/grafana/alloy/internal/component/otelcol" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/featuregate" @@ -150,7 +150,7 @@ func (c *Component) Update(newConfig component.Arguments) error { nextLogs := c.args.Output.Logs fanout := fanoutconsumer.Logs(nextLogs) - logsInterceptor := interceptorconsumer.Logs(fanout, false, + logsInterceptor := interceptconsumer.Logs(fanout, func(ctx context.Context, ld plog.Logs) error { livedebuggingpublisher.PublishLogsIfActive(c.debugDataPublisher, c.opts.ID, ld, nextLogs) return fanout.ConsumeLogs(ctx, ld) diff --git a/internal/component/otelcol/consumer.go b/internal/component/otelcol/consumer.go index cfc4dcfa3d..ce1a17a29d 100644 --- a/internal/component/otelcol/consumer.go +++ b/internal/component/otelcol/consumer.go @@ -12,8 +12,8 @@ type Consumer interface { otelconsumer.Logs } -type ConsumerWithComponentID interface { - Consumer +// ComponentMetadata can be implemented by, for example, consumers exported by components, to provide the ID of the component which is exporting given consumer. This is used for Live Graph / Live Debugging features. +type ComponentMetadata interface { ComponentID() string } diff --git a/internal/component/otelcol/internal/interceptorconsumer/logs.go b/internal/component/otelcol/internal/interceptconsumer/logs.go similarity index 62% rename from internal/component/otelcol/internal/interceptorconsumer/logs.go rename to internal/component/otelcol/internal/interceptconsumer/logs.go index 34905b6048..617c9a7d41 100644 --- a/internal/component/otelcol/internal/interceptorconsumer/logs.go +++ b/internal/component/otelcol/internal/interceptconsumer/logs.go @@ -1,4 +1,4 @@ -package interceptorconsumer +package interceptconsumer import ( "context" @@ -12,13 +12,22 @@ type LogsInterceptorFunc func(context.Context, plog.Logs) error type LogsInterceptor struct { onConsumeLogs LogsInterceptorFunc nextLogs otelconsumer.Logs - mutatesData bool // must be set to true if the provided opts modifies the data + mutatesData bool } -func Logs(nextLogs otelconsumer.Logs, mutatesData bool, f LogsInterceptorFunc) otelconsumer.Logs { +// Use LogsMutating if the interceptor func is modifying the data +func Logs(nextLogs otelconsumer.Logs, f LogsInterceptorFunc) otelconsumer.Logs { return &LogsInterceptor{ nextLogs: nextLogs, - mutatesData: mutatesData, + mutatesData: false, + onConsumeLogs: f, + } +} + +func LogsMutating(nextLogs otelconsumer.Logs, f LogsInterceptorFunc) otelconsumer.Logs { + return &LogsInterceptor{ + nextLogs: nextLogs, + mutatesData: true, onConsumeLogs: f, } } diff --git a/internal/component/otelcol/internal/interceptorconsumer/metrics.go b/internal/component/otelcol/internal/interceptconsumer/metrics.go similarity index 62% rename from internal/component/otelcol/internal/interceptorconsumer/metrics.go rename to internal/component/otelcol/internal/interceptconsumer/metrics.go index 921ba7a8e9..51f9393972 100644 --- a/internal/component/otelcol/internal/interceptorconsumer/metrics.go +++ b/internal/component/otelcol/internal/interceptconsumer/metrics.go @@ -1,4 +1,4 @@ -package interceptorconsumer +package interceptconsumer import ( "context" @@ -12,13 +12,22 @@ type MetricsInterceptorFunc func(context.Context, pmetric.Metrics) error type MetricsInterceptor struct { onConsumeMetrics MetricsInterceptorFunc nextMetrics otelconsumer.Metrics - mutatesData bool // must be set to true if the provided opts modifies the data + mutatesData bool } -func Metrics(nextMetrics otelconsumer.Metrics, mutatesData bool, f MetricsInterceptorFunc) otelconsumer.Metrics { +// Use LogsMutating if the interceptor func is modifying the data +func Metrics(nextMetrics otelconsumer.Metrics, f MetricsInterceptorFunc) otelconsumer.Metrics { return &MetricsInterceptor{ nextMetrics: nextMetrics, - mutatesData: mutatesData, + mutatesData: false, + onConsumeMetrics: f, + } +} + +func MetricsMutating(nextMetrics otelconsumer.Metrics, f MetricsInterceptorFunc) otelconsumer.Metrics { + return &MetricsInterceptor{ + nextMetrics: nextMetrics, + mutatesData: true, onConsumeMetrics: f, } } diff --git a/internal/component/otelcol/internal/interceptorconsumer/traces.go b/internal/component/otelcol/internal/interceptconsumer/traces.go similarity index 62% rename from internal/component/otelcol/internal/interceptorconsumer/traces.go rename to internal/component/otelcol/internal/interceptconsumer/traces.go index 30c72a5079..60fdd48224 100644 --- a/internal/component/otelcol/internal/interceptorconsumer/traces.go +++ b/internal/component/otelcol/internal/interceptconsumer/traces.go @@ -1,4 +1,4 @@ -package interceptorconsumer +package interceptconsumer import ( "context" @@ -12,13 +12,22 @@ type TracesInterceptorFunc func(context.Context, ptrace.Traces) error type TracesInterceptor struct { onConsumeTraces TracesInterceptorFunc nextTraces otelconsumer.Traces - mutatesData bool // must be set to true if the provided opts modifies the data + mutatesData bool } -func Traces(nextTraces otelconsumer.Traces, mutatesData bool, f TracesInterceptorFunc) otelconsumer.Traces { +// Use LogsMutating if the interceptor func is modifying the data +func Traces(nextTraces otelconsumer.Traces, f TracesInterceptorFunc) otelconsumer.Traces { return &TracesInterceptor{ nextTraces: nextTraces, - mutatesData: mutatesData, + mutatesData: false, + onConsumeTraces: f, + } +} + +func TracesMutating(nextTraces otelconsumer.Traces, f TracesInterceptorFunc) otelconsumer.Traces { + return &TracesInterceptor{ + nextTraces: nextTraces, + mutatesData: true, onConsumeTraces: f, } } diff --git a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go index b0df5f7223..08a42d6e41 100644 --- a/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go +++ b/internal/component/otelcol/internal/lazyconsumer/lazyconsumer.go @@ -31,10 +31,10 @@ type Consumer struct { } var ( - _ otelconsumer.Traces = (*Consumer)(nil) - _ otelconsumer.Metrics = (*Consumer)(nil) - _ otelconsumer.Logs = (*Consumer)(nil) - _ otelcol.ConsumerWithComponentID = (*Consumer)(nil) + _ otelconsumer.Traces = (*Consumer)(nil) + _ otelconsumer.Metrics = (*Consumer)(nil) + _ otelconsumer.Logs = (*Consumer)(nil) + _ otelcol.ComponentMetadata = (*Consumer)(nil) ) // New creates a new Consumer. The provided ctx is used to determine when the diff --git a/internal/component/otelcol/internal/livedebuggingpublisher/livedebuggingpublisher.go b/internal/component/otelcol/internal/livedebuggingpublisher/livedebuggingpublisher.go index d078248c0b..3bbd6d6930 100644 --- a/internal/component/otelcol/internal/livedebuggingpublisher/livedebuggingpublisher.go +++ b/internal/component/otelcol/internal/livedebuggingpublisher/livedebuggingpublisher.go @@ -9,16 +9,6 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" ) -func extractIds(consumers []otelcol.Consumer) []string { - ids := make([]string, 0, len(consumers)) - for _, cons := range consumers { - if consWithID, ok := cons.(otelcol.ConsumerWithComponentID); ok { - ids = append(ids, consWithID.ComponentID()) - } - } - return ids -} - func PublishLogsIfActive(debugDataPublisher livedebugging.DebugDataPublisher, componentID string, ld plog.Logs, nextLogs []otelcol.Consumer) { debugDataPublisher.PublishIfActive(livedebugging.NewData( livedebugging.ComponentID(componentID), @@ -66,3 +56,13 @@ func PublishMetricsIfActive(debugDataPublisher livedebugging.DebugDataPublisher, livedebugging.WithTargetComponentIDs(extractIds(nextMetrics)), )) } + +func extractIds(consumers []otelcol.Consumer) []string { + ids := make([]string, 0, len(consumers)) + for _, cons := range consumers { + if consWithID, ok := cons.(otelcol.ComponentMetadata); ok { + ids = append(ids, consWithID.ComponentID()) + } + } + return ids +} diff --git a/internal/component/otelcol/processor/discovery/discovery.go b/internal/component/otelcol/processor/discovery/discovery.go index 64428a26ee..044b40ea6e 100644 --- a/internal/component/otelcol/processor/discovery/discovery.go +++ b/internal/component/otelcol/processor/discovery/discovery.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/alloy/internal/component/discovery" "github.com/grafana/alloy/internal/component/otelcol" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/featuregate" @@ -166,7 +166,7 @@ func (c *Component) Update(newConfig component.Arguments) error { } nextTraces := c.args.Output.Traces fanout := fanoutconsumer.Traces(nextTraces) - tracesInterceptor := interceptorconsumer.Traces(fanout, false, + tracesInterceptor := interceptconsumer.Traces(fanout, func(ctx context.Context, td ptrace.Traces) error { livedebuggingpublisher.PublishTracesIfActive(c.debugDataPublisher.(livedebugging.DebugDataPublisher), c.opts.ID, td, nextTraces) return fanout.ConsumeTraces(ctx, td) diff --git a/internal/component/otelcol/processor/processor.go b/internal/component/otelcol/processor/processor.go index 925e588b88..a87364ff17 100644 --- a/internal/component/otelcol/processor/processor.go +++ b/internal/component/otelcol/processor/processor.go @@ -24,7 +24,7 @@ import ( "github.com/grafana/alloy/internal/component/otelcol" otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazycollector" "github.com/grafana/alloy/internal/component/otelcol/internal/lazyconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" @@ -195,7 +195,7 @@ func (p *Processor) Update(args component.Arguments) error { var tracesProcessor otelprocessor.Traces if len(next.Traces) > 0 { fanout := fanoutconsumer.Traces(next.Traces) - tracesInterceptor := interceptorconsumer.Traces(fanout, false, + tracesInterceptor := interceptconsumer.Traces(fanout, func(ctx context.Context, td ptrace.Traces) error { livedebuggingpublisher.PublishTracesIfActive(p.debugDataPublisher, p.opts.ID, td, next.Traces) return fanout.ConsumeTraces(ctx, td) @@ -212,7 +212,7 @@ func (p *Processor) Update(args component.Arguments) error { var metricsProcessor otelprocessor.Metrics if len(next.Metrics) > 0 { fanout := fanoutconsumer.Metrics(next.Metrics) - metricsInterceptor := interceptorconsumer.Metrics(fanout, false, + metricsInterceptor := interceptconsumer.Metrics(fanout, func(ctx context.Context, md pmetric.Metrics) error { livedebuggingpublisher.PublishMetricsIfActive(p.debugDataPublisher, p.opts.ID, md, next.Metrics) return fanout.ConsumeMetrics(ctx, md) @@ -229,7 +229,7 @@ func (p *Processor) Update(args component.Arguments) error { var logsProcessor otelprocessor.Logs if len(next.Logs) > 0 { fanout := fanoutconsumer.Logs(next.Logs) - logsInterceptor := interceptorconsumer.Logs(fanout, false, + logsInterceptor := interceptconsumer.Logs(fanout, func(ctx context.Context, ld plog.Logs) error { livedebuggingpublisher.PublishLogsIfActive(p.debugDataPublisher, p.opts.ID, ld, next.Logs) return fanout.ConsumeLogs(ctx, ld) diff --git a/internal/component/otelcol/receiver/loki/loki.go b/internal/component/otelcol/receiver/loki/loki.go index 282f64d2be..1f6756c4a2 100644 --- a/internal/component/otelcol/receiver/loki/loki.go +++ b/internal/component/otelcol/receiver/loki/loki.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/alloy/internal/component/common/loki" "github.com/grafana/alloy/internal/component/otelcol" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/featuregate" "github.com/grafana/alloy/internal/runtime/logging/level" @@ -121,7 +121,7 @@ func (c *Component) Update(newConfig component.Arguments) error { c.args = newConfig.(Arguments) nextLogs := c.args.Output.Logs fanout := fanoutconsumer.Logs(nextLogs) - logsInterceptor := interceptorconsumer.Logs(fanout, false, + logsInterceptor := interceptconsumer.Logs(fanout, func(ctx context.Context, ld plog.Logs) error { livedebuggingpublisher.PublishLogsIfActive(c.debugDataPublisher, c.opts.ID, ld, nextLogs) return fanout.ConsumeLogs(ctx, ld) diff --git a/internal/component/otelcol/receiver/prometheus/prometheus.go b/internal/component/otelcol/receiver/prometheus/prometheus.go index f09ec8cd19..cd3ada52df 100644 --- a/internal/component/otelcol/receiver/prometheus/prometheus.go +++ b/internal/component/otelcol/receiver/prometheus/prometheus.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/alloy/internal/component/otelcol" otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/component/otelcol/receiver/prometheus/internal" "github.com/grafana/alloy/internal/featuregate" @@ -162,7 +162,7 @@ func (c *Component) Update(newConfig component.Arguments) error { } nextMetrics := cfg.Output.Metrics fanout := fanoutconsumer.Metrics(nextMetrics) - metricsInterceptor := interceptorconsumer.Metrics(fanout, false, + metricsInterceptor := interceptconsumer.Metrics(fanout, func(ctx context.Context, md pmetric.Metrics) error { livedebuggingpublisher.PublishMetricsIfActive(c.debugDataPublisher, c.opts.ID, md, nextMetrics) return fanout.ConsumeMetrics(ctx, md) diff --git a/internal/component/otelcol/receiver/receiver.go b/internal/component/otelcol/receiver/receiver.go index d5c0f4ff4c..1d8d558d28 100644 --- a/internal/component/otelcol/receiver/receiver.go +++ b/internal/component/otelcol/receiver/receiver.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/alloy/internal/component/otelcol" otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" "github.com/grafana/alloy/internal/component/otelcol/internal/fanoutconsumer" - "github.com/grafana/alloy/internal/component/otelcol/internal/interceptorconsumer" + "github.com/grafana/alloy/internal/component/otelcol/internal/interceptconsumer" "github.com/grafana/alloy/internal/component/otelcol/internal/lazycollector" "github.com/grafana/alloy/internal/component/otelcol/internal/livedebuggingpublisher" "github.com/grafana/alloy/internal/component/otelcol/internal/scheduler" @@ -182,7 +182,7 @@ func (r *Receiver) Update(args component.Arguments) error { if len(next.Traces) > 0 { fanout := fanoutconsumer.Traces(next.Traces) - tracesInterceptor := interceptorconsumer.Traces(fanout, false, + tracesInterceptor := interceptconsumer.Traces(fanout, func(ctx context.Context, td ptrace.Traces) error { livedebuggingpublisher.PublishTracesIfActive(r.debugDataPublisher, r.opts.ID, td, next.Traces) return fanout.ConsumeTraces(ctx, td) @@ -198,7 +198,7 @@ func (r *Receiver) Update(args component.Arguments) error { if len(next.Metrics) > 0 { fanout := fanoutconsumer.Metrics(next.Metrics) - metricsInterceptor := interceptorconsumer.Metrics(fanout, false, + metricsInterceptor := interceptconsumer.Metrics(fanout, func(ctx context.Context, md pmetric.Metrics) error { livedebuggingpublisher.PublishMetricsIfActive(r.debugDataPublisher, r.opts.ID, md, next.Metrics) return fanout.ConsumeMetrics(ctx, md) @@ -214,7 +214,7 @@ func (r *Receiver) Update(args component.Arguments) error { if len(next.Logs) > 0 { fanout := fanoutconsumer.Logs(next.Logs) - logsInterceptor := interceptorconsumer.Logs(fanout, false, + logsInterceptor := interceptconsumer.Logs(fanout, func(ctx context.Context, ld plog.Logs) error { livedebuggingpublisher.PublishLogsIfActive(r.debugDataPublisher, r.opts.ID, ld, next.Logs) return fanout.ConsumeLogs(ctx, ld) diff --git a/internal/service/livedebugging/data.go b/internal/service/livedebugging/data.go index dcae46c9c8..9f6d6a64da 100644 --- a/internal/service/livedebugging/data.go +++ b/internal/service/livedebugging/data.go @@ -11,11 +11,12 @@ const ( OtelTrace DataType = "otel_trace" ) -type DataOption func(*Data) +type DataOption func(Data) Data func WithTargetComponentIDs(ids []string) DataOption { - return func(f *Data) { - f.TargetComponentIDs = ids + return func(d Data) Data { + d.TargetComponentIDs = ids + return d } } @@ -34,8 +35,8 @@ type Data struct { DataFunc func() string } -func NewData(componentID ComponentID, dataType DataType, count uint64, dataFunc func() string, opts ...DataOption) *Data { - data := &Data{ +func NewData(componentID ComponentID, dataType DataType, count uint64, dataFunc func() string, opts ...DataOption) Data { + data := Data{ ComponentID: componentID, Type: dataType, Count: count, @@ -43,7 +44,7 @@ func NewData(componentID ComponentID, dataType DataType, count uint64, dataFunc } for _, opt := range opts { - opt(data) + data = opt(data) } return data diff --git a/internal/service/livedebugging/livedebugging.go b/internal/service/livedebugging/livedebugging.go index a346a34fa9..bc482903cc 100644 --- a/internal/service/livedebugging/livedebugging.go +++ b/internal/service/livedebugging/livedebugging.go @@ -16,12 +16,12 @@ type CallbackID string type CallbackManager interface { // AddCallback sets a callback for a given componentID. // The callback is used to send debugging data to live debugging consumers. - AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error + AddCallback(callbackID CallbackID, componentID ComponentID, callback func(Data)) error // DeleteCallback deletes a callback for a given componentID. DeleteCallback(callbackID CallbackID, componentID ComponentID) // AddCallbackMulti sets a callback to all components. // The callbacks are used to send debugging data to live debugging consumers. - AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error + AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(Data)) error // DeleteCallbackMulti deletes callbacks for all components. DeleteCallbackMulti(callbackID CallbackID, moduleID ModuleID) } @@ -29,11 +29,11 @@ type CallbackManager interface { // DebugDataPublisher is used by components to push information to live debugging consumers. type DebugDataPublisher interface { // Publish sends debugging data for a given componentID if a least one consumer is listening for debugging data for the given componentID. - PublishIfActive(data *Data) + PublishIfActive(data Data) } type liveDebugging struct { loadMut sync.RWMutex - callbacks map[ComponentID]map[CallbackID]func(*Data) + callbacks map[ComponentID]map[CallbackID]func(Data) host service.Host enabled bool } @@ -44,11 +44,11 @@ var _ DebugDataPublisher = &liveDebugging{} // NewLiveDebugging creates a new instance of liveDebugging. func NewLiveDebugging() *liveDebugging { return &liveDebugging{ - callbacks: make(map[ComponentID]map[CallbackID]func(*Data)), + callbacks: make(map[ComponentID]map[CallbackID]func(Data)), } } -func (s *liveDebugging) PublishIfActive(data *Data) { +func (s *liveDebugging) PublishIfActive(data Data) { s.loadMut.RLock() defer s.loadMut.RUnlock() @@ -65,7 +65,7 @@ func (s *liveDebugging) PublishIfActive(data *Data) { } } -func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID ComponentID, callback func(*Data)) error { +func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID ComponentID, callback func(Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -87,14 +87,14 @@ func (s *liveDebugging) AddCallback(callbackID CallbackID, componentID Component } if _, ok := s.callbacks[componentID]; !ok { - s.callbacks[componentID] = make(map[CallbackID]func(*Data)) + s.callbacks[componentID] = make(map[CallbackID]func(Data)) } s.callbacks[componentID][callbackID] = callback return nil } -func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(*Data)) error { +func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleID, callback func(Data)) error { s.loadMut.Lock() defer s.loadMut.Unlock() @@ -117,7 +117,7 @@ func (s *liveDebugging) AddCallbackMulti(callbackID CallbackID, moduleID ModuleI } if _, ok := s.callbacks[ComponentID(cp.ID.String())]; !ok { - s.callbacks[ComponentID(cp.ID.String())] = make(map[CallbackID]func(*Data)) + s.callbacks[ComponentID(cp.ID.String())] = make(map[CallbackID]func(Data)) } s.callbacks[ComponentID(cp.ID.String())][callbackID] = callback } diff --git a/internal/service/livedebugging/livedebugging_test.go b/internal/service/livedebugging/livedebugging_test.go index 082b9b74c1..17532242ac 100644 --- a/internal/service/livedebugging/livedebugging_test.go +++ b/internal/service/livedebugging/livedebugging_test.go @@ -11,7 +11,7 @@ import ( func TestAddCallback(t *testing.T) { livedebugging := NewLiveDebugging() callbackID := CallbackID("callback1") - callback := func(data *Data) {} + callback := func(data Data) {} err := livedebugging.AddCallback(callbackID, "fake.liveDebugging", callback) require.ErrorContains(t, err, "the live debugging service is disabled. Check the documentation to find out how to enable it") @@ -43,8 +43,8 @@ func TestStream(t *testing.T) { componentID := ComponentID("fake.liveDebugging") callbackID := CallbackID("callback1") - var receivedData *Data - callback := func(data *Data) { + var receivedData Data + callback := func(data Data) { receivedData = data } livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) @@ -78,13 +78,13 @@ func TestMultipleStreams(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - var receivedData1 *Data - callback1 := func(data *Data) { + var receivedData1 Data + callback1 := func(data Data) { receivedData1 = data } - var receivedData2 *Data - callback2 := func(data *Data) { + var receivedData2 Data + callback2 := func(data Data) { receivedData2 = data } @@ -104,8 +104,8 @@ func TestDeleteCallback(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - callback1 := func(data *Data) {} - callback2 := func(data *Data) {} + callback1 := func(data Data) {} + callback2 := func(data Data) {} require.NoError(t, livedebugging.AddCallback(callbackID1, componentID, callback1)) require.NoError(t, livedebugging.AddCallback(callbackID2, componentID, callback2)) @@ -137,7 +137,7 @@ func setupServiceHost(liveDebugging *liveDebugging) { func TestAddCallbackMulti(t *testing.T) { livedebugging := NewLiveDebugging() callbackID := CallbackID("callback1") - callback := func(data *Data) {} + callback := func(data Data) {} err := livedebugging.AddCallbackMulti(callbackID, "", callback) require.ErrorContains(t, err, "the live debugging service is disabled. Check the documentation to find out how to enable it") @@ -164,8 +164,8 @@ func TestDeleteCallbackMulti(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - callback1 := func(data *Data) {} - callback2 := func(data *Data) {} + callback1 := func(data Data) {} + callback2 := func(data Data) {} require.NoError(t, livedebugging.AddCallbackMulti(callbackID1, "", callback1)) require.NoError(t, livedebugging.AddCallbackMulti(callbackID2, "", callback2)) @@ -189,13 +189,13 @@ func TestMultiCallbacksMultipleStreams(t *testing.T) { callbackID1 := CallbackID("callback1") callbackID2 := CallbackID("callback2") - var receivedData1 *Data - callback1 := func(data *Data) { + var receivedData1 Data + callback1 := func(data Data) { receivedData1 = data } - var receivedData2 *Data - callback2 := func(data *Data) { + var receivedData2 Data + callback2 := func(data Data) { receivedData2 = data } diff --git a/internal/web/api/api.go b/internal/web/api/api.go index 3868905c67..082c60b4a6 100644 --- a/internal/web/api/api.go +++ b/internal/web/api/api.go @@ -184,16 +184,16 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager, logger moduleID = livedebugging.ModuleID(vars["moduleID"]) } - window := setWindow(w, r.URL.Query().Get("window")) // in seconds + windowSeconds := setWindow(w, r.URL.Query().Get("window")) - dataCh := make(chan *livedebugging.Data, 1000) + dataCh := make(chan livedebugging.Data, 1000) dataMap := make(map[dataKey]liveDebuggingData) ctx := r.Context() id := livedebugging.CallbackID(uuid.New().String()) droppedData := false - err := callbackManager.AddCallbackMulti(id, moduleID, func(data *livedebugging.Data) { + err := callbackManager.AddCallbackMulti(id, moduleID, func(data livedebugging.Data) { select { case <-ctx.Done(): return @@ -219,7 +219,7 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager, logger callbackManager.DeleteCallbackMulti(id, moduleID) }() - ticker := time.NewTicker(time.Duration(window) * time.Second) + ticker := time.NewTicker(time.Duration(windowSeconds)) defer ticker.Stop() for { @@ -243,7 +243,7 @@ func graph(_ service.Host, callbackManager livedebugging.CallbackManager, logger // Flush aggregated data var builder strings.Builder for _, data := range dataMap { - data.Rate = float64(data.Count) / float64(window) + data.Rate = float64(data.Count) / windowSeconds.Seconds() jsonData, err := json.Marshal(data) if err != nil { continue @@ -287,7 +287,7 @@ func liveDebugging(_ service.Host, callbackManager livedebugging.CallbackManager id := livedebugging.CallbackID(uuid.New().String()) droppedData := false - err := callbackManager.AddCallback(id, componentID, func(data *livedebugging.Data) { + err := callbackManager.AddCallback(id, componentID, func(data livedebugging.Data) { select { case <-ctx.Done(): return @@ -354,15 +354,18 @@ func setSampleProb(w http.ResponseWriter, sampleProbParam string) (sampleProb fl } // window is expected to be in seconds, between 1 and 60. -func setWindow(w http.ResponseWriter, windowParam string) (window int64) { - window = 5 - if windowParam != "" { - var err error - window, err = strconv.ParseInt(windowParam, 10, 64) - if err != nil || window < 1 || window > 60 { - http.Error(w, "Invalid window", http.StatusBadRequest) - return 5 - } +func setWindow(w http.ResponseWriter, windowParam string) time.Duration { + const defaultWindow = 5 * time.Second + + if windowParam == "" { + return defaultWindow } - return window + + window, err := strconv.Atoi(windowParam) + if err != nil || window < 1 || window > 60 { + http.Error(w, "Invalid window: must be an integer between 1 and 60", http.StatusBadRequest) + return defaultWindow + } + + return time.Duration(window) * time.Second } From 074354cbeb28d9bfd5fbda9fea2b1b99a00cada2 Mon Sep 17 00:00:00 2001 From: William Dumont Date: Thu, 20 Feb 2025 14:22:48 +0100 Subject: [PATCH 11/11] fix tests --- .../component/otelcol/processor/discovery/discovery.go | 10 ++++++++++ internal/service/livedebugging/livedebugging_test.go | 3 --- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/component/otelcol/processor/discovery/discovery.go b/internal/component/otelcol/processor/discovery/discovery.go index 044b40ea6e..cbef6aca8d 100644 --- a/internal/component/otelcol/processor/discovery/discovery.go +++ b/internal/component/otelcol/processor/discovery/discovery.go @@ -107,11 +107,21 @@ func New(o component.Options, c Arguments) (*Component, error) { return nil, err } + nextTraces := c.Output.Traces + fanout := fanoutconsumer.Traces(nextTraces) + tracesInterceptor := interceptconsumer.Traces(fanout, + func(ctx context.Context, td ptrace.Traces) error { + livedebuggingpublisher.PublishTracesIfActive(debugDataPublisher.(livedebugging.DebugDataPublisher), o.ID, td, nextTraces) + return fanout.ConsumeTraces(ctx, td) + }, + ) + consumerOpts := promsdconsumer.Options{ // Don't bother setting up labels - this will be done by the Update() function. HostLabels: map[string]discovery.Target{}, OperationType: c.OperationType, PodAssociations: c.PodAssociations, + NextConsumer: tracesInterceptor, } consumer, err := promsdconsumer.NewConsumer(consumerOpts, o.Logger) if err != nil { diff --git a/internal/service/livedebugging/livedebugging_test.go b/internal/service/livedebugging/livedebugging_test.go index 17532242ac..897f2d41e8 100644 --- a/internal/service/livedebugging/livedebugging_test.go +++ b/internal/service/livedebugging/livedebugging_test.go @@ -47,9 +47,6 @@ func TestStream(t *testing.T) { callback := func(data Data) { receivedData = data } - livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"}))) - require.Nil(t, receivedData) // nil because there are no active callbacks for it - livedebugging.AddCallback(callbackID, componentID, callback) livedebugging.PublishIfActive(NewData(componentID, PrometheusMetric, 3, func() string { return "test data" }, WithTargetComponentIDs([]string{"component1"})))