287 lines
11 KiB
Go
287 lines
11 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package exportertest // import "go.opentelemetry.io/collector/exporter/exportertest"
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.opentelemetry.io/collector/component"
|
|
"go.opentelemetry.io/collector/component/componenttest"
|
|
"go.opentelemetry.io/collector/exporter"
|
|
"go.opentelemetry.io/collector/pdata/plog"
|
|
"go.opentelemetry.io/collector/pdata/pmetric"
|
|
"go.opentelemetry.io/collector/pdata/ptrace"
|
|
"go.opentelemetry.io/collector/pipeline"
|
|
"go.opentelemetry.io/collector/receiver"
|
|
"go.opentelemetry.io/collector/receiver/receivertest"
|
|
)
|
|
|
|
// uniqueIDAttrName is the attribute name that is used in log records/spans/datapoints as the unique identifier.
|
|
const uniqueIDAttrName = "test_id"
|
|
|
|
// uniqueIDAttrVal is the value type of the uniqueIDAttrName.
|
|
type uniqueIDAttrVal string
|
|
|
|
type CheckConsumeContractParams struct {
|
|
T *testing.T
|
|
NumberOfTestElements int
|
|
// DataType to test for.
|
|
//
|
|
// Deprecated: [v0.110.0] Use Signal instead
|
|
// nolint
|
|
DataType component.DataType
|
|
|
|
Signal pipeline.Signal
|
|
// ExporterFactory to create an exporter to be tested.
|
|
ExporterFactory exporter.Factory
|
|
ExporterConfig component.Config
|
|
// ReceiverFactory to create a mock receiver.
|
|
ReceiverFactory receiver.Factory
|
|
ReceiverConfig component.Config
|
|
}
|
|
|
|
func CheckConsumeContract(params CheckConsumeContractParams) {
|
|
// Different scenarios to test for.
|
|
// The decision function defines the testing scenario (i.e. to test for
|
|
// success case or for error case or a mix of both). See for example randomErrorsConsumeDecision.
|
|
scenarios := []struct {
|
|
name string
|
|
decisionFunc func() error
|
|
checkIfTestPassed func(*testing.T, int, requestCounter)
|
|
}{
|
|
{
|
|
name: "always_succeed",
|
|
// Always succeed. We expect all data to be delivered as is.
|
|
decisionFunc: func() error { return nil },
|
|
checkIfTestPassed: alwaysSucceedsPassed,
|
|
},
|
|
{
|
|
name: "random_non_permanent_error",
|
|
decisionFunc: randomNonPermanentErrorConsumeDecision,
|
|
checkIfTestPassed: randomNonPermanentErrorConsumeDecisionPassed,
|
|
},
|
|
{
|
|
name: "random_permanent_error",
|
|
decisionFunc: randomPermanentErrorConsumeDecision,
|
|
checkIfTestPassed: randomPermanentErrorConsumeDecisionPassed,
|
|
},
|
|
{
|
|
name: "random_error",
|
|
decisionFunc: randomErrorsConsumeDecision,
|
|
checkIfTestPassed: randomErrorConsumeDecisionPassed,
|
|
},
|
|
}
|
|
for _, scenario := range scenarios {
|
|
params.T.Run(
|
|
scenario.name, func(t *testing.T) {
|
|
checkConsumeContractScenario(t, params, scenario.decisionFunc, scenario.checkIfTestPassed)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func checkConsumeContractScenario(t *testing.T, params CheckConsumeContractParams, decisionFunc func() error, checkIfTestPassed func(*testing.T, int, requestCounter)) {
|
|
mockConsumerInstance := newMockConsumer(decisionFunc)
|
|
switch params.Signal {
|
|
case pipeline.SignalLogs:
|
|
r, err := params.ReceiverFactory.CreateLogsReceiver(context.Background(), receivertest.NewNopSettings(), params.ReceiverConfig, &mockConsumerInstance)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.Start(context.Background(), componenttest.NewNopHost()))
|
|
checkLogs(t, params, r, &mockConsumerInstance, checkIfTestPassed)
|
|
case pipeline.SignalTraces:
|
|
r, err := params.ReceiverFactory.CreateTracesReceiver(context.Background(), receivertest.NewNopSettings(), params.ReceiverConfig, &mockConsumerInstance)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.Start(context.Background(), componenttest.NewNopHost()))
|
|
checkTraces(t, params, r, &mockConsumerInstance, checkIfTestPassed)
|
|
case pipeline.SignalMetrics:
|
|
r, err := params.ReceiverFactory.CreateMetricsReceiver(context.Background(), receivertest.NewNopSettings(), params.ReceiverConfig, &mockConsumerInstance)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.Start(context.Background(), componenttest.NewNopHost()))
|
|
checkMetrics(t, params, r, &mockConsumerInstance, checkIfTestPassed)
|
|
default:
|
|
require.FailNow(t, "must specify a valid DataType to test for")
|
|
}
|
|
}
|
|
|
|
func checkMetrics(t *testing.T, params CheckConsumeContractParams, mockReceiver component.Component,
|
|
mockConsumer *mockConsumer, checkIfTestPassed func(*testing.T, int, requestCounter)) {
|
|
ctx := context.Background()
|
|
var exp exporter.Metrics
|
|
var err error
|
|
exp, err = params.ExporterFactory.CreateMetricsExporter(ctx, NewNopSettings(), params.ExporterConfig)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, exp)
|
|
|
|
err = exp.Start(ctx, componenttest.NewNopHost())
|
|
require.NoError(t, err)
|
|
|
|
defer func(exp exporter.Metrics, ctx context.Context) {
|
|
err = exp.Shutdown(ctx)
|
|
require.NoError(t, err)
|
|
err = mockReceiver.Shutdown(ctx)
|
|
require.NoError(t, err)
|
|
mockConsumer.clear()
|
|
}(exp, ctx)
|
|
|
|
for i := 0; i < params.NumberOfTestElements; i++ {
|
|
id := uniqueIDAttrVal(strconv.Itoa(i))
|
|
data := createOneMetricWithID(id)
|
|
|
|
err = exp.ConsumeMetrics(ctx, data)
|
|
}
|
|
|
|
reqCounter := mockConsumer.getRequestCounter()
|
|
// The overall number of requests sent by exporter
|
|
fmt.Printf("Number of export tries: %d\n", reqCounter.total)
|
|
// Successfully delivered items
|
|
fmt.Printf("Total items received successfully: %d\n", reqCounter.success)
|
|
// Number of errors that happened
|
|
fmt.Printf("Number of permanent errors: %d\n", reqCounter.error.permanent)
|
|
fmt.Printf("Number of non-permanent errors: %d\n", reqCounter.error.nonpermanent)
|
|
|
|
assert.EventuallyWithT(t, func(*assert.CollectT) {
|
|
checkIfTestPassed(t, params.NumberOfTestElements, *reqCounter)
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
func checkTraces(t *testing.T, params CheckConsumeContractParams, mockReceiver component.Component, mockConsumer *mockConsumer, checkIfTestPassed func(*testing.T, int, requestCounter)) {
|
|
ctx := context.Background()
|
|
var exp exporter.Traces
|
|
var err error
|
|
exp, err = params.ExporterFactory.CreateTracesExporter(ctx, NewNopSettings(), params.ExporterConfig)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, exp)
|
|
|
|
err = exp.Start(ctx, componenttest.NewNopHost())
|
|
require.NoError(t, err)
|
|
|
|
defer func(exp exporter.Traces, ctx context.Context) {
|
|
err = exp.Shutdown(ctx)
|
|
require.NoError(t, err)
|
|
err = mockReceiver.Shutdown(ctx)
|
|
require.NoError(t, err)
|
|
mockConsumer.clear()
|
|
}(exp, ctx)
|
|
|
|
for i := 0; i < params.NumberOfTestElements; i++ {
|
|
id := uniqueIDAttrVal(strconv.Itoa(i))
|
|
data := createOneTraceWithID(id)
|
|
|
|
err = exp.ConsumeTraces(ctx, data)
|
|
}
|
|
|
|
reqCounter := mockConsumer.getRequestCounter()
|
|
// The overall number of requests sent by exporter
|
|
fmt.Printf("Number of export tries: %d\n", reqCounter.total)
|
|
// Successfully delivered items
|
|
fmt.Printf("Total items received successfully: %d\n", reqCounter.success)
|
|
// Number of errors that happened
|
|
fmt.Printf("Number of permanent errors: %d\n", reqCounter.error.permanent)
|
|
fmt.Printf("Number of non-permanent errors: %d\n", reqCounter.error.nonpermanent)
|
|
|
|
assert.EventuallyWithT(t, func(*assert.CollectT) {
|
|
checkIfTestPassed(t, params.NumberOfTestElements, *reqCounter)
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
func checkLogs(t *testing.T, params CheckConsumeContractParams, mockReceiver component.Component, mockConsumer *mockConsumer, checkIfTestPassed func(*testing.T, int, requestCounter)) {
|
|
ctx := context.Background()
|
|
var exp exporter.Logs
|
|
var err error
|
|
exp, err = params.ExporterFactory.CreateLogsExporter(ctx, NewNopSettings(), params.ExporterConfig)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, exp)
|
|
|
|
err = exp.Start(ctx, componenttest.NewNopHost())
|
|
require.NoError(t, err)
|
|
|
|
defer func(exp exporter.Logs, ctx context.Context) {
|
|
err = exp.Shutdown(ctx)
|
|
require.NoError(t, err)
|
|
err = mockReceiver.Shutdown(ctx)
|
|
require.NoError(t, err)
|
|
mockConsumer.clear()
|
|
}(exp, ctx)
|
|
|
|
for i := 0; i < params.NumberOfTestElements; i++ {
|
|
id := uniqueIDAttrVal(strconv.Itoa(i))
|
|
data := createOneLogWithID(id)
|
|
|
|
err = exp.ConsumeLogs(ctx, data)
|
|
}
|
|
reqCounter := mockConsumer.getRequestCounter()
|
|
// The overall number of requests sent by exporter
|
|
fmt.Printf("Number of export tries: %d\n", reqCounter.total)
|
|
// Successfully delivered items
|
|
fmt.Printf("Total items received successfully: %d\n", reqCounter.success)
|
|
// Number of errors that happened
|
|
fmt.Printf("Number of permanent errors: %d\n", reqCounter.error.permanent)
|
|
fmt.Printf("Number of non-permanent errors: %d\n", reqCounter.error.nonpermanent)
|
|
|
|
assert.EventuallyWithT(t, func(*assert.CollectT) {
|
|
checkIfTestPassed(t, params.NumberOfTestElements, *reqCounter)
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
// Test is successful if all the elements were received successfully and no error was returned
|
|
func alwaysSucceedsPassed(t *testing.T, allRecordsNumber int, reqCounter requestCounter) {
|
|
require.Equal(t, allRecordsNumber, reqCounter.success)
|
|
require.Equal(t, allRecordsNumber, reqCounter.total)
|
|
require.Equal(t, 0, reqCounter.error.nonpermanent)
|
|
require.Equal(t, 0, reqCounter.error.permanent)
|
|
}
|
|
|
|
// Test is successful if all the elements were retried on non-permanent errors
|
|
func randomNonPermanentErrorConsumeDecisionPassed(t *testing.T, allRecordsNumber int, reqCounter requestCounter) {
|
|
// more or equal tries than successes
|
|
require.GreaterOrEqual(t, reqCounter.total, reqCounter.success)
|
|
// it is retried on every error
|
|
require.Equal(t, reqCounter.total-reqCounter.error.nonpermanent, reqCounter.success)
|
|
require.Equal(t, allRecordsNumber+reqCounter.error.nonpermanent, reqCounter.total)
|
|
}
|
|
|
|
// Test is successful if the calls are not retried on permanent errors
|
|
func randomPermanentErrorConsumeDecisionPassed(t *testing.T, allRecordsNumber int, reqCounter requestCounter) {
|
|
require.Equal(t, allRecordsNumber-reqCounter.error.permanent, reqCounter.success)
|
|
require.Equal(t, reqCounter.total, allRecordsNumber)
|
|
}
|
|
|
|
// Test is successful if the calls are not retried on permanent errors
|
|
func randomErrorConsumeDecisionPassed(t *testing.T, allRecordsNumber int, reqCounter requestCounter) {
|
|
require.Equal(t, allRecordsNumber-reqCounter.error.permanent, reqCounter.success)
|
|
require.Equal(t, reqCounter.total, allRecordsNumber+reqCounter.error.nonpermanent)
|
|
}
|
|
|
|
func createOneLogWithID(id uniqueIDAttrVal) plog.Logs {
|
|
data := plog.NewLogs()
|
|
data.ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty().Attributes().PutStr(
|
|
uniqueIDAttrName,
|
|
string(id),
|
|
)
|
|
return data
|
|
}
|
|
|
|
func createOneTraceWithID(id uniqueIDAttrVal) ptrace.Traces {
|
|
data := ptrace.NewTraces()
|
|
data.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty().Attributes().PutStr(
|
|
uniqueIDAttrName,
|
|
string(id),
|
|
)
|
|
return data
|
|
}
|
|
|
|
func createOneMetricWithID(id uniqueIDAttrVal) pmetric.Metrics {
|
|
data := pmetric.NewMetrics()
|
|
data.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty().SetEmptyHistogram().
|
|
DataPoints().AppendEmpty().Attributes().PutStr(uniqueIDAttrName, string(id))
|
|
return data
|
|
}
|