From dac47749e45fc34d7e83888f3ffedc1b0fb8ea28 Mon Sep 17 00:00:00 2001 From: Ruben Vargas Date: Thu, 30 May 2024 09:38:49 -0600 Subject: [PATCH] Add crd metrics usage information (#2825) * Add crd metrics usage information Signed-off-by: Ruben Vargas * Add mode metric Signed-off-by: Ruben Vargas * Refactor CR metrics Signed-off-by: Ruben Vargas * Add annotation to avoid generate Metrics Signed-off-by: Ruben Vargas * Add unit tests Signed-off-by: Ruben Vargas * remove space Signed-off-by: Ruben Vargas * remove global provider Signed-off-by: Ruben Vargas * Update main.go Co-authored-by: Israel Blancas * revert kusttomization.yaml Signed-off-by: Ruben Vargas * rename some constants Signed-off-by: Ruben Vargas * Add connectors metrics Signed-off-by: Ruben Vargas * Update chlog Signed-off-by: Ruben Vargas * merge new with init, rename some functions, improve changelog entry Signed-off-by: Ruben Vargas * improve todo comment Signed-off-by: Ruben Vargas * fix tests Signed-off-by: Ruben Vargas * set flag to default false Signed-off-by: Ruben Vargas * fix lint issues Signed-off-by: Ruben Vargas * breaking line Signed-off-by: Ruben Vargas * Use api reader to avoid cache issues Signed-off-by: Ruben Vargas * Add info metric to changelog entry Signed-off-by: Ruben Vargas --------- Signed-off-by: Ruben Vargas Co-authored-by: Israel Blancas --- .chloggen/usage_metrics.yaml | 25 + apis/v1beta1/collector_webhook.go | 46 +- apis/v1beta1/metrics.go | 231 +++++ apis/v1beta1/metrics_test.go | 842 ++++++++++++++++++ controllers/suite_test.go | 2 +- go.mod | 1 + go.sum | 2 + .../podmutation/webhookhandler_suite_test.go | 2 +- main.go | 21 +- pkg/collector/upgrade/suite_test.go | 2 +- pkg/constants/env.go | 1 + 11 files changed, 1166 insertions(+), 9 deletions(-) create mode 100755 .chloggen/usage_metrics.yaml create mode 100644 apis/v1beta1/metrics.go create mode 100644 apis/v1beta1/metrics_test.go diff --git a/.chloggen/usage_metrics.yaml b/.chloggen/usage_metrics.yaml new file mode 100755 index 0000000000..c4051bde5a --- /dev/null +++ b/.chloggen/usage_metrics.yaml @@ -0,0 +1,25 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action) +component: collector + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add usage metrics for the collector + +# One or more tracking issues related to the change +issues: [2829] + +# (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: | + This change will add metrics to the OpenTelemetry operator about how the collector is used in the cluster, + it will add the following metrics to the opentelemetry-operator metrics endpoint + ``` + opentelemetry_collector_receivers{collector_name="collector_name", namespace="ns", type="otlp"} 1 + opentelemetry_collector_exporters{collector_name="collector_name", namespace="ns", type="otlp"} 1 + opentelemetry_collector_processors{collector_name="collector_name", namespace="ns", type="otlp"} 1 + opentelemetry_collector_connectors{collector_name="collector_name", namespace="ns", type="myconnector"} 0 + opentelemetry_collector_info{collector_name="simplest",namespace="default", type="deployment"} 1 + ``` diff --git a/apis/v1beta1/collector_webhook.go b/apis/v1beta1/collector_webhook.go index 2c4bc80d85..ffe4eff5e5 100644 --- a/apis/v1beta1/collector_webhook.go +++ b/apis/v1beta1/collector_webhook.go @@ -76,6 +76,7 @@ type CollectorWebhook struct { cfg config.Config scheme *runtime.Scheme reviewer *rbac.Reviewer + metrics *Metrics } func (c CollectorWebhook) Default(_ context.Context, obj runtime.Object) error { @@ -166,15 +167,39 @@ func (c CollectorWebhook) ValidateCreate(ctx context.Context, obj runtime.Object if !ok { return nil, fmt.Errorf("expected an OpenTelemetryCollector, received %T", obj) } - return c.validate(ctx, otelcol) + + warnings, err := c.validate(ctx, otelcol) + if err != nil { + return warnings, err + } + if c.metrics != nil { + c.metrics.create(ctx, otelcol) + } + + return warnings, nil } -func (c CollectorWebhook) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) (admission.Warnings, error) { +func (c CollectorWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { otelcol, ok := newObj.(*OpenTelemetryCollector) if !ok { return nil, fmt.Errorf("expected an OpenTelemetryCollector, received %T", newObj) } - return c.validate(ctx, otelcol) + + otelcolOld, ok := oldObj.(*OpenTelemetryCollector) + if !ok { + return nil, fmt.Errorf("expected an OpenTelemetryCollector, received %T", oldObj) + } + + warnings, err := c.validate(ctx, otelcol) + if err != nil { + return warnings, err + } + + if c.metrics != nil { + c.metrics.update(ctx, otelcolOld, otelcol) + } + + return warnings, nil } func (c CollectorWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { @@ -182,7 +207,17 @@ func (c CollectorWebhook) ValidateDelete(ctx context.Context, obj runtime.Object if !ok || otelcol == nil { return nil, fmt.Errorf("expected an OpenTelemetryCollector, received %T", obj) } - return c.validate(ctx, otelcol) + + warnings, err := c.validate(ctx, otelcol) + if err != nil { + return warnings, err + } + + if c.metrics != nil { + c.metrics.delete(ctx, otelcol) + } + + return warnings, nil } func (c CollectorWebhook) validate(ctx context.Context, r *OpenTelemetryCollector) (admission.Warnings, error) { @@ -419,12 +454,13 @@ func checkAutoscalerSpec(autoscaler *AutoscalerSpec) error { return nil } -func SetupCollectorWebhook(mgr ctrl.Manager, cfg config.Config, reviewer *rbac.Reviewer) error { +func SetupCollectorWebhook(mgr ctrl.Manager, cfg config.Config, reviewer *rbac.Reviewer, metrics *Metrics) error { cvw := &CollectorWebhook{ reviewer: reviewer, logger: mgr.GetLogger().WithValues("handler", "CollectorWebhook", "version", "v1beta1"), scheme: mgr.GetScheme(), cfg: cfg, + metrics: metrics, } return ctrl.NewWebhookManagedBy(mgr). For(&OpenTelemetryCollector{}). diff --git a/apis/v1beta1/metrics.go b/apis/v1beta1/metrics.go new file mode 100644 index 0000000000..395306059d --- /dev/null +++ b/apis/v1beta1/metrics.go @@ -0,0 +1,231 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1beta1 + +import ( + "context" + "fmt" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/metric" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +const ( + meterName = "crd-metrics" +) + +// Metric labels + +const ( + prefix = "opentelemetry_collector_" + receiversMetricName = prefix + "receivers" + exportersMetricName = prefix + "exporters" + processorsMetricName = prefix + "processors" + extensionsMetricName = prefix + "extensions" + connectorsMetricName = prefix + "connectors" + modeMetricName = prefix + "info" +) + +// TODO: Refactor this logic, centralize it. See: https://github.com/open-telemetry/opentelemetry-operator/issues/2603 +type components struct { + receivers []string + processors []string + exporters []string + extensions []string + connectors []string +} + +// Metrics hold all gauges for the different metrics related to the CRs +// +kubebuilder:object:generate=false +type Metrics struct { + modeCounter metric.Int64UpDownCounter + receiversCounter metric.Int64UpDownCounter + exporterCounter metric.Int64UpDownCounter + processorCounter metric.Int64UpDownCounter + extensionsCounter metric.Int64UpDownCounter + connectorsCounter metric.Int64UpDownCounter +} + +// BootstrapMetrics configures the OpenTelemetry meter provider with the Prometheus exporter. +func BootstrapMetrics() (metric.MeterProvider, error) { + exporter, err := prometheus.New(prometheus.WithRegisterer(metrics.Registry)) + if err != nil { + return nil, err + } + return sdkmetric.NewMeterProvider(sdkmetric.WithReader(exporter)), err +} + +func NewMetrics(prv metric.MeterProvider, ctx context.Context, cl client.Reader) (*Metrics, error) { + meter := prv.Meter(meterName) + modeCounter, err := meter.Int64UpDownCounter(modeMetricName) + if err != nil { + return nil, err + } + receiversCounter, err := meter.Int64UpDownCounter(receiversMetricName) + if err != nil { + return nil, err + } + + exporterCounter, err := meter.Int64UpDownCounter(exportersMetricName) + if err != nil { + return nil, err + } + + processorCounter, err := meter.Int64UpDownCounter(processorsMetricName) + if err != nil { + return nil, err + } + + extensionsCounter, err := meter.Int64UpDownCounter(extensionsMetricName) + if err != nil { + return nil, err + } + + connectorsCounter, err := meter.Int64UpDownCounter(connectorsMetricName) + if err != nil { + return nil, err + } + + m := &Metrics{ + modeCounter: modeCounter, + receiversCounter: receiversCounter, + exporterCounter: exporterCounter, + processorCounter: processorCounter, + extensionsCounter: extensionsCounter, + connectorsCounter: connectorsCounter, + } + + err = m.init(ctx, cl) + if err != nil { + return nil, err + } + return m, nil +} + +// Init metrics from the first time the operator starts. +func (m *Metrics) init(ctx context.Context, cl client.Reader) error { + opts := []client.ListOption{ + client.MatchingLabels(map[string]string{ + "app.kubernetes.io/managed-by": "opentelemetry-operator", + }), + } + list := &OpenTelemetryCollectorList{} + if err := cl.List(ctx, list, opts...); err != nil { + return fmt.Errorf("failed to list: %w", err) + } + + for i := range list.Items { + m.create(ctx, &list.Items[i]) + } + return nil +} + +func (m *Metrics) create(ctx context.Context, collector *OpenTelemetryCollector) { + m.updateComponentCounters(ctx, collector, true) + m.updateGeneralCRMetricsComponents(ctx, collector, true) +} + +func (m *Metrics) delete(ctx context.Context, collector *OpenTelemetryCollector) { + m.updateComponentCounters(ctx, collector, false) + m.updateGeneralCRMetricsComponents(ctx, collector, false) +} + +func (m *Metrics) update(ctx context.Context, oldCollector *OpenTelemetryCollector, newCollector *OpenTelemetryCollector) { + m.delete(ctx, oldCollector) + m.create(ctx, newCollector) +} + +func (m *Metrics) updateGeneralCRMetricsComponents(ctx context.Context, collector *OpenTelemetryCollector, up bool) { + + inc := 1 + if !up { + inc = -1 + } + m.modeCounter.Add(ctx, int64(inc), metric.WithAttributes( + attribute.Key("collector_name").String(collector.Name), + attribute.Key("namespace").String(collector.Namespace), + attribute.Key("type").String(string(collector.Spec.Mode)), + )) +} +func (m *Metrics) updateComponentCounters(ctx context.Context, collector *OpenTelemetryCollector, up bool) { + components := getComponentsFromConfig(collector.Spec.Config) + moveCounter(ctx, collector, components.receivers, m.receiversCounter, up) + moveCounter(ctx, collector, components.exporters, m.exporterCounter, up) + moveCounter(ctx, collector, components.processors, m.processorCounter, up) + moveCounter(ctx, collector, components.extensions, m.extensionsCounter, up) + moveCounter(ctx, collector, components.connectors, m.connectorsCounter, up) + +} + +func extractElements(elements map[string]interface{}) []string { + // TODO: we should get rid of this method and centralize the parse logic + // see https://github.com/open-telemetry/opentelemetry-operator/issues/2603 + if elements == nil { + return []string{} + } + + itemsMap := map[string]struct{}{} + var items []string + for key := range elements { + itemName := strings.SplitN(key, "/", 2)[0] + itemsMap[itemName] = struct{}{} + } + for key := range itemsMap { + items = append(items, key) + } + return items +} + +func getComponentsFromConfig(yamlContent Config) *components { + + info := &components{ + receivers: extractElements(yamlContent.Receivers.Object), + exporters: extractElements(yamlContent.Exporters.Object), + } + + if yamlContent.Processors != nil { + info.processors = extractElements(yamlContent.Processors.Object) + } + + if yamlContent.Extensions != nil { + info.extensions = extractElements(yamlContent.Extensions.Object) + } + + if yamlContent.Connectors != nil { + info.connectors = extractElements(yamlContent.Connectors.Object) + } + + return info +} + +func moveCounter( + ctx context.Context, collector *OpenTelemetryCollector, types []string, upDown metric.Int64UpDownCounter, up bool) { + for _, exporter := range types { + inc := 1 + if !up { + inc = -1 + } + upDown.Add(ctx, int64(inc), metric.WithAttributes( + attribute.Key("collector_name").String(collector.Name), + attribute.Key("namespace").String(collector.Namespace), + attribute.Key("type").String(exporter), + )) + } +} diff --git a/apis/v1beta1/metrics_test.go b/apis/v1beta1/metrics_test.go new file mode 100644 index 0000000000..71df095c53 --- /dev/null +++ b/apis/v1beta1/metrics_test.go @@ -0,0 +1,842 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1beta1 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var wantInstrumentationScope = instrumentation.Scope{ + Name: "crd-metrics", +} + +func TestOTELCollectorCRDMetrics(t *testing.T) { + + otelcollector1 := &OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "collector1", + Namespace: "test1", + }, + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeDeployment, + Config: Config{ + Processors: &AnyConfig{ + Object: map[string]interface{}{ + "batch": nil, + "foo": nil, + }, + }, + Extensions: &AnyConfig{ + Object: map[string]interface{}{ + "extfoo": nil, + }, + }, + }, + }, + } + + otelcollector2 := &OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "collector2", + Namespace: "test2", + }, + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeSidecar, + Config: Config{ + Processors: &AnyConfig{ + Object: map[string]interface{}{ + "x": nil, + "y": nil, + }, + }, + Extensions: &AnyConfig{ + Object: map[string]interface{}{ + "z/r": nil, + }, + }, + Exporters: AnyConfig{ + Object: map[string]interface{}{ + "w": nil, + }, + }, + }, + }, + } + + updatedCollector1 := &OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "collector1", + Namespace: "test1", + }, + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeSidecar, + Config: Config{ + Processors: &AnyConfig{ + Object: map[string]interface{}{ + "foo": nil, + "y": nil, + }, + }, + Extensions: &AnyConfig{ + Object: map[string]interface{}{ + "z/r": nil, + }, + }, + Exporters: AnyConfig{ + Object: map[string]interface{}{ + "w": nil, + }, + }, + }, + }, + } + + var tests = []struct { + name string + testFunction func(t *testing.T, m *Metrics, collectors []*OpenTelemetryCollector, reader metric.Reader) + }{ + { + name: "create", + testFunction: checkCreate, + }, + { + name: "update", + testFunction: checkUpdate, + }, + { + name: "delete", + testFunction: checkDelete, + }, + } + schemeBuilder := runtime.NewSchemeBuilder(func(s *runtime.Scheme) error { + s.AddKnownTypes(GroupVersion, &OpenTelemetryCollector{}, &OpenTelemetryCollectorList{}) + metav1.AddToGroupVersion(s, GroupVersion) + return nil + }) + scheme := runtime.NewScheme() + err := schemeBuilder.AddToScheme(scheme) + require.NoError(t, err) + reader := sdkmetric.NewManualReader() + provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + crdMetrics, err := NewMetrics(provider, context.Background(), cl) + assert.NoError(t, err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.testFunction(t, crdMetrics, []*OpenTelemetryCollector{otelcollector1, otelcollector2, updatedCollector1}, reader) + }) + } +} + +func TestOTELCollectorInitMetrics(t *testing.T) { + otelcollector1 := OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "collector1", + Namespace: "test1", + Labels: map[string]string{"app.kubernetes.io/managed-by": "opentelemetry-operator"}, + }, + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeDeployment, + Config: Config{ + Processors: &AnyConfig{ + Object: map[string]interface{}{ + "batch": nil, + "foo": nil, + }, + }, + Extensions: &AnyConfig{ + Object: map[string]interface{}{ + "extfoo": nil, + }, + }, + }, + }, + } + + otelcollector2 := OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "collector2", + Namespace: "test2", + Labels: map[string]string{"app.kubernetes.io/managed-by": "opentelemetry-operator"}, + }, + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeSidecar, + Config: Config{ + Processors: &AnyConfig{ + Object: map[string]interface{}{ + "x": nil, + "y": nil, + }, + }, + Extensions: &AnyConfig{ + Object: map[string]interface{}{ + "z/r": nil, + }, + }, + Exporters: AnyConfig{ + Object: map[string]interface{}{ + "w": nil, + }, + }, + }, + }, + } + + schemeBuilder := runtime.NewSchemeBuilder(func(s *runtime.Scheme) error { + s.AddKnownTypes(GroupVersion, &OpenTelemetryCollector{}, &OpenTelemetryCollectorList{}) + metav1.AddToGroupVersion(s, GroupVersion) + return nil + }) + scheme := runtime.NewScheme() + err := schemeBuilder.AddToScheme(scheme) + require.NoError(t, err) + list := &OpenTelemetryCollectorList{ + Items: []OpenTelemetryCollector{otelcollector1, otelcollector2}, + } + require.NoError(t, err, "Should be able to add custom types") + cl := fake.NewClientBuilder().WithLists(list).WithScheme(scheme).Build() + reader := sdkmetric.NewManualReader() + provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + _, err = NewMetrics(provider, context.Background(), cl) + assert.NoError(t, err) + + rm := metricdata.ResourceMetrics{} + err = reader.Collect(context.Background(), &rm) + assert.NoError(t, err) + require.Len(t, rm.ScopeMetrics, 1) + + want := metricdata.ScopeMetrics{ + Scope: wantInstrumentationScope, + Metrics: []metricdata.Metrics{ + { + Name: "opentelemetry_collector_info", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("deployment"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String(string(ModeSidecar)), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_processors", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("batch"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("foo"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("x"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("y"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_extensions", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("extfoo"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("z"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_exporters", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("w"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + }, + } + + metricdatatest.AssertEqual(t, want, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp()) +} + +func checkCreate(t *testing.T, m *Metrics, collectors []*OpenTelemetryCollector, reader metric.Reader) { + provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + otel.SetMeterProvider(provider) + + m.create(context.Background(), collectors[0]) + rm := metricdata.ResourceMetrics{} + err := reader.Collect(context.Background(), &rm) + assert.NoError(t, err) + + want := metricdata.ScopeMetrics{ + Scope: wantInstrumentationScope, + Metrics: []metricdata.Metrics{ + { + Name: "opentelemetry_collector_info", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("deployment"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_processors", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("batch"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("foo"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_extensions", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("extfoo"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + }, + } + require.Len(t, rm.ScopeMetrics, 1) + metricdatatest.AssertEqual(t, want, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp()) + + m.create(context.Background(), collectors[1]) + + rm = metricdata.ResourceMetrics{} + err = reader.Collect(context.Background(), &rm) + assert.NoError(t, err) + require.Len(t, rm.ScopeMetrics, 1) + + want = metricdata.ScopeMetrics{ + Scope: wantInstrumentationScope, + Metrics: []metricdata.Metrics{ + { + Name: "opentelemetry_collector_info", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("deployment"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String(string(ModeSidecar)), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_processors", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("batch"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("foo"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("x"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("y"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_extensions", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("extfoo"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("z"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_exporters", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("w"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + }, + } + + metricdatatest.AssertEqual(t, want, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp()) +} + +func checkUpdate(t *testing.T, m *Metrics, collectors []*OpenTelemetryCollector, reader metric.Reader) { + + m.update(context.Background(), collectors[0], collectors[2]) + + rm := metricdata.ResourceMetrics{} + err := reader.Collect(context.Background(), &rm) + assert.NoError(t, err) + require.Len(t, rm.ScopeMetrics, 1) + + want := metricdata.ScopeMetrics{ + Scope: wantInstrumentationScope, + Metrics: []metricdata.Metrics{ + { + Name: "opentelemetry_collector_info", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String(string(ModeDeployment)), + ), + Value: 0, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String(string(ModeSidecar)), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String(string(ModeSidecar)), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_processors", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("batch"), + ), + Value: 0, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("foo"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("y"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("x"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("y"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_extensions", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("extfoo"), + ), + Value: 0, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("z"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("z"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_exporters", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("w"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("w"), + ), + Value: 1, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + }, + } + metricdatatest.AssertEqual(t, want, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp()) +} + +func checkDelete(t *testing.T, m *Metrics, collectors []*OpenTelemetryCollector, reader metric.Reader) { + m.delete(context.Background(), collectors[1]) + rm := metricdata.ResourceMetrics{} + err := reader.Collect(context.Background(), &rm) + assert.NoError(t, err) + require.Len(t, rm.ScopeMetrics, 1) + want := metricdata.ScopeMetrics{ + Scope: wantInstrumentationScope, + Metrics: []metricdata.Metrics{ + { + Name: "opentelemetry_collector_info", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String(string(ModeDeployment)), + ), + Value: 0, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String(string(ModeSidecar)), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String(string(ModeSidecar)), + ), + Value: 0, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_processors", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("batch"), + ), + Value: 0, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("foo"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("y"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("x"), + ), + Value: 0, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("y"), + ), + Value: 0, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_extensions", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("extfoo"), + ), + Value: 0, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("z"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("z"), + ), + Value: 0, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: "opentelemetry_collector_exporters", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector1"), + attribute.Key("namespace").String("test1"), + attribute.Key("type").String("w"), + ), + Value: 1, + }, + { + Attributes: attribute.NewSet( + attribute.Key("collector_name").String("collector2"), + attribute.Key("namespace").String("test2"), + attribute.Key("type").String("w"), + ), + Value: 0, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + }, + } + metricdatatest.AssertEqual(t, want, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp()) +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index b17379dd8b..0b8ee89adf 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -177,7 +177,7 @@ func TestMain(m *testing.M) { } reviewer := rbac.NewReviewer(clientset) - if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer); err != nil { + if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer, nil); err != nil { fmt.Printf("failed to SetupWebhookWithManager: %v", err) os.Exit(1) } diff --git a/go.mod b/go.mod index c004b55bc5..2be03fe53f 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( go.opentelemetry.io/collector/featuregate v1.5.0 go.opentelemetry.io/otel v1.27.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 + go.opentelemetry.io/otel/exporters/prometheus v0.48.0 go.opentelemetry.io/otel/metric v1.27.0 go.opentelemetry.io/otel/sdk v1.27.0 go.opentelemetry.io/otel/sdk/metric v1.27.0 diff --git a/go.sum b/go.sum index 6d5a46661c..4e292ac41c 100644 --- a/go.sum +++ b/go.sum @@ -649,6 +649,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQP go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= +go.opentelemetry.io/otel/exporters/prometheus v0.48.0 h1:sBQe3VNGUjY9IKWQC6z2lNqa5iGbDSxhs60ABwK4y0s= +go.opentelemetry.io/otel/exporters/prometheus v0.48.0/go.mod h1:DtrbMzoZWwQHyrQmCfLam5DZbnmorsGbOtTbYHycU5o= go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= diff --git a/internal/webhook/podmutation/webhookhandler_suite_test.go b/internal/webhook/podmutation/webhookhandler_suite_test.go index 464649f489..1336cab0e8 100644 --- a/internal/webhook/podmutation/webhookhandler_suite_test.go +++ b/internal/webhook/podmutation/webhookhandler_suite_test.go @@ -105,7 +105,7 @@ func TestMain(m *testing.M) { } reviewer := rbac.NewReviewer(clientset) - if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer); err != nil { + if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer, nil); err != nil { fmt.Printf("failed to SetupWebhookWithManager: %v", err) os.Exit(1) } diff --git a/main.go b/main.go index 4ee518ce35..798bbeada8 100644 --- a/main.go +++ b/main.go @@ -119,6 +119,7 @@ func main() { enableNginxInstrumentation bool enableNodeJSInstrumentation bool enableJavaInstrumentation bool + enableCRMetrics bool collectorImage string targetAllocatorImage string operatorOpAMPBridgeImage string @@ -154,6 +155,8 @@ func main() { pflag.BoolVar(&enableNginxInstrumentation, constants.FlagNginx, false, "Controls whether the operator supports nginx auto-instrumentation") pflag.BoolVar(&enableNodeJSInstrumentation, constants.FlagNodeJS, true, "Controls whether the operator supports nodejs auto-instrumentation") pflag.BoolVar(&enableJavaInstrumentation, constants.FlagJava, true, "Controls whether the operator supports java auto-instrumentation") + pflag.BoolVar(&enableCRMetrics, constants.FlagCRMetrics, false, "Controls whether exposing the CR metrics is enabled") + stringFlagOrEnv(&collectorImage, "collector-image", "RELATED_IMAGE_COLLECTOR", fmt.Sprintf("ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector:%s", v.OpenTelemetryCollector), "The default OpenTelemetry collector image. This image is used when no image is specified in the CustomResource.") stringFlagOrEnv(&targetAllocatorImage, "target-allocator-image", "RELATED_IMAGE_TARGET_ALLOCATOR", fmt.Sprintf("ghcr.io/open-telemetry/opentelemetry-operator/target-allocator:%s", v.TargetAllocator), "The default OpenTelemetry target allocator image. This image is used when no image is specified in the CustomResource.") stringFlagOrEnv(&operatorOpAMPBridgeImage, "operator-opamp-bridge-image", "RELATED_IMAGE_OPERATOR_OPAMP_BRIDGE", fmt.Sprintf("ghcr.io/open-telemetry/opentelemetry-operator/operator-opamp-bridge:%s", v.OperatorOpAMPBridge), "The default OpenTelemetry Operator OpAMP Bridge image. This image is used when no image is specified in the CustomResource.") @@ -334,6 +337,7 @@ func main() { } } } + if cfg.LabelsFilter() != nil { for _, basePattern := range cfg.LabelsFilter() { _, compileErr := regexp.Compile(basePattern) @@ -372,7 +376,22 @@ func main() { } if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = otelv1beta1.SetupCollectorWebhook(mgr, cfg, reviewer); err != nil { + var crdMetrics *otelv1beta1.Metrics + + if enableCRMetrics { + meterProvider, metricsErr := otelv1beta1.BootstrapMetrics() + if metricsErr != nil { + setupLog.Error(metricsErr, "Error bootstrapping CRD metrics") + } + + crdMetrics, err = otelv1beta1.NewMetrics(meterProvider, ctx, mgr.GetAPIReader()) + if err != nil { + setupLog.Error(err, "Error init CRD metrics") + } + + } + + if err = otelv1beta1.SetupCollectorWebhook(mgr, cfg, reviewer, crdMetrics); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "OpenTelemetryCollector") os.Exit(1) } diff --git a/pkg/collector/upgrade/suite_test.go b/pkg/collector/upgrade/suite_test.go index e6e505b760..fdafdca245 100644 --- a/pkg/collector/upgrade/suite_test.go +++ b/pkg/collector/upgrade/suite_test.go @@ -105,7 +105,7 @@ func TestMain(m *testing.M) { } reviewer := rbac.NewReviewer(clientset) - if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer); err != nil { + if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer, nil); err != nil { fmt.Printf("failed to SetupWebhookWithManager: %v", err) os.Exit(1) } diff --git a/pkg/constants/env.go b/pkg/constants/env.go index 8ebd1bb5d9..ed6d5e0f2e 100644 --- a/pkg/constants/env.go +++ b/pkg/constants/env.go @@ -37,6 +37,7 @@ const ( EnvNodeName = "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME" EnvNodeIP = "OTEL_NODE_IP" + FlagCRMetrics = "enable-cr-metrics" FlagApacheHttpd = "enable-apache-httpd-instrumentation" FlagDotNet = "enable-dotnet-instrumentation" FlagGo = "enable-go-instrumentation"