Skip to content

Commit 1d1ff4a

Browse files
[exporter/debug] format metric data points as one-liners in normal verbosity (#10462)
#### Description This pull request is part of #7806; it implements the change for metrics. The changes for [logs](#10225) and [traces](#10280) have been proposed in separate pull requests. This change applies to the Debug exporter only. The behavior of the Logging exporter remains unchanged. To use this behavior, switch from the deprecated Logging exporter to Debug exporter. #### Link to tracking issue - #7806 #### Testing Added unit tests for the formatter. #### Documentation Described the formatting in the Debug exporter's README.
1 parent 3364ba1 commit 1d1ff4a

File tree

6 files changed

+280
-36
lines changed

6 files changed

+280
-36
lines changed

.chloggen/debug-exporter-normal-verbosity-traces.yaml

Lines changed: 0 additions & 25 deletions
This file was deleted.

.chloggen/debug-exporter-normal-verbosity-logs.yaml renamed to .chloggen/debug-exporter-normal-verbosity.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ change_type: enhancement
77
component: exporter/debug
88

99
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10-
note: In `normal` verbosity, display one line of text for each log record
10+
note: In `normal` verbosity, display one line of text for each telemetry record (log, data point, span)
1111

1212
# One or more tracking issues or pull requests related to the change
1313
issues: [7806]

exporter/debugexporter/README.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,12 @@ With `verbosity: normal`, the exporter outputs about one line for each telemetry
6767
The "one line per telemetry record" is not a strict rule.
6868
For example, logs with multiline body will be output as multiple lines.
6969

70-
> [!IMPORTANT]
71-
> Currently the `normal` verbosity is only implemented for logs and traces.
72-
> Metrics are going to be implemented in the future.
73-
> The current behavior for metrics is the same as in `basic` verbosity.
74-
7570
Here's an example output:
7671

7772
```console
78-
2024-05-31T13:26:37.531+0200 info TracesExporter {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 2}
79-
2024-05-31T13:26:37.531+0200 info okey-dokey-0 082bc2f70f519e32a39fd26ae69b43c0 51201084f4d65159
80-
lets-go 082bc2f70f519e32a39fd26ae69b43c0 cd321682f3514378
73+
2024-06-24T15:18:58.559+0200 info TracesExporter {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 2}
74+
2024-06-24T15:18:58.559+0200 info okey-dokey-0 4bdc558f0f0650e3ccaac8f3ae133954 8b69459f015c164b net.peer.ip=1.2.3.4 peer.service=telemetrygen-client
75+
lets-go 4bdc558f0f0650e3ccaac8f3ae133954 8820ee5366817639 net.peer.ip=1.2.3.4 peer.service=telemetrygen-server
8176
{"kind": "exporter", "data_type": "traces", "name": "debug"}
8277
```
8378

@@ -128,3 +123,5 @@ Attributes:
128123
## Warnings
129124

130125
- Unstable Output Format: The output formats for all verbosity levels is not guaranteed and may be changed at any time without a breaking change.
126+
127+
[telemetrygen]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/cmd/telemetrygen

exporter/debugexporter/exporter.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,22 @@ type debugExporter struct {
3030

3131
func newDebugExporter(logger *zap.Logger, verbosity configtelemetry.Level) *debugExporter {
3232
var logsMarshaler plog.Marshaler
33+
var metricsMarshaler pmetric.Marshaler
3334
var tracesMarshaler ptrace.Marshaler
3435
if verbosity == configtelemetry.LevelDetailed {
3536
logsMarshaler = otlptext.NewTextLogsMarshaler()
37+
metricsMarshaler = otlptext.NewTextMetricsMarshaler()
3638
tracesMarshaler = otlptext.NewTextTracesMarshaler()
3739
} else {
3840
logsMarshaler = normal.NewNormalLogsMarshaler()
41+
metricsMarshaler = normal.NewNormalMetricsMarshaler()
3942
tracesMarshaler = normal.NewNormalTracesMarshaler()
4043
}
4144
return &debugExporter{
4245
verbosity: verbosity,
4346
logger: logger,
4447
logsMarshaler: logsMarshaler,
45-
metricsMarshaler: otlptext.NewTextMetricsMarshaler(),
48+
metricsMarshaler: metricsMarshaler,
4649
tracesMarshaler: tracesMarshaler,
4750
}
4851
}
@@ -68,7 +71,7 @@ func (s *debugExporter) pushMetrics(_ context.Context, md pmetric.Metrics) error
6871
zap.Int("resource metrics", md.ResourceMetrics().Len()),
6972
zap.Int("metrics", md.MetricCount()),
7073
zap.Int("data points", md.DataPointCount()))
71-
if s.verbosity != configtelemetry.LevelDetailed {
74+
if s.verbosity == configtelemetry.LevelBasic {
7275
return nil
7376
}
7477

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package normal // import "go.opentelemetry.io/collector/exporter/debugexporter/internal/normal"
5+
6+
import (
7+
"bytes"
8+
"fmt"
9+
"strings"
10+
11+
"go.opentelemetry.io/collector/pdata/pmetric"
12+
)
13+
14+
type normalMetricsMarshaler struct{}
15+
16+
// Ensure normalMetricsMarshaller implements interface pmetric.Marshaler
17+
var _ pmetric.Marshaler = normalMetricsMarshaler{}
18+
19+
// NewNormalMetricsMarshaler returns a pmetric.Marshaler for normal verbosity. It writes one line of text per log record
20+
func NewNormalMetricsMarshaler() pmetric.Marshaler {
21+
return normalMetricsMarshaler{}
22+
}
23+
24+
func (normalMetricsMarshaler) MarshalMetrics(md pmetric.Metrics) ([]byte, error) {
25+
var buffer bytes.Buffer
26+
for i := 0; i < md.ResourceMetrics().Len(); i++ {
27+
resourceMetrics := md.ResourceMetrics().At(i)
28+
for j := 0; j < resourceMetrics.ScopeMetrics().Len(); j++ {
29+
scopeMetrics := resourceMetrics.ScopeMetrics().At(j)
30+
for k := 0; k < scopeMetrics.Metrics().Len(); k++ {
31+
metric := scopeMetrics.Metrics().At(k)
32+
33+
var dataPointLines []string
34+
switch metric.Type() {
35+
case pmetric.MetricTypeGauge:
36+
dataPointLines = writeNumberDataPoints(metric, metric.Gauge().DataPoints())
37+
case pmetric.MetricTypeSum:
38+
dataPointLines = writeNumberDataPoints(metric, metric.Sum().DataPoints())
39+
case pmetric.MetricTypeHistogram:
40+
dataPointLines = writeHistogramDataPoints(metric)
41+
case pmetric.MetricTypeExponentialHistogram:
42+
dataPointLines = writeExponentialHistogramDataPoints(metric)
43+
case pmetric.MetricTypeSummary:
44+
dataPointLines = writeSummaryDataPoints(metric)
45+
}
46+
for _, line := range dataPointLines {
47+
buffer.WriteString(line)
48+
}
49+
}
50+
}
51+
}
52+
return buffer.Bytes(), nil
53+
}
54+
55+
func writeNumberDataPoints(metric pmetric.Metric, dataPoints pmetric.NumberDataPointSlice) (lines []string) {
56+
for i := 0; i < dataPoints.Len(); i++ {
57+
dataPoint := dataPoints.At(i)
58+
dataPointAttributes := writeAttributes(dataPoint.Attributes())
59+
60+
var value string
61+
switch dataPoint.ValueType() {
62+
case pmetric.NumberDataPointValueTypeInt:
63+
value = fmt.Sprintf("%v", dataPoint.IntValue())
64+
case pmetric.NumberDataPointValueTypeDouble:
65+
value = fmt.Sprintf("%v", dataPoint.DoubleValue())
66+
}
67+
68+
dataPointLine := fmt.Sprintf("%s{%s} %s\n", metric.Name(), strings.Join(dataPointAttributes, ","), value)
69+
lines = append(lines, dataPointLine)
70+
}
71+
return lines
72+
}
73+
74+
func writeHistogramDataPoints(metric pmetric.Metric) (lines []string) {
75+
for i := 0; i < metric.Histogram().DataPoints().Len(); i++ {
76+
dataPoint := metric.Histogram().DataPoints().At(i)
77+
dataPointAttributes := writeAttributes(dataPoint.Attributes())
78+
79+
var value string
80+
value = fmt.Sprintf("count=%d", dataPoint.Count())
81+
if dataPoint.HasSum() {
82+
value += fmt.Sprintf(" sum=%v", dataPoint.Sum())
83+
}
84+
if dataPoint.HasMin() {
85+
value += fmt.Sprintf(" min=%v", dataPoint.Min())
86+
}
87+
if dataPoint.HasMax() {
88+
value += fmt.Sprintf(" max=%v", dataPoint.Max())
89+
}
90+
91+
for bucketIndex := 0; bucketIndex < dataPoint.BucketCounts().Len(); bucketIndex++ {
92+
bucketBound := ""
93+
if bucketIndex < dataPoint.ExplicitBounds().Len() {
94+
bucketBound = fmt.Sprintf("le%v=", dataPoint.ExplicitBounds().At(bucketIndex))
95+
}
96+
bucketCount := dataPoint.BucketCounts().At(bucketIndex)
97+
value += fmt.Sprintf(" %s%d", bucketBound, bucketCount)
98+
}
99+
100+
dataPointLine := fmt.Sprintf("%s{%s} %s\n", metric.Name(), strings.Join(dataPointAttributes, ","), value)
101+
lines = append(lines, dataPointLine)
102+
}
103+
return lines
104+
}
105+
106+
func writeExponentialHistogramDataPoints(metric pmetric.Metric) (lines []string) {
107+
for i := 0; i < metric.ExponentialHistogram().DataPoints().Len(); i++ {
108+
dataPoint := metric.ExponentialHistogram().DataPoints().At(i)
109+
dataPointAttributes := writeAttributes(dataPoint.Attributes())
110+
111+
var value string
112+
value = fmt.Sprintf("count=%d", dataPoint.Count())
113+
if dataPoint.HasSum() {
114+
value += fmt.Sprintf(" sum=%v", dataPoint.Sum())
115+
}
116+
if dataPoint.HasMin() {
117+
value += fmt.Sprintf(" min=%v", dataPoint.Min())
118+
}
119+
if dataPoint.HasMax() {
120+
value += fmt.Sprintf(" max=%v", dataPoint.Max())
121+
}
122+
123+
// TODO display buckets
124+
125+
dataPointLine := fmt.Sprintf("%s{%s} %s\n", metric.Name(), strings.Join(dataPointAttributes, ","), value)
126+
lines = append(lines, dataPointLine)
127+
}
128+
return lines
129+
}
130+
131+
func writeSummaryDataPoints(metric pmetric.Metric) (lines []string) {
132+
for i := 0; i < metric.Summary().DataPoints().Len(); i++ {
133+
dataPoint := metric.Summary().DataPoints().At(i)
134+
dataPointAttributes := writeAttributes(dataPoint.Attributes())
135+
136+
var value string
137+
value = fmt.Sprintf("count=%d", dataPoint.Count())
138+
value += fmt.Sprintf(" sum=%f", dataPoint.Sum())
139+
140+
for quantileIndex := 0; quantileIndex < dataPoint.QuantileValues().Len(); quantileIndex++ {
141+
quantile := dataPoint.QuantileValues().At(quantileIndex)
142+
value += fmt.Sprintf(" q%v=%v", quantile.Quantile(), quantile.Value())
143+
}
144+
145+
dataPointLine := fmt.Sprintf("%s{%s} %s\n", metric.Name(), strings.Join(dataPointAttributes, ","), value)
146+
lines = append(lines, dataPointLine)
147+
}
148+
return lines
149+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package normal
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
11+
"go.opentelemetry.io/collector/pdata/pmetric"
12+
)
13+
14+
func TestMarshalMetrics(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
input pmetric.Metrics
18+
expected string
19+
}{
20+
{
21+
name: "empty metrics",
22+
input: pmetric.NewMetrics(),
23+
expected: "",
24+
},
25+
{
26+
name: "sum data point",
27+
input: func() pmetric.Metrics {
28+
metrics := pmetric.NewMetrics()
29+
metric := metrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
30+
metric.SetName("system.cpu.time")
31+
dataPoint := metric.SetEmptySum().DataPoints().AppendEmpty()
32+
dataPoint.SetDoubleValue(123.456)
33+
dataPoint.Attributes().PutStr("state", "user")
34+
dataPoint.Attributes().PutStr("cpu", "0")
35+
return metrics
36+
}(),
37+
expected: `system.cpu.time{state=user,cpu=0} 123.456
38+
`,
39+
},
40+
{
41+
name: "gauge data point",
42+
input: func() pmetric.Metrics {
43+
metrics := pmetric.NewMetrics()
44+
metric := metrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
45+
metric.SetName("system.cpu.utilization")
46+
dataPoint := metric.SetEmptyGauge().DataPoints().AppendEmpty()
47+
dataPoint.SetDoubleValue(78.901234567)
48+
dataPoint.Attributes().PutStr("state", "free")
49+
dataPoint.Attributes().PutStr("cpu", "8")
50+
return metrics
51+
}(),
52+
expected: `system.cpu.utilization{state=free,cpu=8} 78.901234567
53+
`,
54+
},
55+
{
56+
name: "histogram",
57+
input: func() pmetric.Metrics {
58+
metrics := pmetric.NewMetrics()
59+
metric := metrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
60+
metric.SetName("http.server.request.duration")
61+
dataPoint := metric.SetEmptyHistogram().DataPoints().AppendEmpty()
62+
dataPoint.Attributes().PutInt("http.response.status_code", 200)
63+
dataPoint.Attributes().PutStr("http.request.method", "GET")
64+
dataPoint.ExplicitBounds().FromRaw([]float64{0.125, 0.5, 1, 3})
65+
dataPoint.BucketCounts().FromRaw([]uint64{1324, 13, 0, 2, 1})
66+
dataPoint.SetCount(1340)
67+
dataPoint.SetSum(99.573)
68+
dataPoint.SetMin(0.017)
69+
dataPoint.SetMax(8.13)
70+
return metrics
71+
}(),
72+
expected: `http.server.request.duration{http.response.status_code=200,http.request.method=GET} count=1340 sum=99.573 min=0.017 max=8.13 le0.125=1324 le0.5=13 le1=0 le3=2 1
73+
`,
74+
},
75+
{
76+
name: "exponential histogram",
77+
input: func() pmetric.Metrics {
78+
metrics := pmetric.NewMetrics()
79+
metric := metrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
80+
metric.SetName("http.server.request.duration")
81+
dataPoint := metric.SetEmptyExponentialHistogram().DataPoints().AppendEmpty()
82+
dataPoint.Attributes().PutInt("http.response.status_code", 200)
83+
dataPoint.Attributes().PutStr("http.request.method", "GET")
84+
dataPoint.SetCount(1340)
85+
dataPoint.SetSum(99.573)
86+
dataPoint.SetMin(0.017)
87+
dataPoint.SetMax(8.13)
88+
return metrics
89+
}(),
90+
expected: `http.server.request.duration{http.response.status_code=200,http.request.method=GET} count=1340 sum=99.573 min=0.017 max=8.13
91+
`,
92+
},
93+
{
94+
name: "summary",
95+
input: func() pmetric.Metrics {
96+
metrics := pmetric.NewMetrics()
97+
metric := metrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
98+
metric.SetName("summary")
99+
dataPoint := metric.SetEmptySummary().DataPoints().AppendEmpty()
100+
dataPoint.Attributes().PutInt("http.response.status_code", 200)
101+
dataPoint.Attributes().PutStr("http.request.method", "GET")
102+
dataPoint.SetCount(1340)
103+
dataPoint.SetSum(99.573)
104+
quantile := dataPoint.QuantileValues().AppendEmpty()
105+
quantile.SetQuantile(0.01)
106+
quantile.SetValue(15)
107+
return metrics
108+
}(),
109+
expected: `summary{http.response.status_code=200,http.request.method=GET} count=1340 sum=99.573000 q0.01=15
110+
`,
111+
},
112+
}
113+
for _, tt := range tests {
114+
t.Run(tt.name, func(t *testing.T) {
115+
output, err := NewNormalMetricsMarshaler().MarshalMetrics(tt.input)
116+
assert.NoError(t, err)
117+
assert.Equal(t, tt.expected, string(output))
118+
})
119+
}
120+
}

0 commit comments

Comments
 (0)