Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
Signed-off-by: Remy Chantenay <[email protected]>
  • Loading branch information
remychantenay committed Nov 14, 2024
1 parent b03f7d6 commit 93f960b
Show file tree
Hide file tree
Showing 12 changed files with 459 additions and 236 deletions.
8 changes: 2 additions & 6 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
go-version: '1.23'

- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0

- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
Expand All @@ -33,7 +33,3 @@ jobs:
- name: Testing
run: make test

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
52 changes: 45 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,51 @@
# otel-tag
Simple package that extracts OpenTelemetry span attributes from a struct based on tags.
Simple package that extracts OpenTelemetry [span attributes](https://opentelemetry.io/docs/demo/telemetry-features/manual-span-attributes/) and [baggage members](https://opentelemetry.io/docs/concepts/signals/baggage/) from a struct based on tags.

> [!WARNING]
> Please bear in mind that this package does not intend to cater to all use cases and please everyone. The code could also do with some refactoring here and there, but it's doing the job.
## Shortcomings
- Do not support embedded structs.
- Does not support basic type pointers (only struct pointers).

## Usage
```go
package main

import (
"context"

"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/trace"

oteltag "github.com/remychantenay/otel-tag"
)

type User struct {
ID string `otel:"app.user.id"`
Username string `otel:"app.user.username"`
IsPremium bool `otel:"app.user.premium"`

UserDetails UserDetails
}

type UserDetails struct {
Website string `otel:"app.user.website,omitempty"`
Bio string // Not tagged, will be ignored.
}

func main() {
// Span attributes.
_, span := tracer.Start(context.Background(), "someOperation",
trace.WithAttributes(oteltag.SpanAttributes(m)...),
)
defer span.End()

// Baggage Members.
members := oteltag.BaggageMembers(m)
bag, _ := baggage.New(members...)
ctx := baggage.ContextWithBaggage(context.Background(), bag)
}
```

## License
Apache License Version 2.0

TODO
- Shit! What about attribute name prefix???! Init?
- Remove t.Log() calls in tests
- Complete README.md with example
57 changes: 40 additions & 17 deletions baggage.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import (
// BaggageMembers takes in a struct and spits out OpenTelemetry baggage members
// based on the struct tags.
func BaggageMembers(res any) []baggage.Member {
structValue := reflect.ValueOf(res)
return structToBaggageMembers(res)
}

// structToBaggageMembers returns a slice of [baggage.Member] for a struct.
func structToBaggageMembers(s any) []baggage.Member {
structValue := reflect.ValueOf(s)
structType := structValue.Type()
fieldCount := structValue.NumField()

Expand All @@ -22,28 +27,46 @@ func BaggageMembers(res any) []baggage.Member {

members := make([]baggage.Member, 0, fieldCount)
for i := 0; i < fieldCount; i++ {
tag := internal.ExtractTag(structValue, i)
field, fieldValue := structType.Field(i), structValue.Field(i)
if field.Type.Kind() == reflect.Struct {
members = append(members, structToBaggageMembers(fieldValue.Interface())...)
} else if field.Type.Kind() == reflect.Pointer { // Known shortcoming, assuming a pointer can only be a struct.
members = append(members, structToBaggageMembers(fieldValue.Elem().Interface())...)
} else {
member := basicTypeToBaggageMember(structType, structValue, i)
if member.Key() == "" {
continue
}

if len(tag) == 0 || tag == valIgnore {
continue
members = append(members, member)
}
}

var omitEmpty bool
before, after, found := strings.Cut(tag, ",")
if found {
tag = before
if after == valOmitEmpty {
omitEmpty = true
}
}
return members
}

field, value := structType.Field(i), structValue.Field(i)
// basicTypeToBaggageMember returns an [attribute.Member] for a basic type.
func basicTypeToBaggageMember(structType reflect.Type, structValue reflect.Value, index int) baggage.Member {
tag := internal.ExtractTag(structValue, index)

member, zeroValue := internal.BaggageMember(field, value, tag)
if !zeroValue || !omitEmpty {
members = append(members, member)
if tag == "" {
return baggage.Member{}
}

var omitEmpty bool
before, after, found := strings.Cut(tag, ",")
if found {
tag = before
if after == flagOmitEmpty {
omitEmpty = true
}
}

return members
field, fieldValue := structType.Field(index), structValue.Field(index)
attr, zeroValue := internal.BaggageMember(field, fieldValue, tag)
if zeroValue && omitEmpty {
return baggage.Member{}
}

return attr
}
72 changes: 62 additions & 10 deletions baggage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,31 @@ import (

func TestBaggageMembers(t *testing.T) {
t.Run("when non-zero values - should add all members to baggage", func(t *testing.T) {
const wantMemberCount = 12
const wantMemberCount = 10

want := map[string]string{
"val_str": "a_string",
"val_int": "42",
"val_int64": "42000000000",
"val_float64": "99.718281828",
"val_bool": "true",
"val_str_slice": "a_string_1,a_string_2,a_string_3",
"val_int_slice": "1,2,3",
"val_int64_slice": "100000,200000,300000",
"val_float64_slice": "1.1,2.2,3.3",
"val_bool_slice": "true,false,true,false",
}

m := model{
m := testModel{
ValStr: "a_string",
ValInt: 42,
ValInt64: 42000000000,
ValFloat32: 3.14,
ValFloat64: 99.718281828,
ValBool: true,
ValStrSlice: []string{"a_string_1", "a_string_2", "a_string_3"},
ValIntSlice: []int{1, 2, 3},
ValInt64Slice: []int64{100000, 200000, 300000},
ValFloat32Slice: []float32{1.1, 2.2, 3.3},
ValFloat64Slice: []float64{4.4, 5.5, 6.6},
ValFloat64Slice: []float64{1.1, 2.2, 3.3},
ValBoolSlice: []bool{true, false, true, false},
}

Expand All @@ -38,15 +49,22 @@ func TestBaggageMembers(t *testing.T) {
t.Errorf("\ngot %d members\nwant %d", memberCount, wantMemberCount)
}

for i := range bag.Members() {
t.Log(bag.Members()[i].Key())
for k, v := range want {
member := bag.Member(k)
if member.Value() != v {
t.Errorf("\ngot %q for member %q\nwant %q", member.Value(), k, v)
}
}
})

t.Run("when zero values and omitted - should not add members to baggage", func(t *testing.T) {
const wantMemberCount = 1

m := model{ValBool: true}
want := map[string]string{
"val_bool": "true",
}

m := testModel{ValBool: true}

members := oteltag.BaggageMembers(m)
bag, _ := baggage.New(members...)
Expand All @@ -57,11 +75,22 @@ func TestBaggageMembers(t *testing.T) {
if memberCount != wantMemberCount {
t.Errorf("\ngot %d members\nwant %d", memberCount, wantMemberCount)
}

for k, v := range want {
member := bag.Member(k)
if member.Value() != v {
t.Errorf("\ngot %q for member %q\nwant %q", member.Value(), k, v)
}
}
})

t.Run("when zero values and not omitted - should add members to baggage", func(t *testing.T) {
const wantMemberCount = 1

want := map[string]string{
"val_str_not_omitted": "",
}

m := struct {
ValStrNotOmitted string `otel:"val_str_not_omitted"`
}{
Expand All @@ -78,8 +107,31 @@ func TestBaggageMembers(t *testing.T) {
t.Errorf("\ngot %d members\nwant %d", memberCount, wantMemberCount)
}

for i := range bag.Members() {
t.Log(bag.Members()[i].Key())
for k, v := range want {
member := bag.Member(k)
if member.Value() != v {
t.Errorf("\ngot %q for member %q\nwant %q", member.Value(), k, v)
}
}
})

t.Run("when fields are not tagged - should not add members to baggage", func(t *testing.T) {
const wantMemberCount = 0

m := struct {
ValStrIgnored string
}{
ValStrIgnored: "a_string",
}

members := oteltag.BaggageMembers(m)
bag, _ := baggage.New(members...)
ctx := baggage.ContextWithBaggage(context.Background(), bag)
bag = baggage.FromContext(ctx)

memberCount := len(bag.Members())
if memberCount != wantMemberCount {
t.Errorf("\ngot %d members\nwant %d", memberCount, wantMemberCount)
}
})
}
9 changes: 4 additions & 5 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import (
"context"
"testing"

oteltag "github.com/remychantenay/otel-tag"
"go.opentelemetry.io/otel/attribute"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace"

oteltag "github.com/remychantenay/otel-tag"
)

var (
Expand All @@ -34,18 +35,16 @@ func BenchmarkSpanAttributes_With(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m := model{
m := testModel{
ValStr: "a_string",
ValInt: 42,
ValInt64: 42000000000,
ValFloat32: 3.14,
ValFloat64: 99.718281828,
ValBool: true,
ValStrSlice: []string{"a_string_1", "a_string_2", "a_string_3"},
ValIntSlice: []int{1, 2, 3},
ValInt64Slice: []int64{100000, 200000, 300000},
ValFloat32Slice: []float32{1.1, 2.2, 3.3},
ValFloat64Slice: []float64{4.4, 5.5, 6.6},
ValFloat64Slice: []float64{1.1, 2.2, 3.3},
ValBoolSlice: []bool{true, false, true, false},
}

Expand Down
Loading

0 comments on commit 93f960b

Please sign in to comment.