Skip to content

Commit

Permalink
[exporter/bmchelix] New component: BMC Helix Exporter (open-telemetry…
Browse files Browse the repository at this point in the history
…#36964)

#### Description
This pull request introduces a new component for exporting metrics to
BMC Helix. The changes include adding the new component to various
configuration files, creating necessary documentation, and implementing
the component's configuration and factory logic.

Key changes include:

##### New Component Addition:
* Added a new changelog entry for the BMC Helix exporter in
`.chloggen/bmchelixexporter-new-component.yaml`.
* Updated `.github/CODEOWNERS` to include the new BMC Helix exporter.

##### Documentation:
* Created `README.md` for the BMC Helix exporter with detailed setup
instructions and examples.

##### Configuration and Factory Implementation:
* Implemented configuration struct and validation logic in `config.go`.
* Created tests for the configuration in `config_test.go`.
* Added factory methods for creating the exporter in `factory.go`.
* Created tests for the factory methods in `factory_test.go`.

##### Miscellaneous:
* Included the common Makefile in `exporter/bmchelixexporter/Makefile`.
* Added package documentation in `doc.go`.

#### Link to tracking issue
Fixes open-telemetry#36773

---------

Co-authored-by: Bertrand Martin <[email protected]>
  • Loading branch information
2 people authored and chengchuanpeng committed Jan 26, 2025
1 parent 0d6600e commit 25635d5
Show file tree
Hide file tree
Showing 21 changed files with 826 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .chloggen/bmchelixexporter-new-component.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: new_component

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: exporter/bmchelix

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: "Add a new component for exporting metrics to BMC Helix"

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [36773]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ exporter/awss3exporter/ @open-telemetry/collector-cont
exporter/awsxrayexporter/ @open-telemetry/collector-contrib-approvers @wangzlei @srprash
exporter/azuredataexplorerexporter/ @open-telemetry/collector-contrib-approvers @ag-ramachandran
exporter/azuremonitorexporter/ @open-telemetry/collector-contrib-approvers @pcwiese
exporter/bmchelixexporter/ @open-telemetry/collector-contrib-approvers @bertysentry @NassimBtk
exporter/carbonexporter/ @open-telemetry/collector-contrib-approvers @aboguszewski-sumo
exporter/cassandraexporter/ @open-telemetry/collector-contrib-approvers @atoulme @emreyalvac
exporter/clickhouseexporter/ @open-telemetry/collector-contrib-approvers @hanjm @dmitryax @Frapschen @SpencerTorres
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ body:
- exporter/awsxray
- exporter/azuredataexplorer
- exporter/azuremonitor
- exporter/bmchelix
- exporter/carbon
- exporter/cassandra
- exporter/clickhouse
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ body:
- exporter/awsxray
- exporter/azuredataexplorer
- exporter/azuremonitor
- exporter/bmchelix
- exporter/carbon
- exporter/cassandra
- exporter/clickhouse
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/other.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ body:
- exporter/awsxray
- exporter/azuredataexplorer
- exporter/azuremonitor
- exporter/bmchelix
- exporter/carbon
- exporter/cassandra
- exporter/clickhouse
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/unmaintained.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ body:
- exporter/awsxray
- exporter/azuredataexplorer
- exporter/azuremonitor
- exporter/bmchelix
- exporter/carbon
- exporter/cassandra
- exporter/clickhouse
Expand Down
1 change: 1 addition & 0 deletions exporter/bmchelixexporter/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
125 changes: 125 additions & 0 deletions exporter/bmchelixexporter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# BMC Helix Exporter

<!-- status autogenerated section -->
| Status | |
| ------------- |-----------|
| Stability | [development]: metrics |
| Distributions | [] |
| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Aexporter%2Fbmchelix%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Aexporter%2Fbmchelix) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Aexporter%2Fbmchelix%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Aexporter%2Fbmchelix) |
| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | [@bertysentry](https://www.github.com/bertysentry), [@NassimBtk](https://www.github.com/NassimBtk) |

[development]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#development
<!-- end autogenerated section -->

This exporter supports sending metrics to [BMC Helix Operations Management](https://www.bmc.com/it-solutions/bmc-helix-operations-management.html) through its [metric ingestion REST API](https://docs.bmc.com/docs/helixoperationsmanagement/244/en/metric-operation-management-endpoints-in-the-rest-api-1392780044.html).

## Getting Started

The following settings are **required**:

- `endpoint`: is the *BMC Helix Portal URL* of your environment, at **onbmc.com** for a BMC Helix SaaS tenant (e.g., `https://company.onbmc.com`), or your own Helix Portal URL for an on-prem instance.
- `api_key`: API key to authenticate the exporter. Connect to BMC Helix Operations Management, go to the Administration > Repository page, and click on the Copy API Key button to get your API Key. Alternatively, it is recommended to create and use a dedicated [authentication key for external integration](https://docs.bmc.com/docs/helixportal244/using-api-keys-for-external-integrations-1391501992.html).

Example:

```yaml
exporters:
bmchelix/helix1:
endpoint: https://company.onbmc.com
api_key: <api-key>
```
### Optional Settings
The following settings can be **optionally configured**:
- `timeout`: (default = `10s`) Timeout for requests made to the BMC Helix.
- `retry_on_failure` [details here](https://github.com/open-telemetry/opentelemetry-collector/tree/main/exporter/exporterhelper#configuration)
- `enabled` (default = true)
- `initial_interval` (default = 5s) Time to wait after the first failure before retrying; ignored if `enabled` is false.
- `max_interval` (default = 30s) The upper bound on backoff; ignored if `enabled` is false.
- `max_elapsed_time` (default = 300s) The maximum amount of time spent trying to send a batch; ignored if `enabled` is false. If set to 0, the retries are never stopped.

Example:

```yaml
exporters:
bmchelix/helix2:
endpoint: https://company.onbmc.com
api_key: <api-key>
timeout: 20s
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 1m
max_elapsed_time: 8m
```

---

## Setting Required Attributes for Metrics

To ensure metrics are correctly populated in BMC Helix, the following attributes must be set either at the *Resource* level, or at the *Metric* level:

- `entityName`: Unique identifier for the entity. Used as display name if `instanceName` is missing.
- `entityTypeId`: Type identifier for the entity.
- `instanceName`: Display name of the entity.

> **Note:** If `entityName` or `entityTypeId` is missing, the metric will not be exported.

To ensure the necessary attributes are present, it is recommended to leverage the [transform processor](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/transformprocessor) with [OTTL](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/ottl), and include it in the configuration of the telemetry pipeline.

The minimal pipeline most often looks like: `OTEL metrics --> (batch/memory limit) --> transform processor --> bmchelix exporter`.

### Transformer Example for Hardware Metrics

You can use the following OpenTelemetry Transformation Language (OTTL) configuration to map these attributes dynamically:

```yaml
transform/hw_to_helix:
# Apply transformations to all metrics
metric_statements:
- context: datapoint
statements:
# Create a new attribute 'entityName' with the value of 'id'
- set(attributes["entityName"], attributes["id"]) where attributes["id"] != nil
# Create a new attribute 'instanceName' with the value of 'name'
- set(attributes["instanceName"], attributes["name"]) where attributes["name"] != nil
- context: datapoint
conditions:
- IsMatch(metric.name, ".*\\.agent\\..*")
statements:
- set(attributes["entityName"], attributes["host.id"]) where attributes["host.id"] != nil
- set(attributes["instanceName"], attributes["service.name"]) where attributes["service.name"] != nil
- set(attributes["entityTypeId"], "agent")
- context: datapoint
statements:
# Mapping entityTypeId based on metric names and attributes
- set(attributes["entityTypeId"], "connector") where IsMatch(metric.name, ".*\\.connector\\..*")
- set(attributes["entityTypeId"], "host") where IsMatch(metric.name, ".*\\.host\\..*") or attributes["hw.type"] == "host"
- set(attributes["entityTypeId"], "battery") where IsMatch(metric.name, "hw\\.battery\\..*") or attributes["hw.type"] == "battery"
- set(attributes["entityTypeId"], "blade") where IsMatch(metric.name, "hw\\.blade\\..*") or attributes["hw.type"] == "blade"
- set(attributes["entityTypeId"], "cpu") where IsMatch(metric.name, "hw\\.cpu\\..*") or attributes["hw.type"] == "cpu"
- set(attributes["entityTypeId"], "disk_controller") where IsMatch(metric.name, "hw\\.disk_controller\\..*") or attributes["hw.type"] == "disk_controller"
- set(attributes["entityTypeId"], "enclosure") where IsMatch(metric.name, "hw\\.enclosure\\..*") or attributes["hw.type"] == "enclosure"
- set(attributes["entityTypeId"], "fan") where IsMatch(metric.name, "hw\\.fan\\..*") or attributes["hw.type"] == "fan"
- set(attributes["entityTypeId"], "gpu") where IsMatch(metric.name, "hw\\.gpu\\..*") or attributes["hw.type"] == "gpu"
- set(attributes["entityTypeId"], "led") where IsMatch(metric.name, "hw\\.led\\..*") or attributes["hw.type"] == "led"
- set(attributes["entityTypeId"], "logical_disk") where IsMatch(metric.name, "hw\\.logical_disk\\..*") or attributes["hw.type"] == "logical_disk"
- set(attributes["entityTypeId"], "lun") where IsMatch(metric.name, "hw\\.lun\\..*") or attributes["hw.type"] == "lun"
- set(attributes["entityTypeId"], "memory") where IsMatch(metric.name, "hw\\.memory\\..*") or attributes["hw.type"] == "memory"
- set(attributes["entityTypeId"], "network") where IsMatch(metric.name, "hw\\.network\\..*") or attributes["hw.type"] == "network"
- set(attributes["entityTypeId"], "other_device") where IsMatch(metric.name, "hw\\.other_device\\..*") or attributes["hw.type"] == "other_device"
- set(attributes["entityTypeId"], "physical_disk") where IsMatch(metric.name, "hw\\.physical_disk\\..*") or attributes["hw.type"] == "physical_disk"
- set(attributes["entityTypeId"], "power_supply") where IsMatch(metric.name, "hw\\.power_supply\\..*") or attributes["hw.type"] == "power_supply"
- set(attributes["entityTypeId"], "robotics") where IsMatch(metric.name, "hw\\.robotics\\..*") or attributes["hw.type"] == "robotics"
- set(attributes["entityTypeId"], "tape_drive") where IsMatch(metric.name, "hw\\.tape_drive\\..*") or attributes["hw.type"] == "tape_drive"
- set(attributes["entityTypeId"], "temperature") where IsMatch(metric.name, "hw\\.temperature.*") or attributes["hw.type"] == "temperature"
- set(attributes["entityTypeId"], "vm") where IsMatch(metric.name, "hw\\.vm\\..*") or attributes["hw.type"] == "vm"
- set(attributes["entityTypeId"], "voltage") where IsMatch(metric.name, "hw\\.voltage.*") or attributes["hw.type"] == "voltage"
```

This transformer dynamically sets the attributes required for BMC Helix based on metric names and resource attributes.
34 changes: 34 additions & 0 deletions exporter/bmchelixexporter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package bmchelixexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/bmchelixexporter"

import (
"errors"
"time"

"go.opentelemetry.io/collector/config/configretry"
)

// Config struct is used to store the configuration of the exporter
type Config struct {
Endpoint string `mapstructure:"endpoint"`
APIKey string `mapstructure:"api_key"`
Timeout time.Duration `mapstructure:"timeout"`
RetryConfig configretry.BackOffConfig `mapstructure:"retry_on_failure"`
}

// validate the configuration
func (c *Config) Validate() error {
if c.Endpoint == "" {
return errors.New("endpoint is required")
}
if c.APIKey == "" {
return errors.New("api key is required")
}
if c.Timeout <= 0 {
return errors.New("timeout must be a positive integer")
}

return nil
}
132 changes: 132 additions & 0 deletions exporter/bmchelixexporter/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package bmchelixexporter

import (
"path/filepath"
"testing"
"time"

"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/bmchelixexporter/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
errorMessage string
}{
{
id: component.NewIDWithName(metadata.Type, "helix1"),
expected: &Config{
Endpoint: "https://helix1:8080",
APIKey: "api_key",
Timeout: 10 * time.Second,
RetryConfig: configretry.NewDefaultBackOffConfig(),
},
},
{
id: component.NewIDWithName(metadata.Type, "helix2"),
expected: &Config{
Endpoint: "https://helix2:8080",
APIKey: "api_key",
Timeout: 20 * time.Second,
RetryConfig: configretry.BackOffConfig{
Enabled: true,
InitialInterval: 5 * time.Second,
RandomizationFactor: 0.5,
Multiplier: 1.5,
MaxInterval: 1 * time.Minute,
MaxElapsedTime: 8 * time.Minute,
},
},
},
}

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, component.ValidateConfig(cfg))
assert.Equal(t, tt.expected, cfg)
})
}
}

