diff --git a/.github/workflows/ci-badger.yaml b/.github/workflows/ci-badger.yaml index 4f43057ce6d..b6b7cd22f32 100644 --- a/.github/workflows/ci-badger.yaml +++ b/.github/workflows/ci-badger.yaml @@ -18,6 +18,9 @@ permissions: # added using https://github.com/step-security/secure-workflows jobs: badger: runs-on: ubuntu-latest + strategy: + matrix: + version: [v1, v2] steps: - name: Harden Runner uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 @@ -31,7 +34,16 @@ jobs: go-version: 1.22.x - name: Run Badger storage integration tests - run: make badger-storage-integration-test + run: | + case ${{ matrix.version }} in + v1) + make badger-storage-integration-test + ;; + v2) + STORAGE=badger \ + make jaeger-v2-storage-integration-test + ;; + esac - name: Setup CODECOV_TOKEN uses: ./.github/actions/setup-codecov diff --git a/cmd/jaeger/badger_config.yaml b/cmd/jaeger/badger_config.yaml index 950be451250..4643c9cc75a 100644 --- a/cmd/jaeger/badger_config.yaml +++ b/cmd/jaeger/badger_config.yaml @@ -20,12 +20,14 @@ extensions: ephemeral: false maintenance_interval: 5 metrics_update_interval: 10 + span_store_ttl: 72h badger_archive: directory_key: "/tmp/jaeger_archive/" directory_value: "/tmp/jaeger_archive/" ephemeral: false maintenance_interval: 5 metrics_update_interval: 10 + span_store_ttl: 720h receivers: otlp: diff --git a/cmd/jaeger/internal/components.go b/cmd/jaeger/internal/components.go index cf9b10a9e71..c7b9957dbd8 100644 --- a/cmd/jaeger/internal/components.go +++ b/cmd/jaeger/internal/components.go @@ -29,6 +29,7 @@ import ( "github.com/jaegertracing/jaeger/cmd/jaeger/internal/exporters/storageexporter" "github.com/jaegertracing/jaeger/cmd/jaeger/internal/extension/jaegerquery" "github.com/jaegertracing/jaeger/cmd/jaeger/internal/extension/jaegerstorage" + "github.com/jaegertracing/jaeger/cmd/jaeger/internal/integration/storagecleaner" ) type builders struct { @@ -60,6 +61,7 @@ func (b builders) build() (otelcol.Factories, error) { // add-ons jaegerquery.NewFactory(), jaegerstorage.NewFactory(), + storagecleaner.NewFactory(), // TODO add adaptive sampling ) if err != nil { diff --git a/cmd/jaeger/internal/integration/badger_test.go b/cmd/jaeger/internal/integration/badger_test.go new file mode 100644 index 00000000000..877fb79c0f1 --- /dev/null +++ b/cmd/jaeger/internal/integration/badger_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2024 The Jaeger Authors. +// SPDX-License-Identifier: Apache-2.0 + +package integration + +import ( + "fmt" + "net/http" + "os" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "github.com/jaegertracing/jaeger/cmd/jaeger/internal/integration/storagecleaner" + "github.com/jaegertracing/jaeger/plugin/storage/integration" +) + +type BadgerStorageIntegration struct { + E2EStorageIntegration + logger *zap.Logger +} + +func (s *BadgerStorageIntegration) initialize(t *testing.T) { + s.e2eInitialize(t) + + s.CleanUp = s.cleanUp + s.logger = zap.NewNop() +} + +func (s *BadgerStorageIntegration) cleanUp(t *testing.T) { + Addr := fmt.Sprintf("http://%s:%s%s", "0.0.0.0", storagecleaner.Port, storagecleaner.URL) + r, err := http.NewRequest(http.MethodPost, Addr, nil) + require.NoError(t, err) + + client := &http.Client{} + + resp, err := client.Do(r) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestBadgerStorage(t *testing.T) { + integration.SkipUnlessEnv(t, "badger") + + s := &BadgerStorageIntegration{ + E2EStorageIntegration: E2EStorageIntegration{ + ConfigFile: createBadgerCleanerConfig(t), + StorageIntegration: integration.StorageIntegration{ + SkipBinaryAttrs: true, + SkipArchiveTest: true, + + // TODO: remove this once badger supports returning spanKind from GetOperations + // Cf https://github.com/jaegertracing/jaeger/issues/1922 + GetOperationsMissingSpanKind: true, + }, + }, + } + s.initialize(t) + t.Cleanup(func() { + s.e2eCleanUp(t) + }) + s.RunAll(t) +} + +func createBadgerCleanerConfig(t *testing.T) string { + cmd := exec.Command("../../../../scripts/prepare-badger-integration-tests.py") + data, err := cmd.Output() + require.NoError(t, err) + tempFile := string(data) + tempFile = strings.ReplaceAll(tempFile, "\n", "") + t.Cleanup(func() { + os.Remove(tempFile) + }) + return tempFile +} diff --git a/cmd/jaeger/internal/integration/receivers/storagereceiver/testdata/config.yaml b/cmd/jaeger/internal/integration/receivers/storagereceiver/testdata/config.yaml index a7c0da9218d..e590e8f1694 100644 --- a/cmd/jaeger/internal/integration/receivers/storagereceiver/testdata/config.yaml +++ b/cmd/jaeger/internal/integration/receivers/storagereceiver/testdata/config.yaml @@ -3,4 +3,4 @@ jaeger_storage_receiver/defaults: trace_storage: storage jaeger_storage_receiver/filled: trace_storage: storage - pull_interval: 2s \ No newline at end of file + pull_interval: 2s diff --git a/cmd/jaeger/internal/integration/storagecleaner/README.md b/cmd/jaeger/internal/integration/storagecleaner/README.md new file mode 100644 index 00000000000..52f4a47ec4f --- /dev/null +++ b/cmd/jaeger/internal/integration/storagecleaner/README.md @@ -0,0 +1,47 @@ +# storage_cleaner + +This module implements an extension that allows purging the backend storage by exposing a HTTP endpoint for making POST requests. + +The storage_cleaner extension is intended to be used only in tests, providing a way to clear the storage between test runs. Making a POST request to the exposed endpoint will delete all data in storage. + + +```mermaid +flowchart LR + Receiver --> Processor + Processor --> Exporter + JaegerStorageExension -->|"(1) get storage"| Exporter + Exporter -->|"(2) write trace"| Badger + + Badger_e2e_test -->|"(1) POST /purge"| HTTP_endpoint + JaegerStorageExension -->|"(2) getStorage()"| HTTP_endpoint + HTTP_endpoint -.->|"(3) storage.(*Badger).Purge()"| Badger + + subgraph Jaeger Collector + Receiver + Processor + Exporter + + Badger + BadgerCleanerExtension + HTTP_endpoint + subgraph JaegerStorageExension + Badger + end + subgraph BadgerCleanerExtension + HTTP_endpoint + end + end +``` + +# Getting Started + +The following settings are required: + +- `trace_storage` : name of a storage backend defined in `jaegerstorage` extension + +```yaml +extensions: + storage_cleaner: + trace_storage: external-storage +``` + diff --git a/cmd/jaeger/internal/integration/storagecleaner/config.go b/cmd/jaeger/internal/integration/storagecleaner/config.go new file mode 100644 index 00000000000..5b4c9d30812 --- /dev/null +++ b/cmd/jaeger/internal/integration/storagecleaner/config.go @@ -0,0 +1,18 @@ +// Copyright (c) 2024 The Jaeger Authors. +// SPDX-License-Identifier: Apache-2.0 + +package storagecleaner + +import ( + "github.com/asaskevich/govalidator" +) + +type Config struct { + TraceStorage string `valid:"required" mapstructure:"trace_storage"` + Port string `mapstructure:"port"` +} + +func (cfg *Config) Validate() error { + _, err := govalidator.ValidateStruct(cfg) + return err +} diff --git a/cmd/jaeger/internal/integration/storagecleaner/extension.go b/cmd/jaeger/internal/integration/storagecleaner/extension.go new file mode 100644 index 00000000000..43765306b46 --- /dev/null +++ b/cmd/jaeger/internal/integration/storagecleaner/extension.go @@ -0,0 +1,95 @@ +// Copyright (c) 2024 The Jaeger Authors. +// SPDX-License-Identifier: Apache-2.0 + +package storagecleaner + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/gorilla/mux" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/extension" + + "github.com/jaegertracing/jaeger/cmd/jaeger/internal/extension/jaegerstorage" + "github.com/jaegertracing/jaeger/storage" +) + +var ( + _ extension.Extension = (*storageCleaner)(nil) + _ extension.Dependent = (*storageCleaner)(nil) +) + +const ( + Port = "9231" + URL = "/purge" +) + +type storageCleaner struct { + config *Config + server *http.Server +} + +func newStorageCleaner(config *Config) *storageCleaner { + return &storageCleaner{ + config: config, + } +} + +func (c *storageCleaner) Start(ctx context.Context, host component.Host) error { + storageFactory, err := jaegerstorage.GetStorageFactory(c.config.TraceStorage, host) + if err != nil { + return fmt.Errorf("cannot find storage factory for Badger: %w", err) + } + + purgeStorage := func() error { + purger, ok := storageFactory.(storage.Purger) + if !ok { + return fmt.Errorf("storage %s does not implement Purger interface", c.config.TraceStorage) + } + if err := purger.Purge(); err != nil { + return fmt.Errorf("error purging Badger storage: %w", err) + } + return nil + } + + purgeHandler := func(w http.ResponseWriter, r *http.Request) { + if err := purgeStorage(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("Purge request processed successfully")) + } + + r := mux.NewRouter() + r.HandleFunc(URL, purgeHandler).Methods(http.MethodPost) + c.server = &http.Server{ + Addr: ":" + c.config.Port, + Handler: r, + ReadHeaderTimeout: 3 * time.Second, + } + go func() error { + if err := c.server.ListenAndServe(); err != nil { + return fmt.Errorf("error starting storage cleaner server: %w", err) + } + return nil + }() + + return nil +} + +func (c *storageCleaner) Shutdown(ctx context.Context) error { + if c.server != nil { + if err := c.server.Shutdown(ctx); err != nil { + return fmt.Errorf("error shutting down cleaner server: %w", err) + } + } + return nil +} + +func (c *storageCleaner) Dependencies() []component.ID { + return []component.ID{jaegerstorage.ID} +} diff --git a/cmd/jaeger/internal/integration/storagecleaner/factory.go b/cmd/jaeger/internal/integration/storagecleaner/factory.go new file mode 100644 index 00000000000..b4efe135648 --- /dev/null +++ b/cmd/jaeger/internal/integration/storagecleaner/factory.go @@ -0,0 +1,40 @@ +// Copyright (c) 2024 The Jaeger Authors. +// SPDX-License-Identifier: Apache-2.0 + +package storagecleaner + +import ( + "context" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/extension" +) + +// componentType is the name of this extension in configuration. +var componentType = component.MustNewType("storage_cleaner") + +// ID is the identifier of this extension. +var ID = component.NewID(componentType) + +func NewFactory() extension.Factory { + return extension.NewFactory( + componentType, + createDefaultConfig, + createExtension, + component.StabilityLevelBeta, + ) +} + +func createDefaultConfig() component.Config { + return &Config{ + Port: Port, + } +} + +func createExtension( + _ context.Context, + set extension.CreateSettings, + cfg component.Config, +) (extension.Extension, error) { + return newStorageCleaner(cfg.(*Config)), nil +} diff --git a/cmd/jaeger/internal/integration/storagecleaner/factory_test.go b/cmd/jaeger/internal/integration/storagecleaner/factory_test.go new file mode 100644 index 00000000000..7b1ec51d8f3 --- /dev/null +++ b/cmd/jaeger/internal/integration/storagecleaner/factory_test.go @@ -0,0 +1,28 @@ +// Copyright (c) 2024 The Jaeger Authors. +// SPDX-License-Identifier: Apache-2.0 + +package storagecleaner + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/extension/extensiontest" +) + +func TestCreateDefaultConfig(t *testing.T) { + cfg := createDefaultConfig().(*Config) + require.NotNil(t, cfg, "failed to create default config") + require.NoError(t, componenttest.CheckConfigStruct(cfg)) +} + +func TestCreateExtension(t *testing.T) { + cfg := createDefaultConfig().(*Config) + f := NewFactory() + r, err := f.CreateExtension(context.Background(), extensiontest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + assert.NotNil(t, r) +} diff --git a/cmd/jaeger/internal/integration/storagecleaner/package_test.go b/cmd/jaeger/internal/integration/storagecleaner/package_test.go new file mode 100644 index 00000000000..cec15912582 --- /dev/null +++ b/cmd/jaeger/internal/integration/storagecleaner/package_test.go @@ -0,0 +1,14 @@ +// Copyright (c) 2024 The Jaeger Authors. +// SPDX-License-Identifier: Apache-2.0 + +package storagecleaner + +import ( + "testing" + + "github.com/jaegertracing/jaeger/pkg/testutils" +) + +func TestMain(m *testing.M) { + testutils.VerifyGoLeaks(m) +} diff --git a/plugin/storage/badger/factory.go b/plugin/storage/badger/factory.go index 607ab05bf65..1fc2e1dffac 100644 --- a/plugin/storage/badger/factory.go +++ b/plugin/storage/badger/factory.go @@ -50,6 +50,7 @@ var ( // interface comformance checks _ storage.Factory = (*Factory)(nil) _ io.Closer = (*Factory)(nil) _ plugin.Configurable = (*Factory)(nil) + _ storage.Purger = (*Factory)(nil) // TODO badger could implement archive storage // _ storage.ArchiveFactory = (*Factory)(nil) diff --git a/scripts/prepare-badger-integration-tests.py b/scripts/prepare-badger-integration-tests.py new file mode 100755 index 00000000000..f9fe45ceeab --- /dev/null +++ b/scripts/prepare-badger-integration-tests.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +import yaml +import os +import tempfile + +config_file = os.path.join(os.path.dirname(__file__), '../cmd/jaeger/badger_config.yaml') + +with open(config_file, 'r') as f: + config = yaml.safe_load(f) +temp_config = config.copy() + +if 'storage_cleaner' not in temp_config['service']['extensions'] : + config['service']['extensions'].insert(1, 'storage_cleaner') + +temp_config['extensions']['storage_cleaner'] = { + 'trace_storage': 'badger_main' +} + +with tempfile.NamedTemporaryFile(mode='w', delete=False, dir=os.path.dirname(config_file),suffix='.yaml') as f: + temp_config_file = f.name + yaml.dump(temp_config, f, sort_keys=False) + +print(temp_config_file) diff --git a/storage/factory.go b/storage/factory.go index 933d0dce27d..b56e6fdc07f 100644 --- a/storage/factory.go +++ b/storage/factory.go @@ -49,6 +49,13 @@ type Factory interface { CreateDependencyReader() (dependencystore.Reader, error) } +// Purger defines an interface that is capable of purging the storage. +// Only meant to be used from integration tests. +type Purger interface { + // Purge removes all data from the storage. + Purge() error +} + // SamplingStoreFactory defines an interface that is capable of returning the necessary backends for // adaptive sampling. type SamplingStoreFactory interface {