Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[exporter/bmchelix] New component: BMC Helix Exporter #36964

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Validating CODEOWNERS rules …
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
Loading