From 10a79dcb44d9e365e9f68890054de43881fba7b6 Mon Sep 17 00:00:00 2001 From: mcube8 Date: Wed, 6 Dec 2023 20:08:07 +0530 Subject: [PATCH] Add alertmanager exporter implementation (#28906) **Description:** Export Implementation of Alertmanager Exporter Second PR - exporter implementation **Link to tracking Issue:** [#23659](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/23569) **Testing:** Unit tests for exporter implementation **Documentation:** Readme and Sample Configs to use Alertmanager exporter --- .../alertmanager-exporter-implementation.yaml | 27 ++ exporter/alertmanagerexporter/README.md | 3 +- .../alertmanager_exporter.go | 171 ++++++- .../alertmanager_exporter_test.go | 420 ++++++++++++++++++ exporter/alertmanagerexporter/config.go | 9 + exporter/alertmanagerexporter/config_test.go | 43 ++ exporter/alertmanagerexporter/factory.go | 7 +- exporter/alertmanagerexporter/go.mod | 7 +- exporter/alertmanagerexporter/go.sum | 3 + .../testdata/test_cert.pem | 29 ++ 10 files changed, 705 insertions(+), 14 deletions(-) create mode 100644 .chloggen/alertmanager-exporter-implementation.yaml create mode 100644 exporter/alertmanagerexporter/alertmanager_exporter_test.go create mode 100644 exporter/alertmanagerexporter/testdata/test_cert.pem diff --git a/.chloggen/alertmanager-exporter-implementation.yaml b/.chloggen/alertmanager-exporter-implementation.yaml new file mode 100644 index 000000000000..ac48059d91ed --- /dev/null +++ b/.chloggen/alertmanager-exporter-implementation.yaml @@ -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: alertmanagerexporter + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Add Alertmanager exporter implementation and tests" + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [23569] + +# (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] \ No newline at end of file diff --git a/exporter/alertmanagerexporter/README.md b/exporter/alertmanagerexporter/README.md index 08ef4d0b5bf5..986f3b712cc4 100644 --- a/exporter/alertmanagerexporter/README.md +++ b/exporter/alertmanagerexporter/README.md @@ -20,7 +20,7 @@ Supported pipeline types: traces The following settings are required: - `endpoint` : Alertmanager endpoint to send events -- `severity` (default info): Default severity for Alerts +- `severity`: Default severity for Alerts The following settings are optional: @@ -33,7 +33,6 @@ The following settings are optional: eg: If severity_attribute is set to "foo" and the SpanEvent has an attribute called foo, foo's attribute value will be used as the severity value for that particular Alert generated from the SpanEvent. - Example config: ```yaml diff --git a/exporter/alertmanagerexporter/alertmanager_exporter.go b/exporter/alertmanagerexporter/alertmanager_exporter.go index 8050fbf1f349..a6ce5426c976 100644 --- a/exporter/alertmanagerexporter/alertmanager_exporter.go +++ b/exporter/alertmanagerexporter/alertmanager_exporter.go @@ -4,17 +4,27 @@ package alertmanagerexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/alertmanagerexporter" import ( + "bytes" "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + "github.com/prometheus/common/model" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/exporter" "go.opentelemetry.io/collector/exporter/exporterhelper" + "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" + "go.uber.org/zap" ) type alertmanagerExporter struct { config *Config + client *http.Client tracesMarshaler ptrace.Marshaler settings component.TelemetrySettings endpoint string @@ -23,20 +33,167 @@ type alertmanagerExporter struct { severityAttribute string } -func (s *alertmanagerExporter) pushTraces(_ context.Context, _ ptrace.Traces) error { +type alertmanagerEvent struct { + spanEvent ptrace.SpanEvent + traceID string + spanID string + severity string +} + +func (s *alertmanagerExporter) convertEventSliceToArray(eventSlice ptrace.SpanEventSlice, traceID pcommon.TraceID, spanID pcommon.SpanID) []*alertmanagerEvent { + if eventSlice.Len() > 0 { + events := make([]*alertmanagerEvent, eventSlice.Len()) - // To Be Implemented + for i := 0; i < eventSlice.Len(); i++ { + var severity string + severityAttrValue, ok := eventSlice.At(i).Attributes().Get(s.severityAttribute) + if ok { + severity = severityAttrValue.AsString() + } else { + severity = s.defaultSeverity + } + event := alertmanagerEvent{ + spanEvent: eventSlice.At(i), + traceID: traceID.String(), + spanID: spanID.String(), + severity: severity, + } + + events[i] = &event + } + return events + } return nil } -func (s *alertmanagerExporter) start(_ context.Context, _ component.Host) error { +func (s *alertmanagerExporter) extractEvents(td ptrace.Traces) []*alertmanagerEvent { + + // Stitch parent trace ID and span ID + rss := td.ResourceSpans() + var events []*alertmanagerEvent + if rss.Len() == 0 { + return nil + } + + for i := 0; i < rss.Len(); i++ { + resource := rss.At(i).Resource() + ilss := rss.At(i).ScopeSpans() + + if resource.Attributes().Len() == 0 && ilss.Len() == 0 { + return nil + } + + for j := 0; j < ilss.Len(); j++ { + spans := ilss.At(j).Spans() + for k := 0; k < spans.Len(); k++ { + traceID := spans.At(k).TraceID() + spanID := spans.At(k).SpanID() + events = append(events, s.convertEventSliceToArray(spans.At(k).Events(), traceID, spanID)...) + } + } + } + return events +} + +func createAnnotations(event *alertmanagerEvent) model.LabelSet { + labelMap := make(model.LabelSet, event.spanEvent.Attributes().Len()+2) + event.spanEvent.Attributes().Range(func(key string, attr pcommon.Value) bool { + labelMap[model.LabelName(key)] = model.LabelValue(attr.AsString()) + return true + }) + labelMap["TraceID"] = model.LabelValue(event.traceID) + labelMap["SpanID"] = model.LabelValue(event.spanID) + return labelMap +} + +func (s *alertmanagerExporter) convertEventsToAlertPayload(events []*alertmanagerEvent) []model.Alert { + + payload := make([]model.Alert, len(events)) + + for i, event := range events { + annotations := createAnnotations(event) + + alert := model.Alert{ + StartsAt: time.Now(), + Labels: model.LabelSet{"severity": model.LabelValue(event.severity), "event_name": model.LabelValue(event.spanEvent.Name())}, + Annotations: annotations, + GeneratorURL: s.generatorURL, + } + + payload[i] = alert + } + return payload +} + +func (s *alertmanagerExporter) postAlert(ctx context.Context, payload []model.Alert) error { + + msg, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("error marshaling alert to JSON: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", s.endpoint, bytes.NewBuffer(msg)) + if err != nil { + return fmt.Errorf("error creating HTTP request: %w", err) + } + req.Header.Set("Content-Type", "application/json") - // To Be Implemented + resp, err := s.client.Do(req) + if err != nil { + return fmt.Errorf("error sending HTTP request: %w", err) + } + + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + s.settings.Logger.Warn("failed to close response body", zap.Error(closeErr)) + } + }() + + _, err = io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body %w", err) + } + + if resp.StatusCode != http.StatusOK { + s.settings.Logger.Debug("post request to Alertmanager failed", zap.Error(err)) + return fmt.Errorf("request POST %s failed - %q", req.URL.String(), resp.Status) + } + return nil +} + +func (s *alertmanagerExporter) pushTraces(ctx context.Context, td ptrace.Traces) error { + + events := s.extractEvents(td) + + if len(events) == 0 { + return nil + } + + alert := s.convertEventsToAlertPayload(events) + err := s.postAlert(ctx, alert) + + if err != nil { + return err + } + + return nil +} + +func (s *alertmanagerExporter) start(_ context.Context, host component.Host) error { + + client, err := s.config.HTTPClientSettings.ToClient(host, s.settings) + if err != nil { + return fmt.Errorf("failed to create HTTP Client: %w", err) + } + s.client = client return nil } func (s *alertmanagerExporter) shutdown(context.Context) error { - // To Be Implemented + + if s.client != nil { + s.client.CloseIdleConnections() + } return nil } @@ -46,7 +203,7 @@ func newAlertManagerExporter(cfg *Config, set component.TelemetrySettings) *aler config: cfg, settings: set, tracesMarshaler: &ptrace.JSONMarshaler{}, - endpoint: cfg.HTTPClientSettings.Endpoint, + endpoint: fmt.Sprintf("%s/api/v1/alerts", cfg.HTTPClientSettings.Endpoint), generatorURL: cfg.GeneratorURL, defaultSeverity: cfg.DefaultSeverity, severityAttribute: cfg.SeverityAttribute, @@ -54,6 +211,7 @@ func newAlertManagerExporter(cfg *Config, set component.TelemetrySettings) *aler } func newTracesExporter(ctx context.Context, cfg component.Config, set exporter.CreateSettings) (exporter.Traces, error) { + config := cfg.(*Config) s := newAlertManagerExporter(config, set.TelemetrySettings) @@ -64,7 +222,6 @@ func newTracesExporter(ctx context.Context, cfg component.Config, set exporter.C cfg, s.pushTraces, exporterhelper.WithCapabilities(consumer.Capabilities{MutatesData: false}), - // Disable Timeout/RetryOnFailure and SendingQueue exporterhelper.WithStart(s.start), exporterhelper.WithTimeout(config.TimeoutSettings), exporterhelper.WithRetry(config.RetrySettings), diff --git a/exporter/alertmanagerexporter/alertmanager_exporter_test.go b/exporter/alertmanagerexporter/alertmanager_exporter_test.go new file mode 100644 index 000000000000..e30b9d02f98e --- /dev/null +++ b/exporter/alertmanagerexporter/alertmanager_exporter_test.go @@ -0,0 +1,420 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package alertmanagerexporter + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configopaque" + "go.opentelemetry.io/collector/config/configtls" + "go.opentelemetry.io/collector/exporter/exportertest" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + conventions "go.opentelemetry.io/collector/semconv/v1.6.1" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/common/testutil" +) + +func createTracesAndSpan() (ptrace.Traces, ptrace.Span) { + // make a trace + traces := ptrace.NewTraces() + // add trace attributes + rs := traces.ResourceSpans().AppendEmpty() + resource := rs.Resource() + attrs := resource.Attributes() + attrs.Clear() + attrs.EnsureCapacity(4) // service name + 3 attributes + attrs.PutStr(conventions.AttributeServiceName, "unittest-resource") + attrs.PutStr("attr1", "unittest-foo") + attrs.PutInt("attr2", 40) + attrs.PutDouble("attr3", 3.14) + + // add a span + spans := rs.ScopeSpans().AppendEmpty().Spans() + spans.EnsureCapacity(1) + span := spans.AppendEmpty() + // add span attributes + span.SetTraceID(pcommon.TraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})) + span.SetSpanID(pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 3})) + span.SetName("unittest-span") + startTime := pcommon.Timestamp(time.Now().UnixNano()) + span.SetStartTimestamp(startTime) + span.SetEndTimestamp(startTime + 1) + span.SetParentSpanID(pcommon.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 1})) + attrs = span.Attributes() + attrs.Clear() + attrs.EnsureCapacity(4) + attrs.PutStr("attr1", "unittest-bar") + attrs.PutInt("attr2", 41) + attrs.PutDouble("attr3", 4.14) + + return traces, span +} + +func TestAlertManagerExporterExtractEvents(t *testing.T) { + tests := []struct { + name string + events int + }{ + {"TestAlertManagerExporterExtractEvents0", 0}, + {"TestAlertManagerExporterExtractEvents1", 1}, + {"TestAlertManagerExporterExtractEvents5", 5}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + set := exportertest.NewNopCreateSettings() + am := newAlertManagerExporter(cfg, set.TelemetrySettings) + require.NotNil(t, am) + + // make traces & a span + traces, span := createTracesAndSpan() + + // add events + for i := 0; i < tt.events; i++ { + event := span.Events().AppendEmpty() + // add event attributes + startTime := pcommon.Timestamp(time.Now().UnixNano()) + event.SetTimestamp(startTime + 3) + event.SetName(fmt.Sprintf("unittest-event-%d", i)) + attrs := event.Attributes() + attrs.Clear() + attrs.EnsureCapacity(4) + attrs.PutStr("attr1", fmt.Sprintf("unittest-baz-%d", i)) + attrs.PutInt("attr2", 42) + attrs.PutDouble("attr3", 5.14) + } + + // test - events + got := am.extractEvents(traces) + assert.Equal(t, tt.events, len(got)) + }) + } +} + +func TestAlertManagerExporterEventNameAttributes(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + set := exportertest.NewNopCreateSettings() + am := newAlertManagerExporter(cfg, set.TelemetrySettings) + require.NotNil(t, am) + + // make traces & a span + traces, span := createTracesAndSpan() + + // add a span event w/ 3 attributes + event := span.Events().AppendEmpty() + // add event attributes + startTime := pcommon.Timestamp(time.Now().UnixNano()) + event.SetTimestamp(startTime + 3) + event.SetName("unittest-event") + attrs := event.Attributes() + attrs.Clear() + attrs.EnsureCapacity(4) + attrs.PutStr("attr1", "unittest-baz") + attrs.PutInt("attr2", 42) + attrs.PutDouble("attr3", 5.14) + + // test - 1 event + got := am.extractEvents(traces) + + // test - result length + assert.Equal(t, 1, len(got)) + + // test - count of attributes + assert.Equal(t, 3, got[0].spanEvent.Attributes().Len()) + attr, b := got[0].spanEvent.Attributes().Get("attr1") + assert.Equal(t, true, b) + assert.Equal(t, "unittest-event", got[0].spanEvent.Name()) + assert.Equal(t, "unittest-baz", attr.AsString()) + attr, b = got[0].spanEvent.Attributes().Get("attr3") + assert.Equal(t, true, b) + assert.Equal(t, 5.14, attr.Double()) +} + +func TestAlertManagerExporterSeverity(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + cfg.SeverityAttribute = "foo" + set := exportertest.NewNopCreateSettings() + am := newAlertManagerExporter(cfg, set.TelemetrySettings) + require.NotNil(t, am) + + // make traces & a span + traces, span := createTracesAndSpan() + + // add a span event with severity attribute + event := span.Events().AppendEmpty() + // add event attributes + startTime := pcommon.Timestamp(time.Now().UnixNano()) + event.SetTimestamp(startTime + 3) + event.SetName("unittest-event") + attrs := event.Attributes() + attrs.Clear() + attrs.EnsureCapacity(4) + attrs.PutStr("attr1", "unittest-baz") + attrs.PutStr("foo", "debug") + + // add a span event without severity attribute + event = span.Events().AppendEmpty() + // add event attributes + startTime = pcommon.Timestamp(time.Now().UnixNano()) + event.SetTimestamp(startTime + 3) + event.SetName("unittest-event") + attrs = event.Attributes() + attrs.Clear() + attrs.EnsureCapacity(4) + attrs.PutStr("attr1", "unittest-baz") + attrs.PutStr("bar", "debug") + + // test - 0 event + got := am.extractEvents(traces) + alerts := am.convertEventsToAlertPayload(got) + + ls := model.LabelSet{"event_name": "unittest-event", "severity": "debug"} + assert.Equal(t, ls, alerts[0].Labels) + + ls = model.LabelSet{"event_name": "unittest-event", "severity": "info"} + assert.Equal(t, ls, alerts[1].Labels) + +} + +func TestAlertManagerExporterNoDefaultSeverity(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + set := exportertest.NewNopCreateSettings() + am := newAlertManagerExporter(cfg, set.TelemetrySettings) + require.NotNil(t, am) + + // make traces & a span + traces, span := createTracesAndSpan() + + // add a span event with severity attribute + event := span.Events().AppendEmpty() + // add event attributes + startTime := pcommon.Timestamp(time.Now().UnixNano()) + event.SetTimestamp(startTime + 3) + event.SetName("unittest-event") + attrs := event.Attributes() + attrs.Clear() + attrs.EnsureCapacity(4) + attrs.PutStr("attr1", "unittest-baz") + attrs.PutStr("attr2", "debug") + + // test - 0 event + got := am.extractEvents(traces) + alerts := am.convertEventsToAlertPayload(got) + + ls := model.LabelSet{"event_name": "unittest-event", "severity": "info"} + assert.Equal(t, ls, alerts[0].Labels) + +} + +func TestAlertManagerExporterAlertPayload(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + set := exportertest.NewNopCreateSettings() + am := newAlertManagerExporter(cfg, set.TelemetrySettings) + + require.NotNil(t, am) + + // make traces & a span + _, span := createTracesAndSpan() + + // add a span event w/ 3 attributes + event := span.Events().AppendEmpty() + // add event attributes + startTime := pcommon.Timestamp(time.Now().UTC().Unix()) + event.SetTimestamp(startTime + 3) + event.SetName("unittest-event") + attrs := event.Attributes() + attrs.Clear() + attrs.EnsureCapacity(4) + attrs.PutStr("attr1", "unittest-baz") + attrs.PutInt("attr2", 42) + attrs.PutDouble("attr3", 5.14) + + var events []*alertmanagerEvent + events = append(events, &alertmanagerEvent{ + spanEvent: event, + severity: am.defaultSeverity, + traceID: "0000000000000002", + spanID: "00000002", + }) + + got := am.convertEventsToAlertPayload(events) + + // test - count of attributes + expect := model.Alert{ + Labels: model.LabelSet{"severity": "info", "event_name": "unittest-event"}, + Annotations: model.LabelSet{"SpanID": "00000002", "TraceID": "0000000000000002", "attr1": "unittest-baz", "attr2": "42", "attr3": "5.14"}, + GeneratorURL: "opentelemetry-collector", + } + assert.Equal(t, expect.Labels, got[0].Labels) + assert.Equal(t, expect.Annotations, got[0].Annotations) + assert.Equal(t, expect.GeneratorURL, got[0].GeneratorURL) + +} + +func TestAlertManagerTracesExporterNoErrors(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + lte, err := newTracesExporter(context.Background(), cfg, exportertest.NewNopCreateSettings()) + fmt.Println(lte) + require.NotNil(t, lte) + assert.NoError(t, err) +} + +type ( + MockServer struct { + mockserver *httptest.Server // this means MockServer aggreagates 'httptest.Server', but can it's more like inheritance in C++ + fooCalledSuccessfully bool // this is false by default + } +) + +func newMockServer(t *testing.T) *MockServer { + mock := MockServer{ + fooCalledSuccessfully: false, + } + + handler := http.NewServeMux() + handler.HandleFunc("/api/v1/alerts", func(w http.ResponseWriter, r *http.Request) { + _, errWrite := fmt.Fprint(w, "test") + assert.NoError(t, errWrite) + _, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + mock.fooCalledSuccessfully = true + _, _ = w.Write([]byte("hello")) + }) + mock.mockserver = httptest.NewServer(handler) + return &mock +} + +func TestAlertManagerPostAlert(t *testing.T) { + mock := newMockServer(t) + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + + var alerts []model.Alert + alerts = append(alerts, model.Alert{ + Labels: model.LabelSet{"new": "info"}, + Annotations: model.LabelSet{"foo": "bar1"}, + GeneratorURL: "http://example.com/alert", + }) + + cfg.Endpoint = mock.mockserver.URL + set := exportertest.NewNopCreateSettings() + am := newAlertManagerExporter(cfg, set.TelemetrySettings) + err := am.start(context.Background(), componenttest.NewNopHost()) + + assert.NoError(t, err) + + err = am.postAlert(context.Background(), alerts) + assert.NoError(t, err) + if mock.fooCalledSuccessfully == false { + t.Errorf("mock server wasn't called") + } +} + +func TestHTTPClientSettings(t *testing.T) { + endpoint := "http://" + testutil.GetAvailableLocalAddress(t) + fmt.Println(endpoint) + tests := []struct { + name string + config *Config + mustFailOnCreate bool + mustFailOnStart bool + }{ + { + name: "UseSecure", + config: &Config{ + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: endpoint, + TLSSetting: configtls.TLSClientSetting{ + Insecure: false, + }, + }, + }, + }, + { + name: "Headers", + config: &Config{ + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: endpoint, + Headers: map[string]configopaque.String{ + "hdr1": "val1", + "hdr2": "val2", + }, + }, + }, + }, + { + name: "CaCert", + config: &Config{ + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: endpoint, + TLSSetting: configtls.TLSClientSetting{ + TLSSetting: configtls.TLSSetting{ + CAFile: "testdata/test_cert.pem", + }, + }, + }, + }, + }, + { + name: "CertPemFileError", + config: &Config{ + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: endpoint, + TLSSetting: configtls.TLSClientSetting{ + TLSSetting: configtls.TLSSetting{ + CAFile: "nosuchfile", + }, + }, + }, + }, + mustFailOnCreate: false, + mustFailOnStart: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + set := exportertest.NewNopCreateSettings() + am := newAlertManagerExporter(tt.config, set.TelemetrySettings) + + exp, err := newTracesExporter(context.Background(), tt.config, set) + if tt.mustFailOnCreate { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.NotNil(t, exp) + + err = am.start(context.Background(), componenttest.NewNopHost()) + if tt.mustFailOnStart { + assert.Error(t, err) + } + + t.Cleanup(func() { + require.NoError(t, am.shutdown(context.Background())) + }) + }) + } +} diff --git a/exporter/alertmanagerexporter/config.go b/exporter/alertmanagerexporter/config.go index 54ef65734a7f..738294cb8691 100644 --- a/exporter/alertmanagerexporter/config.go +++ b/exporter/alertmanagerexporter/config.go @@ -4,6 +4,8 @@ package alertmanagerexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/alertmanagerexporter" import ( + "errors" + "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/exporter/exporterhelper" @@ -25,5 +27,12 @@ var _ component.Config = (*Config)(nil) // Validate checks if the exporter configuration is valid func (cfg *Config) Validate() error { + + if cfg.HTTPClientSettings.Endpoint == "" { + return errors.New("endpoint must be non-empty") + } + if cfg.DefaultSeverity == "" { + return errors.New("severity must be non-empty") + } return nil } diff --git a/exporter/alertmanagerexporter/config_test.go b/exporter/alertmanagerexporter/config_test.go index 03a45f251150..d3bf292bda8a 100644 --- a/exporter/alertmanagerexporter/config_test.go +++ b/exporter/alertmanagerexporter/config_test.go @@ -95,3 +95,46 @@ func TestLoadConfig(t *testing.T) { }) } } + +func TestConfig_Validate(t *testing.T) { + tests := []struct { + name string + cfg *Config + wantErr string + }{ + { + name: "NoEndpoint", + cfg: func() *Config { + cfg := createDefaultConfig().(*Config) + cfg.HTTPClientSettings.Endpoint = "" + return cfg + }(), + wantErr: "endpoint must be non-empty", + }, + { + name: "NoSeverity", + cfg: func() *Config { + cfg := createDefaultConfig().(*Config) + cfg.DefaultSeverity = "" + return cfg + }(), + wantErr: "severity must be non-empty", + }, + { + name: "Success", + cfg: createDefaultConfig().(*Config), + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tt.wantErr) + } + }) + } +} diff --git a/exporter/alertmanagerexporter/factory.go b/exporter/alertmanagerexporter/factory.go index 5ec549fa5e70..3273aa09a3d6 100644 --- a/exporter/alertmanagerexporter/factory.go +++ b/exporter/alertmanagerexporter/factory.go @@ -33,10 +33,9 @@ func createDefaultConfig() component.Config { RetrySettings: exporterhelper.NewDefaultRetrySettings(), QueueSettings: exporterhelper.NewDefaultQueueSettings(), HTTPClientSettings: confighttp.HTTPClientSettings{ - Endpoint: "http://localhost:9093", - Timeout: 30 * time.Second, - Headers: map[string]configopaque.String{}, - // We almost read 0 bytes, so no need to tune ReadBufferSize. + Endpoint: "http://localhost:9093", + Timeout: 30 * time.Second, + Headers: map[string]configopaque.String{}, WriteBufferSize: 512 * 1024, }, } diff --git a/exporter/alertmanagerexporter/go.mod b/exporter/alertmanagerexporter/go.mod index 2c3d5c453c38..acae218f692b 100644 --- a/exporter/alertmanagerexporter/go.mod +++ b/exporter/alertmanagerexporter/go.mod @@ -4,6 +4,8 @@ go 1.20 require ( github.com/cenkalti/backoff/v4 v4.2.1 + github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.90.1 + github.com/prometheus/common v0.45.0 github.com/stretchr/testify v1.8.4 go.opentelemetry.io/collector/component v0.90.2-0.20231201205146-6e2fdc755b34 go.opentelemetry.io/collector/config/confighttp v0.90.2-0.20231201205146-6e2fdc755b34 @@ -13,6 +15,8 @@ require ( go.opentelemetry.io/collector/consumer v0.90.2-0.20231201205146-6e2fdc755b34 go.opentelemetry.io/collector/exporter v0.90.2-0.20231201205146-6e2fdc755b34 go.opentelemetry.io/collector/pdata v1.0.1-0.20231201205146-6e2fdc755b34 + go.opentelemetry.io/collector/semconv v0.90.2-0.20231201205146-6e2fdc755b34 + go.uber.org/zap v1.26.0 ) require ( @@ -52,7 +56,6 @@ require ( go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect @@ -67,3 +70,5 @@ retract ( v0.76.1 v0.65.0 ) + +replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/common => ../../internal/common diff --git a/exporter/alertmanagerexporter/go.sum b/exporter/alertmanagerexporter/go.sum index 2dadd82ae61c..980367485b86 100644 --- a/exporter/alertmanagerexporter/go.sum +++ b/exporter/alertmanagerexporter/go.sum @@ -90,6 +90,7 @@ github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -142,6 +143,8 @@ go.opentelemetry.io/collector/pdata v1.0.1-0.20231201205146-6e2fdc755b34 h1:dVqK go.opentelemetry.io/collector/pdata v1.0.1-0.20231201205146-6e2fdc755b34/go.mod h1:TsDFgs4JLNG7t6x9D8kGswXUz4mme+MyNChHx8zSF6k= go.opentelemetry.io/collector/receiver v0.90.2-0.20231201205146-6e2fdc755b34 h1:WR6mGsYoNDoqG4ecam1Wyna8GxOB/ATE2r3TbLTdZsE= go.opentelemetry.io/collector/receiver v0.90.2-0.20231201205146-6e2fdc755b34/go.mod h1:KAAfJus9Kn92XTqOQO5/ZftTYKBhpi2S8NW6n7Baefo= +go.opentelemetry.io/collector/semconv v0.90.2-0.20231201205146-6e2fdc755b34 h1:kv7QJcgWCg+gvEtcAeFsmRD3DePlNlTWFOCNyuJ4sEY= +go.opentelemetry.io/collector/semconv v0.90.2-0.20231201205146-6e2fdc755b34/go.mod h1:j/8THcqVxFna1FpvA2zYIsUperEtOaRaqoLYIN4doWw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= diff --git a/exporter/alertmanagerexporter/testdata/test_cert.pem b/exporter/alertmanagerexporter/testdata/test_cert.pem new file mode 100644 index 000000000000..0cb62d37aad7 --- /dev/null +++ b/exporter/alertmanagerexporter/testdata/test_cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE6jCCAtICCQDVU4PtqpqADTANBgkqhkiG9w0BAQsFADA3MQswCQYDVQQGEwJV +UzETMBEGA1UECAwKY2FsaWZvcm5pYTETMBEGA1UECgwKb3BlbmNlbnN1czAeFw0x +OTAzMDQxODA3MjZaFw0yMDAzMDMxODA3MjZaMDcxCzAJBgNVBAYTAlVTMRMwEQYD +VQQIDApjYWxpZm9ybmlhMRMwEQYDVQQKDApvcGVuY2Vuc3VzMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAy9JQiAOMzArcdiS4szbTuzg5yYijSSY6SvGj +XMs4/LEFLxgGmFfyHXxoVQzV26lTu/AiUFlZi4JY2qlkZyPwmmmSg4fmzikpVPiC +Vv9pvSIojs8gs0sHaOt40Q8ym43bNt3Mh8rYrs+XMERi6Ol9//j4LnfePkNU5uEo +qC8KQamckaMR6UEHFNunyOwvNBsipgTPldQUPGVnCsNKk8olYGAXS7DR25bgbPli +4T9VCSElsSPAODmyo+2MEDagVXa1vVYxKyO2k6oeBS0lsvdRqRTmGggcg0B/dk+a +H1CL9ful0cu9P3dQif+hfGay8udPkwDLPEq1+WnjJFut3Pmbk3SqUCas5iWt76kK +eKFh4k8fCy4yiaZxzvSbm9+bEBHAl0ZXd8pjvAsBfCKe6G9SBzE1DK4FjWiiEGCb +5dGsyTKr33q3DekLvT3LF8ZeON/13d9toucX9PqG2HDwMP/Fb4WjQIzOc/H9wIak +pf7u6QBDGUiCMmoDrp1d8RsI1RPbEhoywH0YlLmwgf+cr1dU7vlISf576EsGxFz4 ++/sZjIBvZBHn/x0MH+bs4J8V3vMujfDoRdhL07bK7q/AkEALUxljKEfoWeqiuVzK +F9BVv3xNhiua2kgPVbMNWPrQ5uotkNp8IykJ3QOuQ3p5pzxdGfpLd6f8gmJDmcbi +AI9dWTcCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAVVi4t/Sumre+AGTaU7np9dl2 +tpllbES5ixe6m2uezt5wAzYNNyuQ2mMG2XrSkMy5gvBZRT9nRNSmLV8VEcxZihG0 +YHS5soXnLL3Jdlwxp98WTDPvM1ntxcHyEyqrrg9YDfKn4sOrr5vo2yZzoKwtxtc7 +lue9JormVx7GxMi7NwaUtCbnwAIcqJJpFjt1EhmJOxGqTJPgUvTBdeGvRj30c6fk +pqpUdPbZ7RKPEtbLoMoCBujKnErv+H0G6Vp9WyCHN+Mi9uTMsGwH14cmJjmfwGDC +8/WF4LdlawFnf/arIp9YcVwcP91d4ywyvbuuo2M7qdosQ7k4uRZ3tyggLYShS3RW +BMEhMRDz9dM0oKGF+HnaS824BIh6O6Hn82Vt8uCKS7IbEX99/kkN1KcqqQe6Lwjq +tG/lm4K5yf+FJVDivpZ9mYTvqTBjhTaOp6m3HYSNJfS0hLQVvEuBNXd8bHiXkcLp +rmFOYUWsjxV1Qku3U5Rner0UpB2Fuw9nJcXuDgWG0gjwzAZ83y3du1VIZp0Ad8Vv +IYpaucbImGJszMtNXn3l72K1wvQVIhm9eRwYc3QteJzweHaDsbytZEoS/GhTrZIT +wRe5ZGrjJBJngRANRSm1BH8j6PjLem9mzPb2eytwJJA0lLhUk4vYproVvXcx0vow +5F+5VB1YB8/tbWePmpo= +-----END CERTIFICATE----- \ No newline at end of file