diff --git a/exporter/prometheusexporter/README.md b/exporter/prometheusexporter/README.md index f8d755af7eb..c556d733ce7 100644 --- a/exporter/prometheusexporter/README.md +++ b/exporter/prometheusexporter/README.md @@ -17,6 +17,8 @@ The following settings can be optionally configured: - `send_timestamps` (default = `false`): if true, sends the timestamp of the underlying metric sample in the response. - `metric_expiration` (default = `5m`): defines how long metrics are exposed without updates +- `resource_to_telemetry_conversion` + - `enabled` (default = false): If `enabled` is `true`, all the resource attributes will be converted to metric labels by default. Example: @@ -30,4 +32,6 @@ exporters: "another label": spaced value send_timestamps: true metric_expiration: 180m + resource_to_telemetry_conversion: + enabled: true ``` diff --git a/exporter/prometheusexporter/config.go b/exporter/prometheusexporter/config.go index ac0415bdeb8..a361fba1948 100644 --- a/exporter/prometheusexporter/config.go +++ b/exporter/prometheusexporter/config.go @@ -20,6 +20,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/collector/config" + "go.opentelemetry.io/collector/exporter/exporterhelper" ) // Config defines configuration for Prometheus exporter. @@ -40,6 +41,9 @@ type Config struct { // MetricExpiration defines how long metrics are kept without updates MetricExpiration time.Duration `mapstructure:"metric_expiration"` + + // ResourceToTelemetrySettings defines configuration for converting resource attributes to metric labels. + exporterhelper.ResourceToTelemetrySettings `mapstructure:"resource_to_telemetry_conversion"` } var _ config.Exporter = (*Config)(nil) diff --git a/exporter/prometheusexporter/factory.go b/exporter/prometheusexporter/factory.go index faafb2a9087..9e1f70769ea 100644 --- a/exporter/prometheusexporter/factory.go +++ b/exporter/prometheusexporter/factory.go @@ -52,5 +52,30 @@ func createMetricsExporter( ) (component.MetricsExporter, error) { pcfg := cfg.(*Config) - return newPrometheusExporter(pcfg, params.Logger) + prometheus, err := newPrometheusExporter(pcfg, params.Logger) + if err != nil { + return nil, err + } + + exporter, err := exporterhelper.NewMetricsExporter( + cfg, + params.Logger, + prometheus.ConsumeMetrics, + exporterhelper.WithStart(prometheus.Start), + exporterhelper.WithShutdown(prometheus.Shutdown), + exporterhelper.WithResourceToTelemetryConversion(pcfg.ResourceToTelemetrySettings), + ) + if err != nil { + return nil, err + } + + return &wrapMetricsExpoter{ + MetricsExporter: exporter, + exporter: prometheus, + }, nil +} + +type wrapMetricsExpoter struct { + component.MetricsExporter + exporter *prometheusExporter } diff --git a/exporter/prometheusexporter/factory_test.go b/exporter/prometheusexporter/factory_test.go index 22b2fb988fb..35d3e193cb1 100644 --- a/exporter/prometheusexporter/factory_test.go +++ b/exporter/prometheusexporter/factory_test.go @@ -43,3 +43,19 @@ func TestCreateMetricsExporter(t *testing.T) { require.Equal(t, errBlankPrometheusAddress, err) require.Nil(t, exp) } + +func TestCreateMetricsExporterExportHelperError(t *testing.T) { + cfg, ok := createDefaultConfig().(*Config) + require.True(t, ok) + + cfg.Endpoint = "http://localhost:8889" + + // Should give us an exporterhelper.errNilLogger + exp, err := createMetricsExporter( + context.Background(), + component.ExporterCreateParams{Logger: nil}, + cfg) + + assert.Nil(t, exp) + assert.Error(t, err) +} diff --git a/exporter/prometheusexporter/prometheus_test.go b/exporter/prometheusexporter/prometheus_test.go index 4644effd0fc..b3bec5cd6ec 100644 --- a/exporter/prometheusexporter/prometheus_test.go +++ b/exporter/prometheusexporter/prometheus_test.go @@ -32,6 +32,8 @@ import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componenttest" "go.opentelemetry.io/collector/config" + "go.opentelemetry.io/collector/exporter/exporterhelper" + "go.opentelemetry.io/collector/internal/testdata" "go.opentelemetry.io/collector/translator/internaldata" ) @@ -162,7 +164,7 @@ func TestPrometheusExporter_endToEnd(t *testing.T) { } // Expired metrics should be removed during first scrape - exp.(*prometheusExporter).collector.accumulator.(*lastValueAccumulator).metricExpiration = 1 * time.Millisecond + exp.(*wrapMetricsExpoter).exporter.collector.accumulator.(*lastValueAccumulator).metricExpiration = 1 * time.Millisecond time.Sleep(10 * time.Millisecond) res, err := http.Get("http://localhost:7777/metrics") @@ -240,7 +242,7 @@ func TestPrometheusExporter_endToEndWithTimestamps(t *testing.T) { } // Expired metrics should be removed during first scrape - exp.(*prometheusExporter).collector.accumulator.(*lastValueAccumulator).metricExpiration = 1 * time.Millisecond + exp.(*wrapMetricsExpoter).exporter.collector.accumulator.(*lastValueAccumulator).metricExpiration = 1 * time.Millisecond time.Sleep(10 * time.Millisecond) res, err := http.Get("http://localhost:7777/metrics") @@ -254,6 +256,65 @@ func TestPrometheusExporter_endToEndWithTimestamps(t *testing.T) { require.Emptyf(t, string(blob), "Metrics did not expire") } +func TestPrometheusExporter_endToEndWithResource(t *testing.T) { + cfg := &Config{ + ExporterSettings: config.NewExporterSettings(typeStr), + Namespace: "test", + ConstLabels: map[string]string{ + "foo2": "bar2", + "code2": "one2", + }, + Endpoint: ":7777", + SendTimestamps: true, + MetricExpiration: 120 * time.Minute, + ResourceToTelemetrySettings: exporterhelper.ResourceToTelemetrySettings{ + Enabled: true, + }, + } + + factory := NewFactory() + creationParams := component.ExporterCreateParams{Logger: zap.NewNop()} + exp, err := factory.CreateMetricsExporter(context.Background(), creationParams, cfg) + assert.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, exp.Shutdown(context.Background())) + // trigger a get so that the server cleans up our keepalive socket + http.Get("http://localhost:7777/metrics") + }) + + assert.NotNil(t, exp) + require.NoError(t, exp.Start(context.Background(), componenttest.NewNopHost())) + + md := testdata.GenerateMetricsOneMetric() + assert.NotNil(t, md) + + assert.NoError(t, exp.ConsumeMetrics(context.Background(), md)) + + rsp, err := http.Get("http://localhost:7777/metrics") + require.NoError(t, err, "Failed to perform a scrape") + + if g, w := rsp.StatusCode, 200; g != w { + t.Errorf("Mismatched HTTP response status code: Got: %d Want: %d", g, w) + } + + blob, _ := ioutil.ReadAll(rsp.Body) + _ = rsp.Body.Close() + + want := []string{ + `# HELP test_counter_int`, + `# TYPE test_counter_int counter`, + `test_counter_int{code2="one2",foo2="bar2",label_1="label-value-1",resource_attr="resource-attr-val-1"} 123 1581452773000`, + `test_counter_int{code2="one2",foo2="bar2",label_2="label-value-2",resource_attr="resource-attr-val-1"} 456 1581452773000`, + } + + for _, w := range want { + if !strings.Contains(string(blob), w) { + t.Errorf("Missing %v from response:\n%v", w, string(blob)) + } + } +} + func metricBuilder(delta int64, prefix string) []*metricspb.Metric { return []*metricspb.Metric{ {