mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
569 lines
21 KiB
Go
569 lines
21 KiB
Go
package util
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/e2e"
|
|
e2edb "github.com/grafana/e2e/db"
|
|
"github.com/grafana/tempo/cmd/tempo/app"
|
|
"github.com/grafana/tempo/modules/overrides"
|
|
"github.com/grafana/tempo/tempodb/backend"
|
|
"github.com/grafana/tempo/tempodb/backend/azure"
|
|
"github.com/prometheus/prometheus/model/labels"
|
|
"github.com/stretchr/testify/require"
|
|
"go.yaml.in/yaml/v2"
|
|
)
|
|
|
|
const (
|
|
// tempo
|
|
ServiceDistributor = "distributor"
|
|
ServiceQueryFrontend = "query-frontend"
|
|
ServiceQuerier = "querier"
|
|
ServiceMetricsGenerator = "metrics-generator"
|
|
ServiceLiveStoreZoneA = "live-store-zone-a-0"
|
|
ServiceLiveStoreZoneB = "live-store-zone-b-0"
|
|
ServiceBlockBuilder = "block-builder-0"
|
|
ServiceBackendScheduler = "backend-scheduler"
|
|
ServiceBackendWorker = "backend-worker"
|
|
|
|
// not tempo!
|
|
ServiceObjectStorage = "object-storage"
|
|
ServicePrometheus = "prometheus"
|
|
)
|
|
|
|
var AllTempoServices = []string{
|
|
ServiceDistributor,
|
|
ServiceQueryFrontend,
|
|
ServiceQuerier,
|
|
ServiceMetricsGenerator,
|
|
ServiceLiveStoreZoneA,
|
|
ServiceLiveStoreZoneB,
|
|
ServiceBlockBuilder,
|
|
}
|
|
|
|
const (
|
|
azuriteImage = "mcr.microsoft.com/azure-storage/azurite:3.35.0"
|
|
gcsImage = "fsouza/fake-gcs-server:1.52.2"
|
|
)
|
|
|
|
// DeploymentMode specifies whether to run Tempo as a single binary, microservices or none.
|
|
type DeploymentMode int
|
|
|
|
const (
|
|
// DeploymentModeMicroservices runs Tempo as separate microservices (default)
|
|
DeploymentModeMicroservices DeploymentMode = iota
|
|
// DeploymentModeSingleBinary runs Tempo as a single all-in-one binary
|
|
DeploymentModeSingleBinary
|
|
// DeploymentModeNone does not start any Tempo services. This seems odd but it's used by poller_test.go to just start backends
|
|
DeploymentModeNone
|
|
)
|
|
|
|
// ComponentsMask is a bitmask for controlling which optional Tempo components are started. These flags are meant to be used
|
|
// with the | operator to request multiple component sets from the test harness. For instance if you wanted to test the full
|
|
// querying capabilities of Tempo you would use: util.ComponentsRecentDataQuerying | util.ComponentsBackendQuerying
|
|
type ComponentsMask uint
|
|
|
|
const (
|
|
componentsKafka ComponentsMask = 1 << iota
|
|
componentsPrometheus
|
|
componentsLiveStore
|
|
componentsDistributor
|
|
componentsQueryFrontendQuerier // both query-frontend and querier. bundled b/c the query-frontend will never raise its readiness probe w/o querier
|
|
componentsBlockBuilder
|
|
componentsMetricsGenerator
|
|
componentsBackendSchedulerWorker // both backend-scheduler and backend-worker. too tightly coupled to separate
|
|
|
|
// the config-base.yaml uses live-stores as the memberlist gossip seeds and so always have to start live stores to stabilize the memberlist gossip cluster. even if they are not needed by the test
|
|
// todo: improve this by templating the memberlist join members and dynamically choosing which components are the seeds
|
|
|
|
// ComponentsRecentDataQuerying starts the distributor, kafka, query-frontend, querier, and livestores for recent data querying. (default)
|
|
ComponentsRecentDataQuerying = componentsLiveStore | componentsDistributor | componentsQueryFrontendQuerier | componentsKafka
|
|
// ComponentsBackendQuerying starts the distributor, kafka, query-frontend, querier, and block-builder for backend querying.
|
|
ComponentsBackendQuerying = componentsDistributor | componentsQueryFrontendQuerier | componentsBlockBuilder | componentsKafka | componentsLiveStore
|
|
// ComponentsMetricsGeneration starts the distributor, kafka, metrics generator and prometheus for metrics generation.
|
|
ComponentsMetricsGeneration = componentsDistributor | componentsMetricsGenerator | componentsPrometheus | componentsKafka | componentsLiveStore
|
|
// ComponentsBackendWork starts the backend scheduler and worker for testing backend work.
|
|
ComponentsBackendWork = componentsBackendSchedulerWorker
|
|
)
|
|
|
|
// BackendsMask is a bitmask for controlling which object storage backends are started. These flags are meant to be used
|
|
// with the | operator to request multiple backends from the test harness.
|
|
// The test function will be called once for each backend and so specifying multiple backends will result in longer tests!
|
|
// Use this only when you really need to test the behavior of Tempo against multiple backends.
|
|
type BackendsMask uint
|
|
|
|
const (
|
|
// BackendLocal starts the local backend. (default)
|
|
BackendLocal BackendsMask = 1 << iota
|
|
// BackendObjectStorageS3 starts the S3 backend.
|
|
BackendObjectStorageS3
|
|
// BackendObjectStorageAzure starts the Azure backend.
|
|
BackendObjectStorageAzure
|
|
// BackendObjectStorageGCS starts the GCS backend.
|
|
BackendObjectStorageGCS
|
|
|
|
// BackendObjectStorageAll starts all three object storage backends. A convenience flag to avoid having to specify all three backends manually.
|
|
BackendObjectStorageAll = BackendObjectStorageS3 | BackendObjectStorageAzure | BackendObjectStorageGCS
|
|
)
|
|
|
|
type TempoHarness struct {
|
|
Services map[string]*e2e.HTTPService
|
|
TestScenario *e2e.Scenario
|
|
|
|
overridesPath string
|
|
readinessProbe e2e.ReadinessProbe
|
|
}
|
|
|
|
// TestHarnessConfig provides configuration options for the test harness
|
|
type TestHarnessConfig struct {
|
|
// ConfigOverlay is a config file that will be merged on top of config-base.yaml
|
|
// This allows tests to only specify the differences from the default config
|
|
// If empty, only config-base.yaml will be used
|
|
ConfigOverlay string
|
|
|
|
// ConfigTemplateData provides template variables to expand in the ConfigOverlay
|
|
// Template variables should use Go template syntax: {{ .VariableName }}
|
|
ConfigTemplateData map[string]any
|
|
|
|
// ReadinessProbe is a function to use a custom readiness probe for the Tempo services.
|
|
// It's used by the https tests to swap the readiness port to 3201.
|
|
ReadinessProbe e2e.ReadinessProbe
|
|
|
|
// PreStartHook is called before starting Tempo services to perform any setup or adjustments.
|
|
// It's used by a few tests to copy in required files, start prerequisite services, or adjust the template data for overlays.
|
|
PreStartHook func(*e2e.Scenario, map[string]any) error
|
|
|
|
// DeploymentMode specifies whether to run as single binary, microservices, or none
|
|
// Defaults to DeploymentModeMicroservices
|
|
DeploymentMode DeploymentMode
|
|
|
|
// Backends is a bitmask controlling which object storage backends are started.
|
|
// Defaults to BackendLocal
|
|
Backends BackendsMask
|
|
|
|
// Components is a bitmask controlling which optional Tempo components are started.
|
|
// Recent data components are always started. Use this to add optional components.
|
|
// Defaults to ComponentsRecentDataQuerying
|
|
Components ComponentsMask
|
|
}
|
|
|
|
// RunIntegrationTests sets up Tempo for integration tests as requested through the config and then calls the provided testFunc
|
|
func RunIntegrationTests(t *testing.T, config TestHarnessConfig, testFunc func(*TempoHarness)) {
|
|
t.Helper()
|
|
t.Parallel()
|
|
|
|
// defaults
|
|
if config.Backends == 0 {
|
|
config.Backends = BackendLocal
|
|
}
|
|
|
|
if config.DeploymentMode == 0 {
|
|
config.DeploymentMode = DeploymentModeMicroservices
|
|
}
|
|
|
|
if config.Components == 0 {
|
|
config.Components = ComponentsRecentDataQuerying
|
|
}
|
|
|
|
backendTCs := backendTestCases(config.Backends)
|
|
|
|
// Run tests for each deployment mode and backend combination
|
|
for _, be := range backendTCs {
|
|
// t.Run() with t.Parallel() here will cause some tests to run faster, but others like TestKVStores will fail for unknown reasons.
|
|
runTempoHarness(t, config, be.name, testFunc)
|
|
}
|
|
}
|
|
|
|
type backendTestCase struct {
|
|
mask BackendsMask
|
|
name string
|
|
}
|
|
|
|
// backendTestCases returns the list of backends to test based on the config
|
|
func backendTestCases(backends BackendsMask) []backendTestCase {
|
|
var result []backendTestCase
|
|
|
|
if backends&BackendObjectStorageS3 != 0 {
|
|
result = append(result, backendTestCase{BackendObjectStorageS3, backend.S3})
|
|
}
|
|
if backends&BackendObjectStorageAzure != 0 {
|
|
result = append(result, backendTestCase{BackendObjectStorageAzure, backend.Azure})
|
|
}
|
|
if backends&BackendObjectStorageGCS != 0 {
|
|
result = append(result, backendTestCase{BackendObjectStorageGCS, backend.GCS})
|
|
}
|
|
if backends&BackendLocal != 0 {
|
|
result = append(result, backendTestCase{BackendLocal, backend.Local})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// runTempoHarness is the internal implementation that sets up and runs a single harness instance
|
|
func runTempoHarness(t *testing.T, harnessCfg TestHarnessConfig, requestedBackend string, testFunc func(*TempoHarness)) {
|
|
t.Helper()
|
|
|
|
// Create scenario with normalized test name
|
|
name := normalizeTestName(t.Name())
|
|
s, err := e2e.NewScenario("e2e_" + name)
|
|
require.NoError(t, err)
|
|
defer s.Close()
|
|
|
|
harness := &TempoHarness{
|
|
Services: map[string]*e2e.HTTPService{},
|
|
TestScenario: s,
|
|
readinessProbe: harnessCfg.ReadinessProbe,
|
|
}
|
|
|
|
// Setup config and infrastructure
|
|
tempoCfg := setupConfig(t, s, &harnessCfg, requestedBackend, harness)
|
|
|
|
// Start object storage backend if not using local filesystem
|
|
if tempoCfg.StorageConfig.Trace.Backend != backend.Local {
|
|
backend, err := startBackend(t, s, tempoCfg)
|
|
require.NoError(t, err, "failed to start backend")
|
|
harness.Services[ServiceObjectStorage] = backend
|
|
}
|
|
|
|
// bail out here if we don't need any tempo components
|
|
if harnessCfg.DeploymentMode == DeploymentModeNone {
|
|
return
|
|
}
|
|
|
|
// Start Kafka
|
|
// todo: should we add a field to reference kafka on the harness? not needed atm. maybe to test failure states by stopping it?
|
|
if harnessCfg.Components&componentsKafka != 0 {
|
|
kafka := e2edb.NewKafka()
|
|
require.NoError(t, s.StartAndWaitReady(kafka), "failed to start Kafka")
|
|
}
|
|
|
|
if harnessCfg.Components&componentsPrometheus != 0 {
|
|
prometheus := newPrometheus()
|
|
require.NoError(t, s.StartAndWaitReady(prometheus), "failed to start prometheus")
|
|
harness.Services[ServicePrometheus] = prometheus
|
|
}
|
|
|
|
// Start Tempo services based on deployment mode
|
|
switch harnessCfg.DeploymentMode {
|
|
case DeploymentModeSingleBinary:
|
|
require.NoError(t, harness.startSingleBinary(t), "failed to start single binary")
|
|
case DeploymentModeMicroservices:
|
|
require.NoError(t, harness.startMicroservices(t, harnessCfg), "failed to start microservices")
|
|
default:
|
|
panic(fmt.Sprintf("unknown deployment mode: %d", harnessCfg.DeploymentMode))
|
|
}
|
|
|
|
// Run the test function
|
|
testFunc(harness)
|
|
}
|
|
|
|
// UpdateOverrides updates the tenant overrides file with the provided configuration.
|
|
// The overrides parameter should be a map where keys are tenant IDs and values are
|
|
// override configurations for that tenant.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// h.UpdateOverrides(map[string]*overrides.Overrides{
|
|
// "tenant-1": {
|
|
// Ingestion: overrides.IngestionOverrides{
|
|
// RateLimitBytes: 1000000,
|
|
// },
|
|
// },
|
|
// })
|
|
//
|
|
// Tempo will automatically reload the overrides based on the per_tenant_override_period
|
|
// configured in config-base.yaml.
|
|
func (h *TempoHarness) UpdateOverrides(tenantOverrides map[string]*overrides.Overrides) error {
|
|
overridesConfig := struct {
|
|
Overrides map[string]*overrides.Overrides `yaml:"overrides"`
|
|
}{
|
|
Overrides: tenantOverrides,
|
|
}
|
|
|
|
data, err := yaml.Marshal(overridesConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal overrides: %w", err)
|
|
}
|
|
|
|
err = os.WriteFile(h.overridesPath, data, 0o644) // nolint:gosec // G306: Expect WriteFile permissions to be 0600 or less
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write overrides file: %w", err)
|
|
}
|
|
|
|
// overrides reload every 1 second. wait 5 to make sure it gets loaded
|
|
time.Sleep(5 * time.Second)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetConfig reads and parses the Tempo configuration file that was set up by the harness.
|
|
// Returns the parsed app.Config or an error if reading/parsing fails.
|
|
func (h *TempoHarness) GetConfig() (app.Config, error) {
|
|
configPath := sharedContainerPath(h.TestScenario, tempoConfigFile)
|
|
|
|
buff, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return app.Config{}, fmt.Errorf("failed to read config file: %w", err)
|
|
}
|
|
|
|
var cfg app.Config
|
|
err = yaml.UnmarshalStrict(buff, &cfg)
|
|
if err != nil {
|
|
return app.Config{}, fmt.Errorf("failed to unmarshal config: %w", err)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// restartServiceWithConfigOverlay stops a service, applies a config overlay, and restarts the service.
|
|
// The overlay file is merged onto the existing config, with overlay values taking precedence.
|
|
func (h *TempoHarness) restartServiceWithConfigOverlay(t *testing.T, service *e2e.HTTPService, overlayPath string) error {
|
|
// Stop the service
|
|
err := service.Stop()
|
|
if strings.Contains(err.Error(), "exit status 137") { // 137 is returned by linux when it is force killed b/c it doesn't stop in time.
|
|
t.Logf("service %s was force killed during stop: %v", service.Name(), err)
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to stop service: %w", err)
|
|
}
|
|
|
|
// Apply overlay to current config
|
|
err = applyConfigOverlay(h.TestScenario, overlayPath, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to apply config overlay: %w", err)
|
|
}
|
|
|
|
// Restart the service
|
|
if err := service.Start(h.TestScenario.NetworkName(), h.TestScenario.SharedDir()); err != nil {
|
|
return fmt.Errorf("failed to restart service: %w", err)
|
|
}
|
|
|
|
if err := service.WaitReady(); err != nil {
|
|
return fmt.Errorf("service did not become ready after restart: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *TempoHarness) WaitTracesWritable(t *testing.T) {
|
|
t.Helper()
|
|
|
|
matchers := []*labels.Matcher{
|
|
{Type: labels.MatchEqual, Name: "state", Value: "Active"},
|
|
{Type: labels.MatchEqual, Name: "name", Value: "livestore-partitions"},
|
|
}
|
|
|
|
// distributors have to be non-nil if we're writing anything
|
|
distributor := h.Services[ServiceDistributor]
|
|
require.NoError(t, distributor.WaitSumMetricsWithOptions(
|
|
e2e.Equals(float64(1)),
|
|
[]string{"tempo_partition_ring_partitions"},
|
|
e2e.WithLabelMatchers(matchers...)), "distributor failed to see the partition ring")
|
|
}
|
|
|
|
func (h *TempoHarness) WaitTracesQueryable(t *testing.T, traces int) {
|
|
t.Helper()
|
|
|
|
liveStoreZoneA := h.Services[ServiceLiveStoreZoneA]
|
|
require.NoError(t, liveStoreZoneA.WaitSumMetricsWithOptions(e2e.GreaterOrEqual(float64(traces)), []string{"tempo_live_store_traces_created_total"}, e2e.WaitMissingMetrics))
|
|
|
|
liveStoreZoneB := h.Services[ServiceLiveStoreZoneB]
|
|
require.NoError(t, liveStoreZoneB.WaitSumMetricsWithOptions(e2e.GreaterOrEqual(float64(traces)), []string{"tempo_live_store_traces_created_total"}, e2e.WaitMissingMetrics))
|
|
}
|
|
|
|
func (h *TempoHarness) WaitTracesWrittenToBackend(t *testing.T, traces int) {
|
|
t.Helper()
|
|
|
|
queryFrontend := h.Services[ServiceQueryFrontend]
|
|
require.NoError(t, queryFrontend.WaitSumMetricsWithOptions(e2e.GreaterOrEqual(float64(traces)), []string{"tempodb_backend_objects_total"}, e2e.WaitMissingMetrics))
|
|
}
|
|
|
|
// ForceBackendQuerying restarts the query frontend with the query-backend config overlay applied.
|
|
// Using this function requires re-creating any clients pointing at the frontend!
|
|
func (h *TempoHarness) ForceBackendQuerying(t *testing.T) {
|
|
frontend := h.Services[ServiceQueryFrontend]
|
|
require.NoError(t, h.restartServiceWithConfigOverlay(t, frontend, queryBackendConfigFile))
|
|
}
|
|
|
|
/*
|
|
local object storage
|
|
*/
|
|
// startBackend starts the appropriate object storage backend based on the config
|
|
func startBackend(t *testing.T, s *e2e.Scenario, cfg app.Config) (*e2e.HTTPService, error) {
|
|
t.Helper()
|
|
|
|
var backendService *e2e.HTTPService
|
|
switch cfg.StorageConfig.Trace.Backend {
|
|
case backend.S3:
|
|
port, err := parsePort(cfg.StorageConfig.Trace.S3.Endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
backendService = e2edb.NewMinio(port, "tempo")
|
|
err = s.StartAndWaitReady(backendService)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case backend.Azure:
|
|
port, err := parsePort(cfg.StorageConfig.Trace.Azure.Endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
backendService = newAzurite(port)
|
|
err = s.StartAndWaitReady(backendService)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Get the actual endpoint after the service is started
|
|
cfg.StorageConfig.Trace.Azure.Endpoint = backendService.Endpoint(port)
|
|
_, err = azure.CreateContainer(context.TODO(), cfg.StorageConfig.Trace.Azure)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case backend.GCS:
|
|
port, err := parsePort(cfg.StorageConfig.Trace.GCS.Endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
backendService = newGCS(port)
|
|
err = s.StartAndWaitReady(backendService)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return backendService, nil
|
|
}
|
|
|
|
// parsePort extracts the port number from an endpoint string
|
|
func parsePort(endpoint string) (int, error) {
|
|
substrings := strings.Split(endpoint, ":")
|
|
portStrings := strings.Split(substrings[len(substrings)-1], "/")
|
|
port, err := strconv.Atoi(portStrings[0])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return port, nil
|
|
}
|
|
|
|
// startMicroservices starts all Tempo microservices and waits for them to be ready
|
|
func (h *TempoHarness) startMicroservices(t *testing.T, config TestHarnessConfig) error {
|
|
t.Helper()
|
|
|
|
s := h.TestScenario
|
|
readinessProbe := e2e.ReadinessProbe(e2e.NewHTTPReadinessProbe(3200, "/ready", 200, 299))
|
|
if h.readinessProbe != nil {
|
|
readinessProbe = h.readinessProbe
|
|
}
|
|
|
|
if config.Components&componentsLiveStore != 0 {
|
|
liveStoreZoneA := NewTempoService("live-store-zone-a-0", "live-store", readinessProbe, nil, "-live-store.instance-availability-zone=zone-a")
|
|
h.Services[ServiceLiveStoreZoneA] = liveStoreZoneA
|
|
if err := s.StartAndWaitReady(liveStoreZoneA); err != nil {
|
|
return fmt.Errorf("failed to start live store zone a: %w", err)
|
|
}
|
|
|
|
liveStoreZoneB := NewTempoService("live-store-zone-b-0", "live-store", readinessProbe, nil, "-live-store.instance-availability-zone=zone-b")
|
|
h.Services[ServiceLiveStoreZoneB] = liveStoreZoneB
|
|
if err := s.StartAndWaitReady(liveStoreZoneB); err != nil {
|
|
return fmt.Errorf("failed to start live store zone b: %w", err)
|
|
}
|
|
}
|
|
|
|
if config.Components&componentsDistributor != 0 {
|
|
h.Services[ServiceDistributor] = NewTempoService("distributor", "distributor",
|
|
readinessProbe,
|
|
[]int{14250, 4317, 4318, 9411}, // jaeger grpc ingest, otlp grpc, otlp http, zipkin ingest
|
|
)
|
|
if err := s.StartAndWaitReady(h.Services[ServiceDistributor]); err != nil {
|
|
return fmt.Errorf("failed to start distributor: %w", err)
|
|
}
|
|
}
|
|
|
|
if config.Components&componentsQueryFrontendQuerier != 0 {
|
|
h.Services[ServiceQueryFrontend] = NewTempoService("query-frontend", "query-frontend", readinessProbe, nil)
|
|
h.Services[ServiceQuerier] = NewTempoService("querier", "querier", readinessProbe, nil)
|
|
if err := s.StartAndWaitReady(h.Services[ServiceQueryFrontend], h.Services[ServiceQuerier]); err != nil {
|
|
return fmt.Errorf("failed to start query frontend and querier: %w", err)
|
|
}
|
|
}
|
|
|
|
if config.Components&componentsBlockBuilder != 0 {
|
|
blockBuilder := NewTempoService("block-builder-0", "block-builder", readinessProbe, nil)
|
|
h.Services[ServiceBlockBuilder] = blockBuilder
|
|
if err := s.StartAndWaitReady(blockBuilder); err != nil {
|
|
return fmt.Errorf("failed to start block builder: %w", err)
|
|
}
|
|
}
|
|
|
|
if config.Components&componentsMetricsGenerator != 0 {
|
|
h.Services[ServiceMetricsGenerator] = NewTempoService("metrics-generator", "metrics-generator", readinessProbe, nil)
|
|
if err := s.StartAndWaitReady(h.Services[ServiceMetricsGenerator]); err != nil {
|
|
return fmt.Errorf("failed to start metrics generator: %w", err)
|
|
}
|
|
}
|
|
|
|
if config.Components&componentsBackendSchedulerWorker != 0 {
|
|
scheduler := NewTempoService("backend-scheduler", "backend-scheduler", readinessProbe, nil)
|
|
worker := NewTempoService("backend-worker", "backend-worker", readinessProbe, nil)
|
|
h.Services[ServiceBackendScheduler] = scheduler
|
|
h.Services[ServiceBackendWorker] = worker
|
|
if err := s.StartAndWaitReady(scheduler, worker); err != nil {
|
|
return fmt.Errorf("failed to start backend scheduler and worker: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// startSingleBinary starts Tempo as a single all-in-one binary and waits for it to be ready
|
|
func (h *TempoHarness) startSingleBinary(t *testing.T) error {
|
|
t.Helper()
|
|
|
|
readinessProbe := e2e.ReadinessProbe(e2e.NewHTTPReadinessProbe(3200, "/ready", 200, 299))
|
|
if h.readinessProbe != nil {
|
|
readinessProbe = h.readinessProbe
|
|
}
|
|
|
|
// Create single binary service with custom readiness probe
|
|
// Using port 3201 for readiness to avoid conflicts with main HTTP port
|
|
tempo := NewTempoAllInOne(readinessProbe)
|
|
|
|
h.Services[ServiceDistributor] = tempo
|
|
h.Services[ServiceQueryFrontend] = tempo
|
|
h.Services[ServiceQuerier] = tempo
|
|
h.Services[ServiceLiveStoreZoneA] = tempo
|
|
h.Services[ServiceLiveStoreZoneB] = tempo
|
|
h.Services[ServiceBlockBuilder] = tempo
|
|
h.Services[ServiceMetricsGenerator] = tempo
|
|
h.Services[ServiceBackendScheduler] = tempo
|
|
h.Services[ServiceBackendWorker] = tempo
|
|
|
|
if err := h.TestScenario.StartAndWaitReady(tempo); err != nil {
|
|
return fmt.Errorf("failed to start tempo single binary: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// normalizeTestName creates a valid Docker service name from a test name
|
|
func normalizeTestName(testName string) string {
|
|
// max docker name length is 63. otherwise dns fails silently
|
|
// max test name length is 40 to leave room prefix and suffix. the full container name will be e2e_<test name>_<service name>
|
|
// this means that if two tests have the same first 40 characters in their names they will conflict!!
|
|
maxNameLen := 40
|
|
name := strings.TrimPrefix(testName, "Test")
|
|
if len(name) > maxNameLen {
|
|
name = name[:maxNameLen]
|
|
}
|
|
// docker only allows a-zA-Z0-9_.- in a service name. replace everything else with _
|
|
re := regexp.MustCompile(`[^a-zA-Z0-9_.-]`)
|
|
return re.ReplaceAllString(name, "_")
|
|
}
|