[exporter/debug] display resource and scope in `normal` verbosity (#13011)

#### Description

Adds displaying of resource and scope details in `normal` verbosity.

Each resource is represented with one line showing the resource's
details:
- schema URL
- resource attributes

which looks like `ResourceTraces #0 [resource-schema-url]
resource_attr1=value1 attr2=value2`.

Each instrumentation scope is represented by one line showing the
scope's details:
- scope name
- scope version
- scope schema URL
- scope attributes

which looks like `ScopeTraces #0 scope-name@scope-version
[scope-schema-url] scope_attr1=value1 attr2=value2`.

Before:

```console
2025-05-09T19:57:16.332+0200    info    Traces  {"resource": {}, "otelcol.component.id": "debug/normal", "otelcol.component.kind": "exporter", "otelcol.signal": "traces", "resource spans": 1, "spans": 2}
2025-05-09T19:57:16.332+0200    info    okey-dokey-0 ab1030bd4ee554af936542b01d7b4807 1d8c93663d043aa8 net.sock.peer.addr=1.2.3.4 peer.service=telemetrygen-client
lets-go ab1030bd4ee554af936542b01d7b4807 0d238e8a2f97733f net.sock.peer.addr=1.2.3.4 peer.service=telemetrygen-server
        {"resource": {}, "otelcol.component.id": "debug/normal", "otelcol.component.kind": "exporter", "otelcol.signal": "traces"}
```

After:

```console
2025-05-09T19:57:16.332+0200    info    Traces  {"resource": {}, "otelcol.component.id": "debug/normal", "otelcol.component.kind": "exporter", "otelcol.signal": "traces", "resource spans": 1, "spans": 2}
2025-05-09T19:57:16.332+0200    info    ResourceTraces #0 [https://opentelemetry.io/schemas/1.25.0] service.name=telemetrygen
ScopeTraces #0 telemetrygen
okey-dokey-0 ab1030bd4ee554af936542b01d7b4807 1d8c93663d043aa8 net.sock.peer.addr=1.2.3.4 peer.service=telemetrygen-client
lets-go ab1030bd4ee554af936542b01d7b4807 0d238e8a2f97733f net.sock.peer.addr=1.2.3.4 peer.service=telemetrygen-server
        {"resource": {}, "otelcol.component.id": "debug/normal", "otelcol.component.kind": "exporter", "otelcol.signal": "traces"}
```

#### Link to tracking issue

Fixes
https://github.com/open-telemetry/opentelemetry-collector/issues/10515

#### Testing

Updated unit tests.

#### Documentation

Updated example in documentation.
This commit is contained in:
Andrzej Stencel 2025-05-21 15:44:35 +02:00 committed by GitHub
parent ee2c7845a6
commit e6c05b8bab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 213 additions and 15 deletions

View File

@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.
# 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. otlpreceiver)
component: exporter/debug
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Display resource and scope in `normal` verbosity
# One or more tracking issues or pull requests related to the change
issues: [10515]
# (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:
# 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: []

View File

@ -73,10 +73,12 @@ For example, logs with multiline body will be output as multiple lines.
Here's an example output:
```console
2025-04-17T10:40:44.560+0200 info Traces {"otelcol.component.id": "debug/normal", "otelcol.component.kind": "Exporter", "otelcol.signal": "traces", "resource spans": 1, "spans": 2}
2025-04-17T10:40:44.560+0200 info okey-dokey-0 fafdac970271dd2ce89de2442c0518c7 3875f436d989d0e5 net.sock.peer.addr=1.2.3.4 peer.service=telemetrygen-client
lets-go fafdac970271dd2ce89de2442c0518c7 d98de4cb8e2a0ad6 net.sock.peer.addr=1.2.3.4 peer.service=telemetrygen-server
{"otelcol.component.id": "debug/normal", "otelcol.component.kind": "Exporter", "otelcol.signal": "traces"}
2025-05-09T19:57:16.332+0200 info Traces {"resource": {}, "otelcol.component.id": "debug/normal", "otelcol.component.kind": "exporter", "otelcol.signal": "traces", "resource spans": 1, "spans": 2}
2025-05-09T19:57:16.332+0200 info ResourceTraces #0 [https://opentelemetry.io/schemas/1.25.0] service.name=telemetrygen
ScopeTraces #0 telemetrygen
okey-dokey-0 ab1030bd4ee554af936542b01d7b4807 1d8c93663d043aa8 net.sock.peer.addr=1.2.3.4 peer.service=telemetrygen-client
lets-go ab1030bd4ee554af936542b01d7b4807 0d238e8a2f97733f net.sock.peer.addr=1.2.3.4 peer.service=telemetrygen-server
{"resource": {}, "otelcol.component.id": "debug/normal", "otelcol.component.kind": "exporter", "otelcol.signal": "traces"}
```
### Detailed verbosity

View File

@ -5,6 +5,7 @@ package normal // import "go.opentelemetry.io/collector/exporter/debugexporter/i
import (
"fmt"
"strings"
"go.opentelemetry.io/collector/pdata/pcommon"
)
@ -17,3 +18,38 @@ func writeAttributes(attributes pcommon.Map) (attributeStrings []string) {
}
return attributeStrings
}
// writeAttributesString returns a string in the form " attrKey=attrValue attr2=value2"
func writeAttributesString(attributesMap pcommon.Map) (attributesString string) {
attributes := writeAttributes(attributesMap)
if len(attributes) > 0 {
attributesString = " " + strings.Join(attributes, " ")
}
return attributesString
}
func writeResourceDetails(schemaURL string) (resourceDetails string) {
if len(schemaURL) > 0 {
resourceDetails = " [" + schemaURL + "]"
}
return resourceDetails
}
func writeScopeDetails(name string, version string, schemaURL string) (scopeDetails string) {
if len(name) > 0 {
scopeDetails += name
}
if len(version) > 0 {
scopeDetails += "@" + version
}
if len(schemaURL) > 0 {
if len(scopeDetails) > 0 {
scopeDetails += " "
}
scopeDetails += "[" + schemaURL + "]"
}
if len(scopeDetails) > 0 {
scopeDetails = " " + scopeDetails
}
return scopeDetails
}