func TestValidateConfig(t *testing.T) {
tests := []struct {
name string
config *Config
err string
}{
{
name: "valid_config",
config: &Config{
Endpoint: "https://helix:8080",
APIKey: "api_key",
Timeout: 10 * time.Second,
},
},
{
name: "invalid_config1",
config: &Config{
APIKey: "api_key",
},
err: "endpoint is required",
},
{
name: "invalid_config2",
config: &Config{
Endpoint: "https://helix:8080",
},
err: "api key is required",
},
{
name: "invalid_config3",
config: &Config{
Endpoint: "https://helix:8080",
APIKey: "api_key",
Timeout: -1,
},
err: "timeout must be a positive integer",
},
{
name: "invalid_config4",
config: &Config{
Endpoint: "https://helix:8080",
APIKey: "api_key",
Timeout: 0,
},
err: "timeout must be a positive integer",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.err != "" {
err := tt.config.Validate()
assert.Error(t, err)
assert.Equal(t, tt.err, err.Error())
} else {
assert.NoError(t, tt.config.Validate())
}
})
}
}
7 changes: 7 additions & 0 deletions exporter/bmchelixexporter/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

//go:generate mdatagen metadata.yaml

// Package bmchelixexporter implements an exporter that sends data to BMC Helix.
package bmchelixexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/bmchelixexporter"
Loading

0 comments on commit 25635d5

Please sign in to comment.