Skip to content

Commit e6b87b9

Browse files
authored
harden switching extension components by renaming the prefix (#3015)
* harden switching extension components by renaming the prefix * forgot to commit consistency change * fix tests * forgot to remove old lines in test file * add tests * update docs and improve loop on waiting for isClosed
1 parent 028fcd6 commit e6b87b9

File tree

13 files changed

+246
-27
lines changed

13 files changed

+246
-27
lines changed

extension/badgerextension/README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The Badger Extension provides persistent storage for OpenTelemetry Collector com
77
| Field | Type | Default | Required | Description |
88
| ------------------------------------- | -------- | ----------------- | -------- | -------------------------------------------------------------------------------------------- |
99
| directory.path | string | | `true` | Directory path where BadgerDB files will be stored. Each component gets a subdirectory. |
10+
| directory.path_prefix | string | `badger` | `false` | Optional prefix added to component directory names. Prevents naming collisions when multiple storage extensions share the same directory. Set to empty string to disable. |
1011
| sync_writes | bool | `true` | `false` | Whether to sync writes to disk immediately. `false` survives process crashes via mmap. |
1112
| memory.table_size | int64 | 67108864 (64MB) | `false` | Size of each memtable in bytes. Larger values improve write performance but use more memory. |
1213
| memory.block_cache_size | int64 | 268435456 (256MB) | `false` | Size of block cache in bytes. Larger values improve read performance but use more memory. |
@@ -89,15 +90,35 @@ service:
8990

9091
Each component that uses the badger extension gets an isolated database instance at:
9192

93+
```
94+
{directory.path}/{path_prefix}_{kind}_{type}_{component_name}_{name}/
95+
```
96+
97+
When `directory.path_prefix` is not set or is empty, the format becomes:
98+
9299
```
93100
{directory.path}/{kind}_{type}_{component_name}_{name}/
94101
```
95102

96-
For example, with `directory.path: $OIQ_OTEL_COLLECTOR_STORAGE`:
103+
For example, with `directory.path: $OIQ_OTEL_COLLECTOR_STORAGE` and default prefix:
97104

98105
```
99-
$OIQ_OTEL_COLLECTOR_STORAGE/processor_batch_default/
100-
$OIQ_OTEL_COLLECTOR_STORAGE/exporter_otlp_backup/
106+
$OIQ_OTEL_COLLECTOR_STORAGE/badger_processor_batch_default/
107+
$OIQ_OTEL_COLLECTOR_STORAGE/badger_exporter_otlp_backup/
108+
```
109+
110+
When sharing a storage directory with other extensions, the prefix prevents naming collisions:
111+
112+
```yaml
113+
extensions:
114+
badger:
115+
directory:
116+
path: /var/lib/otelcol/storage
117+
path_prefix: badger
118+
pebble:
119+
directory:
120+
path: /var/lib/otelcol/storage
121+
path_prefix: pebble
101122
```
102123

103-
This ensures components cannot interfere with each other's data.
124+
This ensures components cannot interfere with each other's data, even when using multiple storage backends in the same directory.

extension/badgerextension/config.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ type Config struct {
3232

3333
// DirectoryConfig configures the file storage parameters for the badger storage extension
3434
type DirectoryConfig struct {
35-
Path string `mapstructure:"path"`
36-
_ struct{} // prevent unkeyed literal initialization
35+
Path string `mapstructure:"path"`
36+
PathPrefix string `mapstructure:"path_prefix"`
37+
_ struct{} // prevent unkeyed literal initialization
3738
}
3839

3940
// BlobGarbageCollectionConfig configures the blob garbage collection

extension/badgerextension/extension.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package badgerextension
1616

1717
import (
1818
"context"
19+
"errors"
1920
"fmt"
2021
"path/filepath"
2122
"strings"
@@ -75,6 +76,10 @@ func (b *badgerExtension) GetClient(_ context.Context, kind component.Kind, ent
7576
}
7677
fullName = strings.ReplaceAll(fullName, " ", "")
7778

79+
if b.cfg.Directory.PathPrefix != "" {
80+
fullName = fmt.Sprintf("%s_%s", b.cfg.Directory.PathPrefix, fullName)
81+
}
82+
7883
if b.clients != nil {
7984
b.clientsMutex.RLock()
8085
client, ok := b.clients[fullName]
@@ -185,13 +190,21 @@ func (b *badgerExtension) monitor(ctx context.Context) {
185190
case <-ctx.Done():
186191
return
187192
case <-ticker.C:
188-
b.mb.ExtensionStorageClientCount.Record(ctx, int64(len(b.clients)))
189193
b.clientsMutex.RLock()
194+
clientCount := len(b.clients)
195+
// make a shallow copy of the clients under lock so we can process them outside of the lock
196+
clients := make([]client.Client, 0, clientCount)
190197
for _, c := range b.clients {
198+
clients = append(clients, c)
199+
}
200+
b.clientsMutex.RUnlock()
201+
202+
b.mb.ExtensionStorageClientCount.Record(ctx, int64(clientCount))
203+
// process clients outside of lock to avoid blocking other operations
204+
for _, c := range clients {
191205
diskUsage, err := c.GetDiskUsage()
192206
if err != nil {
193207
b.logger.Error("failed to get disk usage", zap.Error(err))
194-
b.clientsMutex.RUnlock()
195208
continue
196209
}
197210

@@ -213,7 +226,6 @@ func (b *badgerExtension) monitor(ctx context.Context) {
213226
b.otelAttrs,
214227
metric.WithAttributes(attribute.String(internal.OperationTypeAttribute, internal.OperationTypeDelete)))
215228
}
216-
b.clientsMutex.RUnlock()
217229
}
218230
}
219231
}
@@ -248,12 +260,13 @@ func (b *badgerExtension) Shutdown(ctx context.Context) error {
248260
b.clientsMutex.Lock()
249261
defer b.clientsMutex.Unlock()
250262

263+
var shutdownErrors error
251264
for _, c := range b.clients {
252265
if err := c.Close(ctx); err != nil {
253-
return fmt.Errorf("failed to close badger client: %w", err)
266+
shutdownErrors = errors.Join(shutdownErrors, fmt.Errorf("failed to close badger client: %w", err))
254267
}
255268
}
256-
return nil
269+
return shutdownErrors
257270
}
258271

259272
func kindString(k component.Kind) string {

extension/badgerextension/extension_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,66 @@ func TestBadgerExtension_ClientOptions(t *testing.T) {
468468
})
469469
}
470470

471+
func TestBadgerExtension_PathPrefix(t *testing.T) {
472+
t.Run("applies path prefix to client names", func(t *testing.T) {
473+
logger := zap.NewNop()
474+
cfg := &Config{
475+
Directory: &DirectoryConfig{
476+
Path: t.TempDir(),
477+
PathPrefix: "badger",
478+
},
479+
}
480+
481+
ext := newBadgerExtension(logger, cfg, componenttest.NewNopTelemetrySettings(), testID).(*badgerExtension)
482+
483+
client, err := ext.GetClient(
484+
context.Background(),
485+
component.KindExporter,
486+
component.MustNewID("otlp"),
487+
"logs",
488+
)
489+
require.NoError(t, err)
490+
require.NotNil(t, client)
491+
492+
// Verify that the client is stored with the prefixed key
493+
assert.Len(t, ext.clients, 1)
494+
for key := range ext.clients {
495+
assert.Contains(t, key, "badger_")
496+
}
497+
498+
err = ext.Shutdown(context.Background())
499+
require.NoError(t, err)
500+
})
501+
502+
t.Run("empty path prefix works without prefix", func(t *testing.T) {
503+
logger := zap.NewNop()
504+
cfg := &Config{
505+
Directory: &DirectoryConfig{
506+
Path: t.TempDir(),
507+
PathPrefix: "",
508+
},
509+
}
510+
require.NoError(t, cfg.Validate())
511+
ext := newBadgerExtension(logger, cfg, componenttest.NewNopTelemetrySettings(), testID).(*badgerExtension)
512+
513+
client, err := ext.GetClient(
514+
context.Background(),
515+
component.KindExporter,
516+
component.MustNewID("otlp"),
517+
"logs",
518+
)
519+
require.NoError(t, err)
520+
require.NotNil(t, client)
521+
522+
// With empty prefix, the key should not have underscore prefix pattern
523+
assert.Len(t, ext.clients, 1)
524+
525+
// Cleanup
526+
err = ext.Shutdown(context.Background())
527+
require.NoError(t, err)
528+
})
529+
}
530+
471531
func TestKindString(t *testing.T) {
472532
tests := []struct {
473533
name string

extension/badgerextension/factory.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ func NewFactory() extension.Factory {
3939
func createDefaultConfig() component.Config {
4040
return &Config{
4141
SyncWrites: true,
42+
Directory: &DirectoryConfig{
43+
PathPrefix: "badger",
44+
},
4245
Memory: &MemoryConfig{
4346
TableSize: 64 * 1024 * 1024, // Default: 64MB
4447
BlockCacheSize: 256 * 1024 * 1024, // Default: 256MB

extension/badgerextension/factory_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ func TestCreateDefaultConfig(t *testing.T) {
3333

3434
// Verify default values
3535
require.True(t, badgerCfg.SyncWrites)
36-
require.Nil(t, badgerCfg.Directory)
36+
require.NotNil(t, badgerCfg.Directory)
37+
require.Empty(t, badgerCfg.Directory.Path)
38+
require.Equal(t, "badger", badgerCfg.Directory.PathPrefix)
3739

3840
require.NotNil(t, badgerCfg.Memory)
3941
require.Equal(t, int64(64*1024*1024), badgerCfg.Memory.TableSize)

extension/badgerextension/internal/client/client.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"errors"
2121
"fmt"
2222
"sync/atomic"
23+
"time"
2324

2425
"github.com/dgraph-io/badger/v4"
2526
"go.opentelemetry.io/collector/extension/xextension/storage"
@@ -184,8 +185,35 @@ func (c *client) Batch(ctx context.Context, ops ...*storage.Operation) error {
184185
return nil
185186
}
186187

187-
func (c *client) Close(_ context.Context) error {
188-
return c.db.Close()
188+
func (c *client) Close(ctx context.Context) error {
189+
err := c.db.Close()
190+
if err != nil {
191+
return fmt.Errorf("failed to close badger client: %w", err)
192+
}
193+
194+
isFullyClosed := make(chan struct{})
195+
go func() {
196+
ticker := time.NewTicker(100 * time.Millisecond)
197+
defer ticker.Stop()
198+
defer close(isFullyClosed)
199+
for {
200+
select {
201+
case <-ctx.Done():
202+
return
203+
case <-ticker.C:
204+
if c.db.IsClosed() {
205+
return
206+
}
207+
}
208+
}
209+
}()
210+
211+
select {
212+
case <-ctx.Done():
213+
return ctx.Err()
214+
case <-isFullyClosed:
215+
return nil
216+
}
189217
}
190218

191219
func (c *client) RunValueLogGC(discardRatio float64) error {

extension/pebbleextension/README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ The Pebble Extension provides persistent storage for OpenTelemetry Collector com
44

55
## Configuration
66

7-
| Field | Type | Default | Required | Description |
8-
| ------------------ | -------- | ------- | -------- | ------------------------------------------------------------------------------------------------- |
9-
| directory.path | string | | `true` | Directory path where Pebble database files will be stored. Each component gets a subdirectory. |
10-
| cache.size | int64 | `0` | `false` | Size in bytes of the block cache. When 0, uses Pebble's default cache behavior. Larger values improve read performance at the cost of memory usage. |
11-
| sync | bool | `false` | `false` | Whether to sync writes to disk immediately. `false` provides better performance while still being durable. |
7+
| Field | Type | Default | Required | Description |
8+
| ------------------ | -------- | --------- | -------- | ------------------------------------------------------------------------------------------------- |
9+
| directory.path | string | | `true` | Directory path where Pebble database files will be stored. Each component gets a subdirectory. |
10+
| directory.path_prefix | string | `pebble` | `false` | Optional prefix added to component directory names. Prevents naming collisions when multiple storage extensions share the same directory. Set to empty string to disable. |
11+
| cache.size | int64 | `0` | `false` | Size in bytes of the block cache. When 0, uses Pebble's default cache behavior. Larger values improve read performance at the cost of memory usage. |
12+
| sync | bool | `true` | `false` | Whether to sync writes to disk immediately. `true` provides safer durability guarantees. |
1213

1314
## Example Configuration
1415

@@ -71,15 +72,35 @@ service:
7172

7273
Each component that uses the pebble extension gets an isolated database instance at:
7374

75+
```
76+
{directory.path}/{path_prefix}_{kind}_{type}_{component_name}_{name}/
77+
```
78+
79+
When `directory.path_prefix` is not set or is empty, the format becomes:
80+
7481
```
7582
{directory.path}/{kind}_{type}_{component_name}_{name}/
7683
```
7784

78-
For example, with `directory.path: $OIQ_OTEL_COLLECTOR_STORAGE`:
85+
For example, with `directory.path: $OIQ_OTEL_COLLECTOR_STORAGE` and default prefix:
7986

8087
```
81-
$OIQ_OTEL_COLLECTOR_STORAGE/processor_batch_default/
82-
$OIQ_OTEL_COLLECTOR_STORAGE/exporter_otlp_backup/
88+
$OIQ_OTEL_COLLECTOR_STORAGE/pebble_processor_batch_default/
89+
$OIQ_OTEL_COLLECTOR_STORAGE/pebble_exporter_otlp_backup/
90+
```
91+
92+
When sharing a storage directory with other extensions, the prefix prevents naming collisions:
93+
94+
```yaml
95+
extensions:
96+
badger:
97+
directory:
98+
path: /var/lib/otelcol/storage
99+
path_prefix: badger
100+
pebble:
101+
directory:
102+
path: /var/lib/otelcol/storage
103+
path_prefix: pebble
83104
```
84105

85-
This ensures components cannot interfere with each other's data.
106+
This ensures components cannot interfere with each other's data, even when using multiple storage backends in the same directory.

extension/pebbleextension/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ type Config struct {
2626

2727
// DirectoryConfig is the configuration for the directory
2828
type DirectoryConfig struct {
29-
Path string `mapstructure:"path"`
29+
Path string `mapstructure:"path"`
30+
PathPrefix string `mapstructure:"path_prefix"`
3031
}
3132

3233
// CacheConfig is the configuration for the cache

extension/pebbleextension/extension.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ func (p *pebbleExtension) GetClient(_ context.Context, kind component.Kind, ent
5656
}
5757
fullName = strings.ReplaceAll(fullName, " ", "")
5858

59+
if p.cfg.Directory.PathPrefix != "" {
60+
fullName = fmt.Sprintf("%s_%s", p.cfg.Directory.PathPrefix, fullName)
61+
}
62+
5963
if p.clients != nil {
6064
p.clientsMutex.RLock()
6165
client, ok := p.clients[fullName]

0 commit comments

Comments
 (0)