Skip to content

Commit 372d933

Browse files
authored
[receiver/libhoney] Libhoney receiver trace signal (#36902)
#### Description This PR is the implementation for the traces signal related to the new libhoney receiver. <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue #36693
1 parent a205064 commit 372d933

File tree

7 files changed

+751
-9
lines changed

7 files changed

+751
-9
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: libhoneyreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Implement trace signal for libhoney receiver
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [36693]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

receiver/libhoneyreceiver/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
go.opentelemetry.io/collector/receiver v0.116.0
1919
go.opentelemetry.io/collector/receiver/receivertest v0.116.0
2020
go.opentelemetry.io/collector/semconv v0.116.0
21+
go.opentelemetry.io/otel/trace v1.32.0
2122
go.uber.org/goleak v1.3.0
2223
go.uber.org/zap v1.27.0
2324
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53
@@ -68,7 +69,6 @@ require (
6869
go.opentelemetry.io/otel/metric v1.32.0 // indirect
6970
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
7071
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
71-
go.opentelemetry.io/otel/trace v1.32.0 // indirect
7272
go.uber.org/multierr v1.11.0 // indirect
7373
golang.org/x/net v0.32.0 // indirect
7474
golang.org/x/sys v0.28.0 // indirect

receiver/libhoneyreceiver/internal/libhoneyevent/libhoneyevent.go

Lines changed: 186 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44
package libhoneyevent // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/libhoneyreceiver/internal/libhoneyevent"
55

66
import (
7+
"crypto/rand"
8+
"encoding/binary"
9+
"encoding/hex"
710
"encoding/json"
811
"errors"
912
"fmt"
13+
"hash/fnv"
1014
"slices"
15+
"strings"
1116
"time"
1217

1318
"go.opentelemetry.io/collector/pdata/pcommon"
1419
"go.opentelemetry.io/collector/pdata/plog"
1520
"go.opentelemetry.io/collector/pdata/ptrace"
21+
trc "go.opentelemetry.io/otel/trace"
1622
"go.uber.org/zap"
1723

1824
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/libhoneyreceiver/internal/eventtime"
@@ -87,8 +93,29 @@ func (l *LibhoneyEvent) DebugString() string {
8793
}
8894

8995
// SignalType returns the type of signal this event represents. Only log is implemented for now.
90-
func (l *LibhoneyEvent) SignalType() (string, error) {
91-
return "log", nil
96+
func (l *LibhoneyEvent) SignalType(logger zap.Logger) string {
97+
if sig, ok := l.Data["meta.signal_type"]; ok {
98+
switch sig {
99+
case "trace":
100+
if atype, ok := l.Data["meta.annotation_type"]; ok {
101+
if atype == "span_event" {
102+
return "span_event"
103+
} else if atype == "link" {
104+
return "span_link"
105+
}
106+
logger.Warn("invalid annotation type", zap.String("meta.annotation_type", atype.(string)))
107+
return "span"
108+
}
109+
return "span"
110+
case "log":
111+
return "log"
112+
default:
113+
logger.Warn("invalid meta.signal_type", zap.String("meta.signal_type", sig.(string)))
114+
return "log"
115+
}
116+
}
117+
logger.Warn("missing meta.signal_type and meta.annotation_type")
118+
return "log"
92119
}
93120

94121
// GetService returns the service name from the event or the dataset name if no service name is found.
@@ -126,6 +153,36 @@ func (l *LibhoneyEvent) GetScope(fields FieldMapConfig, seen *ScopeHistory, serv
126153
return "libhoney.receiver", errors.New("library name not found")
127154
}
128155

156+
func spanIDFrom(s string) trc.SpanID {
157+
hash := fnv.New64a()
158+
hash.Write([]byte(s))
159+
n := hash.Sum64()
160+
sid := trc.SpanID{}
161+
binary.LittleEndian.PutUint64(sid[:], n)
162+
return sid
163+
}
164+
165+
func traceIDFrom(s string) trc.TraceID {
166+
hash := fnv.New64a()
167+
hash.Write([]byte(s))
168+
n1 := hash.Sum64()
169+
hash.Write([]byte(s))
170+
n2 := hash.Sum64()
171+
tid := trc.TraceID{}
172+
binary.LittleEndian.PutUint64(tid[:], n1)
173+
binary.LittleEndian.PutUint64(tid[8:], n2)
174+
return tid
175+
}
176+
177+
func generateAnId(length int) []byte {
178+
token := make([]byte, length)
179+
_, err := rand.Read(token)
180+
if err != nil {
181+
return []byte{}
182+
}
183+
return token
184+
}
185+
129186
// SimpleScope is a simple struct to hold the scope data
130187
type SimpleScope struct {
131188
ServiceName string
@@ -198,3 +255,130 @@ func (l *LibhoneyEvent) ToPLogRecord(newLog *plog.LogRecord, alreadyUsedFields *
198255
}
199256
return nil
200257
}
258+
259+
// GetParentID returns the parent id from the event or an error if it's not found
260+
func (l *LibhoneyEvent) GetParentID(fieldName string) (trc.SpanID, error) {
261+
if pid, ok := l.Data[fieldName]; ok {
262+
pid := strings.ReplaceAll(pid.(string), "-", "")
263+
pidByteArray, err := hex.DecodeString(pid)
264+
if err == nil {
265+
if len(pidByteArray) == 32 {
266+
pidByteArray = pidByteArray[8:24]
267+
} else if len(pidByteArray) >= 16 {
268+
pidByteArray = pidByteArray[0:16]
269+
}
270+
return trc.SpanID(pidByteArray), nil
271+
}
272+
return trc.SpanID{}, errors.New("parent id is not a valid span id")
273+
}
274+
return trc.SpanID{}, errors.New("parent id not found")
275+
}
276+
277+
// ToPTraceSpan converts a LibhoneyEvent to a Pdata Span
278+
func (l *LibhoneyEvent) ToPTraceSpan(newSpan *ptrace.Span, alreadyUsedFields *[]string, cfg FieldMapConfig, logger zap.Logger) error {
279+
time_ns := l.MsgPackTimestamp.UnixNano()
280+
logger.Debug("processing trace with", zap.Int64("timestamp", time_ns))
281+
282+
var parent_id trc.SpanID
283+
if pid, ok := l.Data[cfg.Attributes.ParentID]; ok {
284+
parent_id = spanIDFrom(pid.(string))
285+
newSpan.SetParentSpanID(pcommon.SpanID(parent_id))
286+
}
287+
288+
duration_ms := 0.0
289+
for _, df := range cfg.Attributes.DurationFields {
290+
if duration, okay := l.Data[df]; okay {
291+
duration_ms = duration.(float64)
292+
break
293+
}
294+
}
295+
end_timestamp := time_ns + (int64(duration_ms) * 1000000)
296+
297+
if tid, ok := l.Data[cfg.Attributes.TraceID]; ok {
298+
tid := strings.ReplaceAll(tid.(string), "-", "")
299+
tidByteArray, err := hex.DecodeString(tid)
300+
if err == nil {
301+
if len(tidByteArray) >= 32 {
302+
tidByteArray = tidByteArray[0:32]
303+
}
304+
newSpan.SetTraceID(pcommon.TraceID(tidByteArray))
305+
} else {
306+
newSpan.SetTraceID(pcommon.TraceID(traceIDFrom(tid)))
307+
}
308+
} else {
309+
newSpan.SetTraceID(pcommon.TraceID(generateAnId(32)))
310+
}
311+
312+
if sid, ok := l.Data[cfg.Attributes.SpanID]; ok {
313+
sid := strings.ReplaceAll(sid.(string), "-", "")
314+
sidByteArray, err := hex.DecodeString(sid)
315+
if err == nil {
316+
if len(sidByteArray) == 32 {
317+
sidByteArray = sidByteArray[8:24]
318+
} else if len(sidByteArray) >= 16 {
319+
sidByteArray = sidByteArray[0:16]
320+
}
321+
newSpan.SetSpanID(pcommon.SpanID(sidByteArray))
322+
} else {
323+
newSpan.SetSpanID(pcommon.SpanID(spanIDFrom(sid)))
324+
}
325+
} else {
326+
newSpan.SetSpanID(pcommon.SpanID(generateAnId(16)))
327+
}
328+
329+
newSpan.SetStartTimestamp(pcommon.Timestamp(time_ns))
330+
newSpan.SetEndTimestamp(pcommon.Timestamp(end_timestamp))
331+
332+
if spanName, ok := l.Data[cfg.Attributes.Name]; ok {
333+
newSpan.SetName(spanName.(string))
334+
}
335+
if spanStatusMessge, ok := l.Data["status_message"]; ok {
336+
newSpan.Status().SetMessage(spanStatusMessge.(string))
337+
}
338+
newSpan.Status().SetCode(ptrace.StatusCodeUnset)
339+
340+
if _, ok := l.Data[cfg.Attributes.Error]; ok {
341+
newSpan.Status().SetCode(ptrace.StatusCodeError)
342+
}
343+
344+
if spanKind, ok := l.Data[cfg.Attributes.SpanKind]; ok {
345+
switch spanKind.(string) {
346+
case "server":
347+
newSpan.SetKind(ptrace.SpanKindServer)
348+
case "client":
349+
newSpan.SetKind(ptrace.SpanKindClient)
350+
case "producer":
351+
newSpan.SetKind(ptrace.SpanKindProducer)
352+
case "consumer":
353+
newSpan.SetKind(ptrace.SpanKindConsumer)
354+
case "internal":
355+
newSpan.SetKind(ptrace.SpanKindInternal)
356+
default:
357+
newSpan.SetKind(ptrace.SpanKindUnspecified)
358+
}
359+
}
360+
361+
newSpan.Attributes().PutInt("SampleRate", int64(l.Samplerate))
362+
363+
for k, v := range l.Data {
364+
if slices.Contains(*alreadyUsedFields, k) {
365+
continue
366+
}
367+
switch v := v.(type) {
368+
case string:
369+
newSpan.Attributes().PutStr(k, v)
370+
case int:
371+
newSpan.Attributes().PutInt(k, int64(v))
372+
case int64, int16, int32:
373+
intv := v.(int64)
374+
newSpan.Attributes().PutInt(k, intv)
375+
case float64:
376+
newSpan.Attributes().PutDouble(k, v)
377+
case bool:
378+
newSpan.Attributes().PutBool(k, v)
379+
default:
380+
logger.Warn("Span data type issue", zap.String("trace.trace_id", newSpan.TraceID().String()), zap.String("trace.span_id", newSpan.SpanID().String()), zap.String("key", k))
381+
}
382+
}
383+
return nil
384+
}

receiver/libhoneyreceiver/internal/libhoneyevent/libhoneyevent_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/stretchr/testify/require"
1313
"go.opentelemetry.io/collector/pdata/pcommon"
1414
"go.opentelemetry.io/collector/pdata/plog"
15+
"go.opentelemetry.io/collector/pdata/ptrace"
1516
"go.uber.org/zap"
1617
)
1718

@@ -292,3 +293,109 @@ func TestLibhoneyEvent_GetScope(t *testing.T) {
292293
})
293294
}
294295
}
296+
297+
func TestToPTraceSpan(t *testing.T) {
298+
now := time.Now()
299+
tests := []struct {
300+
name string
301+
event LibhoneyEvent
302+
want func(ptrace.Span)
303+
wantErr bool
304+
}{
305+
{
306+
name: "basic span conversion",
307+
event: LibhoneyEvent{
308+
Time: now.Format(time.RFC3339),
309+
MsgPackTimestamp: &now,
310+
Data: map[string]any{
311+
"name": "test-span",
312+
"trace.span_id": "1234567890abcdef",
313+
"trace.trace_id": "1234567890abcdef1234567890abcdef",
314+
"duration_ms": 100.0,
315+
"error": true,
316+
"status_message": "error message",
317+
"kind": "server",
318+
"string_attr": "value",
319+
"int_attr": 42,
320+
"bool_attr": true,
321+
},
322+
Samplerate: 1,
323+
},
324+
want: func(s ptrace.Span) {
325+
s.SetName("test-span")
326+
s.SetSpanID([8]byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef})
327+
s.SetTraceID([16]byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef})
328+
s.SetStartTimestamp(pcommon.NewTimestampFromTime(now))
329+
s.SetEndTimestamp(pcommon.NewTimestampFromTime(now.Add(100 * time.Millisecond)))
330+
s.Status().SetCode(ptrace.StatusCodeError)
331+
s.Status().SetMessage("error message")
332+
s.SetKind(ptrace.SpanKindServer)
333+
s.Attributes().PutStr("string_attr", "value")
334+
s.Attributes().PutInt("int_attr", 42)
335+
s.Attributes().PutBool("bool_attr", true)
336+
},
337+
},
338+
}
339+
340+
alreadyUsedFields := []string{"name", "trace.span_id", "trace.trace_id", "duration_ms", "status.code", "status.message", "kind"}
341+
testCfg := FieldMapConfig{
342+
Attributes: AttributesConfig{
343+
Name: "name",
344+
TraceID: "trace.trace_id",
345+
SpanID: "trace.span_id",
346+
ParentID: "trace.parent_id",
347+
Error: "error",
348+
SpanKind: "kind",
349+
DurationFields: []string{"duration_ms"},
350+
},
351+
Resources: ResourcesConfig{
352+
ServiceName: "service.name",
353+
},
354+
Scopes: ScopesConfig{
355+
LibraryName: "library.name",
356+
LibraryVersion: "library.version",
357+
},
358+
}
359+
360+
for _, tt := range tests {
361+
t.Run(tt.name, func(t *testing.T) {
362+
span := ptrace.NewSpan()
363+
err := tt.event.ToPTraceSpan(&span, &alreadyUsedFields, testCfg, *zap.NewNop())
364+
365+
if tt.wantErr {
366+
assert.Error(t, err)
367+
return
368+
}
369+
370+
require.NoError(t, err)
371+
if tt.want != nil {
372+
want := ptrace.NewSpan()
373+
tt.want(want)
374+
375+
// Check basic fields
376+
assert.Equal(t, want.Name(), span.Name())
377+
assert.Equal(t, want.SpanID(), span.SpanID())
378+
assert.Equal(t, want.TraceID(), span.TraceID())
379+
assert.Equal(t, want.StartTimestamp(), span.StartTimestamp())
380+
assert.Equal(t, want.EndTimestamp(), span.EndTimestamp())
381+
assert.Equal(t, want.Kind(), span.Kind())
382+
383+
// Check status
384+
assert.Equal(t, want.Status().Code(), span.Status().Code())
385+
assert.Equal(t, want.Status().Message(), span.Status().Message())
386+
387+
// Check attributes
388+
want.Attributes().Range(func(k string, v pcommon.Value) bool {
389+
got, ok := span.Attributes().Get(k)
390+
assert.True(t, ok, "missing attribute %s", k)
391+
assert.Equal(t, v.Type(), got.Type(), "wrong type for attribute %s", k)
392+
assert.Equal(t, v, got, "wrong value for attribute %s", k)
393+
return true
394+
})
395+
396+
// Verify no fewer attributes, extras are expected
397+
assert.LessOrEqual(t, want.Attributes().Len(), span.Attributes().Len())
398+
}
399+
})
400+
}
401+
}

0 commit comments

Comments
 (0)