View File

@ -25,8 +25,14 @@ func (normalLogsMarshaler) MarshalLogs(ld plog.Logs) ([]byte, error) {
var buffer bytes.Buffer
for i := 0; i < ld.ResourceLogs().Len(); i++ {
resourceLog := ld.ResourceLogs().At(i)
buffer.WriteString(fmt.Sprintf("ResourceLog #%d%s%s\n", i, writeResourceDetails(resourceLog.SchemaUrl()), writeAttributesString(resourceLog.Resource().Attributes())))
for j := 0; j < resourceLog.ScopeLogs().Len(); j++ {
scopeLog := resourceLog.ScopeLogs().At(j)
buffer.WriteString(fmt.Sprintf("ScopeLog #%d%s%s\n", i, writeScopeDetails(scopeLog.Scope().Name(), scopeLog.Scope().Version(), scopeLog.SchemaUrl()), writeAttributesString(scopeLog.Scope().Attributes())))
for k := 0; k < scopeLog.LogRecords().Len(); k++ {
logRecord := scopeLog.LogRecords().At(k)
logAttributes := writeAttributes(logRecord.Attributes())

View File

@ -38,7 +38,37 @@ func TestMarshalLogs(t *testing.T) {
logRecord.Attributes().PutStr("key2", "value2")
return logs
}(),
expected: `Single line log message key1=value1 key2=value2
expected: `ResourceLog #0
ScopeLog #0
Single line log message key1=value1 key2=value2
`,
},
{
name: "one log record with resource and scope attributes",
input: func() plog.Logs {
logs := plog.NewLogs()
resourceLogs := logs.ResourceLogs().AppendEmpty()
resourceLogs.SetSchemaUrl("https://opentelemetry.io/resource-schema-url")
resourceLogs.Resource().Attributes().PutStr("resourceKey1", "resourceValue1")
resourceLogs.Resource().Attributes().PutBool("resourceKey2", false)
scopeLogs := resourceLogs.ScopeLogs().AppendEmpty()
scopeLogs.SetSchemaUrl("http://opentelemetry.io/scope-schema-url")
scopeLogs.Scope().SetName("scope-name")
scopeLogs.Scope().SetVersion("1.2.3")
scopeLogs.Scope().Attributes().PutStr("scopeKey1", "scopeValue1")
scopeLogs.Scope().Attributes().PutBool("scopeKey2", true)
logRecord := scopeLogs.LogRecords().AppendEmpty()
logRecord.SetTimestamp(pcommon.NewTimestampFromTime(time.Date(2024, 1, 23, 17, 54, 41, 153, time.UTC)))
logRecord.SetSeverityNumber(plog.SeverityNumberInfo)
logRecord.SetSeverityText("INFO")
logRecord.Body().SetStr("Single line log message")
logRecord.Attributes().PutStr("key1", "value1")
logRecord.Attributes().PutStr("key2", "value2")
return logs
}(),
expected: `ResourceLog #0 [https://opentelemetry.io/resource-schema-url] resourceKey1=resourceValue1 resourceKey2=false
ScopeLog #0 scope-name@1.2.3 [http://opentelemetry.io/scope-schema-url] scopeKey1=scopeValue1 scopeKey2=true
Single line log message key1=value1 key2=value2
`,
},
{
@ -54,7 +84,9 @@ func TestMarshalLogs(t *testing.T) {
logRecord.Attributes().PutStr("key2", "value2")
return logs
}(),
expected: `First line of the log message
expected: `ResourceLog #0
ScopeLog #0
First line of the log message
second line of the log message key1=value1 key2=value2
`,
},
@ -78,7 +110,9 @@ func TestMarshalLogs(t *testing.T) {
logRecord.Attributes().PutStr("mykey1", "myvalue1")
return logs
}(),
expected: `Single line log message key1=value1 key2=value2
expected: `ResourceLog #0
ScopeLog #0
Single line log message key1=value1 key2=value2
Multi-line
log message mykey2=myvalue2 mykey1=myvalue1
`,
@ -105,7 +139,9 @@ log message mykey2=myvalue2 mykey1=myvalue1
logRecord.Attributes().PutStr("service", "payments")
return logs
}(),
expected: `{"app":"CurrencyConverter","event":{"operation":"convert","result":"success"}} conversion={"destination":{"currency":"EUR"},"source":{"amount":34.22,"currency":"USD"}} service=payments
expected: `ResourceLog #0
ScopeLog #0
{"app":"CurrencyConverter","event":{"operation":"convert","result":"success"}} conversion={"destination":{"currency":"EUR"},"source":{"amount":34.22,"currency":"USD"}} service=payments
`,
},
}

View File

@ -26,8 +26,14 @@ func (normalMetricsMarshaler) MarshalMetrics(md pmetric.Metrics) ([]byte, error)
var buffer bytes.Buffer
for i := 0; i < md.ResourceMetrics().Len(); i++ {
resourceMetrics := md.ResourceMetrics().At(i)
buffer.WriteString(fmt.Sprintf("ResourceMetrics #%d%s%s\n", i, writeResourceDetails(resourceMetrics.SchemaUrl()), writeAttributesString(resourceMetrics.Resource().Attributes())))
for j := 0; j < resourceMetrics.ScopeMetrics().Len(); j++ {
scopeMetrics := resourceMetrics.ScopeMetrics().At(j)
buffer.WriteString(fmt.Sprintf("ScopeMetrics #%d%s%s\n", i, writeScopeDetails(scopeMetrics.Scope().Name(), scopeMetrics.Scope().Version(), scopeMetrics.SchemaUrl()), writeAttributesString(scopeMetrics.Scope().Attributes())))
for k := 0; k < scopeMetrics.Metrics().Len(); k++ {
metric := scopeMetrics.Metrics().At(k)

View File

@ -35,7 +35,36 @@ func TestMarshalMetrics(t *testing.T) {
dataPoint.Attributes().PutStr("cpu", "0")
return metrics
}(),
expected: `system.cpu.time{state=user,cpu=0} 123.456
expected: `ResourceMetrics #0
ScopeMetrics #0
system.cpu.time{state=user,cpu=0} 123.456
`,
},
{
name: "data point with resource and scope attributes",
input: func() pmetric.Metrics {
metrics := pmetric.NewMetrics()
resourceMetrics := metrics.ResourceMetrics().AppendEmpty()
resourceMetrics.SetSchemaUrl("https://opentelemetry.io/resource-schema-url")
resourceMetrics.Resource().Attributes().PutStr("resourceKey1", "resourceValue1")
resourceMetrics.Resource().Attributes().PutBool("resourceKey2", false)
scopeMetrics := resourceMetrics.ScopeMetrics().AppendEmpty()
scopeMetrics.SetSchemaUrl("http://opentelemetry.io/scope-schema-url")
scopeMetrics.Scope().SetName("scope-name")
scopeMetrics.Scope().SetVersion("1.2.3")
scopeMetrics.Scope().Attributes().PutStr("scopeKey1", "scopeValue1")
scopeMetrics.Scope().Attributes().PutBool("scopeKey2", true)
metric := scopeMetrics.Metrics().AppendEmpty()
metric.SetName("system.cpu.time")
dataPoint := metric.SetEmptySum().DataPoints().AppendEmpty()
dataPoint.SetDoubleValue(123.456)
dataPoint.Attributes().PutStr("state", "user")
dataPoint.Attributes().PutStr("cpu", "0")
return metrics
}(),
expected: `ResourceMetrics #0 [https://opentelemetry.io/resource-schema-url] resourceKey1=resourceValue1 resourceKey2=false
ScopeMetrics #0 scope-name@1.2.3 [http://opentelemetry.io/scope-schema-url] scopeKey1=scopeValue1 scopeKey2=true
system.cpu.time{state=user,cpu=0} 123.456
`,
},
{
@ -50,7 +79,9 @@ func TestMarshalMetrics(t *testing.T) {
dataPoint.Attributes().PutStr("cpu", "8")
return metrics
}(),
expected: `system.cpu.utilization{state=free,cpu=8} 78.901234567
expected: `ResourceMetrics #0
ScopeMetrics #0
system.cpu.utilization{state=free,cpu=8} 78.901234567
`,
},
{
@ -70,7 +101,9 @@ func TestMarshalMetrics(t *testing.T) {
dataPoint.SetMax(8.13)
return metrics
}(),
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
expected: `ResourceMetrics #0
ScopeMetrics #0
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
`,
},
{
@ -88,7 +121,9 @@ func TestMarshalMetrics(t *testing.T) {
dataPoint.SetMax(8.13)
return metrics
}(),
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
expected: `ResourceMetrics #0
ScopeMetrics #0
http.server.request.duration{http.response.status_code=200,http.request.method=GET} count=1340 sum=99.573 min=0.017 max=8.13
`,
},
{
@ -107,7 +142,9 @@ func TestMarshalMetrics(t *testing.T) {
quantile.SetValue(15)
return metrics
}(),
expected: `summary{http.response.status_code=200,http.request.method=GET} count=1340 sum=99.573000 q0.01=15
expected: `ResourceMetrics #0
ScopeMetrics #0
summary{http.response.status_code=200,http.request.method=GET} count=1340 sum=99.573000 q0.01=15
`,
},
}

View File

@ -26,8 +26,14 @@ func (normalProfilesMarshaler) MarshalProfiles(pd pprofile.Profiles) ([]byte, er
var buffer bytes.Buffer
for i := 0; i < pd.ResourceProfiles().Len(); i++ {
resourceProfiles := pd.ResourceProfiles().At(i)
buffer.WriteString(fmt.Sprintf("ResourceProfiles #%d%s%s\n", i, writeResourceDetails(resourceProfiles.SchemaUrl()), writeAttributesString(resourceProfiles.Resource().Attributes())))
for j := 0; j < resourceProfiles.ScopeProfiles().Len(); j++ {
scopeProfiles := resourceProfiles.ScopeProfiles().At(j)
buffer.WriteString(fmt.Sprintf("ScopeProfiles #%d%s%s\n", i, writeScopeDetails(scopeProfiles.Scope().Name(), scopeProfiles.Scope().Version(), scopeProfiles.SchemaUrl()), writeAttributesString(scopeProfiles.Scope().Attributes())))
for k := 0; k < scopeProfiles.Profiles().Len(); k++ {
profile := scopeProfiles.Profiles().At(k)

View File

@ -28,6 +28,12 @@ func TestMarshalProfiles(t *testing.T) {
input: func() pprofile.Profiles {
profiles := pprofile.NewProfiles()
profile := profiles.ResourceProfiles().AppendEmpty().ScopeProfiles().AppendEmpty().Profiles().AppendEmpty()
profiles.ResourceProfiles().At(0).SetSchemaUrl("https://example.com/resource")
profiles.ResourceProfiles().At(0).Resource().Attributes().PutStr("resourceKey", "resourceValue")
profiles.ResourceProfiles().At(0).ScopeProfiles().At(0).SetSchemaUrl("https://example.com/scope")
profiles.ResourceProfiles().At(0).ScopeProfiles().At(0).Scope().SetName("scope-name")
profiles.ResourceProfiles().At(0).ScopeProfiles().At(0).Scope().SetVersion("1.2.3")
profiles.ResourceProfiles().At(0).ScopeProfiles().At(0).Scope().Attributes().PutStr("scopeKey", "scopeValue")
profile.SetProfileID([16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10})
profile.Sample().AppendEmpty()
profile.Sample().AppendEmpty()
@ -37,7 +43,9 @@ func TestMarshalProfiles(t *testing.T) {
a.Value().SetStr("value1")
return profiles
}(),
expected: `0102030405060708090a0b0c0d0e0f10 samples=2 key1=value1
expected: `ResourceProfiles #0 [https://example.com/resource] resourceKey=resourceValue
ScopeProfiles #0 scope-name@1.2.3 [https://example.com/scope] scopeKey=scopeValue
0102030405060708090a0b0c0d0e0f10 samples=2 key1=value1
`,
},
}

View File

@ -5,6 +5,7 @@ package normal // import "go.opentelemetry.io/collector/exporter/debugexporter/i
import (
"bytes"
"fmt"
"strings"
"go.opentelemetry.io/collector/pdata/ptrace"
@ -24,8 +25,14 @@ func (normalTracesMarshaler) MarshalTraces(md ptrace.Traces) ([]byte, error) {
var buffer bytes.Buffer
for i := 0; i < md.ResourceSpans().Len(); i++ {
resourceTraces := md.ResourceSpans().At(i)
buffer.WriteString(fmt.Sprintf("ResourceTraces #%d%s%s\n", i, writeResourceDetails(resourceTraces.SchemaUrl()), writeAttributesString(resourceTraces.Resource().Attributes())))
for j := 0; j < resourceTraces.ScopeSpans().Len(); j++ {
scopeTraces := resourceTraces.ScopeSpans().At(j)
buffer.WriteString(fmt.Sprintf("ScopeTraces #%d%s%s\n", i, writeScopeDetails(scopeTraces.Scope().Name(), scopeTraces.Scope().Version(), scopeTraces.SchemaUrl()), writeAttributesString(scopeTraces.Scope().Attributes())))
for k := 0; k < scopeTraces.Spans().Len(); k++ {
span := scopeTraces.Spans().At(k)

View File

@ -23,6 +23,33 @@ func TestMarshalTraces(t *testing.T) {
input: ptrace.NewTraces(),
expected: "",
},
{
name: "one span with resource and scope attributes",
input: func() ptrace.Traces {
traces := ptrace.NewTraces()
resourceSpans := traces.ResourceSpans().AppendEmpty()
resourceSpans.SetSchemaUrl("https://opentelemetry.io/resource-schema-url")
resourceSpans.Resource().Attributes().PutStr("resourceKey1", "resourceValue1")
resourceSpans.Resource().Attributes().PutBool("resourceKey2", false)
scopeSpans := resourceSpans.ScopeSpans().AppendEmpty()
scopeSpans.SetSchemaUrl("http://opentelemetry.io/scope-schema-url")
scopeSpans.Scope().SetName("scope-name")
scopeSpans.Scope().SetVersion("1.2.3")
scopeSpans.Scope().Attributes().PutStr("scopeKey1", "scopeValue1")
scopeSpans.Scope().Attributes().PutBool("scopeKey2", true)
span := scopeSpans.Spans().AppendEmpty()
span.SetName("span-name")
span.SetTraceID([16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10})
span.SetSpanID([8]byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18})
span.Attributes().PutStr("key1", "value1")
span.Attributes().PutStr("key2", "value2")
return traces
}(),
expected: `ResourceTraces #0 [https://opentelemetry.io/resource-schema-url] resourceKey1=resourceValue1 resourceKey2=false
ScopeTraces #0 scope-name@1.2.3 [http://opentelemetry.io/scope-schema-url] scopeKey1=scopeValue1 scopeKey2=true
span-name 0102030405060708090a0b0c0d0e0f10 1112131415161718 key1=value1 key2=value2
`,
},
{
name: "one span",
input: func() ptrace.Traces {
@ -35,7 +62,9 @@ func TestMarshalTraces(t *testing.T) {
span.Attributes().PutStr("key2", "value2")
return traces
}(),
expected: `span-name 0102030405060708090a0b0c0d0e0f10 1112131415161718 key1=value1 key2=value2
expected: `ResourceTraces #0
ScopeTraces #0
span-name 0102030405060708090a0b0c0d0e0f10 1112131415161718 key1=value1 key2=value2
`,
},
}