diff --git a/internal/huawei/backoff.go b/internal/huawei/backoff.go new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/receiver/huaweicloudlogsreceiver/Makefile b/receiver/huaweicloudlogsreceiver/Makefile new file mode 100644 index 000000000000..c1496226e590 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common \ No newline at end of file diff --git a/receiver/huaweicloudlogsreceiver/README.md b/receiver/huaweicloudlogsreceiver/README.md new file mode 100644 index 000000000000..9594918a7459 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/README.md @@ -0,0 +1,205 @@ +# Huawei Cloud CES Receiver + + +| Status | | +| ------------- |-----------| +| Stability | [development]: logs | +| Distributions | [] | +| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Areceiver%2Fhuaweicloudlogs%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Areceiver%2Fhuaweicloudlogs) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Areceiver%2Fhuaweicloudlogs%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Areceiver%2Fhuaweicloudlogs) | +| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | [@heitorganzeli](https://www.github.com/heitorganzeli), [@narcis96](https://www.github.com/narcis96) | + +[development]: https://github.com/open-telemetry/opentelemetry-collector#development + + +This receiver contains the implementation of the Huawei Cloud [Log Tank Service](https://www.huaweicloud.com/intl/en-us/product/lts.html) (LTS) receiver for the OpenTelemetry Collector. The receiver collects logs from Huawei Cloud's LTS service and sends them to the OpenTelemetry Collector for processing and exporting. + +## Configuration + +The following settings are required: + +- `project_id`: The ID of the project in Huawei Cloud. This is used to identify which project's logs are to be collected. See [Obtaining a Project ID](https://support.huaweicloud.com/intl/en-us/devg-apisign/api-sign-provide-proid.html). + +- `region_id`: The ID of the Huawei Cloud region from which logs are collected. For example, `eu-west-101`. The full list of the available regions can be found [here](https://pkg.go.dev/github.com/huaweicloud/huaweicloud-sdk-go-v3@v0.1.110/services/lts/v2/region). + +- `log_group_id`: A string indicating the ID of the log group. + +- `log_stream_id`: A string indicating the ID of the log stream. See [Obtaining Log Group and Log Stream IDs](https://support.huaweicloud.com/intl/en-us/api-lts/lts_api_0006.html#section1). + +- `no_verify_ssl`: A boolean flag indicating whether SSL verification should be disabled. Set to True to disable SSL verification. + +- `access_key`: The access key needed for LTS authentification. Check `Huawei Cloud SDK Authentication Setup` section for more details. + +- `secret_key`: The secret key needed for LTS authentification. Check `Huawei Cloud SDK Authentication Setup` section for more details. + +The following settings are optional: + +- `initial_delay`: The delay before the first collection of logs begins. This is a duration field, such as 5s for 5 seconds. + +- `collection_interval` (default = `60s`): This is the interval at which this receiver collects logs. This value must be a string readable by Golang's [time.ParseDuration](https://pkg.go.dev/time#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. We recommend a polling interval of at least one minute. + +- `retry_on_failure`: The following configurations can be used to control the retry policy of the LTS client. The default values are suitable for most deployment scenarios. + - `enabled` (default true) + - `initial_interval` (default 100ms) + - `max_interval` (default 1s) + - `max_elapsed_time` (default 15s) + - `randomization_factor` (default 0.5) + - `multiplier` (default 1.5) + +### Example Configuration + +```yaml +receivers: + huaweicloudlogsreceiver: + collection_interval: 3h + initial_delay: 5s + access_key: ${env:HUAWEICLOUD_SDK_AK} + secret_key: ${env:HUAWEICLOUD_SDK_SK} + project_id: project_1 + region_id: eu-west-101 + log_group_id: test-group-id + log_stream_id: test-stream-id + no_verify_ssl: True +``` + +The full list of settings exposed for this receiver are documented [here](./config.go). + +### Huawei Cloud SDK Authentication Setup + + +To ensure secure authentication, the Access Key (AK) and Secret Key (SK) used by the Huawei Cloud SDK must be stored in environment variables. See [Obtaining an AK/SK](https://support.huaweicloud.com/intl/en-us/devg-apisign/api-sign-provide-aksk.html). + +Before running the application, you need to set the environment variables `HUAWEICLOUD_SDK_AK` and `HUAWEICLOUD_SDK_SK` in your local environment. Here’s how you can do it: + +1. Open your terminal. +2. Set the environment variables by executing the following commands: + + ```sh + export HUAWEICLOUD_SDK_AK=your-access-key + export HUAWEICLOUD_SDK_SK=your-secret-key + ``` + +3. Verify that the variables are set correctly: + + ```sh + echo $HUAWEICLOUD_SDK_AK + echo $HUAWEICLOUD_SDK_SK + ``` + +## Error handling +If you encounter any LTS errors, please refer to the [Huawei Cloud Error Codes](https://support.huaweicloud.com/intl/en-us/ae-ad-1-api-lts/lts_api_0021.html). + +## Converting LTS Log Representation to OpenTelemetry Log Representation + +| Source Field | Target Field | Description | +|---------------------------|----------------------------------------------------|---------------------------------------------------------------------------------------------------| +| **Log Content (timestamp)** | `logRecord.observedTimestamp` | The timestamp extracted from the log content. It follows the layout `"2006-01-02/15:04:05"`. Converted to Unix time in nanoseconds. | +| **Log Content (body)** | `logRecord.body` | The main content of the log. Stored as a string in the OTLP log body. | +| **Labels (key)** | `logRecord.attributes` | Each label from the LTS log is converted to an attribute in the OTLP log, where the label's key is used as the attribute's key. | +| **Labels (value)** | `logRecord.attributes` | The corresponding value of each LTS log label key is stored as the attribute's value. | +| **Line Number** | `logRecord.attributes.lineNum` | The line number from the LTS log, if available. Stored as an attribute with the key `lineNum`. | +| **Project ID** | `resource.attributes.project.id` | The project ID used in the configuration file of the receiver.. | +| **Region ID** | `resource.attributes.region.id` | The region id used in the configuration file of the receiver. | +| **Group ID** | `resource.attributes.group.id` | The log group id used in the configuration file of the receiver. | +| **Stream ID** | `resource.attributes.stream.id` | The log stream id used in the configuration file of the receiver. | +| *N/A* | `resource.attributes.cloud.provider` | Set to `"huawei_cloud"` as the cloud provider. | +| *N/A* | `scopeLogs.scope.name` | Set to `"huawei_cloud_lts"` as the scope name. | +| *N/A* | `scopeLogs.scope.version` | Set to `"v2"` as the scope version. | + +This mapping ensures that logs from Huawei Cloud's LTS can be seamlessly integrated into systems using the OpenTelemetry Protocol for observability. + + +### Notes + +- The `timestamp` field in the source is converted from milliseconds to nanoseconds in the target field. +- Some fields are added in the target format with constant values to provide additional context and metadata. + +### Example: + +```json +[ + { + "content": "2020-07-25/14:40:00 this logis Error NO 2", + "line_num": "123", + "labels": { + "hostIP": "192.168.0.156", + "hostId": "9787ef31-f171-4eff-ba71-72d580f11f60", + "podName": "default_procname", + "clusterId": "CONFIG_FILE", + "nameSpace":"CONFIG_FILE", + "category": "LTS", + } + }, + { + "content": "2020-07-25/14:50:00 this logis Error NO 3", + "line_num": "456", + "labels": { + "hostIP": "192.168.0.156", + "hostId": "9787ef31-f171-4eff-ba71-72d580f11f60", + "podName": "default_procname", + "clusterId": "CONFIG_FILE", + "nameSpace":"CONFIG_FILE", + "category": "LTS", + } + }, +] +``` + +converts to + +```json +{ + "resourceLogs": [ + { + "resource": { + "attributes": { + "cloud.provider": "huawei_cloud", + "project.id": "project1", + "region.id": "region1", + "group.id": "group1", + "stream.id":"stream1" + }, + "scopeLogs": [ + { + "scope": { + "name": "huawei_cloud_lts", + "version": "v2" + }, + "logRecords": [ + { + "observedTimeUnixNano": "1595688000000000000", + "body": { + "stringValue": "2020-07-25/14:40:00 this log is Error NO 2" + }, + "attributes": { + "hostIP": "192.168.0.156", + "hostId": "9787ef31-f171-4eff-ba71-72d580f11f60", + "podName": "default_procname", + "clusterId": "CONFIG_FILE", + "nameSpace":"CONFIG_FILE", + "category": "LTS", + "lineNum": "123" + }, + }, + { + "observedTimeUnixNano": "1595688600000000000", + "body": { + "stringValue": "2020-07-25/14:50:00 this log is Error NO 3" + }, + "attributes": { + "hostIP": "192.168.0.156", + "hostId": "9787ef31-f171-4eff-ba71-72d580f11f60", + "podName": "default_procname", + "clusterId": "CONFIG_FILE", + "nameSpace":"CONFIG_FILE", + "category": "LTS", + "lineNum": "456" + }, + } + ] + } + ] + } + } + ] +} +``` \ No newline at end of file diff --git a/receiver/huaweicloudlogsreceiver/config.go b/receiver/huaweicloudlogsreceiver/config.go new file mode 100644 index 000000000000..dcd4db3149b4 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/config.go @@ -0,0 +1,122 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package huaweicloudlogsreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver" + +import ( + "errors" + "net/url" + "strconv" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/config" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configopaque" + "go.opentelemetry.io/collector/config/configretry" + "go.opentelemetry.io/collector/receiver/scraperhelper" + "go.uber.org/multierr" +) + +var ( + // Predefined error responses for configuration validation failures + errMissingProjectID = errors.New(`"project_id" is not specified in config`) + errMissingRegionID = errors.New(`"region_id" is not specified in config`) + errMissingGroupID = errors.New(`"log_group_id" is not specified in config`) + errMissingStreamID = errors.New(`"log_stream_id" is not specified in config`) + + errInvalidProxy = errors.New(`"proxy_address" must be specified if "proxy_user" or "proxy_password" is set"`) +) + +// Config represent a configuration for the CloudWatch logs exporter. +type Config struct { + scraperhelper.ControllerConfig `mapstructure:",squash"` + confighttp.ClientConfig `mapstructure:",squash"` + // Set of attributes used to configure huawei's CES SDK connection + HuaweiSessionConfig `mapstructure:",squash"` + + // ProjectID is a string to reference project where logs should be associated with. + // If ProjectID is not filled in, the SDK will automatically call the IAM service to query the project id corresponding to the region. + ProjectID string `mapstructure:"project_id"` + + // RegionID is the ID of the LTS region. + RegionID string `mapstructure:"region_id"` + + // GroupID is the ID of the LTS log group. + GroupID string `mapstructure:"log_group_id"` + + // Stream is the ID of the LTS log stream. + StreamID string `mapstructure:"log_stream_id"` + + BackOffConfig configretry.BackOffConfig `mapstructure:"retry_on_failure"` +} + +type HuaweiSessionConfig struct { + AccessKey configopaque.String `mapstructure:"access_key"` + + SecretKey configopaque.String `mapstructure:"secret_key"` + // Number of seconds before timing out a request. + NoVerifySSL bool `mapstructure:"no_verify_ssl"` + // Upload segments to AWS X-Ray through a proxy. + ProxyAddress string `mapstructure:"proxy_address"` + ProxyUser string `mapstructure:"proxy_user"` + ProxyPassword string `mapstructure:"proxy_password"` +} + +var _ component.Config = (*Config)(nil) + +// Validate config +func (config *Config) Validate() error { + var err error + if config.ProjectID == "" { + err = multierr.Append(err, errMissingProjectID) + } + if config.RegionID == "" { + err = multierr.Append(err, errMissingRegionID) + } + if config.GroupID == "" { + err = multierr.Append(err, errMissingGroupID) + } + if config.StreamID == "" { + err = multierr.Append(err, errMissingStreamID) + } + + // Validate that ProxyAddress is provided if ProxyUser or ProxyPassword is set + if (config.ProxyUser != "" || config.ProxyPassword != "") && config.ProxyAddress == "" { + err = multierr.Append(err, errInvalidProxy) + } + + return err +} + +func createHTTPConfig(cfg HuaweiSessionConfig) (*config.HttpConfig, error) { + if cfg.ProxyAddress == "" { + return config.DefaultHttpConfig().WithIgnoreSSLVerification(cfg.NoVerifySSL), nil + } + proxy, err := configureHTTPProxy(cfg) + if err != nil { + return nil, err + } + return config.DefaultHttpConfig().WithProxy(proxy), nil +} + +func configureHTTPProxy(cfg HuaweiSessionConfig) (*config.Proxy, error) { + proxyURL, err := url.Parse(cfg.ProxyAddress) + if err != nil { + return nil, err + } + + proxy := config.NewProxy(). + WithSchema(proxyURL.Scheme). + WithHost(proxyURL.Hostname()) + if len(proxyURL.Port()) > 0 { + if i, err := strconv.Atoi(proxyURL.Port()); err == nil { + proxy = proxy.WithPort(i) + } + } + + // Configure the username and password if the proxy requires authentication + if len(cfg.ProxyUser) > 0 { + proxy = proxy.WithUsername(cfg.ProxyUser).WithPassword(cfg.ProxyPassword) + } + return proxy, nil +} diff --git a/receiver/huaweicloudlogsreceiver/config_test.go b/receiver/huaweicloudlogsreceiver/config_test.go new file mode 100644 index 000000000000..750dec39403a --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/config_test.go @@ -0,0 +1,115 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package huaweicloudlogsreceiver + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/receiver/scraperhelper" +) + +func TestConfig_Validate(t *testing.T) { + tests := []struct { + name string + config Config + expectedError string + }{ + { + name: "Valid config", + config: Config{ + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: time.Hour, + }, + RegionID: "cn-north-1", + ProjectID: "my_project", + GroupID: "group-1", + StreamID: "stream-1", + }, + expectedError: "", + }, + { + name: "Missing region name", + config: Config{ + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: time.Hour, + }, + ProjectID: "my_project", + }, + expectedError: errMissingRegionID.Error(), + }, + { + name: "Missing project id", + config: Config{ + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: time.Hour, + }, + RegionID: "cn-north-1", + }, + expectedError: errMissingProjectID.Error(), + }, + { + name: "Proxy user without proxy address", + config: Config{ + HuaweiSessionConfig: HuaweiSessionConfig{ + ProxyUser: "user", + }, + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: time.Hour, + }, + RegionID: "cn-north-1", + ProjectID: "my_project", + GroupID: "group-1", + StreamID: "stream-1", + }, + expectedError: errInvalidProxy.Error(), + }, + { + name: "Proxy password without proxy address", + config: Config{ + HuaweiSessionConfig: HuaweiSessionConfig{ + ProxyPassword: "password", + }, + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: time.Hour, + }, + RegionID: "cn-north-1", + ProjectID: "my_project", + GroupID: "group-1", + StreamID: "stream-1", + }, + expectedError: errInvalidProxy.Error(), + }, + { + name: "Proxy address with proxy user and password", + config: Config{ + HuaweiSessionConfig: HuaweiSessionConfig{ + ProxyAddress: "http://proxy.example.com", + ProxyUser: "user", + ProxyPassword: "password", + }, + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: time.Hour, + }, + RegionID: "cn-north-1", + ProjectID: "my_project", + GroupID: "group-1", + StreamID: "stream-1", + }, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + if tt.expectedError == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tt.expectedError) + } + }) + } +} diff --git a/receiver/huaweicloudlogsreceiver/factory.go b/receiver/huaweicloudlogsreceiver/factory.go new file mode 100644 index 000000000000..91662fa72231 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/factory.go @@ -0,0 +1,54 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package huaweicloudlogsreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver" + +import ( + "context" + "time" + + "github.com/cenkalti/backoff/v4" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configretry" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/receiver" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver/internal/metadata" +) + +func NewFactory() receiver.Factory { + return receiver.NewFactory( + metadata.Type, + createDefaultConfig, + receiver.WithLogs(createLogsReceiver, metadata.LogsStability)) +} + +func createDefaultConfig() component.Config { + return &Config{ + BackOffConfig: configretry.BackOffConfig{ + Enabled: true, + InitialInterval: 100 * time.Millisecond, + MaxInterval: time.Second, + MaxElapsedTime: 15 * time.Second, + RandomizationFactor: backoff.DefaultRandomizationFactor, + Multiplier: backoff.DefaultMultiplier, + }, + HuaweiSessionConfig: HuaweiSessionConfig{ + NoVerifySSL: false, + }, + } +} + +func createLogsReceiver( + _ context.Context, + params receiver.Settings, + cfg component.Config, + next consumer.Logs) (receiver.Logs, error) { + + cesCfg := cfg.(*Config) + + cesReceiver := newHuaweiCloudLogsReceiver(params, cesCfg, next) + + return cesReceiver, nil + +} diff --git a/receiver/huaweicloudlogsreceiver/factory_test.go b/receiver/huaweicloudlogsreceiver/factory_test.go new file mode 100644 index 000000000000..970a666768af --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/factory_test.go @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package huaweicloudlogsreceiver + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver/receivertest" +) + +func TestNewFactory(t *testing.T) { + factory := NewFactory() + assert.NotNil(t, factory) + assert.Equal(t, component.MustNewType("huaweicloudlogsreceiver"), factory.Type()) +} + +func TestCreateDefaultConfig(t *testing.T) { + factory := NewFactory() + config := factory.CreateDefaultConfig() + assert.NotNil(t, config) + assert.NoError(t, componenttest.CheckConfigStruct(config)) +} + +func TestCreateLogsReceiver(t *testing.T) { + factory := NewFactory() + config := factory.CreateDefaultConfig() + + rConfig := config.(*Config) + rConfig.CollectionInterval = 60 * time.Second + rConfig.InitialDelay = time.Second + + nextConsumer := new(consumertest.LogsSink) + receiver, err := factory.CreateLogsReceiver(context.Background(), receivertest.NewNopSettings(), config, nextConsumer) + assert.NoError(t, err) + assert.NotNil(t, receiver) +} diff --git a/receiver/huaweicloudlogsreceiver/generated_component_test.go b/receiver/huaweicloudlogsreceiver/generated_component_test.go new file mode 100644 index 000000000000..c6ce35029500 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/generated_component_test.go @@ -0,0 +1,69 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package huaweicloudlogsreceiver + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/confmap/confmaptest" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/receiver/receivertest" +) + +func TestComponentFactoryType(t *testing.T) { + require.Equal(t, "huaweicloudlogsreceiver", NewFactory().Type().String()) +} + +func TestComponentConfigStruct(t *testing.T) { + require.NoError(t, componenttest.CheckConfigStruct(NewFactory().CreateDefaultConfig())) +} + +func TestComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct { + name string + createFn func(ctx context.Context, set receiver.Settings, cfg component.Config) (component.Component, error) + }{ + + { + name: "logs", + createFn: func(ctx context.Context, set receiver.Settings, cfg component.Config) (component.Component, error) { + return factory.CreateLogsReceiver(ctx, set, cfg, consumertest.NewNop()) + }, + }, + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, sub.Unmarshal(&cfg)) + + for _, test := range tests { + t.Run(test.name+"-shutdown", func(t *testing.T) { + c, err := test.createFn(context.Background(), receivertest.NewNopSettings(), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + t.Run(test.name+"-lifecycle", func(t *testing.T) { + firstRcvr, err := test.createFn(context.Background(), receivertest.NewNopSettings(), cfg) + require.NoError(t, err) + host := componenttest.NewNopHost() + require.NoError(t, err) + require.NoError(t, firstRcvr.Start(context.Background(), host)) + require.NoError(t, firstRcvr.Shutdown(context.Background())) + secondRcvr, err := test.createFn(context.Background(), receivertest.NewNopSettings(), cfg) + require.NoError(t, err) + require.NoError(t, secondRcvr.Start(context.Background(), host)) + require.NoError(t, secondRcvr.Shutdown(context.Background())) + }) + } +} diff --git a/receiver/huaweicloudlogsreceiver/generated_package_test.go b/receiver/huaweicloudlogsreceiver/generated_package_test.go new file mode 100644 index 000000000000..3b6ca7e307c7 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/generated_package_test.go @@ -0,0 +1,12 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package huaweicloudlogsreceiver + +import ( + "go.uber.org/goleak" + "testing" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/receiver/huaweicloudlogsreceiver/go.mod b/receiver/huaweicloudlogsreceiver/go.mod new file mode 100644 index 000000000000..b68e0f441b64 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/go.mod @@ -0,0 +1,95 @@ +module github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver + +go 1.22.0 + +toolchain go1.22.3 + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.110 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.108.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.107.0 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector/component v0.107.0 + go.opentelemetry.io/collector/config/confighttp v0.107.0 + go.opentelemetry.io/collector/config/configopaque v1.13.0 + go.opentelemetry.io/collector/config/configretry v1.13.0 + go.opentelemetry.io/collector/confmap v0.107.0 + go.opentelemetry.io/collector/consumer v0.107.0 + go.opentelemetry.io/collector/consumer/consumertest v0.107.0 + go.opentelemetry.io/collector/pdata v1.14.1 + go.opentelemetry.io/collector/receiver v0.106.1 + go.uber.org/goleak v1.3.0 + go.uber.org/multierr v1.11.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.4 // 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/klauspost/compress v1.17.9 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.1 // 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.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.108.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rs/cors v1.11.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect + go.mongodb.org/mongo-driver v1.15.0 // indirect + go.opentelemetry.io/collector v0.107.0 // indirect + go.opentelemetry.io/collector/client v1.13.0 // indirect + go.opentelemetry.io/collector/config/configauth v0.107.0 // indirect + go.opentelemetry.io/collector/config/configcompression v1.13.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.107.0 // indirect + go.opentelemetry.io/collector/config/configtls v1.13.0 // indirect + go.opentelemetry.io/collector/config/internal v0.107.0 // indirect + go.opentelemetry.io/collector/consumer/consumerprofiles v0.107.0 // indirect + go.opentelemetry.io/collector/extension v0.107.0 // indirect + go.opentelemetry.io/collector/extension/auth v0.107.0 // indirect + go.opentelemetry.io/collector/featuregate v1.13.0 // indirect + go.opentelemetry.io/collector/internal/globalgates v0.107.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.107.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.50.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil => ../../pkg/pdatautil + +replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest => ../../pkg/pdatatest + +replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => ../../pkg/golden diff --git a/receiver/huaweicloudlogsreceiver/go.sum b/receiver/huaweicloudlogsreceiver/go.sum new file mode 100644 index 000000000000..c9dc3a760183 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/go.sum @@ -0,0 +1,310 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/huaweicloud/huaweicloud-sdk-go-v3 v0.1.110 h1:XaWasqaF3tzRGw9cFmhXM1IiPAom8Nhg760F9LYHPKg= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.110/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI= +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/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/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.opentelemetry.io/collector v0.107.0 h1:C1Mng03iE73flGhEg795IFVlr3qhDLef5GESjIVtx5g= +go.opentelemetry.io/collector v0.107.0/go.mod h1:7xDYvzBb3Ez6qFQl0IArBbmNNazIxZMVoRkbgJYRjyg= +go.opentelemetry.io/collector/client v1.13.0 h1:I0GzZPcOG+F6BY46SgsdcpCN+L0xPfjzTRN2Y4Etdrw= +go.opentelemetry.io/collector/client v1.13.0/go.mod h1:GOE/UvRdklwPCqYLRUOSiU/syl1BGQWa48ex7OGOq9Y= +go.opentelemetry.io/collector/component v0.107.0 h1:3ReaEAtKwrPj7HrlKjEGBDKbBaxdRMPC2mfZ9b6zjXE= +go.opentelemetry.io/collector/component v0.107.0/go.mod h1:1xMIYKvpnP7laipjgEw7kq1ozG7ySLkA0Evhr2Bp8M4= +go.opentelemetry.io/collector/config/configauth v0.107.0 h1:2rAoMJjSfdP+ao5i7Ku68QYW5p0jjZ9y6a0nj3cIFp0= +go.opentelemetry.io/collector/config/configauth v0.107.0/go.mod h1:IQx5BBZRoyPX9Qr3W4Ajy/4AnyJZb6jylI4rITrZMHQ= +go.opentelemetry.io/collector/config/configcompression v1.13.0 h1:2LApl3qDZgjsVblY1Qu0gJaw9ZyYnZ6ZDUvid9rCZVg= +go.opentelemetry.io/collector/config/configcompression v1.13.0/go.mod h1:6+m0GKCv7JKzaumn7u80A2dLNCuYf5wdR87HWreoBO0= +go.opentelemetry.io/collector/config/confighttp v0.107.0 h1:HnfFg/n3xu+XH7onWrFZl8jBrVVUCe+updh3ANUB3w8= +go.opentelemetry.io/collector/config/confighttp v0.107.0/go.mod h1:/slm41hcfOwAxv8ZcGCKHC22jnQZ71z42OSWChKuIgU= +go.opentelemetry.io/collector/config/configopaque v1.13.0 h1:EDB9JIifmBth1z9IsEduoE1bT1Q8jV0sR005EMW7q1w= +go.opentelemetry.io/collector/config/configopaque v1.13.0/go.mod h1:0xURn2sOy5j4fbaocpEYfM97HPGsiffkkVudSPyTJlM= +go.opentelemetry.io/collector/config/configretry v1.13.0 h1:gcjWB6FOG1u1e5ecs3nOtOysXWtxJxeL+cNiFLI+nCo= +go.opentelemetry.io/collector/config/configretry v1.13.0/go.mod h1:P+RA0IA+QoxnDn4072uyeAk1RIoYiCbxYsjpKX5eFC4= +go.opentelemetry.io/collector/config/configtelemetry v0.107.0 h1:pSGd4FWQ/Up/Af+XZTR8JNneH/wmQ/TAU4Z16JHQeUc= +go.opentelemetry.io/collector/config/configtelemetry v0.107.0/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40= +go.opentelemetry.io/collector/config/configtls v1.13.0 h1:N57vOibMIPX9YZq4ZLFjj5ZjUHMYW7bpkPkygU3vt8w= +go.opentelemetry.io/collector/config/configtls v1.13.0/go.mod h1:3CBJYFQYTCYIPJMRvzn3NVtasv8out21ZNXkSCPepuY= +go.opentelemetry.io/collector/config/internal v0.107.0 h1:aENT1bVin8HCHZuKXc8U1sTYalzl6+RpOMDWpt+VoCQ= +go.opentelemetry.io/collector/config/internal v0.107.0/go.mod h1:2LQPVQPDeIyXN6AXazlls1M0zmlhIM7q80V4K7mQ6PM= +go.opentelemetry.io/collector/confmap v0.107.0 h1:M2o7jvQM9bnMU3pE2N6BK4KHYtSnvsSZkegUD89y8BU= +go.opentelemetry.io/collector/confmap v0.107.0/go.mod h1:9Fs/ZEIeiMa38VqkqIpn+JKQkcPf/lhAKA9fHu6c9GY= +go.opentelemetry.io/collector/consumer v0.107.0 h1:fF/+xyv9BfXQUvuJqkljrpzKyBQExDQt6zB5rzGyuHs= +go.opentelemetry.io/collector/consumer v0.107.0/go.mod h1:wgWpFes9sbnZ11XeJPSeutU8GJx6dT/gzSUqHpaZZQA= +go.opentelemetry.io/collector/consumer/consumerprofiles v0.107.0 h1:SEP5rLm4KgBaELciRQO4m9U2q3xn16KGjpIw8zQn6Ik= +go.opentelemetry.io/collector/consumer/consumerprofiles v0.107.0/go.mod h1:Vi/aqlZjCBdGgGu+iOEfUyHvq2TJBar0WfsQSOMhR6Y= +go.opentelemetry.io/collector/consumer/consumertest v0.107.0 h1:BfjFHHAqbTmCN32akYvMhWKYC+ayHTX935/fRChwohM= +go.opentelemetry.io/collector/consumer/consumertest v0.107.0/go.mod h1:qNMedscdVyuxbV+wWUt4yGKQM3c0YEgQJTFeAtGZjRY= +go.opentelemetry.io/collector/extension v0.107.0 h1:wstZXb24RwdcchZu3juTH9M0xryKG4sYmb1/w6J3tqQ= +go.opentelemetry.io/collector/extension v0.107.0/go.mod h1:w/+HXzRO7jPutUIMyBlzX6deUPl205QCEwteB0YgWOg= +go.opentelemetry.io/collector/extension/auth v0.107.0 h1:xv+MI9ce8RG1UP4XihTaPR3pCKVvKOl2iIyFqYa0bvE= +go.opentelemetry.io/collector/extension/auth v0.107.0/go.mod h1:tFMzaKaUAx3eWpp3lOLZ8OcLbryydyof9+GCvCWmKmU= +go.opentelemetry.io/collector/featuregate v1.13.0 h1:rc84eCf5hesXQ8/bP6Zc15wqthbomfLBHmox5tT7AwM= +go.opentelemetry.io/collector/featuregate v1.13.0/go.mod h1:PsOINaGgTiFc+Tzu2K/X2jP+Ngmlp7YKGV1XrnBkH7U= +go.opentelemetry.io/collector/internal/globalgates v0.107.0 h1:PaD6WgQg80YTVxg8OF+YEqgI7WRd13wMu/R6GIG7uNU= +go.opentelemetry.io/collector/internal/globalgates v0.107.0/go.mod h1:hca7Tpzu6JmBrAOgmlyp/ZM6kxprPRMKqSYoq/Tdzjw= +go.opentelemetry.io/collector/pdata v1.14.1 h1:wXZjtQA7Vy5HFqco+yA95ENyMQU5heBB1IxMHQf6mUk= +go.opentelemetry.io/collector/pdata v1.14.1/go.mod h1:z1dTjwwtcoXxZx2/nkHysjxMeaxe9pEmYTEr4SMNIx8= +go.opentelemetry.io/collector/pdata/pprofile v0.107.0 h1:F25VZrEkSaneIBNcNI9LEBWf9nRC/WHKluSBTP0gKAA= +go.opentelemetry.io/collector/pdata/pprofile v0.107.0/go.mod h1:1GrwsKtgogRCt8aG/0lfJ037yDdFtYqF+OtJr+snxRQ= +go.opentelemetry.io/collector/pdata/testdata v0.107.0 h1:02CqvJrYjkrBlWDD+6yrByN1AhG2zT61OScLPhyyMwU= +go.opentelemetry.io/collector/pdata/testdata v0.107.0/go.mod h1:bqaeiDH1Lc5DFJXvjVHwO50x00TXj+oFre+EbOVeZXs= +go.opentelemetry.io/collector/receiver v0.106.1 h1:9kDLDJmInnz+AzAV9oV/UGMoc1+oI1pwMMs7+uMiJq4= +go.opentelemetry.io/collector/receiver v0.106.1/go.mod h1:3j9asWz7mqsgE77rPaNhlNQhRwgFhRynf0UEPs/4rkM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0 h1:2Ewsda6hejmbhGFyUvWZjUThC98Cf8Zy6g0zkIimOng= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0/go.mod h1:pMm5PkUo5YwbLiuEf7t2xg4wbP0/eSJrMxIMxKosynY= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= +go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +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= +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.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +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= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/receiver/huaweicloudlogsreceiver/integration_test.go b/receiver/huaweicloudlogsreceiver/integration_test.go new file mode 100644 index 000000000000..22494f271d30 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/integration_test.go @@ -0,0 +1,99 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package huaweicloudlogsreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudcesreceiver" + +import ( + "context" + "path/filepath" + "testing" + "time" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/lts/v2/model" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver/receivertest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/plogtest" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver/internal/mocks" +) + +func TestHuaweiCloudLogsReceiverIntegration(t *testing.T) { + mc := mocks.NewLtsClient(t) + + mc.On("ListLogs", mock.Anything).Return(&model.ListLogsResponse{ + Logs: &[]model.LogContents{ + { + Content: stringPtr("2020-07-25/14:40:00 this log is Error NO 2"), + LineNum: stringPtr("123"), + Labels: map[string]string{ + "hostName": "ecs-kwxtest", + "hostIP": "192.168.0.156", + "appName": "default_appname", + "containerName": "CONFIG_FILE", + "clusterName": "CONFIG_FILE", + "hostId": "9787ef31-f171-4eff-ba71-72d580f11f55", + "podName": "default_procname", + "clusterId": "CONFIG_FILE", + "nameSpace": "CONFIG_FILE", + "category": "LTS", + }, + }, + { + Content: stringPtr("2020-07-25/14:50:00 this log is Error NO 3"), + LineNum: stringPtr("456"), + Labels: map[string]string{ + "hostName": "ecs-kwxtest", + "hostIP": "192.168.0.156", + "appName": "default_appname", + "containerName": "CONFIG_FILE", + "clusterName": "CONFIG_FILE", + "hostId": "9787ef31-f171-4eff-ba71-72d580f11f55", + "podName": "default_procname", + "clusterId": "CONFIG_FILE", + "nameSpace": "CONFIG_FILE", + "category": "LTS", + }, + }, + }, + }, nil) + + sink := &consumertest.LogsSink{} + cfg := createDefaultConfig().(*Config) + cfg.RegionID = "us-east-2" + cfg.CollectionInterval = time.Second + cfg.ProjectID = "my-project" + cfg.GroupID = "group-1" + cfg.StreamID = "stream-1" + + recv, err := NewFactory().CreateLogsReceiver( + context.Background(), + receivertest.NewNopSettings(), + cfg, + sink, + ) + require.NoError(t, err) + + rcvr, ok := recv.(*logsReceiver) + require.True(t, ok) + rcvr.client = mc + + err = recv.Start(context.Background(), componenttest.NewNopHost()) + require.NoError(t, err) + + require.Eventually(t, func() bool { + return sink.LogRecordCount() > 0 + }, 5*time.Second, 10*time.Millisecond) + + err = recv.Shutdown(context.Background()) + require.NoError(t, err) + + logs := sink.AllLogs()[0] + + expectedLogs, err := golden.ReadLogs(filepath.Join("testdata", "golden", "logs_golden.json")) + require.NoError(t, err) + require.NoError(t, plogtest.CompareLogs(expectedLogs, logs)) +} diff --git a/receiver/huaweicloudlogsreceiver/internal/backoff.go b/receiver/huaweicloudlogsreceiver/internal/backoff.go new file mode 100644 index 000000000000..7ad525bfa62c --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/internal/backoff.go @@ -0,0 +1,88 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudcesreceiver/internal" + +import ( + "context" + "fmt" + "time" + + "github.com/cenkalti/backoff/v4" + "go.uber.org/zap" +) + +// Generic function to make an API call with exponential backoff and context cancellation handling. +func MakeAPICallWithRetry[T any]( + ctx context.Context, + shutdownChan chan struct{}, + logger *zap.Logger, + apiCall func() (*T, error), + isThrottlingError func(error) bool, + backOffConfig *backoff.ExponentialBackOff, +) (*T, error) { + // Immediately check for context cancellation or server shutdown. + select { + case <-ctx.Done(): + return nil, fmt.Errorf("request was cancelled or timed out") + case <-shutdownChan: + return nil, fmt.Errorf("request is cancelled due to server shutdown") + case <-time.After(50 * time.Millisecond): + } + + // Make the initial API call. + resp, err := apiCall() + if err == nil { + return resp, nil + } + + // If the error is not due to request throttling, return the error. + if !isThrottlingError(err) { + return nil, err + } + + // Initialize the backoff mechanism for retrying the API call. + expBackoff := &backoff.ExponentialBackOff{ + InitialInterval: backOffConfig.InitialInterval, + RandomizationFactor: backOffConfig.RandomizationFactor, + Multiplier: backOffConfig.Multiplier, + MaxInterval: backOffConfig.MaxInterval, + MaxElapsedTime: backOffConfig.MaxElapsedTime, + Stop: backoff.Stop, + Clock: backoff.SystemClock, + } + expBackoff.Reset() + attempts := 0 + + // Retry loop for handling throttling errors. + for { + attempts++ + delay := expBackoff.NextBackOff() + if delay == backoff.Stop { + return resp, err + } + logger.Warn("server busy, retrying request", + zap.Int("attempts", attempts), + zap.Duration("delay", delay)) + + // Handle context cancellation or shutdown before retrying. + select { + case <-ctx.Done(): + return nil, fmt.Errorf("request was cancelled or timed out") + case <-shutdownChan: + return nil, fmt.Errorf("request is cancelled due to server shutdown") + case <-time.After(delay): + } + + // Retry the API call. + resp, err = apiCall() + if err == nil { + return resp, nil + } + if !isThrottlingError(err) { + break + } + } + + return nil, err +} diff --git a/receiver/huaweicloudlogsreceiver/internal/backoff_test.go b/receiver/huaweicloudlogsreceiver/internal/backoff_test.go new file mode 100644 index 000000000000..402a1f23ace8 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/internal/backoff_test.go @@ -0,0 +1,129 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zaptest" +) + +func TestMakeAPICallWithRetrySuccess(t *testing.T) { + logger := zaptest.NewLogger(t) + apiCall := func() (*string, error) { + result := "success" + return &result, nil + } + isThrottlingError := func(_ error) bool { + return false + } + + resp, err := MakeAPICallWithRetry(context.TODO(), make(chan struct{}), logger, apiCall, isThrottlingError, backoff.NewExponentialBackOff()) + + assert.NoError(t, err) + assert.Equal(t, "success", *resp) +} + +func TestMakeAPICallWithRetryImmediateFailure(t *testing.T) { + logger := zaptest.NewLogger(t) + apiCall := func() (*string, error) { + return nil, errors.New("some error") + } + isThrottlingError := func(_ error) bool { + return false + } + + resp, err := MakeAPICallWithRetry(context.TODO(), make(chan struct{}), logger, apiCall, isThrottlingError, backoff.NewExponentialBackOff()) + + assert.Error(t, err) + assert.Nil(t, resp) + assert.Equal(t, "some error", err.Error()) +} + +func TestMakeAPICallWithRetryThrottlingWithSuccess(t *testing.T) { + logger := zaptest.NewLogger(t) + callCount := 0 + apiCall := func() (*string, error) { + callCount++ + if callCount == 3 { + result := "success" + return &result, nil + } + return nil, errors.New("throttling error") + } + isThrottlingError := func(err error) bool { + return err.Error() == "throttling error" + } + + backOffConfig := backoff.NewExponentialBackOff() + backOffConfig.InitialInterval = 10 * time.Millisecond + + resp, err := MakeAPICallWithRetry(context.TODO(), make(chan struct{}), logger, apiCall, isThrottlingError, backOffConfig) + + assert.NoError(t, err) + assert.Equal(t, "success", *resp) + assert.Equal(t, 3, callCount) +} + +func TestMakeAPICallWithRetryThrottlingMaxRetries(t *testing.T) { + logger := zaptest.NewLogger(t) + apiCall := func() (*string, error) { + return nil, errors.New("throttling error") + } + isThrottlingError := func(err error) bool { + return err.Error() == "throttling error" + } + + backOffConfig := backoff.NewExponentialBackOff() + backOffConfig.MaxElapsedTime = 50 * time.Millisecond + + resp, err := MakeAPICallWithRetry(context.TODO(), make(chan struct{}), logger, apiCall, isThrottlingError, backOffConfig) + + assert.Error(t, err) + assert.Nil(t, resp) + assert.Equal(t, "throttling error", err.Error()) +} + +func TestMakeAPICallWithRetryContextCancellation(t *testing.T) { + logger := zaptest.NewLogger(t) + ctx, cancel := context.WithCancel(context.TODO()) + time.AfterFunc(time.Second, cancel) + + apiCall := func() (*string, error) { + return nil, errors.New("throttling error") + } + isThrottlingError := func(err error) bool { + return err.Error() == "throttling error" + } + + resp, err := MakeAPICallWithRetry(ctx, make(chan struct{}), logger, apiCall, isThrottlingError, backoff.NewExponentialBackOff()) + + assert.Error(t, err) + assert.Nil(t, resp) + assert.Equal(t, "request was cancelled or timed out", err.Error()) +} + +func TestMakeAPICallWithRetryServerShutdown(t *testing.T) { + logger := zaptest.NewLogger(t) + shutdownChan := make(chan struct{}) + time.AfterFunc(time.Second, func() { close(shutdownChan) }) + + apiCall := func() (*string, error) { + return nil, errors.New("throttling error") + } + isThrottlingError := func(err error) bool { + return err.Error() == "throttling error" + } + + resp, err := MakeAPICallWithRetry(context.TODO(), shutdownChan, logger, apiCall, isThrottlingError, backoff.NewExponentialBackOff()) + + assert.Error(t, err) + assert.Nil(t, resp) + assert.Equal(t, "request is cancelled due to server shutdown", err.Error()) +} diff --git a/receiver/huaweicloudlogsreceiver/internal/lts_client.go b/receiver/huaweicloudlogsreceiver/internal/lts_client.go new file mode 100644 index 000000000000..8755afaf5e9d --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/internal/lts_client.go @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver/internal" + +import ( + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/lts/v2/model" +) + +// This interface should have all the function defined inside https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/v0.1.110/services/lts/v2/lts_client.go +// Check https://github.com/vektra/mockery on how to install it on your machine. +// +//go:generate mockery --name LtsClient --case=underscore --output=./mocks +type LtsClient interface { + ListLogs(request *model.ListLogsRequest) (*model.ListLogsResponse, error) +} diff --git a/receiver/huaweicloudlogsreceiver/internal/lts_to_otlp.go b/receiver/huaweicloudlogsreceiver/internal/lts_to_otlp.go new file mode 100644 index 000000000000..d954ae4696ab --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/internal/lts_to_otlp.go @@ -0,0 +1,73 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver/internal" + +import ( + "fmt" + "regexp" + "time" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/lts/v2/model" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" +) + +const layout = "2006-01-02/15:04:05" + +// example of log content : "2020-07-25/14:44:43 this log is Error NO 2" +func extractTimestamp(log string) (time.Time, error) { + // Define the regex pattern to match the timestamp + pattern := `(\d{4}-\d{2}-\d{2}/\d{2}:\d{2}:\d{2})` + re := regexp.MustCompile(pattern) + + matches := re.FindStringSubmatch(log) + if len(matches) == 0 { + return time.Time{}, fmt.Errorf("timestamp not found") + } + + timestamp, err := time.Parse(layout, matches[0]) + if err != nil { + return time.Time{}, err + } + return timestamp, nil +} + +func ConvertLTSLogsToOTLP(projectID, regionID, groupID, streamD string, ltsLogs []model.LogContents) plog.Logs { + logs := plog.NewLogs() + resourceMetric := logs.ResourceLogs().AppendEmpty() + + resource := resourceMetric.Resource() + resource.Attributes().PutStr("cloud.provider", "huawei_cloud") + resource.Attributes().PutStr("project.id", projectID) + resource.Attributes().PutStr("region.id", regionID) + resource.Attributes().PutStr("group.id", groupID) + resource.Attributes().PutStr("stream.id", streamD) + + if len(ltsLogs) == 0 { + return logs + } + scopedLog := resourceMetric.ScopeLogs().AppendEmpty() + scopedLog.Scope().SetName("huawei_cloud_lts") + scopedLog.Scope().SetVersion("v2") + + for _, ltsLog := range ltsLogs { + + logRecord := scopedLog.LogRecords().AppendEmpty() + if ltsLog.Content != nil { + content := *ltsLog.Content + if ts, err := extractTimestamp(content); err == nil { + logRecord.SetObservedTimestamp(pcommon.Timestamp(ts.UnixNano())) + } + logRecord.Body().SetStr(content) + } + for key, value := range ltsLog.Labels { + logRecord.Attributes().PutStr(key, value) + } + if ltsLog.LineNum != nil { + logRecord.Attributes().PutStr("lineNum", *ltsLog.LineNum) + } + } + + return logs +} diff --git a/receiver/huaweicloudlogsreceiver/internal/lts_to_otlp_test.go b/receiver/huaweicloudlogsreceiver/internal/lts_to_otlp_test.go new file mode 100644 index 000000000000..684c6442588c --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/internal/lts_to_otlp_test.go @@ -0,0 +1,235 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + "fmt" + "testing" + "time" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/lts/v2/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" +) + +func mustParseTime(t *testing.T, value string) time.Time { + ts, err := time.Parse(layout, value) + if err != nil { + require.NoError(t, err) + } + return ts +} + +func TestExtractTimestamp(t *testing.T) { + tests := []struct { + name string + log string + expectedTime time.Time + expectedError bool + }{ + { + name: "Valid Timestamp", + log: "2020-07-25/14:44:43 this log is Error NO 2", + expectedTime: time.Date(2020, 7, 25, 14, 44, 43, 0, time.UTC), + }, + { + name: "Invalid Timestamp Format", + log: "2020/07/25 14:44:43 this log is Error NO 2", + expectedError: true, + }, + { + name: "No Timestamp", + log: "this log is Error NO 2", + expectedError: true, + }, + { + name: "Empty Log", + log: "", + expectedError: true, + }, + { + name: "Partial Timestamp", + log: "2020-07-25/14:44 this log is Error NO 2", + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := extractTimestamp(tt.log) + + if (err != nil) != tt.expectedError { + t.Errorf("extractTimestamp() error = %v, expectedError %v", err, tt.expectedError) + return + } + if !tt.expectedError && !got.Equal(tt.expectedTime) { + t.Errorf("extractTimestamp() = %v, expected %v", got, tt.expectedTime) + } + }) + } +} + +func TestConvertLTSLogsToOTLP(t *testing.T) { + tests := []struct { + name string + projectID string + regionID string + groupID string + streamID string + ltsLogs []model.LogContents + expectLogs int + expectedTime time.Time + }{ + { + name: "Valid log content with timestamp", + projectID: "project1", + regionID: "region1", + groupID: "group1", + streamID: "stream1", + ltsLogs: []model.LogContents{ + { + Content: stringPointer("2020-07-25/14:40:00 this log is Error NO 2"), + LineNum: stringPointer("123"), + Labels: map[string]string{ + "hostName": "ecs-kwxtest", + "hostIP": "192.168.0.156", + "appName": "default_appname", + "containerName": "CONFIG_FILE", + "clusterName": "CONFIG_FILE", + "hostId": "9787ef31-f171-4eff-ba71-72d580f11f60", + "podName": "default_procname", + "clusterId": "CONFIG_FILE", + "nameSpace": "CONFIG_FILE", + "category": "LTS", + }, + }, + { + Content: stringPointer("2020-07-25/14:50:00 this log is Error NO 2"), + LineNum: stringPointer("456"), + Labels: map[string]string{ + "hostName": "ecs-kwxtest", + "hostIP": "192.168.0.156", + "appName": "default_appname", + "containerName": "CONFIG_FILE", + "clusterName": "CONFIG_FILE", + "hostId": "9787ef31-f171-4eff-ba71-72d580f11f60", + "podName": "default_procname", + "clusterId": "CONFIG_FILE", + "nameSpace": "CONFIG_FILE", + "category": "LTS", + }, + }, + }, + expectLogs: 2, + expectedTime: mustParseTime(t, "2020-07-25/14:44:43"), + }, + { + name: "Log content without timestamp", + projectID: "project2", + regionID: "region2", + groupID: "group2", + streamID: "stream2", + ltsLogs: []model.LogContents{ + { + Content: stringPointer("this log has no timestamp"), + Labels: map[string]string{"level": "info"}, + LineNum: stringPointer("456"), + }, + }, + expectLogs: 1, + expectedTime: time.Time{}, + }, + { + name: "Empty log content", + projectID: "project3", + regionID: "region3", + groupID: "group3", + streamID: "stream3", + ltsLogs: []model.LogContents{ + { + Content: stringPointer(""), + Labels: map[string]string{"level": "debug"}, + LineNum: stringPointer("789"), + }, + }, + expectLogs: 1, + expectedTime: time.Time{}, + }, + { + name: "Multiple logs", + projectID: "project4", + regionID: "region4", + groupID: "group4", + streamID: "stream4", + ltsLogs: []model.LogContents{ + { + Content: stringPointer("2020-07-25/14:44:43 log1"), + Labels: map[string]string{"level": "error"}, + LineNum: stringPointer("1"), + }, + { + Content: stringPointer("2020-07-25/14:45:43 log2"), + Labels: map[string]string{"level": "info"}, + LineNum: stringPointer("2"), + }, + }, + expectLogs: 2, + expectedTime: time.Time{}, + }, + { + name: "Invalid timestamp format", + projectID: "project5", + regionID: "region5", + groupID: "group5", + streamID: "stream5", + ltsLogs: []model.LogContents{ + { + Content: stringPointer("25/07/2020 14:44:43 this log has an invalid timestamp"), + Labels: map[string]string{"level": "error"}, + LineNum: stringPointer("123"), + }, + }, + expectLogs: 1, + expectedTime: time.Time{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logs := ConvertLTSLogsToOTLP(tt.projectID, tt.regionID, tt.groupID, tt.streamID, tt.ltsLogs) + m := &plog.JSONMarshaler{} + + jsonLogs, err := m.MarshalLogs(logs) + require.NoError(t, err) + fmt.Println(tt.name, string(jsonLogs)) + require.Equal(t, 1, logs.ResourceLogs().Len()) + require.Equal(t, 1, logs.ResourceLogs().At(0).ScopeLogs().Len()) + + logRecords := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords() + assert.Equal(t, tt.expectLogs, logRecords.Len(), "unexpected number of logs") + + for i := 0; i < tt.expectLogs; i++ { + logRecord := logRecords.At(i) + if tt.ltsLogs[i].Content != nil { + content := *tt.ltsLogs[i].Content + if ts, err := extractTimestamp(content); err == nil { + assert.Equal(t, pcommon.Timestamp(ts.UnixNano()), logRecord.ObservedTimestamp(), "unexpected timestamp in log record") + } + } + + for key, value := range tt.ltsLogs[i].Labels { + attr, found := logRecord.Attributes().Get(key) + assert.True(t, found, "expected attribute %s not found", key) + assert.Equal(t, value, attr.Str(), "unexpected value for attribute %s", key) + } + } + }) + } +} + +func stringPointer(s string) *string { + return &s +} diff --git a/receiver/huaweicloudlogsreceiver/internal/metadata/generated_status.go b/receiver/huaweicloudlogsreceiver/internal/metadata/generated_status.go new file mode 100644 index 000000000000..6ec59b8b2775 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/internal/metadata/generated_status.go @@ -0,0 +1,16 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" +) + +var ( + Type = component.MustNewType("huaweicloudlogsreceiver") + ScopeName = "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver" +) + +const ( + LogsStability = component.StabilityLevelDevelopment +) diff --git a/receiver/huaweicloudlogsreceiver/internal/mocks/lts_client.go b/receiver/huaweicloudlogsreceiver/internal/mocks/lts_client.go new file mode 100644 index 000000000000..32865bd87791 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/internal/mocks/lts_client.go @@ -0,0 +1,57 @@ +// Code generated by mockery v2.44.1. DO NOT EDIT. + +package mocks + +import ( + model "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/lts/v2/model" + mock "github.com/stretchr/testify/mock" +) + +// LtsClient is an autogenerated mock type for the LtsClient type +type LtsClient struct { + mock.Mock +} + +// ListLogs provides a mock function with given fields: request +func (_m *LtsClient) ListLogs(request *model.ListLogsRequest) (*model.ListLogsResponse, error) { + ret := _m.Called(request) + + if len(ret) == 0 { + panic("no return value specified for ListLogs") + } + + var r0 *model.ListLogsResponse + var r1 error + if rf, ok := ret.Get(0).(func(*model.ListLogsRequest) (*model.ListLogsResponse, error)); ok { + return rf(request) + } + if rf, ok := ret.Get(0).(func(*model.ListLogsRequest) *model.ListLogsResponse); ok { + r0 = rf(request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.ListLogsResponse) + } + } + + if rf, ok := ret.Get(1).(func(*model.ListLogsRequest) error); ok { + r1 = rf(request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewLtsClient creates a new instance of LtsClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewLtsClient(t interface { + mock.TestingT + Cleanup(func()) +}) *LtsClient { + mock := &LtsClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/receiver/huaweicloudlogsreceiver/metadata.yaml b/receiver/huaweicloudlogsreceiver/metadata.yaml new file mode 100644 index 000000000000..b3f516ad2f16 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/metadata.yaml @@ -0,0 +1,9 @@ +type: huaweicloudlogsreceiver + +status: + class: receiver + stability: + development: [logs] + distributions: [] + codeowners: + active: [heitorganzeli, narcis96] diff --git a/receiver/huaweicloudlogsreceiver/receiver.go b/receiver/huaweicloudlogsreceiver/receiver.go new file mode 100644 index 000000000000..0263f6ed50df --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/receiver.go @@ -0,0 +1,200 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package huaweicloudlogsreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver" + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" + lts "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/lts/v2" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/lts/v2/model" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/lts/v2/region" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/receiver" + "go.uber.org/zap" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver/internal" +) + +const ( + // See https://support.huaweicloud.com/intl/en-us/devg-apisign/api-sign-errorcode.html + requestThrottledErrMsg = "APIGW.0308" +) + +type logsReceiver struct { + logger *zap.Logger + client internal.LtsClient + cancel context.CancelFunc + + host component.Host + nextConsumer consumer.Logs + lastTs time.Time + config *Config + shutdownChan chan struct{} +} + +func newHuaweiCloudLogsReceiver(settings receiver.Settings, cfg *Config, next consumer.Logs) *logsReceiver { + rcvr := &logsReceiver{ + logger: settings.Logger, + config: cfg, + nextConsumer: next, + shutdownChan: make(chan struct{}, 1), + } + return rcvr +} + +func (rcvr *logsReceiver) Start(ctx context.Context, host component.Host) error { + rcvr.host = host + ctx, rcvr.cancel = context.WithCancel(ctx) + + if rcvr.client == nil { + client, err := rcvr.createClient() + if err != nil { + rcvr.logger.Error(err.Error()) + return nil + } + rcvr.client = client + } + + go rcvr.startReadingLogs(ctx) + return nil +} + +func (rcvr *logsReceiver) startReadingLogs(ctx context.Context) { + if rcvr.config.InitialDelay > 0 { + <-time.After(rcvr.config.InitialDelay) + } + if err := rcvr.pollLogsAndConsume(ctx); err != nil { + rcvr.logger.Error(err.Error()) + } + ticker := time.NewTicker(rcvr.config.CollectionInterval) + + defer ticker.Stop() + for { + select { + case <-ticker.C: + // TODO: Improve error handling for client-server interactions + // The current implementation lacks robust error handling, especially for + // scenarios such as service unavailability, timeouts, and request errors. + // - Investigate how to handle service unavailability or timeouts gracefully. + // - Implement appropriate actions or retries for different types of request errors. + // - Refer to the Huawei SDK documentation to identify + // all possible client/request errors and determine how to manage them. + // - Consider implementing custom error messages or fallback mechanisms for critical failures. + + if err := rcvr.pollLogsAndConsume(ctx); err != nil { + rcvr.logger.Error(err.Error()) + } + case <-ctx.Done(): + return + } + } +} + +func (rcvr *logsReceiver) createClient() (*lts.LtsClient, error) { + auth, err := basic.NewCredentialsBuilder(). + WithAk(string(rcvr.config.AccessKey)). + WithSk(string(rcvr.config.SecretKey)). + WithProjectId(rcvr.config.ProjectID). + SafeBuild() + + if err != nil { + return nil, err + } + + httpConfig, err := createHTTPConfig(rcvr.config.HuaweiSessionConfig) + if err != nil { + return nil, err + } + r, err := region.SafeValueOf(rcvr.config.RegionID) + if err != nil { + return nil, err + } + + hcHTTPConfig, err := lts.LtsClientBuilder(). + WithRegion(r). + WithCredential(auth). + WithHttpConfig(httpConfig). + SafeBuild() + + if err != nil { + return nil, err + } + + return lts.NewLtsClient(hcHTTPConfig), nil +} + +func (rcvr *logsReceiver) pollLogsAndConsume(ctx context.Context) error { + if rcvr.client == nil { + return errors.New("invalid client") + } + to := time.Now() + from := rcvr.lastTs + if from.IsZero() { + from = to.Add(-1 * rcvr.config.CollectionInterval) + } + logs, err := rcvr.listLogs(ctx, from, to) + if err != nil { + return err + } + config := rcvr.config + otpLogs := internal.ConvertLTSLogsToOTLP(config.ProjectID, config.RegionID, config.GroupID, config.StreamID, logs) + if err := rcvr.nextConsumer.ConsumeLogs(ctx, otpLogs); err != nil { + return err + } + rcvr.lastTs = to + return nil +} + +func (rcvr *logsReceiver) listLogs(ctx context.Context, from, to time.Time) ([]model.LogContents, error) { + backOffConfig := rcvr.config.BackOffConfig + // TODO: Add pagination logic. Check IsQueryComplete field + response, err := internal.MakeAPICallWithRetry( + ctx, + rcvr.shutdownChan, + rcvr.logger, + func() (*model.ListLogsResponse, error) { + return rcvr.client.ListLogs(&model.ListLogsRequest{ + LogGroupId: "test", + LogStreamId: "test", + Body: &model.QueryLtsLogParams{ + StartTime: from.UTC().String(), + EndTime: to.UTC().String(), + }, + }) + }, + func(err error) bool { return strings.Contains(err.Error(), requestThrottledErrMsg) }, + &backoff.ExponentialBackOff{ + InitialInterval: backOffConfig.InitialInterval, + RandomizationFactor: backOffConfig.RandomizationFactor, + Multiplier: backOffConfig.Multiplier, + MaxInterval: backOffConfig.MaxInterval, + MaxElapsedTime: backOffConfig.MaxElapsedTime, + Stop: backoff.Stop, + Clock: backoff.SystemClock, + }, + ) + if err != nil { + return []model.LogContents{}, err + } + if response == nil || response.Logs == nil || len((*response.Logs)) == 0 { + return []model.LogContents{}, errors.New("unexpected empty list of logs") + } + + return *response.Logs, nil +} + +func (rcvr *logsReceiver) Shutdown(_ context.Context) error { + if rcvr.cancel != nil { + rcvr.cancel() + } + rcvr.shutdownChan <- struct{}{} + close(rcvr.shutdownChan) + return nil +} diff --git a/receiver/huaweicloudlogsreceiver/receiver_test.go b/receiver/huaweicloudlogsreceiver/receiver_test.go new file mode 100644 index 000000000000..d269d718a87f --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/receiver_test.go @@ -0,0 +1,136 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package huaweicloudlogsreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver" + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/lts/v2/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver/receivertest" + "go.opentelemetry.io/collector/receiver/scraperhelper" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/huaweicloudlogsreceiver/internal/mocks" +) + +func stringPtr(s string) *string { + return &s +} + +func TestNewReceiver(t *testing.T) { + cfg := &Config{ + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: 1 * time.Second, + }, + } + mr := newHuaweiCloudLogsReceiver(receivertest.NewNopSettings(), cfg, new(consumertest.LogsSink)) + assert.NotNil(t, mr) +} + +func TestListLogsSuccess(t *testing.T) { + mc := mocks.NewLtsClient(t) + + mc.On("ListLogs", mock.Anything).Return(&model.ListLogsResponse{ + Logs: &[]model.LogContents{ + { + Content: stringPtr("2020-07-25/14:40:43 this log is Error NO 2"), + LineNum: stringPtr("10"), + Labels: map[string]string{ + "hostName": "ecs-kwxtest", + "hostIP": "192.168.0.156", + "appName": "default_appname", + "containerName": "CONFIG_FILE", + "clusterName": "CONFIG_FILE", + "hostId": "9787ef31-fd7b-4eff-ba71-72d580f11f55", + "podName": "default_procname", + "clusterId": "CONFIG_FILE", + "nameSpace": "CONFIG_FILE", + "category": "LTS", + }, + }, + { + Content: stringPtr("2020-07-26/15:00:43 this log is Error NO 3"), + }, + }, + }, nil) + + receiver := &logsReceiver{ + client: mc, + config: createDefaultConfig().(*Config), + } + + logs, err := receiver.listLogs(context.Background(), time.Now(), time.Now()) + + assert.NoError(t, err) + assert.Len(t, logs, 2) + mc.AssertExpectations(t) +} + +func TestListLogsFailure(t *testing.T) { + mc := mocks.NewLtsClient(t) + + mc.On("ListLogs", mock.Anything).Return(nil, errors.New("failed to list logs")) + receiver := &logsReceiver{ + client: mc, + config: createDefaultConfig().(*Config), + } + + logs, err := receiver.listLogs(context.Background(), time.Now(), time.Now()) + + assert.Error(t, err) + assert.Len(t, logs, 0) + assert.Equal(t, "failed to list logs", err.Error()) + mc.AssertExpectations(t) +} + +func TestPollLogsAndConsumeSuccess(t *testing.T) { + mc := mocks.NewLtsClient(t) + next := new(consumertest.LogsSink) + receiver := newHuaweiCloudLogsReceiver(receivertest.NewNopSettings(), &Config{}, next) + receiver.client = mc + + mc.On("ListLogs", mock.Anything).Return(&model.ListLogsResponse{ + Logs: &[]model.LogContents{ + { + Content: stringPtr("2020-07-25/14:40:43 this log is Error NO 2"), + }, + { + Content: stringPtr("2020-07-26/15:00:43 this log is Error NO 3"), + }, + }, + }, nil) + + err := receiver.pollLogsAndConsume(context.Background()) + + require.NoError(t, err) + assert.Equal(t, 2, next.LogRecordCount()) +} + +func TestCreateHTTPConfigNoVerifySSL(t *testing.T) { + cfg, err := createHTTPConfig(HuaweiSessionConfig{NoVerifySSL: true}) + require.NoError(t, err) + assert.Equal(t, cfg.IgnoreSSLVerification, true) +} + +func TestCreateHTTPConfigWithProxy(t *testing.T) { + cfg, err := createHTTPConfig(HuaweiSessionConfig{ + ProxyAddress: "https://127.0.0.1:8888", + ProxyUser: "admin", + ProxyPassword: "pass", + AccessKey: "123", + SecretKey: "secret", + }) + require.NoError(t, err) + assert.Equal(t, cfg.HttpProxy.Schema, "https") + assert.Equal(t, cfg.HttpProxy.Host, "127.0.0.1") + assert.Equal(t, cfg.HttpProxy.Port, 8888) + assert.Equal(t, cfg.IgnoreSSLVerification, false) + +} diff --git a/receiver/huaweicloudlogsreceiver/testdata/golden/logs_golden.yaml b/receiver/huaweicloudlogsreceiver/testdata/golden/logs_golden.yaml new file mode 100644 index 000000000000..b1fc9fee3056 --- /dev/null +++ b/receiver/huaweicloudlogsreceiver/testdata/golden/logs_golden.yaml @@ -0,0 +1,29 @@ +resourceLogs: + - resource: + attributes: + - key: cloud.provider + value: + stringValue: huawei_cloud + - key: project.id + value: + stringValue: my-project + - key: region.id + value: + stringValue: us-east-2 + - key: group.id + value: + stringValue: group-1 + - key: stream.id + value: + stringValue: stream-1 + scopeLogs: + - scope: + name: huawei_cloud_lts + version: v2 + - logRecords: + - body: 2020-07-25/14:40:00 this log is Error NO 2 + attrivutes: + - key: hostName + value: + stringValue: ecs-kwxtest + observedTimestamp: 2020-07-25/14:40:00