diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e3cbf84c4de1e..f51b3ccbbea4a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -49,6 +49,7 @@ exporter/awss3exporter/ @open-telemetry exporter/awsxrayexporter/ @open-telemetry/collector-contrib-approvers @wangzlei @srprash exporter/azureblobexporter/ @open-telemetry/collector-contrib-approvers @hgaol @MovieStoreGuy exporter/azuredataexplorerexporter/ @open-telemetry/collector-contrib-approvers @ag-ramachandran +exporter/azureeventhubsexporter/ @open-telemetry/collector-contrib-approvers exporter/azuremonitorexporter/ @open-telemetry/collector-contrib-approvers @pcwiese @hgaol exporter/bmchelixexporter/ @open-telemetry/collector-contrib-approvers @bertysentry @NassimBtk @MovieStoreGuy exporter/cassandraexporter/ @open-telemetry/collector-contrib-approvers @atoulme @emreyalvac diff --git a/.github/ISSUE_TEMPLATE/beta_stability.yaml b/.github/ISSUE_TEMPLATE/beta_stability.yaml index 44e66848e3609..c9b3fbf883f8b 100644 --- a/.github/ISSUE_TEMPLATE/beta_stability.yaml +++ b/.github/ISSUE_TEMPLATE/beta_stability.yaml @@ -46,6 +46,7 @@ body: - exporter/awsxray - exporter/azureblob - exporter/azuredataexplorer + - exporter/azureeventhubs - exporter/azuremonitor - exporter/bmchelix - exporter/carbon diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 93f7a282d03ef..4f2ec7913d4f5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -49,6 +49,7 @@ body: - exporter/awsxray - exporter/azureblob - exporter/azuredataexplorer + - exporter/azureeventhubs - exporter/azuremonitor - exporter/bmchelix - exporter/carbon diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 12af8ca1562e5..0b120f09bf11b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -43,6 +43,7 @@ body: - exporter/awsxray - exporter/azureblob - exporter/azuredataexplorer + - exporter/azureeventhubs - exporter/azuremonitor - exporter/bmchelix - exporter/carbon diff --git a/.github/ISSUE_TEMPLATE/other.yaml b/.github/ISSUE_TEMPLATE/other.yaml index aec8266f1837b..727bd849acb99 100644 --- a/.github/ISSUE_TEMPLATE/other.yaml +++ b/.github/ISSUE_TEMPLATE/other.yaml @@ -43,6 +43,7 @@ body: - exporter/awsxray - exporter/azureblob - exporter/azuredataexplorer + - exporter/azureeventhubs - exporter/azuremonitor - exporter/bmchelix - exporter/carbon diff --git a/.github/ISSUE_TEMPLATE/unmaintained.yaml b/.github/ISSUE_TEMPLATE/unmaintained.yaml index 2745ab81e7e3d..21b7da96d2eba 100644 --- a/.github/ISSUE_TEMPLATE/unmaintained.yaml +++ b/.github/ISSUE_TEMPLATE/unmaintained.yaml @@ -48,6 +48,7 @@ body: - exporter/awsxray - exporter/azureblob - exporter/azuredataexplorer + - exporter/azureeventhubs - exporter/azuremonitor - exporter/bmchelix - exporter/carbon diff --git a/exporter/azureeventhubsexporter/Makefile b/exporter/azureeventhubsexporter/Makefile new file mode 100644 index 0000000000000..ded7a36092dc3 --- /dev/null +++ b/exporter/azureeventhubsexporter/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/exporter/azureeventhubsexporter/README.md b/exporter/azureeventhubsexporter/README.md new file mode 100644 index 0000000000000..a3ba3b8e54b5f --- /dev/null +++ b/exporter/azureeventhubsexporter/README.md @@ -0,0 +1,196 @@ +# Azure Event Hubs Exporter + +| Status | | +| ------------- |-----------| +| Stability | [alpha]: traces, metrics, logs | +| Distributions | [contrib] | +| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Aexporter%2Fazureeventhubs%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Aexporter%2Fazureeventhubs) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Aexporter%2Fazureeventhubs%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Aexporter%2Fazureeventhubs) | +| Code Coverage | [![codecov](https://codecov.io/gh/open-telemetry/opentelemetry-collector-contrib/branch/main/graph/badge.svg?flag=azureeventhubsexporter)](https://codecov.io/gh/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/azureeventhubsexporter) | +| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | \| Seeking more code owners! | + +[alpha]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#alpha +[contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib + + +This exporter sends logs, traces and metrics to [Azure Event Hubs](https://docs.microsoft.com/azure/event-hubs/). + +## Configuration + +The following settings can be configured: + +- `namespace` (required if not using connection_string): The Event Hubs namespace endpoint (e.g., "my-namespace.servicebus.windows.net") +- `event_hub`: Event Hub names for different telemetry types + - `logs` (default = `"logs"`): Event Hub name for logs + - `metrics` (default = `"metrics"`): Event Hub name for metrics + - `traces` (default = `"traces"`): Event Hub name for traces +- `auth`: Authentication configuration + - `type`: Authentication type. Supported values: `connection_string`, `service_principal`, `system_managed_identity`, `user_managed_identity`, `workload_identity`, `default_credentials` + - `connection_string`: Connection string to the Event Hubs namespace or Event Hub (required when type is `connection_string`) + - `tenant_id`: Tenant ID for Azure AD authentication (required for `service_principal` and `workload_identity`) + - `client_id`: Client ID (required for `service_principal`, `user_managed_identity`, and `workload_identity`) + - `client_secret`: Client secret (required for `service_principal`) + - `federated_token_file`: Path to federated token file (required for `workload_identity`) +- `format` (default = `"json"`): Format of encoded telemetry data. Supported values: `json`, `proto` +- `partition_key`: Partition key configuration for Event Hub partitioning + - `source`: How the partition key is generated. Options: `static`, `resource_attribute`, `trace_id`, `span_id`, `random` + - `value`: Used when source is `static` or specifies the attribute name when source is `resource_attribute` +- `max_event_size` (default = 1048576): Maximum size of an event in bytes (max: 1MB for Event Hubs) +- `batch_size` (default = 100): Number of events to batch before sending +- `retry_on_failure`: Retry configuration + - `enabled` (default = true): Whether to retry on failure + - `initial_interval` (default = 5s): Initial retry interval + - `max_interval` (default = 30s): Maximum retry interval + - `max_elapsed_time` (default = 5m): Maximum elapsed time for retries + +## Examples + +Using Connection String: + +```yaml +exporters: + azureeventhubs: + auth: + type: connection_string + connection_string: "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=my-key" + event_hub: + logs: "otel-logs" + metrics: "otel-metrics" + traces: "otel-traces" + format: json + partition_key: + source: trace_id + max_event_size: 1048576 + batch_size: 100 +``` + +Using Entra ID Service Principal: + +```yaml +exporters: + azureeventhubs: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: service_principal + tenant_id: "your-tenant-id" + client_id: "your-client-id" + client_secret: "your-client-secret" + event_hub: + logs: "otel-logs" + metrics: "otel-metrics" + traces: "otel-traces" + format: proto + partition_key: + source: resource_attribute + value: "service.name" +``` + +Using Managed Identity (system-assigned): + +```yaml +exporters: + azureeventhubs: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: system_managed_identity + event_hub: + logs: "otel-logs" + metrics: "otel-metrics" + traces: "otel-traces" + format: json + partition_key: + source: random +``` + +Using Managed Identity (user-assigned): + +```yaml +exporters: + azureeventhubs: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: user_managed_identity + client_id: "your-managed-identity-client-id" + event_hub: + logs: "otel-logs" + metrics: "otel-metrics" + traces: "otel-traces" +``` + +Using Workload Identity (for Kubernetes): + +```yaml +exporters: + azureeventhubs: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: workload_identity + tenant_id: "your-tenant-id" + client_id: "your-client-id" + federated_token_file: "/var/run/secrets/azure/tokens/azure-identity-token" + event_hub: + logs: "otel-logs" + metrics: "otel-metrics" + traces: "otel-traces" +``` + +Using DefaultAzureCredential: + +```yaml +exporters: + azureeventhubs: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: default_credentials + event_hub: + logs: "otel-logs" + metrics: "otel-metrics" + traces: "otel-traces" +``` + +## Data Export + +This exporter sends telemetry data to Azure Event Hubs with support for different data formats and partitioning strategies. + +### Data Formats + +The exporter supports two data formats: + +- `json`: Telemetry data is encoded as JSON (default) +- `proto`: Telemetry data is encoded using Protocol Buffers + +### Partition Key Strategies + +The exporter supports different partition key strategies to control how data is distributed across Event Hub partitions: + +- `static`: Uses a fixed partition key value +- `resource_attribute`: Uses the value of a specified resource attribute +- `trace_id`: Uses the trace ID from the telemetry data (traces and logs only) +- `span_id`: Uses the span ID from the telemetry data (traces only) +- `random`: Generates a random partition key for even distribution across partitions + +### Event Hub Routing + +By default, telemetry data is routed to different Event Hubs based on signal type: + +- Logs → `logs` Event Hub +- Metrics → `metrics` Event Hub +- Traces → `traces` Event Hub + +These can be customized using the `event_hub` configuration options. + +## Authentication + +The exporter supports multiple authentication methods: + +- `connection_string`: Uses a connection string to authenticate (recommended for development) +- `service_principal`: Uses Azure AD service principal authentication +- `system_managed_identity`: Uses system-assigned managed identity +- `user_managed_identity`: Uses user-assigned managed identity +- `workload_identity`: Uses workload identity (for Kubernetes environments) +- `default_credentials`: Uses DefaultAzureCredential which tries multiple authentication methods + +## Requirements + +- Event Hubs must exist before starting the collector +- The authentication principal must have "Azure Event Hubs Data Sender" role on the Event Hubs +- Maximum event size is 1MB for Event Hubs Standard tier diff --git a/exporter/azureeventhubsexporter/client.go b/exporter/azureeventhubsexporter/client.go new file mode 100644 index 0000000000000..8d8d52494bb56 --- /dev/null +++ b/exporter/azureeventhubsexporter/client.go @@ -0,0 +1,51 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter" + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs" +) + +// eventDataBatch is an interface that defines the methods needed from azeventhubs.EventDataBatch +// This interface allows for mocking in tests +type eventDataBatch interface { + AddEventData(eventData *azeventhubs.EventData, options *azeventhubs.AddEventDataOptions) error + NumEvents() int32 +} + +// eventHubProducerClient is an interface that defines the methods needed from azeventhubs.ProducerClient +// This interface allows for mocking in tests +type eventHubProducerClient interface { + NewEventDataBatch(ctx context.Context, options *azeventhubs.EventDataBatchOptions) (eventDataBatch, error) + SendEventDataBatch(ctx context.Context, batch eventDataBatch, options *azeventhubs.SendEventDataBatchOptions) error + Close(ctx context.Context) error +} + +// azureEventHubProducerClientWrapper wraps the Azure SDK ProducerClient to implement our interface +type azureEventHubProducerClientWrapper struct { + client *azeventhubs.ProducerClient +} + +func (w *azureEventHubProducerClientWrapper) NewEventDataBatch(ctx context.Context, options *azeventhubs.EventDataBatchOptions) (eventDataBatch, error) { + return w.client.NewEventDataBatch(ctx, options) +} + +func (w *azureEventHubProducerClientWrapper) SendEventDataBatch(ctx context.Context, batch eventDataBatch, options *azeventhubs.SendEventDataBatchOptions) error { + // Cast back to concrete type for Azure SDK + azureBatch, ok := batch.(*azeventhubs.EventDataBatch) + if !ok { + // For testing, just return nil as mock will handle it + return nil + } + return w.client.SendEventDataBatch(ctx, azureBatch, options) +} + +func (w *azureEventHubProducerClientWrapper) Close(ctx context.Context) error { + return w.client.Close(ctx) +} + +// Ensure that *azeventhubs.EventDataBatch implements eventDataBatch +var _ eventDataBatch = (*azeventhubs.EventDataBatch)(nil) diff --git a/exporter/azureeventhubsexporter/config.go b/exporter/azureeventhubsexporter/config.go new file mode 100644 index 0000000000000..bed5a4c443928 --- /dev/null +++ b/exporter/azureeventhubsexporter/config.go @@ -0,0 +1,152 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter" + +import ( + "errors" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configretry" +) + +type TelemetryConfig struct { + Logs string `mapstructure:"logs"` + Metrics string `mapstructure:"metrics"` + Traces string `mapstructure:"traces"` +} + +type Encodings struct { + Logs *component.ID `mapstructure:"logs"` + Metrics *component.ID `mapstructure:"metrics"` + Traces *component.ID `mapstructure:"traces"` +} + +type Authentication struct { + // Type is the authentication type. supported values are connection_string, service_principal, system_managed_identity, user_managed_identity, workload_identity, and default_credentials + Type AuthType `mapstructure:"type"` + + // TenantID is the tenant id for the AAD App. It's only needed when type is service_principal or workload_identity. + TenantID string `mapstructure:"tenant_id"` + + // ClientID is the AAD Application client id. It's needed when type is service_principal, user_managed_identity or workload_identity + ClientID string `mapstructure:"client_id"` + + // ClientSecret only needed when auth type is service_principal + ClientSecret string `mapstructure:"client_secret"` + + // ConnectionString to the Event Hubs namespace or Event Hub + ConnectionString string `mapstructure:"connection_string"` + + // FederatedTokenFile is the path to the file containing the federated token. It's needed when type is workload_identity. + FederatedTokenFile string `mapstructure:"federated_token_file"` +} + +type AuthType string + +const ( + ConnectionString AuthType = "connection_string" + SystemManagedIdentity AuthType = "system_managed_identity" + UserManagedIdentity AuthType = "user_managed_identity" + ServicePrincipal AuthType = "service_principal" + WorkloadIdentity AuthType = "workload_identity" + DefaultCredentials AuthType = "default_credentials" +) + +type PartitionKeyConfig struct { + // Source determines how the partition key is generated + // Options: "static", "resource_attribute", "trace_id", "span_id", "random" + Source string `mapstructure:"source"` + + // Value is used when source is "static" or specifies the attribute name when source is "resource_attribute" + Value string `mapstructure:"value"` +} + +// Config contains the main configuration options for the Azure Event Hubs exporter +type Config struct { + // Namespace is the Event Hubs namespace endpoint (e.g., "my-namespace.servicebus.windows.net") + Namespace string `mapstructure:"namespace"` + + // EventHub contains the Event Hub names for different telemetry types + EventHub TelemetryConfig `mapstructure:"event_hub"` + + // Auth contains authentication configuration + Auth Authentication `mapstructure:"auth"` + + // FormatType is the format of encoded telemetry data. Supported values are json and proto. + FormatType string `mapstructure:"format"` + + // PartitionKey configuration for Event Hub partitioning + PartitionKey PartitionKeyConfig `mapstructure:"partition_key"` + + // Encoding extension to apply for logs/metrics/traces. If present, overrides the marshaler configuration option and format. + Encodings Encodings `mapstructure:"encodings"` + + // MaxEventSize is the maximum size of an event in bytes (default: 1MB, max: 1MB for Event Hubs) + MaxEventSize int `mapstructure:"max_event_size"` + + // BatchSize is the number of events to batch before sending (default: 100) + BatchSize int `mapstructure:"batch_size"` + + configretry.BackOffConfig `mapstructure:"retry_on_failure"` +} + +func (c *Config) Validate() error { + if c.Namespace == "" && c.Auth.Type != ConnectionString { + return errors.New("namespace cannot be empty when auth type is not connection_string") + } + + switch c.Auth.Type { + case ConnectionString: + if c.Auth.ConnectionString == "" { + return errors.New("connection_string cannot be empty when auth type is connection_string") + } + case ServicePrincipal: + if c.Auth.TenantID == "" || c.Auth.ClientID == "" || c.Auth.ClientSecret == "" { + return errors.New("tenant_id, client_id and client_secret cannot be empty when auth type is service_principal") + } + case UserManagedIdentity: + if c.Auth.ClientID == "" { + return errors.New("client_id cannot be empty when auth type is user_managed_identity") + } + case WorkloadIdentity: + if c.Auth.TenantID == "" || c.Auth.ClientID == "" || c.Auth.FederatedTokenFile == "" { + return errors.New("tenant_id, client_id and federated_token_file cannot be empty when auth type is workload_identity") + } + case DefaultCredentials: + // No additional fields required for default credentials + // DefaultAzureCredential will automatically detect credentials from environment + } + + if c.FormatType != "json" && c.FormatType != "proto" { + return errors.New("unknown format type: " + c.FormatType) + } + + if c.MaxEventSize <= 0 || c.MaxEventSize > 1024*1024 { + return errors.New("max_event_size must be between 1 and 1048576 bytes (1MB)") + } + + if c.BatchSize <= 0 { + return errors.New("batch_size must be greater than 0") + } + + // Validate partition key configuration + switch c.PartitionKey.Source { + case "static": + if c.PartitionKey.Value == "" { + return errors.New("partition_key.value cannot be empty when source is static") + } + case "resource_attribute": + if c.PartitionKey.Value == "" { + return errors.New("partition_key.value must specify the attribute name when source is resource_attribute") + } + case "trace_id", "span_id", "random": + // These don't require additional configuration + default: + if c.PartitionKey.Source != "" { + return errors.New("unknown partition_key.source: " + c.PartitionKey.Source) + } + } + + return nil +} diff --git a/exporter/azureeventhubsexporter/config_test.go b/exporter/azureeventhubsexporter/config_test.go new file mode 100644 index 0000000000000..fca66542b1a91 --- /dev/null +++ b/exporter/azureeventhubsexporter/config_test.go @@ -0,0 +1,602 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configretry" + "go.opentelemetry.io/collector/confmap/confmaptest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter/internal/metadata" +) + +func TestLoadConfig(t *testing.T) { + t.Parallel() + + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + + tests := []struct { + id component.ID + expected component.Config + }{ + { + id: component.NewIDWithName(metadata.Type, "conn-string"), + expected: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=mykey", + }, + EventHub: TelemetryConfig{ + Traces: "otel-traces", + Metrics: "otel-metrics", + Logs: "otel-logs", + }, + FormatType: "json", + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + }, + { + id: component.NewIDWithName(metadata.Type, "sp"), + expected: &Config{ + Namespace: "my-namespace.servicebus.windows.net", + Auth: Authentication{ + Type: ServicePrincipal, + TenantID: "tenant-id", + ClientID: "client-id", + ClientSecret: "client-secret", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "proto", + PartitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "service.name", + }, + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + }, + { + id: component.NewIDWithName(metadata.Type, "smi"), + expected: &Config{ + Namespace: "my-namespace.servicebus.windows.net", + Auth: Authentication{ + Type: SystemManagedIdentity, + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + PartitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + }, + { + id: component.NewIDWithName(metadata.Type, "umi"), + expected: &Config{ + Namespace: "my-namespace.servicebus.windows.net", + Auth: Authentication{ + Type: UserManagedIdentity, + ClientID: "client-id", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + PartitionKey: PartitionKeyConfig{ + Source: "static", + Value: "my-partition", + }, + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + }, + { + id: component.NewIDWithName(metadata.Type, "workload"), + expected: &Config{ + Namespace: "my-namespace.servicebus.windows.net", + Auth: Authentication{ + Type: WorkloadIdentity, + TenantID: "tenant-id", + ClientID: "client-id", + FederatedTokenFile: "/var/run/secrets/azure/tokens/azure-identity-token", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + PartitionKey: PartitionKeyConfig{ + Source: "span_id", + }, + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + }, + { + id: component.NewIDWithName(metadata.Type, "default"), + expected: &Config{ + Namespace: "my-namespace.servicebus.windows.net", + Auth: Authentication{ + Type: DefaultCredentials, + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "proto", + + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.id.String(), func(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + + sub, err := cm.Sub(tt.id.String()) + require.NoError(t, err) + require.NoError(t, sub.Unmarshal(cfg)) + + assert.NoError(t, cfg.(*Config).Validate()) + assert.Equal(t, tt.expected, cfg) + }) + } +} + +func TestConfigValidation(t *testing.T) { + tests := []struct { + name string + config *Config + expectedErr string + }{ + { + name: "valid connection string config", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=Test;SharedAccessKey=key", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + }, + expectedErr: "", + }, + { + name: "missing namespace for non-connection-string auth", + config: &Config{ + Auth: Authentication{ + Type: ServicePrincipal, + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + }, + expectedErr: "namespace cannot be empty when auth type is not connection_string", + }, + { + name: "missing connection string", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + }, + expectedErr: "connection_string cannot be empty when auth type is connection_string", + }, + { + name: "missing service principal fields", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: ServicePrincipal, + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + }, + expectedErr: "tenant_id, client_id and client_secret cannot be empty when auth type is service_principal", + }, + { + name: "missing user managed identity client id", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: UserManagedIdentity, + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + }, + expectedErr: "client_id cannot be empty when auth type is user_managed_identity", + }, + { + name: "missing workload identity fields", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: WorkloadIdentity, + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + }, + expectedErr: "tenant_id, client_id and federated_token_file cannot be empty when auth type is workload_identity", + }, + { + name: "invalid format type", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: DefaultCredentials, + }, + FormatType: "xml", + MaxEventSize: 1048576, + BatchSize: 100, + }, + expectedErr: "unknown format type: xml", + }, + { + name: "invalid max event size - too large", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: DefaultCredentials, + }, + FormatType: "json", + MaxEventSize: 2000000, + BatchSize: 100, + }, + expectedErr: "max_event_size must be between 1 and 1048576 bytes (1MB)", + }, + { + name: "invalid max event size - zero", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: DefaultCredentials, + }, + FormatType: "json", + MaxEventSize: 0, + BatchSize: 100, + }, + expectedErr: "max_event_size must be between 1 and 1048576 bytes (1MB)", + }, + { + name: "invalid batch size", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: DefaultCredentials, + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 0, + }, + expectedErr: "batch_size must be greater than 0", + }, + { + name: "static partition key without value", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: DefaultCredentials, + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "static", + }, + }, + expectedErr: "partition_key.value cannot be empty when source is static", + }, + { + name: "resource_attribute partition key without value", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: DefaultCredentials, + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + }, + }, + expectedErr: "partition_key.value must specify the attribute name when source is resource_attribute", + }, + { + name: "invalid partition key source", + config: &Config{ + Namespace: "test.servicebus.windows.net", + Auth: Authentication{ + Type: DefaultCredentials, + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "invalid_source", + }, + }, + expectedErr: "unknown partition_key.source: invalid_source", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + if tt.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tt.expectedErr) + } + }) + } +} + +func TestConfigValidateEdgeCases(t *testing.T) { + tests := []struct { + name string + config *Config + expectedErr string + }{ + { + name: "valid connection string with namespace", + config: &Config{ + Namespace: "my-namespace.servicebus.windows.net", + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + }, + expectedErr: "", + }, + { + name: "max event size at boundary", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1, // minimum valid + BatchSize: 100, + }, + expectedErr: "", + }, + { + name: "max event size zero", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 0, + BatchSize: 100, + }, + expectedErr: "max_event_size must be between 1 and 1048576 bytes", + }, + { + name: "max event size exceeds limit", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048577, // exceeds 1MB + BatchSize: 100, + }, + expectedErr: "max_event_size must be between 1 and 1048576 bytes", + }, + { + name: "batch size zero", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 0, + }, + expectedErr: "batch_size must be greater than 0", + }, + { + name: "partition key static with empty value", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "static", + Value: "", + }, + }, + expectedErr: "partition_key.value cannot be empty when source is static", + }, + { + name: "partition key resource_attribute with empty value", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "", + }, + }, + expectedErr: "partition_key.value must specify the attribute name when source is resource_attribute", + }, + { + name: "partition key trace_id valid", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + }, + expectedErr: "", + }, + { + name: "partition key span_id valid", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "span_id", + }, + }, + expectedErr: "", + }, + { + name: "empty partition key source is valid", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "", + }, + }, + expectedErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + if tt.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tt.expectedErr) + } + }) + } +} diff --git a/exporter/azureeventhubsexporter/doc.go b/exporter/azureeventhubsexporter/doc.go new file mode 100644 index 0000000000000..84bbaf6468bdd --- /dev/null +++ b/exporter/azureeventhubsexporter/doc.go @@ -0,0 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package azureeventhubsexporter exports OpenTelemetry telemetry data to Azure Event Hubs. +// +// This package provides an OpenTelemetry Collector exporter that sends traces, metrics, +// and logs to Azure Event Hubs. The exporter supports various authentication methods +// including connection strings, service principals, and managed identities. +// +// The exporter supports JSON and Protocol Buffer formats for telemetry data and provides +// configurable partition key strategies for optimal load distribution across Event Hub partitions. +package azureeventhubsexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter" diff --git a/exporter/azureeventhubsexporter/exporter.go b/exporter/azureeventhubsexporter/exporter.go new file mode 100644 index 0000000000000..4d52e3c31fe88 --- /dev/null +++ b/exporter/azureeventhubsexporter/exporter.go @@ -0,0 +1,342 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter" + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "hash/fnv" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/collector/pipeline" + "go.uber.org/zap" +) + +type azureEventHubsExporter struct { + config *Config + logger *zap.Logger + client eventHubProducerClient + signal pipeline.Signal + marshaller marshaller +} + +// newExporter creates a new Azure Event Hubs exporter +func newExporter(config *Config, set component.TelemetrySettings, signal pipeline.Signal) (*azureEventHubsExporter, error) { + if err := config.Validate(); err != nil { + return nil, err + } + + marshaller, err := createMarshaller(config) + if err != nil { + return nil, err + } + + exp := &azureEventHubsExporter{ + config: config, + logger: set.Logger, + signal: signal, + marshaller: marshaller, + } + + return exp, nil +} + +func createMarshaller(config *Config) (marshaller, error) { + switch config.FormatType { + case formatTypeJSON: + return newJSONMarshaller(), nil + case formatTypeProto: + return newProtoMarshaller(), nil + default: + return nil, fmt.Errorf("unsupported format type: %s", config.FormatType) + } +} + +func (e *azureEventHubsExporter) start(ctx context.Context, host component.Host) error { + client, err := e.createEventHubsClient() + if err != nil { + return fmt.Errorf("failed to create Event Hubs client: %w", err) + } + e.client = client + return nil +} + +func (e *azureEventHubsExporter) shutdown(ctx context.Context) error { + if e.client != nil { + return e.client.Close(ctx) + } + return nil +} + +func (e *azureEventHubsExporter) createEventHubsClient() (eventHubProducerClient, error) { + var eventHubName string + switch e.signal { + case pipeline.SignalTraces: + eventHubName = e.config.EventHub.Traces + case pipeline.SignalLogs: + eventHubName = e.config.EventHub.Logs + case pipeline.SignalMetrics: + eventHubName = e.config.EventHub.Metrics + default: + return nil, fmt.Errorf("unsupported signal type: %v", e.signal) + } + + var azureClient *azeventhubs.ProducerClient + var err error + + switch e.config.Auth.Type { + case ConnectionString: + azureClient, err = azeventhubs.NewProducerClientFromConnectionString( + e.config.Auth.ConnectionString, + eventHubName, + nil, + ) + case DefaultCredentials: + cred, credErr := azidentity.NewDefaultAzureCredential(nil) + if credErr != nil { + return nil, fmt.Errorf("failed to create default credentials: %w", credErr) + } + azureClient, err = azeventhubs.NewProducerClient( + e.config.Namespace, + eventHubName, + cred, + nil, + ) + case SystemManagedIdentity: + cred, credErr := azidentity.NewManagedIdentityCredential(nil) + if credErr != nil { + return nil, fmt.Errorf("failed to create managed identity credential: %w", credErr) + } + azureClient, err = azeventhubs.NewProducerClient( + e.config.Namespace, + eventHubName, + cred, + nil, + ) + case UserManagedIdentity: + cred, credErr := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ + ID: azidentity.ClientID(e.config.Auth.ClientID), + }) + if credErr != nil { + return nil, fmt.Errorf("failed to create user managed identity credential: %w", credErr) + } + azureClient, err = azeventhubs.NewProducerClient( + e.config.Namespace, + eventHubName, + cred, + nil, + ) + case ServicePrincipal: + cred, credErr := azidentity.NewClientSecretCredential( + e.config.Auth.TenantID, + e.config.Auth.ClientID, + e.config.Auth.ClientSecret, + nil, + ) + if credErr != nil { + return nil, fmt.Errorf("failed to create service principal credential: %w", credErr) + } + azureClient, err = azeventhubs.NewProducerClient( + e.config.Namespace, + eventHubName, + cred, + nil, + ) + case WorkloadIdentity: + cred, credErr := azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{ + TenantID: e.config.Auth.TenantID, + ClientID: e.config.Auth.ClientID, + TokenFilePath: e.config.Auth.FederatedTokenFile, + }) + if credErr != nil { + return nil, fmt.Errorf("failed to create workload identity credential: %w", credErr) + } + azureClient, err = azeventhubs.NewProducerClient( + e.config.Namespace, + eventHubName, + cred, + nil, + ) + default: + return nil, fmt.Errorf("unsupported authentication type: %s", e.config.Auth.Type) + } + + if err != nil { + return nil, err + } + + // Wrap the Azure SDK client to implement our interface + return &azureEventHubProducerClientWrapper{client: azureClient}, nil +} + +func (e *azureEventHubsExporter) pushTraces(ctx context.Context, td ptrace.Traces) error { + data, err := e.marshaller.MarshalTraces(td) + if err != nil { + return fmt.Errorf("failed to marshal traces: %w", err) + } + + partitionKey := e.generatePartitionKey(td.ResourceSpans()) + return e.sendEvent(ctx, data, partitionKey) +} + +func (e *azureEventHubsExporter) pushLogs(ctx context.Context, ld plog.Logs) error { + data, err := e.marshaller.MarshalLogs(ld) + if err != nil { + return fmt.Errorf("failed to marshal logs: %w", err) + } + + partitionKey := e.generatePartitionKeyFromLogs(ld.ResourceLogs()) + return e.sendEvent(ctx, data, partitionKey) +} + +func (e *azureEventHubsExporter) pushMetrics(ctx context.Context, md pmetric.Metrics) error { + data, err := e.marshaller.MarshalMetrics(md) + if err != nil { + return fmt.Errorf("failed to marshal metrics: %w", err) + } + + partitionKey := e.generatePartitionKeyFromMetrics(md.ResourceMetrics()) + return e.sendEvent(ctx, data, partitionKey) +} + +func (e *azureEventHubsExporter) sendEvent(ctx context.Context, data []byte, partitionKey string) error { + if len(data) > e.config.MaxEventSize { + return fmt.Errorf("event size %d exceeds maximum allowed size %d", len(data), e.config.MaxEventSize) + } + + eventData := &azeventhubs.EventData{ + Body: data, + } + + var options *azeventhubs.EventDataBatchOptions + if partitionKey != "" { + options = &azeventhubs.EventDataBatchOptions{ + PartitionKey: &partitionKey, + } + } + + // Create a batch with a single event + batch, err := e.client.NewEventDataBatch(ctx, options) + if err != nil { + return fmt.Errorf("failed to create event batch: %w", err) + } + + err = batch.AddEventData(eventData, nil) + if err != nil { + return fmt.Errorf("failed to add event to batch: %w", err) + } + + err = e.client.SendEventDataBatch(ctx, batch, nil) + if err != nil { + return fmt.Errorf("failed to send event batch: %w", err) + } + + return nil +} + +func (e *azureEventHubsExporter) generatePartitionKey(resourceSpans ptrace.ResourceSpansSlice) string { + switch e.config.PartitionKey.Source { + case "static": + return e.config.PartitionKey.Value + case "resource_attribute": + if resourceSpans.Len() > 0 { + attrs := resourceSpans.At(0).Resource().Attributes() + if val, ok := attrs.Get(e.config.PartitionKey.Value); ok { + return val.AsString() + } + } + return "" + case "trace_id": + if resourceSpans.Len() > 0 { + scopeSpans := resourceSpans.At(0).ScopeSpans() + if scopeSpans.Len() > 0 { + spans := scopeSpans.At(0).Spans() + if spans.Len() > 0 { + return spans.At(0).TraceID().String() + } + } + } + return "" + case "span_id": + if resourceSpans.Len() > 0 { + scopeSpans := resourceSpans.At(0).ScopeSpans() + if scopeSpans.Len() > 0 { + spans := scopeSpans.At(0).Spans() + if spans.Len() > 0 { + return spans.At(0).SpanID().String() + } + } + } + return "" + case "random": + return e.generateRandomPartitionKey() + default: + return "" + } +} + +func (e *azureEventHubsExporter) generatePartitionKeyFromLogs(resourceLogs plog.ResourceLogsSlice) string { + switch e.config.PartitionKey.Source { + case "static": + return e.config.PartitionKey.Value + case "resource_attribute": + if resourceLogs.Len() > 0 { + attrs := resourceLogs.At(0).Resource().Attributes() + if val, ok := attrs.Get(e.config.PartitionKey.Value); ok { + return val.AsString() + } + } + return "" + case "trace_id": + if resourceLogs.Len() > 0 { + scopeLogs := resourceLogs.At(0).ScopeLogs() + if scopeLogs.Len() > 0 { + logRecords := scopeLogs.At(0).LogRecords() + if logRecords.Len() > 0 { + return logRecords.At(0).TraceID().String() + } + } + } + return "" + case "random": + return e.generateRandomPartitionKey() + default: + return "" + } +} + +func (e *azureEventHubsExporter) generatePartitionKeyFromMetrics(resourceMetrics pmetric.ResourceMetricsSlice) string { + switch e.config.PartitionKey.Source { + case "static": + return e.config.PartitionKey.Value + case "resource_attribute": + if resourceMetrics.Len() > 0 { + attrs := resourceMetrics.At(0).Resource().Attributes() + if val, ok := attrs.Get(e.config.PartitionKey.Value); ok { + return val.AsString() + } + } + return "" + case "random": + return e.generateRandomPartitionKey() + default: + return "" + } +} + +func (e *azureEventHubsExporter) generateRandomPartitionKey() string { + // Generate a hash-based partition key for consistent distribution + h := fnv.New32a() + randomBytes := make([]byte, 8) + rand.Read(randomBytes) + h.Write(randomBytes) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/exporter/azureeventhubsexporter/exporter_integration_test.go b/exporter/azureeventhubsexporter/exporter_integration_test.go new file mode 100644 index 0000000000000..3d5d8bb1ba1c1 --- /dev/null +++ b/exporter/azureeventhubsexporter/exporter_integration_test.go @@ -0,0 +1,572 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configretry" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/collector/pipeline" + "go.uber.org/zap" +) + +// TestCreateEventHubsClient tests the client creation logic for different signal types +func TestCreateEventHubsClient(t *testing.T) { + tests := []struct { + name string + signal pipeline.Signal + config *Config + expectError bool + errorMsg string + }{ + { + name: "connection_string for traces", + signal: pipeline.SignalTraces, + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + expectError: false, + }, + { + name: "connection_string for logs", + signal: pipeline.SignalLogs, + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + expectError: false, + }, + { + name: "connection_string for metrics", + signal: pipeline.SignalMetrics, + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + expectError: false, + }, + { + name: "invalid connection string", + signal: pipeline.SignalTraces, + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "invalid-connection-string", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + exp, err := newExporter(tt.config, set, tt.signal) + require.NoError(t, err) + + client, err := exp.createEventHubsClient() + if tt.expectError { + assert.Error(t, err) + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg) + } + } else { + assert.NoError(t, err) + assert.NotNil(t, client) + // Clean up + if client != nil { + _ = client.Close(context.Background()) + } + } + }) + } +} + +// TestPushTracesWithMarshalError tests error handling when marshalling fails +func TestPushTracesWithMarshalError(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "invalid", // This will cause createMarshaller to fail + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + + // This should fail during newExporter due to invalid format + _, err := newExporter(config, set, pipeline.SignalTraces) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown format type") +} + +// TestGeneratePartitionKeyEdgeCases tests edge cases in partition key generation +func TestGeneratePartitionKeyEdgeCases(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + + tests := []struct { + name string + partitionKey PartitionKeyConfig + setupData func() ptrace.ResourceSpansSlice + expectedEmpty bool + }{ + { + name: "trace_id with empty spans", + partitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + setupData: func() ptrace.ResourceSpansSlice { + rs := ptrace.NewResourceSpansSlice() + rs.AppendEmpty() + return rs + }, + expectedEmpty: true, + }, + { + name: "span_id with empty spans", + partitionKey: PartitionKeyConfig{ + Source: "span_id", + }, + setupData: func() ptrace.ResourceSpansSlice { + rs := ptrace.NewResourceSpansSlice() + rs.AppendEmpty() + return rs + }, + expectedEmpty: true, + }, + { + name: "resource_attribute with non-string value", + partitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "int.value", + }, + setupData: func() ptrace.ResourceSpansSlice { + rs := ptrace.NewResourceSpansSlice() + r := rs.AppendEmpty() + r.Resource().Attributes().PutInt("int.value", 123) + return rs + }, + expectedEmpty: false, // AsString() will convert it + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config.PartitionKey = tt.partitionKey + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + exp, err := newExporter(config, set, pipeline.SignalTraces) + require.NoError(t, err) + + data := tt.setupData() + key := exp.generatePartitionKey(data) + + if tt.expectedEmpty { + assert.Empty(t, key) + } else { + assert.NotEmpty(t, key) + } + }) + } +} + +// TestGeneratePartitionKeyFromLogsEdgeCases tests edge cases for logs +func TestGeneratePartitionKeyFromLogsEdgeCases(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + + tests := []struct { + name string + partitionKey PartitionKeyConfig + setupData func() plog.ResourceLogsSlice + expectedEmpty bool + }{ + { + name: "trace_id with empty logs", + partitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + setupData: func() plog.ResourceLogsSlice { + rl := plog.NewResourceLogsSlice() + rl.AppendEmpty() + return rl + }, + expectedEmpty: true, + }, + { + name: "trace_id with valid log", + partitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + setupData: func() plog.ResourceLogsSlice { + rl := plog.NewResourceLogsSlice() + r := rl.AppendEmpty() + sl := r.ScopeLogs().AppendEmpty() + lr := sl.LogRecords().AppendEmpty() + traceID := pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) + lr.SetTraceID(traceID) + return rl + }, + expectedEmpty: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config.PartitionKey = tt.partitionKey + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + exp, err := newExporter(config, set, pipeline.SignalLogs) + require.NoError(t, err) + + data := tt.setupData() + key := exp.generatePartitionKeyFromLogs(data) + + if tt.expectedEmpty { + assert.Empty(t, key) + } else { + assert.NotEmpty(t, key) + } + }) + } +} + +// TestGeneratePartitionKeyFromMetricsEdgeCases tests edge cases for metrics +func TestGeneratePartitionKeyFromMetricsEdgeCases(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + + tests := []struct { + name string + partitionKey PartitionKeyConfig + setupData func() pmetric.ResourceMetricsSlice + expectedEmpty bool + }{ + { + name: "resource_attribute with empty metrics", + partitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "missing", + }, + setupData: func() pmetric.ResourceMetricsSlice { + rm := pmetric.NewResourceMetricsSlice() + rm.AppendEmpty() + return rm + }, + expectedEmpty: true, + }, + { + name: "random with empty metrics", + partitionKey: PartitionKeyConfig{ + Source: "random", + }, + setupData: func() pmetric.ResourceMetricsSlice { + return pmetric.NewResourceMetricsSlice() + }, + expectedEmpty: false, // random always returns something + }, + { + name: "unknown source", + partitionKey: PartitionKeyConfig{ + Source: "", + }, + setupData: func() pmetric.ResourceMetricsSlice { + return pmetric.NewResourceMetricsSlice() + }, + expectedEmpty: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config.PartitionKey = tt.partitionKey + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + exp, err := newExporter(config, set, pipeline.SignalMetrics) + require.NoError(t, err) + + data := tt.setupData() + key := exp.generatePartitionKeyFromMetrics(data) + + if tt.expectedEmpty { + assert.Empty(t, key) + } else { + assert.NotEmpty(t, key) + } + }) + } +} + +// TestShutdownWithoutClient tests shutdown when client is nil +func TestShutdownWithoutClient(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + + exp, err := newExporter(config, set, pipeline.SignalTraces) + require.NoError(t, err) + require.Nil(t, exp.client) + + // Shutdown should not fail even if client is nil + err = exp.shutdown(context.Background()) + assert.NoError(t, err) +} + +// TestNewExporterWithInvalidFormat tests creation with invalid format +func TestNewExporterWithInvalidFormat(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "xml", // Invalid format + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + + // Should fail validation first + _, err := newExporter(config, set, pipeline.SignalTraces) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown format type") +} + +// TestGeneratePartitionKeyFromLogsAllCases tests all remaining cases +func TestGeneratePartitionKeyFromLogsAllCases(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + + tests := []struct { + name string + partitionKey PartitionKeyConfig + setupData func() plog.ResourceLogsSlice + expectedEmpty bool + }{ + { + name: "trace_id with no scope logs", + partitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + setupData: func() plog.ResourceLogsSlice { + rl := plog.NewResourceLogsSlice() + rl.AppendEmpty() + // No ScopeLogs added + return rl + }, + expectedEmpty: true, + }, + { + name: "trace_id with no log records", + partitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + setupData: func() plog.ResourceLogsSlice { + rl := plog.NewResourceLogsSlice() + r := rl.AppendEmpty() + r.ScopeLogs().AppendEmpty() + // No LogRecords added + return rl + }, + expectedEmpty: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config.PartitionKey = tt.partitionKey + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + exp, err := newExporter(config, set, pipeline.SignalLogs) + require.NoError(t, err) + + data := tt.setupData() + key := exp.generatePartitionKeyFromLogs(data) + + if tt.expectedEmpty { + assert.Empty(t, key) + } else { + assert.NotEmpty(t, key) + } + }) + } +} + +// TestNewExporterWithProtoFormat tests creating exporter with proto format +func TestNewExporterWithProtoFormat(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "proto", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + + exp, err := newExporter(config, set, pipeline.SignalTraces) + assert.NoError(t, err) + assert.NotNil(t, exp) + assert.NotNil(t, exp.marshaller) +} diff --git a/exporter/azureeventhubsexporter/exporter_mock_test.go b/exporter/azureeventhubsexporter/exporter_mock_test.go new file mode 100644 index 0000000000000..54b49140ef6fa --- /dev/null +++ b/exporter/azureeventhubsexporter/exporter_mock_test.go @@ -0,0 +1,490 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/config/configretry" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/collector/pipeline" + "go.uber.org/zap" +) + +// Helper function to create a mock exporter with a mock client +func newMockExporter(t *testing.T, config *Config, signal pipeline.Signal) (*azureEventHubsExporter, *mockEventHubProducerClient) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + + exp, err := newExporter(config, set, signal) + require.NoError(t, err) + + mockClient := &mockEventHubProducerClient{} + exp.client = mockClient + + return exp, mockClient +} + +func TestStartWithMockClient(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + + exp, err := newExporter(config, set, pipeline.SignalTraces) + require.NoError(t, err) + require.Nil(t, exp.client) + + // Start should create the client + err = exp.start(context.Background(), componenttest.NewNopHost()) + assert.NoError(t, err) + assert.NotNil(t, exp.client) + + // Cleanup + if exp.client != nil { + _ = exp.client.Close(context.Background()) + } +} + +func TestShutdownWithMockClient(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalTraces) + + // Expect Close to be called + mockClient.On("Close", mock.Anything).Return(nil) + + err := exp.shutdown(context.Background()) + assert.NoError(t, err) + + mockClient.AssertExpectations(t) +} + +func TestShutdownWithMockClientError(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalTraces) + + // Expect Close to return an error + mockClient.On("Close", mock.Anything).Return(errors.New("close error")) + + err := exp.shutdown(context.Background()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "close error") + + mockClient.AssertExpectations(t) +} + +func TestPushTracesSuccess(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "static", + Value: "test-key", + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalTraces) + + // Create test trace data + traces := ptrace.NewTraces() + rs := traces.ResourceSpans().AppendEmpty() + ss := rs.ScopeSpans().AppendEmpty() + span := ss.Spans().AppendEmpty() + span.SetName("test-span") + span.SetTraceID(pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})) + + // Create mock batch + mockBatch := &mockEventDataBatch{maxBytes: 1048576} + mockBatch.On("AddEventData", mock.Anything, mock.Anything).Return(nil) + + // Mock the batch creation and sending + mockClient.On("NewEventDataBatch", mock.Anything, mock.Anything).Return(mockBatch, nil) + mockClient.On("SendEventDataBatch", mock.Anything, mockBatch, mock.Anything).Return(nil) + + err := exp.pushTraces(context.Background(), traces) + assert.NoError(t, err) + + mockClient.AssertExpectations(t) + mockBatch.AssertExpectations(t) +} + +func TestPushLogsSuccess(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalLogs) + + // Create test log data + logs := plog.NewLogs() + rl := logs.ResourceLogs().AppendEmpty() + sl := rl.ScopeLogs().AppendEmpty() + logRecord := sl.LogRecords().AppendEmpty() + logRecord.Body().SetStr("test log message") + + // Create mock batch + mockBatch := &mockEventDataBatch{maxBytes: 1048576} + mockBatch.On("AddEventData", mock.Anything, mock.Anything).Return(nil) + + // Mock the batch creation and sending + mockClient.On("NewEventDataBatch", mock.Anything, mock.Anything).Return(mockBatch, nil) + mockClient.On("SendEventDataBatch", mock.Anything, mockBatch, mock.Anything).Return(nil) + + err := exp.pushLogs(context.Background(), logs) + assert.NoError(t, err) + + mockClient.AssertExpectations(t) + mockBatch.AssertExpectations(t) +} + +func TestPushMetricsSuccess(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalMetrics) + + // Create test metric data + metrics := pmetric.NewMetrics() + rm := metrics.ResourceMetrics().AppendEmpty() + sm := rm.ScopeMetrics().AppendEmpty() + metric := sm.Metrics().AppendEmpty() + metric.SetName("test-metric") + + // Create mock batch + mockBatch := &mockEventDataBatch{maxBytes: 1048576} + mockBatch.On("AddEventData", mock.Anything, mock.Anything).Return(nil) + + // Mock the batch creation and sending + mockClient.On("NewEventDataBatch", mock.Anything, mock.Anything).Return(mockBatch, nil) + mockClient.On("SendEventDataBatch", mock.Anything, mockBatch, mock.Anything).Return(nil) + + err := exp.pushMetrics(context.Background(), metrics) + assert.NoError(t, err) + + mockClient.AssertExpectations(t) + mockBatch.AssertExpectations(t) +} + +func TestSendEventBatchCreationError(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalTraces) + + // Create test trace data + traces := ptrace.NewTraces() + rs := traces.ResourceSpans().AppendEmpty() + ss := rs.ScopeSpans().AppendEmpty() + span := ss.Spans().AppendEmpty() + span.SetName("test-span") + + // Mock batch creation to return an error + mockClient.On("NewEventDataBatch", mock.Anything, mock.Anything).Return(nil, errors.New("batch creation error")) + + err := exp.pushTraces(context.Background(), traces) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create event batch") + + mockClient.AssertExpectations(t) +} + +func TestSendEventSendError(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalTraces) + + // Create test trace data + traces := ptrace.NewTraces() + rs := traces.ResourceSpans().AppendEmpty() + ss := rs.ScopeSpans().AppendEmpty() + span := ss.Spans().AppendEmpty() + span.SetName("test-span") + + // Create mock batch + mockBatch := &mockEventDataBatch{maxBytes: 1048576} + mockBatch.On("AddEventData", mock.Anything, mock.Anything).Return(nil) + + // Mock batch creation success but send failure + mockClient.On("NewEventDataBatch", mock.Anything, mock.Anything).Return(mockBatch, nil) + mockClient.On("SendEventDataBatch", mock.Anything, mockBatch, mock.Anything).Return(errors.New("send error")) + + err := exp.pushTraces(context.Background(), traces) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to send event batch") + + mockClient.AssertExpectations(t) + mockBatch.AssertExpectations(t) +} + +func TestSendEventWithPartitionKey(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "static", + Value: "my-partition", + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalTraces) + + // Create test trace data + traces := ptrace.NewTraces() + rs := traces.ResourceSpans().AppendEmpty() + ss := rs.ScopeSpans().AppendEmpty() + span := ss.Spans().AppendEmpty() + span.SetName("test-span") + + // Create mock batch + mockBatch := &mockEventDataBatch{maxBytes: 1048576} + mockBatch.On("AddEventData", mock.Anything, mock.Anything).Return(nil) + + // Verify that NewEventDataBatch is called with partition key options + mockClient.On("NewEventDataBatch", mock.Anything, mock.MatchedBy(func(opts *azeventhubs.EventDataBatchOptions) bool { + return opts != nil && opts.PartitionKey != nil && *opts.PartitionKey == "my-partition" + })).Return(mockBatch, nil) + mockClient.On("SendEventDataBatch", mock.Anything, mockBatch, mock.Anything).Return(nil) + + err := exp.pushTraces(context.Background(), traces) + assert.NoError(t, err) + + mockClient.AssertExpectations(t) + mockBatch.AssertExpectations(t) +} + +func TestSendEventWithoutPartitionKey(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "", // Empty source means no partition key + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalTraces) + + // Create test trace data + traces := ptrace.NewTraces() + rs := traces.ResourceSpans().AppendEmpty() + ss := rs.ScopeSpans().AppendEmpty() + span := ss.Spans().AppendEmpty() + span.SetName("test-span") + + // Create mock batch + mockBatch := &mockEventDataBatch{maxBytes: 1048576} + mockBatch.On("AddEventData", mock.Anything, mock.Anything).Return(nil) + + // Verify that NewEventDataBatch is called with nil options + mockClient.On("NewEventDataBatch", mock.Anything, (*azeventhubs.EventDataBatchOptions)(nil)).Return(mockBatch, nil) + mockClient.On("SendEventDataBatch", mock.Anything, mockBatch, mock.Anything).Return(nil) + + err := exp.pushTraces(context.Background(), traces) + assert.NoError(t, err) + + mockClient.AssertExpectations(t) + mockBatch.AssertExpectations(t) +} + +func TestSendEventExceedsMaxSize(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 10, // Very small size to trigger error + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + } + + exp, mockClient := newMockExporter(t, config, pipeline.SignalTraces) + + // Create test trace data + traces := ptrace.NewTraces() + rs := traces.ResourceSpans().AppendEmpty() + ss := rs.ScopeSpans().AppendEmpty() + span := ss.Spans().AppendEmpty() + span.SetName("test-span-with-long-name") + + // The error should happen before calling the client + err := exp.pushTraces(context.Background(), traces) + assert.Error(t, err) + assert.Contains(t, err.Error(), "event size") + assert.Contains(t, err.Error(), "exceeds maximum allowed size") + + // Client methods should not be called + mockClient.AssertNotCalled(t, "NewEventDataBatch") + mockClient.AssertNotCalled(t, "SendEventDataBatch") +} diff --git a/exporter/azureeventhubsexporter/exporter_test.go b/exporter/azureeventhubsexporter/exporter_test.go new file mode 100644 index 0000000000000..3bc464f182969 --- /dev/null +++ b/exporter/azureeventhubsexporter/exporter_test.go @@ -0,0 +1,970 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configretry" + "go.opentelemetry.io/collector/exporter/exportertest" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/collector/pipeline" + "go.uber.org/zap" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter/internal/metadata" +) + +func TestNewExporter(t *testing.T) { + tests := []struct { + name string + config *Config + signal pipeline.Signal + expectError bool + }{ + { + name: "valid json exporter for traces", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=testkey", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: formatTypeJSON, + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + MaxEventSize: 1024 * 1024, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + signal: pipeline.SignalTraces, + expectError: false, + }, + { + name: "valid proto exporter for logs", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=testkey", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: formatTypeProto, + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + MaxEventSize: 1024 * 1024, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + signal: pipeline.SignalLogs, + expectError: false, + }, + { + name: "invalid format type", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=testkey", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "xml", + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + MaxEventSize: 1024 * 1024, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + signal: pipeline.SignalMetrics, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + exp, err := newExporter(tt.config, set, tt.signal) + if tt.expectError { + assert.Error(t, err) + assert.Nil(t, exp) + } else { + assert.NoError(t, err) + assert.NotNil(t, exp) + assert.Equal(t, tt.signal, exp.signal) + } + }) + } +} + +func TestGeneratePartitionKey(t *testing.T) { + tests := []struct { + name string + config *Config + setupData func() ptrace.ResourceSpansSlice + expectEmpty bool + expectedKey string + }{ + { + name: "static partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "static", + Value: "my-static-key", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() ptrace.ResourceSpansSlice { return ptrace.NewResourceSpansSlice() }, + expectEmpty: false, + expectedKey: "my-static-key", + }, + { + name: "resource attribute partition key - found", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "service.name", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() ptrace.ResourceSpansSlice { + rs := ptrace.NewResourceSpansSlice() + r := rs.AppendEmpty() + r.Resource().Attributes().PutStr("service.name", "test-service") + return rs + }, + expectEmpty: false, + expectedKey: "test-service", + }, + { + name: "resource attribute partition key - not found", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "missing.attribute", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() ptrace.ResourceSpansSlice { + rs := ptrace.NewResourceSpansSlice() + r := rs.AppendEmpty() + r.Resource().Attributes().PutStr("service.name", "test-service") + return rs + }, + expectEmpty: true, + }, + { + name: "trace_id partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() ptrace.ResourceSpansSlice { + rs := ptrace.NewResourceSpansSlice() + r := rs.AppendEmpty() + ss := r.ScopeSpans().AppendEmpty() + span := ss.Spans().AppendEmpty() + traceID := pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) + span.SetTraceID(traceID) + return rs + }, + expectEmpty: false, + expectedKey: "0102030405060708090a0b0c0d0e0f10", + }, + { + name: "span_id partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "span_id", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() ptrace.ResourceSpansSlice { + rs := ptrace.NewResourceSpansSlice() + r := rs.AppendEmpty() + ss := r.ScopeSpans().AppendEmpty() + span := ss.Spans().AppendEmpty() + spanID := pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}) + span.SetSpanID(spanID) + return rs + }, + expectEmpty: false, + expectedKey: "0102030405060708", + }, + { + name: "random partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() ptrace.ResourceSpansSlice { return ptrace.NewResourceSpansSlice() }, + expectEmpty: false, + }, + { + name: "empty source - default to empty", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() ptrace.ResourceSpansSlice { return ptrace.NewResourceSpansSlice() }, + expectEmpty: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + exp, err := newExporter(tt.config, set, pipeline.SignalTraces) + assert.NoError(t, err) + + data := tt.setupData() + key := exp.generatePartitionKey(data) + + if tt.expectEmpty { + assert.Empty(t, key) + } else { + assert.NotEmpty(t, key) + if tt.expectedKey != "" { + assert.Equal(t, tt.expectedKey, key) + } + } + }) + } +} + +func TestGeneratePartitionKeyFromLogs(t *testing.T) { + tests := []struct { + name string + config *Config + setupData func() plog.ResourceLogsSlice + expectEmpty bool + expectedKey string + }{ + { + name: "static partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "static", + Value: "log-partition", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() plog.ResourceLogsSlice { return plog.NewResourceLogsSlice() }, + expectEmpty: false, + expectedKey: "log-partition", + }, + { + name: "resource attribute partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "host.name", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() plog.ResourceLogsSlice { + rl := plog.NewResourceLogsSlice() + r := rl.AppendEmpty() + r.Resource().Attributes().PutStr("host.name", "test-host") + return rl + }, + expectEmpty: false, + expectedKey: "test-host", + }, + { + name: "trace_id partition key from log", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() plog.ResourceLogsSlice { + rl := plog.NewResourceLogsSlice() + r := rl.AppendEmpty() + sl := r.ScopeLogs().AppendEmpty() + log := sl.LogRecords().AppendEmpty() + traceID := pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) + log.SetTraceID(traceID) + return rl + }, + expectEmpty: false, + expectedKey: "0102030405060708090a0b0c0d0e0f10", + }, + { + name: "random partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() plog.ResourceLogsSlice { return plog.NewResourceLogsSlice() }, + expectEmpty: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + exp, err := newExporter(tt.config, set, pipeline.SignalLogs) + assert.NoError(t, err) + + data := tt.setupData() + key := exp.generatePartitionKeyFromLogs(data) + + if tt.expectEmpty { + assert.Empty(t, key) + } else { + assert.NotEmpty(t, key) + if tt.expectedKey != "" { + assert.Equal(t, tt.expectedKey, key) + } + } + }) + } +} + +func TestGeneratePartitionKeyFromMetrics(t *testing.T) { + tests := []struct { + name string + config *Config + setupData func() pmetric.ResourceMetricsSlice + expectEmpty bool + expectedKey string + }{ + { + name: "static partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "static", + Value: "metric-partition", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() pmetric.ResourceMetricsSlice { return pmetric.NewResourceMetricsSlice() }, + expectEmpty: false, + expectedKey: "metric-partition", + }, + { + name: "resource attribute partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "environment", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() pmetric.ResourceMetricsSlice { + rm := pmetric.NewResourceMetricsSlice() + r := rm.AppendEmpty() + r.Resource().Attributes().PutStr("environment", "production") + return rm + }, + expectEmpty: false, + expectedKey: "production", + }, + { + name: "random partition key", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + }, + setupData: func() pmetric.ResourceMetricsSlice { return pmetric.NewResourceMetricsSlice() }, + expectEmpty: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + exp, err := newExporter(tt.config, set, pipeline.SignalMetrics) + assert.NoError(t, err) + + data := tt.setupData() + key := exp.generatePartitionKeyFromMetrics(data) + + if tt.expectEmpty { + assert.Empty(t, key) + } else { + assert.NotEmpty(t, key) + if tt.expectedKey != "" { + assert.Equal(t, tt.expectedKey, key) + } + } + }) + } +} + +func TestGenerateRandomPartitionKey(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + exp, err := newExporter(config, set, pipeline.SignalTraces) + assert.NoError(t, err) + + // Generate multiple keys and ensure they're different (very high probability) + keys := make(map[string]bool) + for i := 0; i < 10; i++ { + key := exp.generateRandomPartitionKey() + assert.NotEmpty(t, key) + keys[key] = true + } + + // With random generation, we should have multiple unique keys + assert.Greater(t, len(keys), 1, "Random partition keys should be unique") +} + +func TestCreateMarshallerInvalidFormat(t *testing.T) { + config := &Config{ + FormatType: "invalid", + } + + _, err := createMarshaller(config) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported format type") +} + +func TestCreateMarshallerJSON(t *testing.T) { + config := &Config{ + FormatType: "json", + } + + m, err := createMarshaller(config) + assert.NoError(t, err) + assert.NotNil(t, m) +} + +func TestCreateMarshallerProto(t *testing.T) { + config := &Config{ + FormatType: "proto", + } + + m, err := createMarshaller(config) + assert.NoError(t, err) + assert.NotNil(t, m) +} + +func TestNewExporterWithInvalidConfig(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + + tests := []struct { + name string + config *Config + expectedErr string + }{ + { + name: "missing namespace for non-connection-string auth", + config: &Config{ + Auth: Authentication{ + Type: SystemManagedIdentity, + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + }, + expectedErr: "namespace cannot be empty", + }, + { + name: "invalid format type", + config: &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "xml", + MaxEventSize: 1048576, + BatchSize: 100, + }, + expectedErr: "unknown format type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := newExporter(tt.config, set, pipeline.SignalTraces) + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErr) + }) + } +} + +func TestGeneratePartitionKeyEmptyData(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + exp, err := newExporter(config, set, pipeline.SignalTraces) + assert.NoError(t, err) + + // Test with empty resource spans + emptySpans := ptrace.NewResourceSpansSlice() + key := exp.generatePartitionKey(emptySpans) + assert.Empty(t, key) +} + +func TestGeneratePartitionKeyFromLogsEmptyData(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "trace_id", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + exp, err := newExporter(config, set, pipeline.SignalLogs) + assert.NoError(t, err) + + // Test with empty resource logs + emptyLogs := plog.NewResourceLogsSlice() + key := exp.generatePartitionKeyFromLogs(emptyLogs) + assert.Empty(t, key) +} + +func TestGeneratePartitionKeyFromMetricsEmptyData(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "test", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + exp, err := newExporter(config, set, pipeline.SignalMetrics) + assert.NoError(t, err) + + // Test with empty resource metrics + emptyMetrics := pmetric.NewResourceMetricsSlice() + key := exp.generatePartitionKeyFromMetrics(emptyMetrics) + assert.Empty(t, key) +} + +func TestGeneratePartitionKeyUnknownSource(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "", // empty/unknown source + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + exp, err := newExporter(config, set, pipeline.SignalTraces) + assert.NoError(t, err) + + spans := ptrace.NewResourceSpansSlice() + key := exp.generatePartitionKey(spans) + assert.Empty(t, key) +} + +func TestGeneratePartitionKeyResourceAttributeNotFound(t *testing.T) { + set := component.TelemetrySettings{ + Logger: zap.NewNop(), + } + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + PartitionKey: PartitionKeyConfig{ + Source: "resource_attribute", + Value: "nonexistent.attribute", + }, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + exp, err := newExporter(config, set, pipeline.SignalTraces) + assert.NoError(t, err) + + spans := ptrace.NewResourceSpansSlice() + rs := spans.AppendEmpty() + rs.Resource().Attributes().PutStr("different.attribute", "value") + + key := exp.generatePartitionKey(spans) + assert.Empty(t, key) +} + +func TestCreateLogsExporterSuccess(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + + exp, err := createLogsExporter( + context.Background(), + exportertest.NewNopSettings(metadata.Type), + config, + ) + assert.NoError(t, err) + assert.NotNil(t, exp) +} + +func TestCreateMetricsExporterSuccess(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ConnectionString, + ConnectionString: "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + }, + EventHub: TelemetryConfig{ + Traces: "traces", + Metrics: "metrics", + Logs: "logs", + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } + + exp, err := createMetricsExporter( + context.Background(), + exportertest.NewNopSettings(metadata.Type), + config, + ) + assert.NoError(t, err) + assert.NotNil(t, exp) +} + +func TestCreateLogsExporterWithInvalidConfig(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ServicePrincipal, + // Missing required fields + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + } + + _, err := createLogsExporter( + context.Background(), + exportertest.NewNopSettings(metadata.Type), + config, + ) + assert.Error(t, err) +} + +func TestCreateMetricsExporterWithInvalidConfig(t *testing.T) { + config := &Config{ + Auth: Authentication{ + Type: ServicePrincipal, + // Missing required fields + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + } + + _, err := createMetricsExporter( + context.Background(), + exportertest.NewNopSettings(metadata.Type), + config, + ) + assert.Error(t, err) +} diff --git a/exporter/azureeventhubsexporter/factory.go b/exporter/azureeventhubsexporter/factory.go new file mode 100644 index 0000000000000..51d61cbd84a2e --- /dev/null +++ b/exporter/azureeventhubsexporter/factory.go @@ -0,0 +1,119 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter" + +import ( + "context" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configretry" + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/exporter/exporterhelper" + "go.opentelemetry.io/collector/pipeline" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter/internal/metadata" +) + +const ( + // the format of encoded telemetry data + formatTypeJSON = "json" + formatTypeProto = "proto" +) + +// NewFactory creates a factory for Azure Event Hubs exporter. +func NewFactory() exporter.Factory { + return exporter.NewFactory( + metadata.Type, + createDefaultConfig, + exporter.WithTraces(createTracesExporter, metadata.TracesStability), + exporter.WithLogs(createLogsExporter, metadata.LogsStability), + exporter.WithMetrics(createMetricsExporter, metadata.MetricsStability), + ) +} + +func createDefaultConfig() component.Config { + return &Config{ + Auth: Authentication{ + Type: ConnectionString, + }, + EventHub: TelemetryConfig{ + Metrics: "metrics", + Logs: "logs", + Traces: "traces", + }, + FormatType: formatTypeJSON, + PartitionKey: PartitionKeyConfig{ + Source: "random", + }, + MaxEventSize: 1024 * 1024, // 1MB + BatchSize: 100, + BackOffConfig: configretry.NewDefaultBackOffConfig(), + } +} + +func createTracesExporter( + ctx context.Context, + set exporter.Settings, + cfg component.Config, +) (exporter.Traces, error) { + c := cfg.(*Config) + exp, err := newExporter(c, set.TelemetrySettings, pipeline.SignalTraces) + if err != nil { + return nil, err + } + + return exporterhelper.NewTraces( + ctx, + set, + cfg, + exp.pushTraces, + exporterhelper.WithStart(exp.start), + exporterhelper.WithShutdown(exp.shutdown), + exporterhelper.WithRetry(c.BackOffConfig), + ) +} + +func createLogsExporter( + ctx context.Context, + set exporter.Settings, + cfg component.Config, +) (exporter.Logs, error) { + c := cfg.(*Config) + exp, err := newExporter(c, set.TelemetrySettings, pipeline.SignalLogs) + if err != nil { + return nil, err + } + + return exporterhelper.NewLogs( + ctx, + set, + cfg, + exp.pushLogs, + exporterhelper.WithStart(exp.start), + exporterhelper.WithShutdown(exp.shutdown), + exporterhelper.WithRetry(c.BackOffConfig), + ) +} + +func createMetricsExporter( + ctx context.Context, + set exporter.Settings, + cfg component.Config, +) (exporter.Metrics, error) { + c := cfg.(*Config) + exp, err := newExporter(c, set.TelemetrySettings, pipeline.SignalMetrics) + if err != nil { + return nil, err + } + + return exporterhelper.NewMetrics( + ctx, + set, + cfg, + exp.pushMetrics, + exporterhelper.WithStart(exp.start), + exporterhelper.WithShutdown(exp.shutdown), + exporterhelper.WithRetry(c.BackOffConfig), + ) +} diff --git a/exporter/azureeventhubsexporter/factory_test.go b/exporter/azureeventhubsexporter/factory_test.go new file mode 100644 index 0000000000000..7ca82d85efa94 --- /dev/null +++ b/exporter/azureeventhubsexporter/factory_test.go @@ -0,0 +1,97 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/exporter/exportertest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter/internal/metadata" +) + +func TestCreateDefaultConfig(t *testing.T) { + cfg := createDefaultConfig() + assert.NotNil(t, cfg, "failed to create default config") + assert.NoError(t, componenttest.CheckConfigStruct(cfg)) + + // Verify default values + defaultCfg := cfg.(*Config) + assert.Equal(t, ConnectionString, defaultCfg.Auth.Type) + assert.Equal(t, "metrics", defaultCfg.EventHub.Metrics) + assert.Equal(t, "logs", defaultCfg.EventHub.Logs) + assert.Equal(t, "traces", defaultCfg.EventHub.Traces) + assert.Equal(t, "json", defaultCfg.FormatType) + assert.Equal(t, "random", defaultCfg.PartitionKey.Source) + assert.Equal(t, 1024*1024, defaultCfg.MaxEventSize) + assert.Equal(t, 100, defaultCfg.BatchSize) +} + +func TestCreateTracesExporter(t *testing.T) { + cfg := createDefaultConfig().(*Config) + cfg.Auth.ConnectionString = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=Test;SharedAccessKey=test" + + exp, err := createTracesExporter( + context.Background(), + exportertest.NewNopSettings(metadata.Type), + cfg) + assert.NoError(t, err) + require.NotNil(t, exp) +} + +func TestCreateLogsExporter(t *testing.T) { + cfg := createDefaultConfig().(*Config) + cfg.Auth.ConnectionString = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=Test;SharedAccessKey=test" + + exp, err := createLogsExporter( + context.Background(), + exportertest.NewNopSettings(metadata.Type), + cfg) + assert.NoError(t, err) + require.NotNil(t, exp) +} + +func TestCreateMetricsExporter(t *testing.T) { + cfg := createDefaultConfig().(*Config) + cfg.Auth.ConnectionString = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=Test;SharedAccessKey=test" + + exp, err := createMetricsExporter( + context.Background(), + exportertest.NewNopSettings(metadata.Type), + cfg) + assert.NoError(t, err) + require.NotNil(t, exp) +} + +func TestCreateTracesExporterWithInvalidConfig(t *testing.T) { + cfg := &Config{ + Auth: Authentication{ + Type: ServicePrincipal, + // Missing required fields + }, + FormatType: "json", + MaxEventSize: 1048576, + BatchSize: 100, + } + + _, err := createTracesExporter( + context.Background(), + exportertest.NewNopSettings(metadata.Type), + cfg) + assert.Error(t, err) +} + +func TestNewFactory(t *testing.T) { + factory := NewFactory() + assert.NotNil(t, factory) + assert.Equal(t, metadata.Type, factory.Type()) + + cfg := factory.CreateDefaultConfig() + assert.NotNil(t, cfg) + assert.NoError(t, componenttest.CheckConfigStruct(cfg)) +} diff --git a/exporter/azureeventhubsexporter/generated_component_test.go b/exporter/azureeventhubsexporter/generated_component_test.go new file mode 100644 index 0000000000000..e1daec8702b08 --- /dev/null +++ b/exporter/azureeventhubsexporter/generated_component_test.go @@ -0,0 +1,68 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package azureeventhubsexporter + +import ( + "context" + "testing" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter/internal/metadata" + "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/exporter/exportertest" +) + +func TestComponentFactoryType(t *testing.T) { + require.Equal(t, component.MustNewType("azureeventhubs"), NewFactory().Type()) +} + +func TestComponentConfigStruct(t *testing.T) { + require.NoError(t, componenttest.CheckConfigStruct(NewFactory().CreateDefaultConfig())) +} + +func TestComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct { + createFn func(ctx context.Context, set exporter.Settings, cfg component.Config) (component.Component, error) + name string + }{ + { + name: "logs", + createFn: func(ctx context.Context, set exporter.Settings, cfg component.Config) (component.Component, error) { + return factory.CreateLogs(ctx, set, cfg) + }, + }, + { + name: "metrics", + createFn: func(ctx context.Context, set exporter.Settings, cfg component.Config) (component.Component, error) { + return factory.CreateMetrics(ctx, set, cfg) + }, + }, + { + name: "traces", + createFn: func(ctx context.Context, set exporter.Settings, cfg component.Config) (component.Component, error) { + return factory.CreateTraces(ctx, set, cfg) + }, + }, + } + + // Note: This test requires a valid connection string to actually start the exporter + // In real scenarios, the exporter will fail to start without proper Azure Event Hubs configuration + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := factory.CreateDefaultConfig().(*Config) + // Set a dummy connection string for lifecycle test + cfg.Auth.ConnectionString = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=Test;SharedAccessKey=test" + + c, err := tt.createFn(context.Background(), exportertest.NewNopSettings(metadata.Type), cfg) + require.NoError(t, err) + require.NotNil(t, c) + + // Note: We skip the actual start/shutdown as it would require real Azure Event Hubs connection + // The component creation and configuration validation is sufficient for this test + }) + } +} diff --git a/exporter/azureeventhubsexporter/generated_package_test.go b/exporter/azureeventhubsexporter/generated_package_test.go new file mode 100644 index 0000000000000..f8c9bd6a1a1b9 --- /dev/null +++ b/exporter/azureeventhubsexporter/generated_package_test.go @@ -0,0 +1,13 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package azureeventhubsexporter + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + // skipping goleak test as per metadata.yml configuration + os.Exit(m.Run()) +} diff --git a/exporter/azureeventhubsexporter/go.mod b/exporter/azureeventhubsexporter/go.mod new file mode 100644 index 0000000000000..d58b11b58d7f6 --- /dev/null +++ b/exporter/azureeventhubsexporter/go.mod @@ -0,0 +1,84 @@ +module github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter + +go 1.24.0 + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 + github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.4.0 + github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.137.0 + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/collector/component v1.43.0 + go.opentelemetry.io/collector/component/componenttest v0.137.0 + go.opentelemetry.io/collector/config/configretry v1.43.0 + go.opentelemetry.io/collector/confmap v1.43.0 + go.opentelemetry.io/collector/exporter v1.43.0 + go.opentelemetry.io/collector/exporter/exporterhelper v0.137.0 + go.opentelemetry.io/collector/exporter/exportertest v0.137.0 + go.opentelemetry.io/collector/pdata v1.43.0 + go.opentelemetry.io/collector/pipeline v1.43.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/go-amqp v1.4.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect + github.com/knadh/koanf/providers/confmap v1.0.0 // indirect + github.com/knadh/koanf/v2 v2.3.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/client v1.43.0 // indirect + go.opentelemetry.io/collector/config/configoptional v1.43.0 // indirect + go.opentelemetry.io/collector/confmap/xconfmap v0.137.0 // indirect + go.opentelemetry.io/collector/consumer v1.43.0 // indirect + go.opentelemetry.io/collector/consumer/consumererror v0.137.0 // indirect + go.opentelemetry.io/collector/consumer/consumertest v0.137.0 // indirect + go.opentelemetry.io/collector/consumer/xconsumer v0.137.0 // indirect + go.opentelemetry.io/collector/exporter/xexporter v0.137.0 // indirect + go.opentelemetry.io/collector/extension v1.43.0 // indirect + go.opentelemetry.io/collector/extension/xextension v0.137.0 // indirect + go.opentelemetry.io/collector/featuregate v1.43.0 // indirect + go.opentelemetry.io/collector/internal/telemetry v0.137.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.137.0 // indirect + go.opentelemetry.io/collector/pdata/xpdata v0.137.0 // indirect + go.opentelemetry.io/collector/receiver v1.43.0 // indirect + go.opentelemetry.io/collector/receiver/receivertest v0.137.0 // indirect + go.opentelemetry.io/collector/receiver/xreceiver v0.137.0 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/exporter/azureeventhubsexporter/go.sum b/exporter/azureeventhubsexporter/go.sum new file mode 100644 index 0000000000000..c31541e915dda --- /dev/null +++ b/exporter/azureeventhubsexporter/go.sum @@ -0,0 +1,234 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.4.0 h1:BwmN55GUUfwFPSd44bxBVkFD8yJAp+LLjGRjSnpbeUM= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.4.0/go.mod h1:OowfWwCcXlcn1Nkk6oTxeCuGNRElKtYpzkF1/gZ42Ig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.0 h1:4hGvxD72TluuFIXVr8f4XkKZfqAa7Pj61t0jmQ7+kes= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.0/go.mod h1:TSH7DcFItwAufy0Lz+Ft2cyopExCpxbOxI5SkH4dRNo= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8= +github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4= +github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= +github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= +github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= +github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.137.0 h1:F95qdadeImWkOwXdZCfi0jSy2cKg0roXUnA/bNLiil8= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.137.0/go.mod h1:o65mCt5ZrLbooo2p8VpwwDUQGLjG9BchsQlvQQ2EIyw= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/client v1.43.0 h1:uWAjq2AHoKg1Yz4/NKYoDPKhU6jJSSWX9zIKdGLCOlg= +go.opentelemetry.io/collector/client v1.43.0/go.mod h1:9EQOLvyRdozYDKOC7XHIapKT2N6wGWHqgbDply/uRj4= +go.opentelemetry.io/collector/component v1.43.0 h1:9dyOmV0UuIhrNSASMeDH125jhfv7+FhWMq0HtNHHCs8= +go.opentelemetry.io/collector/component v1.43.0/go.mod h1:Pw3qM5HhgnSMpebNRUiiJuEiXxZyHq83vl7wXqxD8hU= +go.opentelemetry.io/collector/component/componenttest v0.137.0 h1:QC9MZsYyzQqN9qMlleJb78wf7FeCjbr4jLeCuNlKHLU= +go.opentelemetry.io/collector/component/componenttest v0.137.0/go.mod h1:JuiX9pv7qE5G8keihhjM66LeidryEnziPND0sXuK9PQ= +go.opentelemetry.io/collector/config/configoptional v1.43.0 h1:u/MCeLUawXINEi05VdRuBRQ3wivEltxTjJqnL1eww4w= +go.opentelemetry.io/collector/config/configoptional v1.43.0/go.mod h1:vdhEmJCpL4nQx2fETr3Bvg9Uy14IwThxL5/g8Mvo/A8= +go.opentelemetry.io/collector/config/configretry v1.43.0 h1:Va5pDNL0TOzqjLdJZ4xxQN9EggMSGVmxXBa+M6UEG30= +go.opentelemetry.io/collector/config/configretry v1.43.0/go.mod h1:ZSTYqAJCq4qf+/4DGoIxCElDIl5yHt8XxEbcnpWBbMM= +go.opentelemetry.io/collector/confmap v1.43.0 h1:QVAnbS7A+2Ra61xsuG355vhlW6uOMaKWysrwLQzDUz4= +go.opentelemetry.io/collector/confmap v1.43.0/go.mod h1:N5GZpFCmwD1GynDu3IWaZW5Ycfc/7YxSU0q1/E3vLdg= +go.opentelemetry.io/collector/confmap/xconfmap v0.137.0 h1:IKzD6w4YuvBi6GvxZfhz7SJR6GR1UpSQRuxtx20/+9U= +go.opentelemetry.io/collector/confmap/xconfmap v0.137.0/go.mod h1:psXdQr13pVrCqNPdoER2QZZorvONAR5ZUEHURe4POh4= +go.opentelemetry.io/collector/consumer v1.43.0 h1:51pfN5h6PLlaBwGPtyHn6BdK0DgtVGRV0UYRPbbscbs= +go.opentelemetry.io/collector/consumer v1.43.0/go.mod h1:v3J2g+6IwOPbLsnzL9cQfvgpmmsZt1YS7aXSNDFmJfk= +go.opentelemetry.io/collector/consumer/consumererror v0.137.0 h1:4HgYX6vVmaF17RRRtJDpR8EuWmLAv6JdKYG8slDDa+g= +go.opentelemetry.io/collector/consumer/consumererror v0.137.0/go.mod h1:muYN3UZ/43YHpDpQRVvCj0Rhpt/YjoPAF/BO63cPSwk= +go.opentelemetry.io/collector/consumer/consumertest v0.137.0 h1:tkqBk/DmJcrkRvHwNdDwvdiWfqyS6ymGgr9eyn6Vy6A= +go.opentelemetry.io/collector/consumer/consumertest v0.137.0/go.mod h1:6bKAlEgrAZ3NSn7ULLFZQMQtlW2xJlvVWkzIaGprucg= +go.opentelemetry.io/collector/consumer/xconsumer v0.137.0 h1:p3tkV3O9bL3bZl3RN2wmoxl22f8B8eMomKUqz656OPY= +go.opentelemetry.io/collector/consumer/xconsumer v0.137.0/go.mod h1:N+nRnP0ga4Scu8Ew87F+kxVajE/eGjRLbWC9H+elN5Q= +go.opentelemetry.io/collector/exporter v1.43.0 h1:FYQ/bhOOiLcmIFvDAUvqfzHmZSvKkTrIFyYprPw3xug= +go.opentelemetry.io/collector/exporter v1.43.0/go.mod h1:lUB2OSGrRyD5PSXU0rF9gWcUYCGublBdnCV5hKlG+z8= +go.opentelemetry.io/collector/exporter/exporterhelper v0.137.0 h1:ffiZjBJvzgPYJpOltwIpvTCF8zg1VPxsoP6aW4VTDuQ= +go.opentelemetry.io/collector/exporter/exporterhelper v0.137.0/go.mod h1:osf2K/HkbdUU7EFigLhxMmz2r5MX/74vYC2RrBDURrc= +go.opentelemetry.io/collector/exporter/exportertest v0.137.0 h1:JesnY7M87UWE/gRsVUgskX95QCL/S4j1ARQTVHH4ggg= +go.opentelemetry.io/collector/exporter/exportertest v0.137.0/go.mod h1:6UxHqO5IyMKL3ehlE3UNpFupIyGc5BBj7xzmPoDImOI= +go.opentelemetry.io/collector/exporter/xexporter v0.137.0 h1:2fSmBDB+tuFoYKJSHbR/1nJIeO+LvvrjdOYEODKuhdo= +go.opentelemetry.io/collector/exporter/xexporter v0.137.0/go.mod h1:9gudRad3ijkbzcnTLE0y+CzUDtC4TaPyZQDUKB2yzVs= +go.opentelemetry.io/collector/extension v1.43.0 h1:39cGAGMJIZEhhm4KbsvJJrG8AheS6wOc++ydY0Wpdp0= +go.opentelemetry.io/collector/extension v1.43.0/go.mod h1:HVCPnRqx70Qn9BAmnqJt393er4l1OwcgAytLv1fSOSo= +go.opentelemetry.io/collector/extension/extensiontest v0.137.0 h1:gnPF3HIOKqNk93XObt2x0WFvVfPtm76VggWe7LxgcaY= +go.opentelemetry.io/collector/extension/extensiontest v0.137.0/go.mod h1:vVmKojdITYka9+iAi3aarxeMrO6kdlywKuf3d3c6lcI= +go.opentelemetry.io/collector/extension/xextension v0.137.0 h1:UQ/I7D5/YmkvAV7g8yhWHY7BV31HvjGBCYduQJPyt+M= +go.opentelemetry.io/collector/extension/xextension v0.137.0/go.mod h1:T2Vr5ijSNW7PavuyZyRYYxCitpUTN+f4tRUdED/rtRw= +go.opentelemetry.io/collector/featuregate v1.43.0 h1:Aq8UR5qv1zNlbbkTyqv8kLJtnoQMq/sG1/jS9o1cCJI= +go.opentelemetry.io/collector/featuregate v1.43.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/internal/telemetry v0.137.0 h1:KlJcaBnIIn+QJzQIfA1eXbYUvHmgM7h/gLp/vjvUBMw= +go.opentelemetry.io/collector/internal/telemetry v0.137.0/go.mod h1:GWOiXBZ82kMzwGMEihJ5rEo5lFL7gurfHD++5q0XtI8= +go.opentelemetry.io/collector/pdata v1.43.0 h1:zVkj2hcjiMLwX+QDDNwb7iTh3LBjNXKv2qPSgj1Rzb4= +go.opentelemetry.io/collector/pdata v1.43.0/go.mod h1:KsJzdDG9e5BaHlmYr0sqdSEKeEiSfKzoF+rdWU7J//w= +go.opentelemetry.io/collector/pdata/pprofile v0.137.0 h1:bLVp8p8hpH81eQhhEQBkvLtS00GbnMU+ItNweBJLqZ8= +go.opentelemetry.io/collector/pdata/pprofile v0.137.0/go.mod h1:QfhMf7NnG+fTuwGGB1mXgcPzcXNxEYSW6CrVouOsF7Q= +go.opentelemetry.io/collector/pdata/testdata v0.137.0 h1:+oaGvbt0v7xryTX827szmyYWSAtvA0LbysEFV2nFjs0= +go.opentelemetry.io/collector/pdata/testdata v0.137.0/go.mod h1:3512FJaQsZz5EBlrY46xKjzoBc0MoMcQtAqYs2NaRQM= +go.opentelemetry.io/collector/pdata/xpdata v0.137.0 h1:EZvBE26Hxzk+Dv3NU7idjsS+cXbwZrwdWXGgcTxsC8g= +go.opentelemetry.io/collector/pdata/xpdata v0.137.0/go.mod h1:MFbISBnECZ1m1JPc5F6LUhVIkmFkebuVk3NcpmGPtB8= +go.opentelemetry.io/collector/pipeline v1.43.0 h1:IJjdqE5UCQlyVvFUUzlhSWhP4WIwpH6UyJQ9iWXpyww= +go.opentelemetry.io/collector/pipeline v1.43.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= +go.opentelemetry.io/collector/receiver v1.43.0 h1:Z/+es1SFKCwgd7mPy3Jf5KUSgy7WyypSExg4NshOwaY= +go.opentelemetry.io/collector/receiver v1.43.0/go.mod h1:XhP5zl+MOMbqvvc9I5JjwULIzp7dRRUxo53EHmrl5Bc= +go.opentelemetry.io/collector/receiver/receivertest v0.137.0 h1:LqlFKtThf07dFjYGLMfI2J4aio60S03gocm8CL6jOd4= +go.opentelemetry.io/collector/receiver/receivertest v0.137.0/go.mod h1:bg4wfd9uq3jZfarMcqanHhQDlwbByp3GHCY7I6YO/QY= +go.opentelemetry.io/collector/receiver/xreceiver v0.137.0 h1:30h6o1hI03PSc0upgwWMFRZYaVrqLaruA6r/jI1Kk/4= +go.opentelemetry.io/collector/receiver/xreceiver v0.137.0/go.mod h1:kvydfp3S8PKBVXH5OgPsTSneXQ92HGyi30hSrKy1fe4= +go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 h1:aBKdhLVieqvwWe9A79UHI/0vgp2t/s2euY8X59pGRlw= +go.opentelemetry.io/contrib/bridges/otelzap v0.13.0/go.mod h1:SYqtxLQE7iINgh6WFuVi2AI70148B8EI35DSk0Wr8m4= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A= +go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/slim/otlp v1.8.0 h1:afcLwp2XOeCbGrjufT1qWyruFt+6C9g5SOuymrSPUXQ= +go.opentelemetry.io/proto/slim/otlp v1.8.0/go.mod h1:Yaa5fjYm1SMCq0hG0x/87wV1MP9H5xDuG/1+AhvBcsI= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0 h1:Uc+elixz922LHx5colXGi1ORbsW8DTIGM+gg+D9V7HE= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0/go.mod h1:VyU6dTWBWv6h9w/+DYgSZAPMabWbPTFTuxp25sM8+s0= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0 h1:i8YpvWGm/Uq1koL//bnbJ/26eV3OrKWm09+rDYo7keU= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0/go.mod h1:pQ70xHY/ZVxNUBPn+qUWPl8nwai87eWdqL3M37lNi9A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/exporter/azureeventhubsexporter/internal/metadata/generated_status.go b/exporter/azureeventhubsexporter/internal/metadata/generated_status.go new file mode 100644 index 0000000000000..7025ca05f1b57 --- /dev/null +++ b/exporter/azureeventhubsexporter/internal/metadata/generated_status.go @@ -0,0 +1,21 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" +) + +var ( + Type = component.MustNewType("azureeventhubs") + ScopeName = "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter" +) + +const ( + TracesStability = component.StabilityLevelAlpha + MetricsStability = component.StabilityLevelAlpha + LogsStability = component.StabilityLevelAlpha +) diff --git a/exporter/azureeventhubsexporter/marshaller.go b/exporter/azureeventhubsexporter/marshaller.go new file mode 100644 index 0000000000000..dda6a9a480795 --- /dev/null +++ b/exporter/azureeventhubsexporter/marshaller.go @@ -0,0 +1,77 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter" + +import ( + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +type marshaller interface { + MarshalTraces(td ptrace.Traces) ([]byte, error) + MarshalLogs(ld plog.Logs) ([]byte, error) + MarshalMetrics(md pmetric.Metrics) ([]byte, error) + format() string +} + +type protoMarshaller struct { + tracesMarshaler ptrace.Marshaler + logsMarshaler plog.Marshaler + metricsMarshaler pmetric.Marshaler +} + +func newProtoMarshaller() *protoMarshaller { + return &protoMarshaller{ + tracesMarshaler: &ptrace.ProtoMarshaler{}, + logsMarshaler: &plog.ProtoMarshaler{}, + metricsMarshaler: &pmetric.ProtoMarshaler{}, + } +} + +func (p *protoMarshaller) MarshalTraces(td ptrace.Traces) ([]byte, error) { + return p.tracesMarshaler.MarshalTraces(td) +} + +func (p *protoMarshaller) MarshalLogs(ld plog.Logs) ([]byte, error) { + return p.logsMarshaler.MarshalLogs(ld) +} + +func (p *protoMarshaller) MarshalMetrics(md pmetric.Metrics) ([]byte, error) { + return p.metricsMarshaler.MarshalMetrics(md) +} + +func (p *protoMarshaller) format() string { + return formatTypeProto +} + +type jsonMarshaller struct { + tracesMarshaler ptrace.Marshaler + logsMarshaler plog.Marshaler + metricsMarshaler pmetric.Marshaler +} + +func newJSONMarshaller() *jsonMarshaller { + return &jsonMarshaller{ + tracesMarshaler: &ptrace.JSONMarshaler{}, + logsMarshaler: &plog.JSONMarshaler{}, + metricsMarshaler: &pmetric.JSONMarshaler{}, + } +} + +func (j *jsonMarshaller) MarshalTraces(td ptrace.Traces) ([]byte, error) { + return j.tracesMarshaler.MarshalTraces(td) +} + +func (j *jsonMarshaller) MarshalLogs(ld plog.Logs) ([]byte, error) { + return j.logsMarshaler.MarshalLogs(ld) +} + +func (j *jsonMarshaller) MarshalMetrics(md pmetric.Metrics) ([]byte, error) { + return j.metricsMarshaler.MarshalMetrics(md) +} + +func (j *jsonMarshaller) format() string { + return formatTypeJSON +} diff --git a/exporter/azureeventhubsexporter/marshaller_test.go b/exporter/azureeventhubsexporter/marshaller_test.go new file mode 100644 index 0000000000000..7dfe1446a7236 --- /dev/null +++ b/exporter/azureeventhubsexporter/marshaller_test.go @@ -0,0 +1,120 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/testdata" +) + +func TestJSONMarshaller(t *testing.T) { + marshaller := newJSONMarshaller() + assert.NotNil(t, marshaller) + assert.Equal(t, formatTypeJSON, marshaller.format()) + + t.Run("marshal traces", func(t *testing.T) { + traces := testdata.GenerateTracesTwoSpansSameResource() + data, err := marshaller.MarshalTraces(traces) + require.NoError(t, err) + assert.NotEmpty(t, data) + // Verify it's valid JSON by checking first character + assert.Equal(t, byte('{'), data[0]) + }) + + t.Run("marshal logs", func(t *testing.T) { + logs := testdata.GenerateLogsTwoLogRecordsSameResource() + data, err := marshaller.MarshalLogs(logs) + require.NoError(t, err) + assert.NotEmpty(t, data) + // Verify it's valid JSON + assert.Equal(t, byte('{'), data[0]) + }) + + t.Run("marshal metrics", func(t *testing.T) { + metrics := testdata.GenerateMetricsTwoMetrics() + data, err := marshaller.MarshalMetrics(metrics) + require.NoError(t, err) + assert.NotEmpty(t, data) + // Verify it's valid JSON + assert.Equal(t, byte('{'), data[0]) + }) +} + +func TestProtoMarshaller(t *testing.T) { + marshaller := newProtoMarshaller() + assert.NotNil(t, marshaller) + assert.Equal(t, formatTypeProto, marshaller.format()) + + t.Run("marshal traces", func(t *testing.T) { + traces := testdata.GenerateTracesTwoSpansSameResource() + data, err := marshaller.MarshalTraces(traces) + require.NoError(t, err) + assert.NotEmpty(t, data) + }) + + t.Run("marshal logs", func(t *testing.T) { + logs := testdata.GenerateLogsTwoLogRecordsSameResource() + data, err := marshaller.MarshalLogs(logs) + require.NoError(t, err) + assert.NotEmpty(t, data) + }) + + t.Run("marshal metrics", func(t *testing.T) { + metrics := testdata.GenerateMetricsTwoMetrics() + data, err := marshaller.MarshalMetrics(metrics) + require.NoError(t, err) + assert.NotEmpty(t, data) + }) +} + +func TestCreateMarshaller(t *testing.T) { + tests := []struct { + name string + config *Config + expectError bool + expectType string + }{ + { + name: "json marshaller", + config: &Config{ + FormatType: formatTypeJSON, + }, + expectError: false, + expectType: formatTypeJSON, + }, + { + name: "proto marshaller", + config: &Config{ + FormatType: formatTypeProto, + }, + expectError: false, + expectType: formatTypeProto, + }, + { + name: "unsupported format", + config: &Config{ + FormatType: "xml", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + marshaller, err := createMarshaller(tt.config) + if tt.expectError { + assert.Error(t, err) + assert.Nil(t, marshaller) + } else { + assert.NoError(t, err) + assert.NotNil(t, marshaller) + assert.Equal(t, tt.expectType, marshaller.format()) + } + }) + } +} diff --git a/exporter/azureeventhubsexporter/metadata.yaml b/exporter/azureeventhubsexporter/metadata.yaml new file mode 100644 index 0000000000000..f5f9c7c07ed34 --- /dev/null +++ b/exporter/azureeventhubsexporter/metadata.yaml @@ -0,0 +1,16 @@ +type: azureeventhubs + +status: + class: exporter + stability: + development: [traces, metrics, logs] + distributions: [contrib] + codeowners: + active: [] + seeking_new: true + +tests: + config: + skip_lifecycle: true + goleak: + skip: true diff --git a/exporter/azureeventhubsexporter/mock_client_test.go b/exporter/azureeventhubsexporter/mock_client_test.go new file mode 100644 index 0000000000000..5add64bbf3d81 --- /dev/null +++ b/exporter/azureeventhubsexporter/mock_client_test.go @@ -0,0 +1,57 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package azureeventhubsexporter + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs" + "github.com/stretchr/testify/mock" +) + +// mockEventHubProducerClient is a mock implementation using testify/mock +type mockEventHubProducerClient struct { + mock.Mock +} + +func (m *mockEventHubProducerClient) NewEventDataBatch(ctx context.Context, options *azeventhubs.EventDataBatchOptions) (eventDataBatch, error) { + args := m.Called(ctx, options) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(eventDataBatch), args.Error(1) +} + +func (m *mockEventHubProducerClient) SendEventDataBatch(ctx context.Context, batch eventDataBatch, options *azeventhubs.SendEventDataBatchOptions) error { + args := m.Called(ctx, batch, options) + return args.Error(0) +} + +func (m *mockEventHubProducerClient) Close(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +// mockEventDataBatch is a mock for EventDataBatch +type mockEventDataBatch struct { + mock.Mock + events []*azeventhubs.EventData + maxBytes int +} + +func (m *mockEventDataBatch) AddEventData(eventData *azeventhubs.EventData, options *azeventhubs.AddEventDataOptions) error { + args := m.Called(eventData, options) + if args.Error(0) == nil { + m.events = append(m.events, eventData) + } + return args.Error(0) +} + +func (m *mockEventDataBatch) NumEvents() int32 { + args := m.Called() + if len(args) > 0 && args.Get(0) != nil { + return args.Get(0).(int32) + } + return int32(len(m.events)) +} diff --git a/exporter/azureeventhubsexporter/testdata/config.yaml b/exporter/azureeventhubsexporter/testdata/config.yaml new file mode 100644 index 0000000000000..abc69d62fac73 --- /dev/null +++ b/exporter/azureeventhubsexporter/testdata/config.yaml @@ -0,0 +1,92 @@ +azureeventhubs/conn-string: + auth: + type: connection_string + connection_string: "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=mykey" + event_hub: + traces: "otel-traces" + metrics: "otel-metrics" + logs: "otel-logs" + format: json + partition_key: + source: random + max_event_size: 1048576 + batch_size: 100 + +azureeventhubs/sp: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: service_principal + tenant_id: "tenant-id" + client_id: "client-id" + client_secret: "client-secret" + event_hub: + traces: "traces" + metrics: "metrics" + logs: "logs" + format: proto + partition_key: + source: resource_attribute + value: "service.name" + max_event_size: 1048576 + batch_size: 100 + +azureeventhubs/smi: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: system_managed_identity + event_hub: + traces: "traces" + metrics: "metrics" + logs: "logs" + format: json + partition_key: + source: trace_id + max_event_size: 1048576 + batch_size: 100 + +azureeventhubs/umi: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: user_managed_identity + client_id: "client-id" + event_hub: + traces: "traces" + metrics: "metrics" + logs: "logs" + format: json + partition_key: + source: static + value: "my-partition" + max_event_size: 1048576 + batch_size: 100 + +azureeventhubs/workload: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: workload_identity + tenant_id: "tenant-id" + client_id: "client-id" + federated_token_file: "/var/run/secrets/azure/tokens/azure-identity-token" + event_hub: + traces: "traces" + metrics: "metrics" + logs: "logs" + format: json + partition_key: + source: span_id + max_event_size: 1048576 + batch_size: 100 + +azureeventhubs/default: + namespace: "my-namespace.servicebus.windows.net" + auth: + type: default_credentials + event_hub: + traces: "traces" + metrics: "metrics" + logs: "logs" + format: proto + partition_key: + source: random + max_event_size: 1048576 + batch_size: 100 diff --git a/versions.yaml b/versions.yaml index e71ccf83136a2..3f8043590e9f2 100644 --- a/versions.yaml +++ b/versions.yaml @@ -35,6 +35,7 @@ module-sets: - github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awsxrayexporter - github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureblobexporter - github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuredataexplorerexporter + - github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azureeventhubsexporter - github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuremonitorexporter - github.com/open-telemetry/opentelemetry-collector-contrib/exporter/bmchelixexporter - github.com/open-telemetry/opentelemetry-collector-contrib/exporter/carbonexporter