From 89345f01890ac393b9c262e881a243164c3f4e3c Mon Sep 17 00:00:00 2001 From: mateagluhak Date: Wed, 3 Sep 2025 17:12:31 +0200 Subject: [PATCH 01/17] errors are cleared from cache with given interval and cached sourcemaps can be removed from cache if cache minimum ttl is specified --- CHANGELOG.md | 6 + .../components/faro/faro.receiver.md | 22 ++- internal/component/faro/receiver/arguments.go | 21 ++- internal/component/faro/receiver/receiver.go | 19 ++- .../component/faro/receiver/sourcemaps.go | 83 ++++++++-- .../faro/receiver/sourcemaps_test.go | 156 ++++++++++++++++++ 6 files changed, 274 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 898470ff9ab..44de952f849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,12 @@ Main (unreleased) - Reduce memory overhead of `prometheus.remote_write`'s WAL by bringing in an upstream change to only track series in a slice if there's a hash conflict. (@kgeckhart) +- Add `cache_minimum_ttl` argument to `faro.receiver.sourcemaps` block to optionally specify the duration after which the cache map clears itself if sourcemap was not used for the specified duration (@mateagluhak) + +- Add `cache_error_cleanup_interval` argument to `faro.receiver.sourcemaps` block to specify the duration after which the cached sourcemap errors are removed from the cache (@mateagluhak) + +- Add `cache_cleanup_check_interval` argument to `faro.receiver.sourcemaps` block to specify how often to check if any sourcemaps need to be cleared from the cache (@mateagluhak) + ### Bugfixes - Update `webdevops/go-common` dependency to resolve concurrent map write panic. (@dehaansa) diff --git a/docs/sources/reference/components/faro/faro.receiver.md b/docs/sources/reference/components/faro/faro.receiver.md index 4fa987477d9..3f561e01236 100644 --- a/docs/sources/reference/components/faro/faro.receiver.md +++ b/docs/sources/reference/components/faro/faro.receiver.md @@ -134,11 +134,15 @@ Configuring the `rate` argument determines how fast the bucket refills, and conf The `sourcemaps` block configures how to retrieve sourcemaps. Sourcemaps are then used to transform file and line information from minified code into the file and line information from the original source code. -| Name | Type | Description | Default | Required | -|-------------------------|----------------|--------------------------------------------|---------|----------| -| `download` | `bool` | Whether to download sourcemaps. | `true` | no | -| `download_from_origins` | `list(string)` | Which origins to download sourcemaps from. | `["*"]` | no | -| `download_timeout` | `duration` | Timeout when downloading sourcemaps. | `"1s"` | no | +| Name | Type | Description | Default | Required | +|------------------------------------|----------------|-----------------------------------------------------------------------------------|---------|----------| +| `download` | `bool` | Whether to download sourcemaps. | `true` | no | +| `download_from_origins` | `list(string)` | Which origins to download sourcemaps from. | `["*"]` | no | +| `download_timeout` | `duration` | Timeout when downloading sourcemaps. | `"1s"` | no | +| `cache_minimum_ttl` | `duration` | Duration after which source map is deleted from cache if not used | `inf` | no | +| `cache_error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried | `"1h"` | no | +| `cache_cleanup_check_interval` | `duration` | How often should cached sourcemaps be checked for cleanup | `"1h"` | no | + When exceptions are sent to the `faro.receiver` component, it can download sourcemaps from the web application. You can disable this behavior by setting the `download` argument to `false`. @@ -151,6 +155,12 @@ The `*` character indicates a wildcard. By default, sourcemap downloads are subject to a timeout of `"1s"`, specified by the `download_timeout` argument. Setting `download_timeout` to `"0s"` disables timeouts. +By default, sourcemaps are held in memory indefinitely. By setting the `cache_minimum_ttl` sourcemaps will be cleared if not used during the specified duration. + +By default, if there is an error while downloading or parsing a sourcemap, error gets cached. After duration specified by `cache_error_cleanup_interval`, all errors get cleared from cache. + +By default, every 30s cached sourcemaps are checked for cleanup. The frequency of checking can be modified by setting the `cache_cleanup_check_interval` argument. + To retrieve sourcemaps from disk instead of the network, specify one or more [`location` blocks][location]. When `location` blocks are provided, they're checked first for sourcemaps before falling back to downloading. @@ -209,7 +219,7 @@ The template value is replaced with the release value provided by the [Faro Web * `faro_receiver_request_message_bytes` (histogram): Size (in bytes) of HTTP requests received from clients. * `faro_receiver_response_message_bytes` (histogram): Size (in bytes) of HTTP responses sent to clients. * `faro_receiver_inflight_requests` (gauge): Current number of inflight requests. -* `faro_receiver_sourcemap_cache_size` (counter): Number of items in sourcemap cache per origin. +* `faro_receiver_sourcemap_cache_size` (gauge): Number of items in sourcemap cache per origin. * `faro_receiver_sourcemap_downloads_total` (counter): Total number of sourcemap downloads performed per origin and status. * `faro_receiver_sourcemap_file_reads_total` (counter): Total number of sourcemap retrievals using the filesystem per origin and status. diff --git a/internal/component/faro/receiver/arguments.go b/internal/component/faro/receiver/arguments.go index 232f561a944..2dfd4137478 100644 --- a/internal/component/faro/receiver/arguments.go +++ b/internal/component/faro/receiver/arguments.go @@ -3,6 +3,7 @@ package receiver import ( "encoding" "fmt" + "math" "time" "github.com/alecthomas/units" @@ -71,17 +72,23 @@ func (r *RateLimitingArguments) SetToDefault() { // SourceMapsArguments configures how app_agent_receiver will retrieve source // maps for transforming stack traces. type SourceMapsArguments struct { - Download bool `alloy:"download,attr,optional"` - DownloadFromOrigins []string `alloy:"download_from_origins,attr,optional"` - DownloadTimeout time.Duration `alloy:"download_timeout,attr,optional"` - Locations []LocationArguments `alloy:"location,block,optional"` + Download bool `alloy:"download,attr,optional"` + DownloadFromOrigins []string `alloy:"download_from_origins,attr,optional"` + DownloadTimeout time.Duration `alloy:"download_timeout,attr,optional"` + CacheMinimumTtl time.Duration `alloy:"cache_minimum_ttl,attr,optional"` + CacheErrorCleanupInterval time.Duration `alloy:"cache_error_cleanup_interval,attr,optional"` + CacheCleanupCheckInterval time.Duration `alloy:"cache_cleanup_check_interval,attr,optional"` + Locations []LocationArguments `alloy:"location,block,optional"` } func (s *SourceMapsArguments) SetToDefault() { *s = SourceMapsArguments{ - Download: true, - DownloadFromOrigins: []string{"*"}, - DownloadTimeout: time.Second, + Download: true, + DownloadFromOrigins: []string{"*"}, + DownloadTimeout: time.Second, + CacheErrorCleanupInterval: time.Hour, + CacheMinimumTtl: time.Duration(math.MaxInt64), + CacheCleanupCheckInterval: time.Second * 30, } } diff --git a/internal/component/faro/receiver/receiver.go b/internal/component/faro/receiver/receiver.go index aebab7ebcec..cef80e44f31 100644 --- a/internal/component/faro/receiver/receiver.go +++ b/internal/component/faro/receiver/receiver.go @@ -131,13 +131,28 @@ func (c *Component) Update(args component.Arguments) error { c.handler.Update(newArgs.Server) - c.lazySourceMaps.SetInner(newSourceMapsStore( + innerStore := newSourceMapsStore( log.With(c.log, "subcomponent", "handler"), newArgs.SourceMaps, c.sourceMapsMetrics, nil, // Use default HTTP client. nil, // Use default FS implementation. - )) + ) + c.lazySourceMaps.SetInner(innerStore) + + go func(s *sourceMapsStoreImpl) { + for { + time.Sleep(newArgs.SourceMaps.CacheCleanupCheckInterval) + s.CleanOldCacheEntries() + } + }(innerStore) + + go func(s *sourceMapsStoreImpl) { + for { + time.Sleep(newArgs.SourceMaps.CacheErrorCleanupInterval) + s.CleanCachedErrors() + } + }(innerStore) c.logs.SetReceivers(newArgs.Output.Logs) c.traces.SetConsumers(newArgs.Output.Traces) diff --git a/internal/component/faro/receiver/sourcemaps.go b/internal/component/faro/receiver/sourcemaps.go index 48533a8a3bf..293e83f7ffd 100644 --- a/internal/component/faro/receiver/sourcemaps.go +++ b/internal/component/faro/receiver/sourcemaps.go @@ -13,6 +13,7 @@ import ( "strings" "sync" "text/template" + "time" "github.com/go-kit/log" "github.com/go-sourcemap/sourcemap" @@ -67,14 +68,14 @@ func (fs osFileService) ReadFile(name string) ([]byte, error) { } type sourceMapMetrics struct { - cacheSize *prometheus.CounterVec + cacheSize *prometheus.GaugeVec downloads *prometheus.CounterVec fileReads *prometheus.CounterVec } func newSourceMapMetrics(reg prometheus.Registerer) *sourceMapMetrics { m := &sourceMapMetrics{ - cacheSize: prometheus.NewCounterVec(prometheus.CounterOpts{ + cacheSize: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "faro_receiver_sourcemap_cache_size", Help: "number of items in source map cache, per origin", }, []string{"origin"}), @@ -88,7 +89,7 @@ func newSourceMapMetrics(reg prometheus.Registerer) *sourceMapMetrics { }, []string{"origin", "status"}), } - m.cacheSize = util.MustRegisterOrGet(reg, m.cacheSize).(*prometheus.CounterVec) + m.cacheSize = util.MustRegisterOrGet(reg, m.cacheSize).(*prometheus.GaugeVec) m.downloads = util.MustRegisterOrGet(reg, m.downloads).(*prometheus.CounterVec) m.fileReads = util.MustRegisterOrGet(reg, m.fileReads).(*prometheus.CounterVec) return m @@ -99,6 +100,16 @@ type sourcemapFileLocation struct { pathTemplate *template.Template } +type timeSource interface { + Now() time.Time +} + +type realTimeSource struct{} + +func (realTimeSource) Now() time.Time { + return time.Now() +} + type sourceMapsStoreImpl struct { log log.Logger cli httpClient @@ -107,8 +118,14 @@ type sourceMapsStoreImpl struct { metrics *sourceMapMetrics locs []*sourcemapFileLocation - cacheMut sync.Mutex - cache map[string]*sourcemap.Consumer + cacheMut sync.Mutex + cache map[string]*cachedSourceMap + timeSource timeSource +} + +type cachedSourceMap struct { + consumer *sourcemap.Consumer + lastUsed time.Time } // newSourceMapStore creates an implementation of sourceMapsStore. The returned @@ -141,27 +158,29 @@ func newSourceMapsStore(log log.Logger, args SourceMapsArguments, metrics *sourc } return &sourceMapsStoreImpl{ - log: log, - cli: cli, - fs: fs, - args: args, - cache: make(map[string]*sourcemap.Consumer), - metrics: metrics, - locs: locs, + log: log, + cli: cli, + fs: fs, + args: args, + cache: make(map[string]*cachedSourceMap), + metrics: metrics, + locs: locs, + timeSource: realTimeSource{}, } } func (store *sourceMapsStoreImpl) GetSourceMap(sourceURL string, release string) (*sourcemap.Consumer, error) { - // TODO(rfratto): GetSourceMap is weak to transient errors, since it always - // caches the result, even when there's an error. This means that transient - // errors will be cached forever, preventing source maps from being retrieved. store.cacheMut.Lock() defer store.cacheMut.Unlock() cacheKey := fmt.Sprintf("%s__%s", sourceURL, release) - if sm, ok := store.cache[cacheKey]; ok { - return sm, nil + if cached, ok := store.cache[cacheKey]; ok { + if cached != nil { + cached.lastUsed = store.timeSource.Now() + return cached.consumer, nil + } + return nil, nil } content, sourceMapURL, err := store.getSourceMapContent(sourceURL, release) @@ -177,11 +196,39 @@ func (store *sourceMapsStoreImpl) GetSourceMap(sourceURL string, release string) return nil, err } level.Info(store.log).Log("msg", "successfully parsed source map", "url", sourceMapURL, "release", release) - store.cache[cacheKey] = consumer + store.cache[cacheKey] = &cachedSourceMap{ + consumer: consumer, + lastUsed: store.timeSource.Now(), + } store.metrics.cacheSize.WithLabelValues(getOrigin(sourceURL)).Inc() return consumer, nil } +func (store *sourceMapsStoreImpl) CleanOldCacheEntries() { + store.cacheMut.Lock() + defer store.cacheMut.Unlock() + + for key, cached := range store.cache { + if cached != nil && cached.lastUsed.Before(store.timeSource.Now().Add(-store.args.CacheMinimumTtl)) { + srcUrl := strings.SplitN(key, "__", 2)[0] + origin := getOrigin(srcUrl) + store.metrics.cacheSize.WithLabelValues(origin).Dec() + delete(store.cache, key) + } + } +} + +func (store *sourceMapsStoreImpl) CleanCachedErrors() { + store.cacheMut.Lock() + defer store.cacheMut.Unlock() + + for key, cached := range store.cache { + if cached == nil { + delete(store.cache, key) + } + } +} + func (store *sourceMapsStoreImpl) getSourceMapContent(sourceURL string, release string) (content []byte, sourceMapURL string, err error) { // Attempt to find the source map in the filesystem first. for _, loc := range store.locs { diff --git a/internal/component/faro/receiver/sourcemaps_test.go b/internal/component/faro/receiver/sourcemaps_test.go index c4ed78f03e0..a1508c12e60 100644 --- a/internal/component/faro/receiver/sourcemaps_test.go +++ b/internal/component/faro/receiver/sourcemaps_test.go @@ -9,13 +9,24 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/grafana/alloy/internal/component/faro/receiver/internal/payload" alloyutil "github.com/grafana/alloy/internal/util" + "github.com/grafana/pyroscope/ebpf/util" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" ) +// mockTimeSource is a test helper for controlling time. +type mockTimeSource struct { + now time.Time +} + +func (m *mockTimeSource) Now() time.Time { + return m.now +} + func Test_sourceMapsStoreImpl_DownloadSuccess(t *testing.T) { var ( logger = alloyutil.TestLogger(t) @@ -604,6 +615,150 @@ func Test_sourceMapsStoreImpl_RealWorldPathValidation(t *testing.T) { require.Empty(t, fileService.reads, "should not read file when stat fails") } +func TestSourceMapsStoreImpl_CleanCachedErrors(t *testing.T) { + tt := []struct { + name string + cache map[string]*cachedSourceMap + expectedCacheSize int + }{ + { + name: "should remove cached error", + cache: map[string]*cachedSourceMap{ + "http://shouldRemoveCachedErrors.com__v1": nil, + }, + expectedCacheSize: 0, + }, + { + name: "should not remove from map if no errors", + cache: map[string]*cachedSourceMap{ + "http://shouldNotRemoveFromCache.com__v2": {}, + }, + expectedCacheSize: 1, + }, + { + name: "should not remove from map if no errors", + cache: map[string]*cachedSourceMap{ + "http://shouldNotRemoveFromCache.com__v1": {}, + "http://shouldNotRemoveFromCache.com__v2": {}, + }, + expectedCacheSize: 2, + }, + { + name: "should remove only cached errors", + cache: map[string]*cachedSourceMap{ + "http://shouldNotRemoveFromCache.com__v1": nil, + "http://shouldNotRemoveFromCache.com__v2": {}, + }, + expectedCacheSize: 1, + }, + } + + logger := util.TestLogger(t) + + for _, tc := range tt { + + reg := prometheus.NewRegistry() + metrics := newSourceMapMetrics(reg) + + store := &sourceMapsStoreImpl{ + log: logger, + args: SourceMapsArguments{CacheMinimumTtl: 5 * time.Minute}, + metrics: metrics, + cli: &mockHTTPClient{}, + fs: newTestFileService(), + cache: tc.cache, + timeSource: &mockTimeSource{now: time.Now()}, + } + + t.Run(tc.name, func(t *testing.T) { + store.CleanCachedErrors() + require.Equal(t, tc.expectedCacheSize, len(store.cache)) + }) + } +} + +func TestSourceMapsStoreImpl_CleanOldCachedEntries(t *testing.T) { + tt := []struct { + name string + cache map[string]*cachedSourceMap + timeSource *mockTimeSource + cacheTimeout time.Duration + expectedCacheSize int + }{ + { + name: "should clear entry from cache if too old", + cache: map[string]*cachedSourceMap{ + "http://shouldRemoveCachedErrors.com__v1": {lastUsed: time.Now()}, + }, + timeSource: &mockTimeSource{now: time.Now().Add(5 * time.Minute)}, + cacheTimeout: 5 * time.Minute, + expectedCacheSize: 0, + }, + { + name: "should not clear entry from cache if not too old", + cache: map[string]*cachedSourceMap{ + "http://shouldRemoveCachedErrors.com__v1": {lastUsed: time.Now()}, + }, + timeSource: &mockTimeSource{now: time.Now().Add(3 * time.Minute)}, + cacheTimeout: 5 * time.Minute, + expectedCacheSize: 1, + }, + { + name: "should clear only old entries from cache", + cache: map[string]*cachedSourceMap{ + "http://shouldRemoveCachedErrors.com__v1": {lastUsed: time.Now()}, + "http://shouldRemoveCachedErrors.com__v2": {lastUsed: time.Now().Add(-5 * time.Minute)}, + }, + timeSource: &mockTimeSource{now: time.Now()}, + cacheTimeout: 5 * time.Minute, + expectedCacheSize: 1, + }, + { + name: "should not clear multiple entries", + cache: map[string]*cachedSourceMap{ + "http://shouldRemoveCachedErrors.com__v1": {lastUsed: time.Now().Add(3 * time.Minute)}, + "http://shouldRemoveCachedErrors.com__v2": {lastUsed: time.Now().Add(4 * time.Minute)}, + }, + timeSource: &mockTimeSource{now: time.Now()}, + cacheTimeout: 5 * time.Minute, + expectedCacheSize: 2, + }, + { + name: "should clear multiple old entries from cache", + cache: map[string]*cachedSourceMap{ + "http://shouldRemoveCachedErrors.com__v1": {lastUsed: time.Now().Add(-10 * time.Minute)}, + "http://shouldRemoveCachedErrors.com__v2": {lastUsed: time.Now().Add(-7 * time.Minute)}, + }, + timeSource: &mockTimeSource{now: time.Now()}, + cacheTimeout: 5 * time.Minute, + expectedCacheSize: 0, + }, + } + + logger := util.TestLogger(t) + + for _, tc := range tt { + + reg := prometheus.NewRegistry() + metrics := newSourceMapMetrics(reg) + + store := &sourceMapsStoreImpl{ + log: logger, + args: SourceMapsArguments{CacheMinimumTtl: tc.cacheTimeout}, + metrics: metrics, + cli: &mockHTTPClient{}, + fs: newTestFileService(), + cache: tc.cache, + timeSource: tc.timeSource, + } + + t.Run(tc.name, func(t *testing.T) { + store.CleanOldCacheEntries() + require.Equal(t, tc.expectedCacheSize, len(store.cache)) + }) + } +} + type mockHTTPClient struct { responses []struct { *http.Response @@ -695,4 +850,5 @@ func newTestFileService() *testFileService { stats: make([]string, 0), reads: make([]string, 0), } + } From e8b8725d9127872f81b65257898559219e157c89 Mon Sep 17 00:00:00 2001 From: mateagluhak <116516597+mateagluhak@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:29:00 +0200 Subject: [PATCH 02/17] Update docs/sources/reference/components/faro/faro.receiver.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> --- docs/sources/reference/components/faro/faro.receiver.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/sources/reference/components/faro/faro.receiver.md b/docs/sources/reference/components/faro/faro.receiver.md index 3f561e01236..965f5481bc6 100644 --- a/docs/sources/reference/components/faro/faro.receiver.md +++ b/docs/sources/reference/components/faro/faro.receiver.md @@ -155,11 +155,14 @@ The `*` character indicates a wildcard. By default, sourcemap downloads are subject to a timeout of `"1s"`, specified by the `download_timeout` argument. Setting `download_timeout` to `"0s"` disables timeouts. -By default, sourcemaps are held in memory indefinitely. By setting the `cache_minimum_ttl` sourcemaps will be cleared if not used during the specified duration. +By default, sourcemaps are held in memory indefinitely. +You can set `cache_minimum_ttl` to clear sourcemaps that aren't used during the specified duration. -By default, if there is an error while downloading or parsing a sourcemap, error gets cached. After duration specified by `cache_error_cleanup_interval`, all errors get cleared from cache. +By default, if there's an error while downloading or parsing a sourcemap, the error is cached. +After the duration specified by `cache_error_cleanup_interval`, all errors are cleared from the cache. -By default, every 30s cached sourcemaps are checked for cleanup. The frequency of checking can be modified by setting the `cache_cleanup_check_interval` argument. +By default, cached sourcemaps are checked for cleanup every 30 seconds. +You can modify the frequency by setting the `cache_cleanup_check_interval` argument. To retrieve sourcemaps from disk instead of the network, specify one or more [`location` blocks][location]. When `location` blocks are provided, they're checked first for sourcemaps before falling back to downloading. From e9cbce9cbe16f601cc2e4346272be745e0f9557b Mon Sep 17 00:00:00 2001 From: mateagluhak <116516597+mateagluhak@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:29:10 +0200 Subject: [PATCH 03/17] Update docs/sources/reference/components/faro/faro.receiver.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> --- .../reference/components/faro/faro.receiver.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sources/reference/components/faro/faro.receiver.md b/docs/sources/reference/components/faro/faro.receiver.md index 965f5481bc6..afa9e7ddb59 100644 --- a/docs/sources/reference/components/faro/faro.receiver.md +++ b/docs/sources/reference/components/faro/faro.receiver.md @@ -134,14 +134,14 @@ Configuring the `rate` argument determines how fast the bucket refills, and conf The `sourcemaps` block configures how to retrieve sourcemaps. Sourcemaps are then used to transform file and line information from minified code into the file and line information from the original source code. -| Name | Type | Description | Default | Required | -|------------------------------------|----------------|-----------------------------------------------------------------------------------|---------|----------| -| `download` | `bool` | Whether to download sourcemaps. | `true` | no | -| `download_from_origins` | `list(string)` | Which origins to download sourcemaps from. | `["*"]` | no | -| `download_timeout` | `duration` | Timeout when downloading sourcemaps. | `"1s"` | no | -| `cache_minimum_ttl` | `duration` | Duration after which source map is deleted from cache if not used | `inf` | no | -| `cache_error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried | `"1h"` | no | -| `cache_cleanup_check_interval` | `duration` | How often should cached sourcemaps be checked for cleanup | `"1h"` | no | +| Name | Type | Description | Default | Required | +| ------------------------------ | -------------- | --------------------------------------------------------------------------------- | ------- | -------- | +| `cache_cleanup_check_interval` | `duration` | How often should cached sourcemaps be checked for cleanup | `"1h"` | no | +| `cache_error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried | `"1h"` | no | +| `cache_minimum_ttl` | `duration` | Duration after which source map is deleted from cache if not used | `inf` | no | +| `download_from_origins` | `list(string)` | Which origins to download sourcemaps from. | `["*"]` | no | +| `download_timeout` | `duration` | Timeout when downloading sourcemaps. | `"1s"` | no | +| `download` | `bool` | Whether to download sourcemaps. | `true` | no | When exceptions are sent to the `faro.receiver` component, it can download sourcemaps from the web application. From 13d4b81e75798d1b78e468abfb007caa0063d7ec Mon Sep 17 00:00:00 2001 From: mateagluhak <116516597+mateagluhak@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:58:34 +0200 Subject: [PATCH 04/17] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 476c94572dd..31be56b9ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,12 @@ Main (unreleased) - Update the `prometheus.exporter.process` component to get the `remove_empty_groups` option. (@dehaansa) - Remove unnecessary allocations in `stage.static_labels`. (@kalleep) + +- Add `cache_minimum_ttl` argument to `faro.receiver.sourcemaps` block to optionally specify the duration after which the cache map clears itself if sourcemap was not used for the specified duration (@mateagluhak) + +- Add `cache_error_cleanup_interval` argument to `faro.receiver.sourcemaps` block to specify the duration after which the cached sourcemap errors are removed from the cache (@mateagluhak) + +- Add `cache_cleanup_check_interval` argument to `faro.receiver.sourcemaps` block to specify how often to check if any sourcemaps need to be cleared from the cache (@mateagluhak) - Upgrade `beyla.ebpf` from Beyla version v2.2.5 to v2.5.8 The full list of changes can be found in the [Beyla release notes](https://github.com/grafana/beyla/releases/tag/v2.5.2) (@marctc) From afe64d82d8f97af43eba290d7f091584cdb7b58f Mon Sep 17 00:00:00 2001 From: mateagluhak <116516597+mateagluhak@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:49:36 +0200 Subject: [PATCH 05/17] Update docs/sources/reference/components/faro/faro.receiver.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> --- .../reference/components/faro/faro.receiver.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/sources/reference/components/faro/faro.receiver.md b/docs/sources/reference/components/faro/faro.receiver.md index afa9e7ddb59..4c7142d3ddb 100644 --- a/docs/sources/reference/components/faro/faro.receiver.md +++ b/docs/sources/reference/components/faro/faro.receiver.md @@ -134,15 +134,14 @@ Configuring the `rate` argument determines how fast the bucket refills, and conf The `sourcemaps` block configures how to retrieve sourcemaps. Sourcemaps are then used to transform file and line information from minified code into the file and line information from the original source code. -| Name | Type | Description | Default | Required | -| ------------------------------ | -------------- | --------------------------------------------------------------------------------- | ------- | -------- | -| `cache_cleanup_check_interval` | `duration` | How often should cached sourcemaps be checked for cleanup | `"1h"` | no | -| `cache_error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried | `"1h"` | no | -| `cache_minimum_ttl` | `duration` | Duration after which source map is deleted from cache if not used | `inf` | no | -| `download_from_origins` | `list(string)` | Which origins to download sourcemaps from. | `["*"]` | no | -| `download_timeout` | `duration` | Timeout when downloading sourcemaps. | `"1s"` | no | -| `download` | `bool` | Whether to download sourcemaps. | `true` | no | - +| Name | Type | Description | Default | Required | +| ------------------------------ | -------------- | ---------------------------------------------------------------------------------- | ------- | -------- | +| `cache_cleanup_check_interval` | `duration` | How often should cached sourcemaps be checked for cleanup. | `"1h"` | no | +| `cache_error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried. | `"1h"` | no | +| `cache_minimum_ttl` | `duration` | Duration after which source map is deleted from cache if not used. | `inf` | no | +| `download_from_origins` | `list(string)` | Which origins to download sourcemaps from. | `["*"]` | no | +| `download_timeout` | `duration` | Timeout when downloading sourcemaps. | `"1s"` | no | +| `download` | `bool` | Whether to download sourcemaps. | `true` | no | When exceptions are sent to the `faro.receiver` component, it can download sourcemaps from the web application. You can disable this behavior by setting the `download` argument to `false`. From 9448e54c0b12daab6c3e559e511045b0613cfda1 Mon Sep 17 00:00:00 2001 From: mateagluhak <116516597+mateagluhak@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:16:47 +0200 Subject: [PATCH 06/17] Update receiver.go --- internal/component/faro/receiver/receiver.go | 80 ++++++++++++++++---- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/internal/component/faro/receiver/receiver.go b/internal/component/faro/receiver/receiver.go index cef80e44f31..a136315f9ac 100644 --- a/internal/component/faro/receiver/receiver.go +++ b/internal/component/faro/receiver/receiver.go @@ -25,6 +25,11 @@ func init() { }) } +type cleanupRoutines struct { + cancel context.CancelFunc + wg sync.WaitGroup +} + type Component struct { log log.Logger handler *handler @@ -35,6 +40,9 @@ type Component struct { argsMut sync.RWMutex args Arguments + cleanupMut sync.Mutex + cleanup *cleanupRoutines + metrics *metricsExporter logs *logsExporter traces *tracesExporter @@ -93,6 +101,7 @@ func (c *Component) Run(ctx context.Context) error { if cancelCurrentActor != nil { cancelCurrentActor() } + c.stopCleanup() }() for { @@ -140,19 +149,8 @@ func (c *Component) Update(args component.Arguments) error { ) c.lazySourceMaps.SetInner(innerStore) - go func(s *sourceMapsStoreImpl) { - for { - time.Sleep(newArgs.SourceMaps.CacheCleanupCheckInterval) - s.CleanOldCacheEntries() - } - }(innerStore) - - go func(s *sourceMapsStoreImpl) { - for { - time.Sleep(newArgs.SourceMaps.CacheErrorCleanupInterval) - s.CleanCachedErrors() - } - }(innerStore) + c.stopCleanup() + c.startCleanup(newArgs, innerStore) c.logs.SetReceivers(newArgs.Output.Logs) c.traces.SetConsumers(newArgs.Output.Traces) @@ -247,3 +245,59 @@ func (vs *varSourceMapsStore) SetInner(inner sourceMapsStore) { vs.inner = inner } + +func (c *Component) stopCleanup() { + c.cleanupMut.Lock() + defer c.cleanupMut.Unlock() + if c.cleanup != nil { + c.cleanup.cancel() // signal goroutines to exit + c.cleanup.wg.Wait() // wait for them + c.cleanup = nil + } +} + +func (c *Component) startCleanup(args Arguments, s *sourceMapsStoreImpl) { + c.cleanupMut.Lock() + defer c.cleanupMut.Unlock() + + cleanupCtx, cleanupCancel := context.WithCancel(context.Background()) + cr := &cleanupRoutines{cancel: cleanupCancel} + + if d := args.SourceMaps.CacheCleanupCheckInterval; d > 0 { + cr.wg.Add(1) + go func(interval time.Duration) { + defer cr.wg.Done() + s.CleanOldCacheEntries() + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-cleanupCtx.Done(): + return + case <-ticker.C: + s.CleanOldCacheEntries() + } + } + }(d) + } + + if d := args.SourceMaps.CacheErrorCleanupInterval; d > 0 { + cr.wg.Add(1) + go func(interval time.Duration) { + defer cr.wg.Done() + s.CleanCachedErrors() + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-cleanupCtx.Done(): + return + case <-ticker.C: + s.CleanCachedErrors() + } + } + }(d) + } + + c.cleanup = cr +} From c2ddfeec22d8d1ab17e940def59d70207f130701 Mon Sep 17 00:00:00 2001 From: mateagluhak Date: Mon, 15 Dec 2025 15:39:49 +0100 Subject: [PATCH 07/17] Added cache block inside sourcemaps block --- CHANGELOG.md | 2 + .../components/faro/faro.receiver.md | 38 ++++++++++++------- internal/component/faro/receiver/arguments.go | 38 ++++++++++++------- internal/component/faro/receiver/receiver.go | 7 +++- .../component/faro/receiver/sourcemaps.go | 3 +- .../faro/receiver/sourcemaps_test.go | 4 +- 6 files changed, 60 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fa6fde3f9d..05341414952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ Main (unreleased) ### Enhancements +- Add `cache` block to `faro.receiver.sourcemaps` block to configure sourcemap caching behavior. The `cache` block contains three optional attributes: `ttl` to specify the duration after which unused sourcemaps are cleared from cache, `error_cleanup_interval` to specify the duration after which cached sourcemap errors are removed, and `cleanup_check_interval` to specify how often to check if any sourcemaps need to be cleared. (@mgluhak) + - update promtail converter to use `file_match` block for `loki.source.file` instead of going through `local.file_match`. (@kalleep) - Add `send_traceparent` option for `tracing` config to enable traceparent header propagation. (@MyDigitalLife) diff --git a/docs/sources/reference/components/faro/faro.receiver.md b/docs/sources/reference/components/faro/faro.receiver.md index 1e94537b2cb..54621bb96b6 100644 --- a/docs/sources/reference/components/faro/faro.receiver.md +++ b/docs/sources/reference/components/faro/faro.receiver.md @@ -61,11 +61,13 @@ You can use the following blocks with `faro.receiver`: | [`server`][server] | Configures the HTTP server. | no | | `server` > [`rate_limiting`][rate_limiting] | Configures rate limiting for the HTTP server. | no | | [`sourcemaps`][sourcemaps] | Configures sourcemap retrieval. | no | +| `sourcemaps` > [`cache`][cache] | Configures sourcemap caching behavior. | no | | `sourcemaps` > [`location`][location] | Configures on-disk location for sourcemap retrieval. | no | The > symbol indicates deeper levels of nesting. For example, `sourcemaps` > `location` refers to a `location` block defined inside a `sourcemaps` block. +[cache]: #cache [location]: #location [output]: #output [rate_limiting]: #rate_limiting @@ -148,14 +150,11 @@ Configuring the `rate` argument determines how fast the bucket refills, and conf The `sourcemaps` block configures how to retrieve sourcemaps. Sourcemaps are then used to transform file and line information from minified code into the file and line information from the original source code. -| Name | Type | Description | Default | Required | -| ------------------------------ | -------------- | ---------------------------------------------------------------------------------- | ------- | -------- | -| `cache_cleanup_check_interval` | `duration` | How often should cached sourcemaps be checked for cleanup. | `"1h"` | no | -| `cache_error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried. | `"1h"` | no | -| `cache_minimum_ttl` | `duration` | Duration after which source map is deleted from cache if not used. | `inf` | no | -| `download_from_origins` | `list(string)` | Which origins to download sourcemaps from. | `["*"]` | no | -| `download_timeout` | `duration` | Timeout when downloading sourcemaps. | `"1s"` | no | -| `download` | `bool` | Whether to download sourcemaps. | `true` | no | +| Name | Type | Description | Default | Required | +| ----------------------- | -------------- | ------------------------------------------ | ------- | -------- | +| `download` | `bool` | Whether to download sourcemaps. | `true` | no | +| `download_from_origins` | `list(string)` | Which origins to download sourcemaps from. | `["*"]` | no | +| `download_timeout` | `duration` | Timeout when downloading sourcemaps. | `"1s"` | no | When exceptions are sent to the `faro.receiver` component, it can download sourcemaps from the web application. You can disable this behavior by setting the `download` argument to `false`. @@ -168,17 +167,28 @@ The `*` character indicates a wildcard. By default, sourcemap downloads are subject to a timeout of `"1s"`, specified by the `download_timeout` argument. Setting `download_timeout` to `"0s"` disables timeouts. +To retrieve sourcemaps from disk instead of the network, specify one or more [`location` blocks][location]. +When `location` blocks are provided, they're checked first for sourcemaps before falling back to downloading. + +#### `cache` + +The `cache` block configures sourcemap caching behavior. +All cache settings are optional with sensible defaults. + +| Name | Type | Description | Default | Required | +| ------------------------ | ---------- | ---------------------------------------------------------------------------------- | ------- | -------- | +| `ttl` | `duration` | Duration after which source map is deleted from cache if not used. | `inf` | no | +| `error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried. | `"1h"` | no | +| `cleanup_check_interval` | `duration` | How often cached sourcemaps are checked for cleanup. | `"30s"` | no | + By default, sourcemaps are held in memory indefinitely. -You can set `cache_minimum_ttl` to clear sourcemaps that aren't used during the specified duration. +You can set `ttl` to clear sourcemaps that aren't used during the specified duration. By default, if there's an error while downloading or parsing a sourcemap, the error is cached. -After the duration specified by `cache_error_cleanup_interval`, all errors are cleared from the cache. +After the duration specified by `error_cleanup_interval`, all errors are cleared from the cache. By default, cached sourcemaps are checked for cleanup every 30 seconds. -You can modify the frequency by setting the `cache_cleanup_check_interval` argument. - -To retrieve sourcemaps from disk instead of the network, specify one or more [`location` blocks][location]. -When `location` blocks are provided, they're checked first for sourcemaps before falling back to downloading. +You can modify the frequency by setting the `cleanup_check_interval` argument. #### `location` diff --git a/internal/component/faro/receiver/arguments.go b/internal/component/faro/receiver/arguments.go index 3d7382e0dd9..386c55b6ab1 100644 --- a/internal/component/faro/receiver/arguments.go +++ b/internal/component/faro/receiver/arguments.go @@ -74,23 +74,35 @@ func (r *RateLimitingArguments) SetToDefault() { // SourceMapsArguments configures how app_agent_receiver will retrieve source // maps for transforming stack traces. type SourceMapsArguments struct { - Download bool `alloy:"download,attr,optional"` - DownloadFromOrigins []string `alloy:"download_from_origins,attr,optional"` - DownloadTimeout time.Duration `alloy:"download_timeout,attr,optional"` - CacheMinimumTtl time.Duration `alloy:"cache_minimum_ttl,attr,optional"` - CacheErrorCleanupInterval time.Duration `alloy:"cache_error_cleanup_interval,attr,optional"` - CacheCleanupCheckInterval time.Duration `alloy:"cache_cleanup_check_interval,attr,optional"` - Locations []LocationArguments `alloy:"location,block,optional"` + Download bool `alloy:"download,attr,optional"` + DownloadFromOrigins []string `alloy:"download_from_origins,attr,optional"` + DownloadTimeout time.Duration `alloy:"download_timeout,attr,optional"` + Cache *CacheArguments `alloy:"cache,block,optional"` + Locations []LocationArguments `alloy:"location,block,optional"` } func (s *SourceMapsArguments) SetToDefault() { *s = SourceMapsArguments{ - Download: true, - DownloadFromOrigins: []string{"*"}, - DownloadTimeout: time.Second, - CacheErrorCleanupInterval: time.Hour, - CacheMinimumTtl: time.Duration(math.MaxInt64), - CacheCleanupCheckInterval: time.Second * 30, + Download: true, + DownloadFromOrigins: []string{"*"}, + DownloadTimeout: time.Second, + Cache: &CacheArguments{}, + } + s.Cache.SetToDefault() +} + +// CacheArguments configures sourcemap caching behavior. +type CacheArguments struct { + Ttl time.Duration `alloy:"ttl,attr,optional"` + ErrorCleanupInterval time.Duration `alloy:"error_cleanup_interval,attr,optional"` + CleanupCheckInterval time.Duration `alloy:"cleanup_check_interval,attr,optional"` +} + +func (c *CacheArguments) SetToDefault() { + *c = CacheArguments{ + Ttl: time.Duration(math.MaxInt64), + ErrorCleanupInterval: time.Hour, + CleanupCheckInterval: time.Second * 30, } } diff --git a/internal/component/faro/receiver/receiver.go b/internal/component/faro/receiver/receiver.go index 46d93f045ef..6e3c88a5173 100644 --- a/internal/component/faro/receiver/receiver.go +++ b/internal/component/faro/receiver/receiver.go @@ -274,7 +274,10 @@ func (c *Component) startCleanup(args Arguments, s *sourceMapsStoreImpl) { cleanupCtx, cleanupCancel := context.WithCancel(context.Background()) cr := &cleanupRoutines{cancel: cleanupCancel} - if d := args.SourceMaps.CacheCleanupCheckInterval; d > 0 { + // Get cache config or use defaults if not specified + var cacheConfig = *args.SourceMaps.Cache + + if d := cacheConfig.CleanupCheckInterval; d > 0 { cr.wg.Add(1) go func(interval time.Duration) { defer cr.wg.Done() @@ -292,7 +295,7 @@ func (c *Component) startCleanup(args Arguments, s *sourceMapsStoreImpl) { }(d) } - if d := args.SourceMaps.CacheErrorCleanupInterval; d > 0 { + if d := cacheConfig.ErrorCleanupInterval; d > 0 { cr.wg.Add(1) go func(interval time.Duration) { defer cr.wg.Done() diff --git a/internal/component/faro/receiver/sourcemaps.go b/internal/component/faro/receiver/sourcemaps.go index 139777ef767..ca6c8a6132b 100644 --- a/internal/component/faro/receiver/sourcemaps.go +++ b/internal/component/faro/receiver/sourcemaps.go @@ -208,8 +208,9 @@ func (store *sourceMapsStoreImpl) CleanOldCacheEntries() { store.cacheMut.Lock() defer store.cacheMut.Unlock() + ttl := store.args.Cache.Ttl for key, cached := range store.cache { - if cached != nil && cached.lastUsed.Before(store.timeSource.Now().Add(-store.args.CacheMinimumTtl)) { + if cached != nil && cached.lastUsed.Before(store.timeSource.Now().Add(-ttl)) { srcUrl := strings.SplitN(key, "__", 2)[0] origin := getOrigin(srcUrl) store.metrics.cacheSize.WithLabelValues(origin).Dec() diff --git a/internal/component/faro/receiver/sourcemaps_test.go b/internal/component/faro/receiver/sourcemaps_test.go index 8dc7d259362..859e262ff36 100644 --- a/internal/component/faro/receiver/sourcemaps_test.go +++ b/internal/component/faro/receiver/sourcemaps_test.go @@ -689,7 +689,7 @@ func TestSourceMapsStoreImpl_CleanCachedErrors(t *testing.T) { store := &sourceMapsStoreImpl{ log: logger, - args: SourceMapsArguments{CacheMinimumTtl: 5 * time.Minute}, + args: SourceMapsArguments{Cache: &CacheArguments{Ttl: 5 * time.Minute}}, metrics: metrics, cli: &mockHTTPClient{}, fs: newTestFileService(), @@ -771,7 +771,7 @@ func TestSourceMapsStoreImpl_CleanOldCachedEntries(t *testing.T) { store := &sourceMapsStoreImpl{ log: logger, - args: SourceMapsArguments{CacheMinimumTtl: tc.cacheTimeout}, + args: SourceMapsArguments{Cache: &CacheArguments{Ttl: tc.cacheTimeout}}, metrics: metrics, cli: &mockHTTPClient{}, fs: newTestFileService(), From 7ba669f935fe406adb8ef0f8536132605857c5a9 Mon Sep 17 00:00:00 2001 From: mateagluhak Date: Tue, 16 Dec 2025 10:28:12 +0100 Subject: [PATCH 08/17] Remove changelogs from CHANGELOG.md --- CHANGELOG.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05341414952..e4d2c661fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,6 @@ Main (unreleased) ### Enhancements -- Add `cache` block to `faro.receiver.sourcemaps` block to configure sourcemap caching behavior. The `cache` block contains three optional attributes: `ttl` to specify the duration after which unused sourcemaps are cleared from cache, `error_cleanup_interval` to specify the duration after which cached sourcemap errors are removed, and `cleanup_check_interval` to specify how often to check if any sourcemaps need to be cleared. (@mgluhak) - - update promtail converter to use `file_match` block for `loki.source.file` instead of going through `local.file_match`. (@kalleep) - Add `send_traceparent` option for `tracing` config to enable traceparent header propagation. (@MyDigitalLife) @@ -366,12 +364,6 @@ v1.11.0 - Update the `prometheus.exporter.process` component to get the `remove_empty_groups` option. (@dehaansa) - Remove unnecessary allocations in `stage.static_labels`. (@kalleep) - -- Add `cache_minimum_ttl` argument to `faro.receiver.sourcemaps` block to optionally specify the duration after which the cache map clears itself if sourcemap was not used for the specified duration (@mateagluhak) - -- Add `cache_error_cleanup_interval` argument to `faro.receiver.sourcemaps` block to specify the duration after which the cached sourcemap errors are removed from the cache (@mateagluhak) - -- Add `cache_cleanup_check_interval` argument to `faro.receiver.sourcemaps` block to specify how often to check if any sourcemaps need to be cleared from the cache (@mateagluhak) - Upgrade `beyla.ebpf` from Beyla version v2.2.5 to v2.5.8 The full list of changes can be found in the [Beyla release notes](https://github.com/grafana/beyla/releases/tag/v2.5.2) (@marctc) From 42adc6478c002aef8b4cd95970343adb7114af73 Mon Sep 17 00:00:00 2001 From: mateagluhak Date: Tue, 16 Dec 2025 10:35:12 +0100 Subject: [PATCH 09/17] Change "Ttl" to "TTL" --- internal/component/faro/receiver/arguments.go | 4 ++-- internal/component/faro/receiver/sourcemaps.go | 2 +- internal/component/faro/receiver/sourcemaps_test.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/component/faro/receiver/arguments.go b/internal/component/faro/receiver/arguments.go index 386c55b6ab1..e8a9c0ef7bb 100644 --- a/internal/component/faro/receiver/arguments.go +++ b/internal/component/faro/receiver/arguments.go @@ -93,14 +93,14 @@ func (s *SourceMapsArguments) SetToDefault() { // CacheArguments configures sourcemap caching behavior. type CacheArguments struct { - Ttl time.Duration `alloy:"ttl,attr,optional"` + TTL time.Duration `alloy:"ttl,attr,optional"` ErrorCleanupInterval time.Duration `alloy:"error_cleanup_interval,attr,optional"` CleanupCheckInterval time.Duration `alloy:"cleanup_check_interval,attr,optional"` } func (c *CacheArguments) SetToDefault() { *c = CacheArguments{ - Ttl: time.Duration(math.MaxInt64), + TTL: time.Duration(math.MaxInt64), ErrorCleanupInterval: time.Hour, CleanupCheckInterval: time.Second * 30, } diff --git a/internal/component/faro/receiver/sourcemaps.go b/internal/component/faro/receiver/sourcemaps.go index ca6c8a6132b..c2a63b11d8e 100644 --- a/internal/component/faro/receiver/sourcemaps.go +++ b/internal/component/faro/receiver/sourcemaps.go @@ -208,7 +208,7 @@ func (store *sourceMapsStoreImpl) CleanOldCacheEntries() { store.cacheMut.Lock() defer store.cacheMut.Unlock() - ttl := store.args.Cache.Ttl + ttl := store.args.Cache.TTL for key, cached := range store.cache { if cached != nil && cached.lastUsed.Before(store.timeSource.Now().Add(-ttl)) { srcUrl := strings.SplitN(key, "__", 2)[0] diff --git a/internal/component/faro/receiver/sourcemaps_test.go b/internal/component/faro/receiver/sourcemaps_test.go index 859e262ff36..e85a46f2bea 100644 --- a/internal/component/faro/receiver/sourcemaps_test.go +++ b/internal/component/faro/receiver/sourcemaps_test.go @@ -689,7 +689,7 @@ func TestSourceMapsStoreImpl_CleanCachedErrors(t *testing.T) { store := &sourceMapsStoreImpl{ log: logger, - args: SourceMapsArguments{Cache: &CacheArguments{Ttl: 5 * time.Minute}}, + args: SourceMapsArguments{Cache: &CacheArguments{TTL: 5 * time.Minute}}, metrics: metrics, cli: &mockHTTPClient{}, fs: newTestFileService(), @@ -771,7 +771,7 @@ func TestSourceMapsStoreImpl_CleanOldCachedEntries(t *testing.T) { store := &sourceMapsStoreImpl{ log: logger, - args: SourceMapsArguments{Cache: &CacheArguments{Ttl: tc.cacheTimeout}}, + args: SourceMapsArguments{Cache: &CacheArguments{TTL: tc.cacheTimeout}}, metrics: metrics, cli: &mockHTTPClient{}, fs: newTestFileService(), From b7e19245e96a3f1a6a745758659bcbcaf84eabe8 Mon Sep 17 00:00:00 2001 From: mateagluhak Date: Tue, 16 Dec 2025 12:12:17 +0100 Subject: [PATCH 10/17] Move Start and Stop functions to sourcemaps.go --- internal/component/faro/receiver/receiver.go | 81 +++---------------- .../component/faro/receiver/sourcemaps.go | 80 ++++++++++++++++++ 2 files changed, 91 insertions(+), 70 deletions(-) diff --git a/internal/component/faro/receiver/receiver.go b/internal/component/faro/receiver/receiver.go index 6e3c88a5173..b8e03237f89 100644 --- a/internal/component/faro/receiver/receiver.go +++ b/internal/component/faro/receiver/receiver.go @@ -25,11 +25,6 @@ func init() { }) } -type cleanupRoutines struct { - cancel context.CancelFunc - wg sync.WaitGroup -} - type Component struct { log log.Logger handler *handler @@ -40,9 +35,6 @@ type Component struct { argsMut sync.RWMutex args Arguments - cleanupMut sync.Mutex - cleanup *cleanupRoutines - metrics *metricsExporter logs *logsExporter traces *tracesExporter @@ -101,7 +93,6 @@ func (c *Component) Run(ctx context.Context) error { if cancelCurrentActor != nil { cancelCurrentActor() } - c.stopCleanup() }() for { @@ -140,6 +131,15 @@ func (c *Component) Update(args component.Arguments) error { c.handler.Update(newArgs.Server) + // Stop old store's cleanup if there is one + c.lazySourceMaps.mut.RLock() + if oldStore := c.lazySourceMaps.inner; oldStore != nil { + if impl, ok := oldStore.(*sourceMapsStoreImpl); ok { + impl.Stop() + } + } + c.lazySourceMaps.mut.RUnlock() + innerStore := newSourceMapsStore( log.With(c.log, "subcomponent", "handler"), newArgs.SourceMaps, @@ -149,8 +149,8 @@ func (c *Component) Update(args component.Arguments) error { ) c.lazySourceMaps.SetInner(innerStore) - c.stopCleanup() - c.startCleanup(newArgs, innerStore) + // Start cleanup for new store + innerStore.Start() c.logs.SetReceivers(newArgs.Output.Logs) c.traces.SetConsumers(newArgs.Output.Traces) @@ -256,62 +256,3 @@ func (vs *varSourceMapsStore) SetInner(inner sourceMapsStore) { vs.inner = inner } - -func (c *Component) stopCleanup() { - c.cleanupMut.Lock() - defer c.cleanupMut.Unlock() - if c.cleanup != nil { - c.cleanup.cancel() // signal goroutines to exit - c.cleanup.wg.Wait() // wait for them - c.cleanup = nil - } -} - -func (c *Component) startCleanup(args Arguments, s *sourceMapsStoreImpl) { - c.cleanupMut.Lock() - defer c.cleanupMut.Unlock() - - cleanupCtx, cleanupCancel := context.WithCancel(context.Background()) - cr := &cleanupRoutines{cancel: cleanupCancel} - - // Get cache config or use defaults if not specified - var cacheConfig = *args.SourceMaps.Cache - - if d := cacheConfig.CleanupCheckInterval; d > 0 { - cr.wg.Add(1) - go func(interval time.Duration) { - defer cr.wg.Done() - s.CleanOldCacheEntries() - ticker := time.NewTicker(interval) - defer ticker.Stop() - for { - select { - case <-cleanupCtx.Done(): - return - case <-ticker.C: - s.CleanOldCacheEntries() - } - } - }(d) - } - - if d := cacheConfig.ErrorCleanupInterval; d > 0 { - cr.wg.Add(1) - go func(interval time.Duration) { - defer cr.wg.Done() - s.CleanCachedErrors() - ticker := time.NewTicker(interval) - defer ticker.Stop() - for { - select { - case <-cleanupCtx.Done(): - return - case <-ticker.C: - s.CleanCachedErrors() - } - } - }(d) - } - - c.cleanup = cr -} diff --git a/internal/component/faro/receiver/sourcemaps.go b/internal/component/faro/receiver/sourcemaps.go index c2a63b11d8e..3103ba19927 100644 --- a/internal/component/faro/receiver/sourcemaps.go +++ b/internal/component/faro/receiver/sourcemaps.go @@ -2,6 +2,7 @@ package receiver import ( "bytes" + "context" "fmt" "io" "io/fs" @@ -121,6 +122,12 @@ type sourceMapsStoreImpl struct { cacheMut sync.Mutex cache map[string]*cachedSourceMap timeSource timeSource + + cleanupMut sync.Mutex + cleanupCtx context.Context + cleanupCancel context.CancelFunc + cleanupWg sync.WaitGroup + isStarted bool } type cachedSourceMap struct { @@ -230,6 +237,79 @@ func (store *sourceMapsStoreImpl) CleanCachedErrors() { } } +// Start begins the cleanup routines based on configured cache intervals. +func (store *sourceMapsStoreImpl) Start() { + store.cleanupMut.Lock() + defer store.cleanupMut.Unlock() + + if store.isStarted { + return + } + store.isStarted = true + + cacheConfig := store.args.Cache + if cacheConfig == nil { + return + } + + store.cleanupCtx, store.cleanupCancel = context.WithCancel(context.Background()) + + if d := cacheConfig.CleanupCheckInterval; d > 0 { + store.cleanupWg.Add(1) + go func(interval time.Duration) { + defer store.cleanupWg.Done() + store.CleanOldCacheEntries() + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-store.cleanupCtx.Done(): + return + case <-ticker.C: + store.CleanOldCacheEntries() + } + } + }(d) + } + + if d := cacheConfig.ErrorCleanupInterval; d > 0 { + store.cleanupWg.Add(1) + go func(interval time.Duration) { + defer store.cleanupWg.Done() + store.CleanCachedErrors() + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-store.cleanupCtx.Done(): + return + case <-ticker.C: + store.CleanCachedErrors() + } + } + }(d) + } +} + +// Stop terminates all cleanup goroutines and waits for them to finish. +func (store *sourceMapsStoreImpl) Stop() { + store.cleanupMut.Lock() + defer store.cleanupMut.Unlock() + + if !store.isStarted { + return + } + store.isStarted = false + + if store.cleanupCancel != nil { + store.cleanupCancel() + store.cleanupCancel = nil + } + + store.cleanupWg.Wait() + store.cleanupCtx = nil +} + func (store *sourceMapsStoreImpl) getSourceMapContent(sourceURL string, release string) (content []byte, sourceMapURL string, err error) { // Attempt to find the source map in the filesystem first. for _, loc := range store.locs { From 603ad629eddef89e4561312ada2a47ceb36fdd42 Mon Sep 17 00:00:00 2001 From: mateagluhak Date: Wed, 24 Dec 2025 11:02:16 +0100 Subject: [PATCH 11/17] Remove cleanupMut and consolidate to RWMutex, add Start/Stop to sourceMapsStore interface --- internal/component/faro/receiver/receiver.go | 28 +++++++++++++------ .../component/faro/receiver/sourcemaps.go | 18 ++++++------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/internal/component/faro/receiver/receiver.go b/internal/component/faro/receiver/receiver.go index b8e03237f89..441d7dc4fad 100644 --- a/internal/component/faro/receiver/receiver.go +++ b/internal/component/faro/receiver/receiver.go @@ -132,13 +132,7 @@ func (c *Component) Update(args component.Arguments) error { c.handler.Update(newArgs.Server) // Stop old store's cleanup if there is one - c.lazySourceMaps.mut.RLock() - if oldStore := c.lazySourceMaps.inner; oldStore != nil { - if impl, ok := oldStore.(*sourceMapsStoreImpl); ok { - impl.Stop() - } - } - c.lazySourceMaps.mut.RUnlock() + c.lazySourceMaps.Stop() innerStore := newSourceMapsStore( log.With(c.log, "subcomponent", "handler"), @@ -150,7 +144,7 @@ func (c *Component) Update(args component.Arguments) error { c.lazySourceMaps.SetInner(innerStore) // Start cleanup for new store - innerStore.Start() + c.lazySourceMaps.Start() c.logs.SetReceivers(newArgs.Output.Logs) c.traces.SetConsumers(newArgs.Output.Traces) @@ -256,3 +250,21 @@ func (vs *varSourceMapsStore) SetInner(inner sourceMapsStore) { vs.inner = inner } + +func (vs *varSourceMapsStore) Start() { + vs.mut.RLock() + defer vs.mut.RUnlock() + + if vs.inner != nil { + vs.inner.Start() + } +} + +func (vs *varSourceMapsStore) Stop() { + vs.mut.RLock() + defer vs.mut.RUnlock() + + if vs.inner != nil { + vs.inner.Stop() + } +} diff --git a/internal/component/faro/receiver/sourcemaps.go b/internal/component/faro/receiver/sourcemaps.go index 3103ba19927..cc0c94c439e 100644 --- a/internal/component/faro/receiver/sourcemaps.go +++ b/internal/component/faro/receiver/sourcemaps.go @@ -30,6 +30,8 @@ import ( // transforming minified source locations to the original source location. type sourceMapsStore interface { GetSourceMap(sourceURL string, release string) (*sourcemap.Consumer, error) + Start() + Stop() } // Stub interfaces for easier mocking. @@ -119,11 +121,9 @@ type sourceMapsStoreImpl struct { metrics *sourceMapMetrics locs []*sourcemapFileLocation - cacheMut sync.Mutex - cache map[string]*cachedSourceMap - timeSource timeSource - - cleanupMut sync.Mutex + cacheMut sync.RWMutex + cache map[string]*cachedSourceMap + timeSource timeSource cleanupCtx context.Context cleanupCancel context.CancelFunc cleanupWg sync.WaitGroup @@ -239,8 +239,8 @@ func (store *sourceMapsStoreImpl) CleanCachedErrors() { // Start begins the cleanup routines based on configured cache intervals. func (store *sourceMapsStoreImpl) Start() { - store.cleanupMut.Lock() - defer store.cleanupMut.Unlock() + store.cacheMut.Lock() + defer store.cacheMut.Unlock() if store.isStarted { return @@ -293,8 +293,8 @@ func (store *sourceMapsStoreImpl) Start() { // Stop terminates all cleanup goroutines and waits for them to finish. func (store *sourceMapsStoreImpl) Stop() { - store.cleanupMut.Lock() - defer store.cleanupMut.Unlock() + store.cacheMut.Lock() + defer store.cacheMut.Unlock() if !store.isStarted { return From b53386e054eea06c3d992bfc8d5a908810326f33 Mon Sep 17 00:00:00 2001 From: mateagluhak <116516597+mateagluhak@users.noreply.github.com> Date: Wed, 7 Jan 2026 09:17:07 +0100 Subject: [PATCH 12/17] Update docs/sources/reference/components/faro/faro.receiver.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> --- docs/sources/reference/components/faro/faro.receiver.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/reference/components/faro/faro.receiver.md b/docs/sources/reference/components/faro/faro.receiver.md index 54621bb96b6..08d2303de50 100644 --- a/docs/sources/reference/components/faro/faro.receiver.md +++ b/docs/sources/reference/components/faro/faro.receiver.md @@ -177,9 +177,9 @@ All cache settings are optional with sensible defaults. | Name | Type | Description | Default | Required | | ------------------------ | ---------- | ---------------------------------------------------------------------------------- | ------- | -------- | -| `ttl` | `duration` | Duration after which source map is deleted from cache if not used. | `inf` | no | -| `error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried. | `"1h"` | no | | `cleanup_check_interval` | `duration` | How often cached sourcemaps are checked for cleanup. | `"30s"` | no | +| `error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried. | `"1h"` | no | +| `ttl` | `duration` | Duration after which source map is deleted from cache if not used. | `inf` | no | By default, sourcemaps are held in memory indefinitely. You can set `ttl` to clear sourcemaps that aren't used during the specified duration. From b4c01327612849b5f4bfbfcca7cc49cf25f79414 Mon Sep 17 00:00:00 2001 From: mateagluhak Date: Mon, 19 Jan 2026 13:29:05 +0100 Subject: [PATCH 13/17] Change RWMutex to Mutex in sourcemap cache --- internal/component/faro/receiver/sourcemaps.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/component/faro/receiver/sourcemaps.go b/internal/component/faro/receiver/sourcemaps.go index cc0c94c439e..2b1dd6e862e 100644 --- a/internal/component/faro/receiver/sourcemaps.go +++ b/internal/component/faro/receiver/sourcemaps.go @@ -121,7 +121,7 @@ type sourceMapsStoreImpl struct { metrics *sourceMapMetrics locs []*sourcemapFileLocation - cacheMut sync.RWMutex + cacheMut sync.Mutex cache map[string]*cachedSourceMap timeSource timeSource cleanupCtx context.Context From 7593b1c91a2668c0cb297f0fd2a595f67b8fea38 Mon Sep 17 00:00:00 2001 From: mateagluhak <116516597+mateagluhak@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:45:37 +0100 Subject: [PATCH 14/17] Update docs/sources/reference/components/faro/faro.receiver.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> --- docs/sources/reference/components/faro/faro.receiver.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/sources/reference/components/faro/faro.receiver.md b/docs/sources/reference/components/faro/faro.receiver.md index 08d2303de50..3f8097117d6 100644 --- a/docs/sources/reference/components/faro/faro.receiver.md +++ b/docs/sources/reference/components/faro/faro.receiver.md @@ -173,7 +173,6 @@ When `location` blocks are provided, they're checked first for sourcemaps before #### `cache` The `cache` block configures sourcemap caching behavior. -All cache settings are optional with sensible defaults. | Name | Type | Description | Default | Required | | ------------------------ | ---------- | ---------------------------------------------------------------------------------- | ------- | -------- | From 829a19a063e1e928f77f89b267925cc1997b43b0 Mon Sep 17 00:00:00 2001 From: mateagluhak <116516597+mateagluhak@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:45:56 +0100 Subject: [PATCH 15/17] Update docs/sources/reference/components/faro/faro.receiver.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> --- .../sources/reference/components/faro/faro.receiver.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources/reference/components/faro/faro.receiver.md b/docs/sources/reference/components/faro/faro.receiver.md index 3f8097117d6..223629b1c51 100644 --- a/docs/sources/reference/components/faro/faro.receiver.md +++ b/docs/sources/reference/components/faro/faro.receiver.md @@ -174,11 +174,11 @@ When `location` blocks are provided, they're checked first for sourcemaps before The `cache` block configures sourcemap caching behavior. -| Name | Type | Description | Default | Required | -| ------------------------ | ---------- | ---------------------------------------------------------------------------------- | ------- | -------- | -| `cleanup_check_interval` | `duration` | How often cached sourcemaps are checked for cleanup. | `"30s"` | no | -| `error_cleanup_interval` | `duration` | Duration after which the download of source map that previously failed is retried. | `"1h"` | no | -| `ttl` | `duration` | Duration after which source map is deleted from cache if not used. | `inf` | no | +| Name | Type | Description | Default | Required | +| ------------------------ | ---------- | ----------------------------------------------------------------------------------------- | ------- | -------- | +| `cleanup_check_interval` | `duration` | How often {{< param "PRODUCT_NAME" >}} checks cached sourcemaps for cleanup. | `"30s"` | no | +| `error_cleanup_interval` | `duration` | How long {{< param "PRODUCT_NAME" >}} waits before retrying a failed source map download. | `"1h"` | no | +| `ttl` | `duration` | How long {{< param "PRODUCT_NAME" >}} keeps an unused source map in the cache. | `inf` | no | By default, sourcemaps are held in memory indefinitely. You can set `ttl` to clear sourcemaps that aren't used during the specified duration. From 9bbfa05540f05850ee01cd6d8f670e0872919ca9 Mon Sep 17 00:00:00 2001 From: mateagluhak <116516597+mateagluhak@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:46:09 +0100 Subject: [PATCH 16/17] Update docs/sources/reference/components/faro/faro.receiver.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> --- .../reference/components/faro/faro.receiver.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sources/reference/components/faro/faro.receiver.md b/docs/sources/reference/components/faro/faro.receiver.md index 223629b1c51..20e94799314 100644 --- a/docs/sources/reference/components/faro/faro.receiver.md +++ b/docs/sources/reference/components/faro/faro.receiver.md @@ -180,14 +180,14 @@ The `cache` block configures sourcemap caching behavior. | `error_cleanup_interval` | `duration` | How long {{< param "PRODUCT_NAME" >}} waits before retrying a failed source map download. | `"1h"` | no | | `ttl` | `duration` | How long {{< param "PRODUCT_NAME" >}} keeps an unused source map in the cache. | `inf` | no | -By default, sourcemaps are held in memory indefinitely. -You can set `ttl` to clear sourcemaps that aren't used during the specified duration. +By default, {{< param "PRODUCT_NAME" >}} keeps sourcemaps in memory indefinitely. +Set `ttl` to remove sourcemaps that are not accessed within the specified duration. -By default, if there's an error while downloading or parsing a sourcemap, the error is cached. -After the duration specified by `error_cleanup_interval`, all errors are cleared from the cache. +{{< param "PRODUCT_NAME" >}} caches errors that occur while downloading or parsing a sourcemap. +Use `error_cleanup_interval` to control how long these errors remain cached. -By default, cached sourcemaps are checked for cleanup every 30 seconds. -You can modify the frequency by setting the `cleanup_check_interval` argument. +Cached sourcemaps are checked for cleanup every 30 seconds by default. +Set `cleanup_check_interval` to adjust this frequency. #### `location` From dd460f56e8e49f27323ce4ade9615b39743094e1 Mon Sep 17 00:00:00 2001 From: mateagluhak Date: Wed, 21 Jan 2026 14:54:11 +0100 Subject: [PATCH 17/17] Remove unnecessary whitespaces --- internal/component/faro/receiver/sourcemaps.go | 1 - internal/component/faro/receiver/sourcemaps_test.go | 3 --- 2 files changed, 4 deletions(-) diff --git a/internal/component/faro/receiver/sourcemaps.go b/internal/component/faro/receiver/sourcemaps.go index 2b1dd6e862e..f6ff7214b7d 100644 --- a/internal/component/faro/receiver/sourcemaps.go +++ b/internal/component/faro/receiver/sourcemaps.go @@ -177,7 +177,6 @@ func newSourceMapsStore(log log.Logger, args SourceMapsArguments, metrics *sourc } func (store *sourceMapsStoreImpl) GetSourceMap(sourceURL string, release string) (*sourcemap.Consumer, error) { - store.cacheMut.Lock() defer store.cacheMut.Unlock() diff --git a/internal/component/faro/receiver/sourcemaps_test.go b/internal/component/faro/receiver/sourcemaps_test.go index e85a46f2bea..b19eec69350 100644 --- a/internal/component/faro/receiver/sourcemaps_test.go +++ b/internal/component/faro/receiver/sourcemaps_test.go @@ -683,7 +683,6 @@ func TestSourceMapsStoreImpl_CleanCachedErrors(t *testing.T) { logger := util.TestLogger(t) for _, tc := range tt { - reg := prometheus.NewRegistry() metrics := newSourceMapMetrics(reg) @@ -765,7 +764,6 @@ func TestSourceMapsStoreImpl_CleanOldCachedEntries(t *testing.T) { logger := util.TestLogger(t) for _, tc := range tt { - reg := prometheus.NewRegistry() metrics := newSourceMapMetrics(reg) @@ -877,5 +875,4 @@ func newTestFileService() *testFileService { stats: make([]string, 0), reads: make([]string, 0), } - }