Skip to content

Commit 9050c00

Browse files
Mobile crash support (#191)
* Traversing logs * Renaming files * Changing package name * Reorganizing files * Reorganizing files * Adding test case for crash events * Making mobile enricher internal * Moving resource enricher * Passing resource config to resource enrichment * Enriching log resources * Extracting attribute config to an independent package to get reused * Using resource and attribute configs for logs * Testing log resource enrichment * Adding timestamp.us verification * Extracting getting timestamp.us tool * Adding timestamp.us to crash events * Moving attributeconfig to elasticattr * Adding error.id * Improving event tests * Updatig tests * Validating error.id * Adding java stacktrace testdata * Adding groupingkey go files * Adding stacktrace without message to testdata * Creating test for curating stacktraces * Curating stacktraces * Validating curating different types of stacktraces * Updating tests * Creating grouping key for java stacktraces * Updating tests * Adding grouping_key to crash events * Adding error.type crash to crash events * Using camelcase for variable names * Updating tests * Using observerdtimestamp when timestamp is not available * Adding validation for non-crash events * Validating only events are sent to the event enricher * Updating stacktrace test data * Using elasticattr attributes * Adding license headers * Update enrichments/logs/internal/mobile/groupingkey.go Co-authored-by: Felix Barnsteiner <felixbarny@users.noreply.github.com> * Keeping cause exception name * Removing stacktrace line numbers * Updating groupingkey tests * Updating event tests * Replacing sha256 by xxhash * Replace SHA-256 with xxHash and optimize regex patterns in groupingkey.go * Update test expectations in event_test.go for xxHash output * Make error grouping key conditional on Java language * Addressing struct pointer lint issue * Avoiding creating new map instances on each loop iteration * Move Android stacktrace test data to dedicated subfolder * Adding ios crash test files * Add Swift stack trace grouping key function using xxHash for iOS crashes * Adding testdata curated swift stacktraces * Removing unicode chars from ios stacktraces * removing unicode chars * Creating swift stacktrace groupingkey * Add Swift error grouping key support to enrichCrashEvent function * Renaming function * Add test case for Swift error grouping key support * Ensuring swift grouping keys don't contain the thread number * Updating swift curated stacktrace test files * Removing frame and image load addresses from swift stacktraces * Moving mobile enrichments to enricher * Clean up * Moving GetTimestampUs out of elasticattr --------- Co-authored-by: Felix Barnsteiner <felixbarny@users.noreply.github.com>
1 parent 8159ee3 commit 9050c00

25 files changed

+1251
-15
lines changed

elasticattr/attributes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const (
4141
SpanType = "span.type"
4242
SpanSubtype = "span.subtype"
4343
EventOutcome = "event.outcome"
44+
EventKind = "event.kind"
4445
SuccessCount = "event.success_count"
4546
ServiceTargetType = "service.target.type"
4647
ServiceTargetName = "service.target.name"
@@ -55,4 +56,5 @@ const (
5556
ErrorExceptionHandled = "error.exception.handled"
5657
ErrorGroupingKey = "error.grouping_key"
5758
ErrorGroupingName = "error.grouping_name"
59+
ErrorType = "error.type"
5860
)

enrichments/enricher.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (e *Enricher) EnrichTraces(pt ptrace.Traces) {
4444
resSpans := pt.ResourceSpans()
4545
for i := 0; i < resSpans.Len(); i++ {
4646
resSpan := resSpans.At(i)
47-
elastic.EnrichResource(resSpan.Resource(), e.Config)
47+
elastic.EnrichResource(resSpan.Resource(), e.Config.Resource)
4848
scopeSpans := resSpan.ScopeSpans()
4949
for j := 0; j < scopeSpans.Len(); j++ {
5050
scopeSpan := scopeSpans.At(j)
@@ -63,14 +63,16 @@ func (e *Enricher) EnrichLogs(pl plog.Logs) {
6363
resLogs := pl.ResourceLogs()
6464
for i := 0; i < resLogs.Len(); i++ {
6565
resLog := resLogs.At(i)
66-
elastic.EnrichResource(resLog.Resource(), e.Config)
66+
resource := resLog.Resource()
67+
elastic.EnrichResource(resource, e.Config.Resource)
68+
resourceAttrs := resource.Attributes().AsRaw()
6769
scopeLogs := resLog.ScopeLogs()
6870
for j := 0; j < scopeLogs.Len(); j++ {
6971
scopeSpan := scopeLogs.At(j)
7072
elastic.EnrichScope(scopeSpan.Scope(), e.Config)
7173
logRecords := scopeSpan.LogRecords()
7274
for k := 0; k < logRecords.Len(); k++ {
73-
elastic.EnrichLog(logRecords.At(k), e.Config)
75+
elastic.EnrichLog(resourceAttrs, logRecords.At(k), e.Config)
7476
}
7577
}
7678
}
@@ -83,7 +85,7 @@ func (e *Enricher) EnrichMetrics(pl pmetric.Metrics) {
8385
for i := 0; i < resMetrics.Len(); i++ {
8486
resMetric := resMetrics.At(i)
8587
elastic.EnrichMetric(resMetric, e.Config)
86-
elastic.EnrichResource(resMetric.Resource(), e.Config)
88+
elastic.EnrichResource(resMetric.Resource(), e.Config.Resource)
8789
scopeMetics := resMetric.ScopeMetrics()
8890
for j := 0; j < scopeMetics.Len(); j++ {
8991
scopeMetric := scopeMetics.At(j)

enrichments/internal/elastic/log.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,35 @@ package elastic
2020
import (
2121
"github.com/elastic/opentelemetry-lib/elasticattr"
2222
"github.com/elastic/opentelemetry-lib/enrichments/config"
23+
"github.com/elastic/opentelemetry-lib/enrichments/internal/elastic/mobile"
2324
"go.opentelemetry.io/collector/pdata/plog"
2425
)
2526

26-
func EnrichLog(log plog.LogRecord, cfg config.Config) {
27+
func EnrichLog(resourceAttrs map[string]any, log plog.LogRecord, cfg config.Config) {
2728
if cfg.Log.ProcessorEvent.Enabled {
2829
if _, exists := log.Attributes().Get(elasticattr.ProcessorEvent); !exists {
2930
log.Attributes().PutStr(elasticattr.ProcessorEvent, "log")
3031
}
3132
}
33+
eventName, ok := getEventName(log)
34+
if ok {
35+
ctx := mobile.EventContext{
36+
ResourceAttributes: resourceAttrs,
37+
EventName: eventName,
38+
}
39+
mobile.EnrichLogEvent(ctx, log)
40+
}
41+
}
42+
43+
// getEventName returns the event name from the log record.
44+
// If the event name is not set, it returns an empty string.
45+
func getEventName(logRecord plog.LogRecord) (string, bool) {
46+
if logRecord.EventName() != "" {
47+
return logRecord.EventName(), true
48+
}
49+
attributeValue, ok := logRecord.Attributes().Get("event.name")
50+
if ok {
51+
return attributeValue.AsString(), true
52+
}
53+
return "", false
3254
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package mobile
19+
20+
import (
21+
"crypto/rand"
22+
"encoding/hex"
23+
"io"
24+
25+
"github.com/elastic/opentelemetry-lib/elasticattr"
26+
"go.opentelemetry.io/collector/pdata/pcommon"
27+
"go.opentelemetry.io/collector/pdata/plog"
28+
)
29+
30+
// EventContext contains contextual information for log event enrichment
31+
type EventContext struct {
32+
ResourceAttributes map[string]any
33+
EventName string
34+
}
35+
36+
func EnrichLogEvent(ctx EventContext, logRecord plog.LogRecord) {
37+
logRecord.Attributes().PutStr(elasticattr.EventKind, "event")
38+
39+
if ctx.EventName == "device.crash" {
40+
enrichCrashEvent(logRecord, ctx.ResourceAttributes)
41+
}
42+
}
43+
44+
func enrichCrashEvent(logRecord plog.LogRecord, resourceAttrs map[string]any) {
45+
timestamp := logRecord.Timestamp()
46+
if timestamp == 0 {
47+
timestamp = logRecord.ObservedTimestamp()
48+
}
49+
logRecord.Attributes().PutStr(elasticattr.ProcessorEvent, "error")
50+
logRecord.Attributes().PutInt(elasticattr.TimestampUs, getTimestampUs(timestamp))
51+
if id, err := newUniqueID(); err == nil {
52+
logRecord.Attributes().PutStr(elasticattr.ErrorID, id)
53+
}
54+
stacktrace, ok := logRecord.Attributes().Get("exception.stacktrace")
55+
if ok {
56+
language, hasLanguage := resourceAttrs["telemetry.sdk.language"]
57+
if hasLanguage {
58+
switch language {
59+
case "java":
60+
logRecord.Attributes().PutStr(elasticattr.ErrorGroupingKey, CreateJavaStacktraceGroupingKey(stacktrace.AsString()))
61+
case "swift":
62+
if key, err := CreateSwiftStacktraceGroupingKey(stacktrace.AsString()); err == nil {
63+
logRecord.Attributes().PutStr(elasticattr.ErrorGroupingKey, key)
64+
}
65+
}
66+
}
67+
}
68+
logRecord.Attributes().PutStr(elasticattr.ErrorType, "crash")
69+
}
70+
71+
func newUniqueID() (string, error) {
72+
var u [16]byte
73+
if _, err := io.ReadFull(rand.Reader, u[:]); err != nil {
74+
return "", err
75+
}
76+
77+
// convert to string
78+
buf := make([]byte, 32)
79+
hex.Encode(buf, u[:])
80+
81+
return string(buf), nil
82+
}
83+
84+
func getTimestampUs(ts pcommon.Timestamp) int64 {
85+
return int64(ts) / 1000
86+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package mobile
19+
20+
import (
21+
"testing"
22+
"time"
23+
24+
"maps"
25+
26+
"github.com/google/go-cmp/cmp"
27+
"github.com/stretchr/testify/assert"
28+
"go.opentelemetry.io/collector/pdata/pcommon"
29+
"go.opentelemetry.io/collector/pdata/plog"
30+
)
31+
32+
func TestEnrichEvents(t *testing.T) {
33+
now := time.Unix(3600, 0)
34+
timestamp := pcommon.NewTimestampFromTime(now)
35+
javaStacktrace := "Exception in thread \"main\" java.lang.RuntimeException: Test exception\n at com.example.GenerateTrace.methodB(GenerateTrace.java:13)\n at com.example.GenerateTrace.methodA(GenerateTrace.java:9)\n at com.example.GenerateTrace.main(GenerateTrace.java:5)"
36+
javaStacktraceHash := "e25c4196dc720d91"
37+
38+
swiftStacktrace := readSwiftStacktraceFile(t, "thread-8-crash.txt")
39+
swiftStacktraceHash := "e737b0da1c8f9d5a"
40+
41+
for _, tc := range []struct {
42+
name string
43+
eventName string
44+
input func() plog.LogRecord
45+
resourceAttrs map[string]any
46+
expectedAttributes map[string]any
47+
}{
48+
{
49+
name: "crash_event_java",
50+
eventName: "device.crash",
51+
resourceAttrs: map[string]any{
52+
"telemetry.sdk.language": "java",
53+
},
54+
input: func() plog.LogRecord {
55+
logRecord := plog.NewLogRecord()
56+
logRecord.SetTimestamp(timestamp)
57+
logRecord.Attributes().PutStr("event.name", "device.crash")
58+
logRecord.Attributes().PutStr("exception.message", "Exception message")
59+
logRecord.Attributes().PutStr("exception.type", "java.lang.RuntimeException")
60+
logRecord.Attributes().PutStr("exception.stacktrace", javaStacktrace)
61+
return logRecord
62+
},
63+
expectedAttributes: map[string]any{
64+
"processor.event": "error",
65+
"timestamp.us": timestamp.AsTime().UnixMicro(),
66+
"error.grouping_key": javaStacktraceHash,
67+
"error.type": "crash",
68+
"event.kind": "event",
69+
},
70+
},
71+
{
72+
name: "crash_event_without_timestamp_java",
73+
eventName: "device.crash",
74+
resourceAttrs: map[string]any{
75+
"telemetry.sdk.language": "java",
76+
},
77+
input: func() plog.LogRecord {
78+
logRecord := plog.NewLogRecord()
79+
logRecord.SetObservedTimestamp(timestamp)
80+
logRecord.Attributes().PutStr("event.name", "device.crash")
81+
logRecord.Attributes().PutStr("exception.message", "Exception message")
82+
logRecord.Attributes().PutStr("exception.type", "java.lang.RuntimeException")
83+
logRecord.Attributes().PutStr("exception.stacktrace", javaStacktrace)
84+
return logRecord
85+
},
86+
expectedAttributes: map[string]any{
87+
"processor.event": "error",
88+
"timestamp.us": timestamp.AsTime().UnixMicro(),
89+
"error.grouping_key": javaStacktraceHash,
90+
"error.type": "crash",
91+
"event.kind": "event",
92+
},
93+
},
94+
{
95+
name: "crash_event_non_java",
96+
eventName: "device.crash",
97+
resourceAttrs: map[string]any{
98+
"telemetry.sdk.language": "go",
99+
},
100+
input: func() plog.LogRecord {
101+
logRecord := plog.NewLogRecord()
102+
logRecord.SetTimestamp(timestamp)
103+
logRecord.Attributes().PutStr("event.name", "device.crash")
104+
logRecord.Attributes().PutStr("exception.message", "Exception message")
105+
logRecord.Attributes().PutStr("exception.type", "go.error")
106+
logRecord.Attributes().PutStr("exception.stacktrace", javaStacktrace)
107+
return logRecord
108+
},
109+
expectedAttributes: map[string]any{
110+
"processor.event": "error",
111+
"timestamp.us": timestamp.AsTime().UnixMicro(),
112+
"error.type": "crash",
113+
"event.kind": "event",
114+
},
115+
},
116+
{
117+
name: "non_crash_event",
118+
eventName: "othername",
119+
resourceAttrs: map[string]any{},
120+
input: func() plog.LogRecord {
121+
logRecord := plog.NewLogRecord()
122+
logRecord.Attributes().PutStr("event.name", "othername")
123+
return logRecord
124+
},
125+
expectedAttributes: map[string]any{
126+
"event.kind": "event",
127+
},
128+
},
129+
{
130+
name: "crash_event_swift",
131+
eventName: "device.crash",
132+
resourceAttrs: map[string]any{
133+
"telemetry.sdk.language": "swift",
134+
},
135+
input: func() plog.LogRecord {
136+
logRecord := plog.NewLogRecord()
137+
logRecord.SetTimestamp(timestamp)
138+
logRecord.Attributes().PutStr("event.name", "device.crash")
139+
logRecord.Attributes().PutStr("exception.type", "SIGTRAP")
140+
logRecord.Attributes().PutStr("exception.stacktrace", swiftStacktrace)
141+
return logRecord
142+
},
143+
expectedAttributes: map[string]any{
144+
"processor.event": "error",
145+
"timestamp.us": timestamp.AsTime().UnixMicro(),
146+
"error.grouping_key": swiftStacktraceHash,
147+
"error.type": "crash",
148+
"event.kind": "event",
149+
},
150+
},
151+
} {
152+
t.Run(tc.name, func(t *testing.T) {
153+
inputLogRecord := tc.input()
154+
155+
maps.Copy(tc.expectedAttributes, inputLogRecord.Attributes().AsRaw())
156+
157+
ctx := EventContext{
158+
ResourceAttributes: tc.resourceAttrs,
159+
EventName: tc.eventName,
160+
}
161+
EnrichLogEvent(ctx, inputLogRecord)
162+
163+
assert.Empty(t, cmp.Diff(inputLogRecord.Attributes().AsRaw(), tc.expectedAttributes, ignoreMapKey("error.id")))
164+
errorId, ok := inputLogRecord.Attributes().Get("error.id")
165+
if ok {
166+
assert.Equal(t, "device.crash", tc.eventName)
167+
assert.Equal(t, 32, len(errorId.AsString()))
168+
} else {
169+
assert.NotEqual(t, "device.crash", tc.eventName)
170+
}
171+
})
172+
}
173+
}
174+
175+
func ignoreMapKey(k string) cmp.Option {
176+
return cmp.FilterPath(func(p cmp.Path) bool {
177+
mapIndex, ok := p.Last().(cmp.MapIndex)
178+
if !ok {
179+
return false
180+
}
181+
return mapIndex.Key().String() == k
182+
}, cmp.Ignore())
183+
}

0 commit comments

Comments
 (0)