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 1d5cd05
Show file tree
Hide file tree
Showing 12 changed files with 456 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 }}
49 changes: 42 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
# 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.

## 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 1d5cd05

Please sign in to comment.