Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sample rate to logs if set #275

Merged
merged 7 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions otlp/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"math"
"net/http"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -40,6 +41,8 @@ const (
defaultServiceName = "unknown_service"
unknownLogSource = "unknown_log_source"

defaultSampleRate = int32(1)

// maxDepth is the maximum depth of a nested kvlist attribute that will be flattened.
// If the depth is exceeded, the attribute should be added as a JSON string instead.
maxDepth = 5
Expand Down Expand Up @@ -578,3 +581,68 @@ func shouldTrimTraceId(traceID []byte) bool {
}
return true
}

// Sample Rate must be a whole positive integer
func getSampleRate(attrs map[string]interface{}) int32 {
sampleRateKey := getSampleRateKey(attrs)
if sampleRateKey == "" {
return defaultSampleRate
}

sampleRate := defaultSampleRate
sampleRateVal := attrs[sampleRateKey]
switch v := sampleRateVal.(type) {
JamieDanielson marked this conversation as resolved.
Show resolved Hide resolved
case string:
if i, err := strconv.ParseFloat(v, 64); err == nil {
if i >= float64(math.MaxInt32) {
sampleRate = math.MaxInt32
} else {
j := int(i + 0.5)
sampleRate = int32(j)
}
}
case int32:
sampleRate = v
case int:
if v < math.MaxInt32 {
JamieDanielson marked this conversation as resolved.
Show resolved Hide resolved
sampleRate = int32(v)
} else {
sampleRate = math.MaxInt32
}
case int64:
if v < math.MaxInt32 {
sampleRate = int32(v)
} else {
sampleRate = math.MaxInt32
}
// Floats get rounded and converted to ints
case float32:
if v < math.MaxInt32 {
sampleRate = int32(v + 0.5)
} else {
sampleRate = math.MaxInt32
}
case float64:
if v < math.MaxInt32 {
sampleRate = int32(v + 0.5)
} else {
sampleRate = math.MaxInt32
}
}
// To make sampleRate consistent between Otel and Honeycomb, we coerce all 0 values to 1 here.
// Negative values are also invalid and so we convert to 1
if sampleRate <= 0 {
sampleRate = defaultSampleRate
}
delete(attrs, sampleRateKey) // remove attr
return sampleRate
}

func getSampleRateKey(attrs map[string]interface{}) string {
for key := range attrs {
if strings.EqualFold(key, "sampleRate") {
return "sampleRate"
}
}
return ""
}
82 changes: 82 additions & 0 deletions otlp/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"encoding/base64"
"encoding/hex"
"io"
"math"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"

Expand Down Expand Up @@ -776,3 +778,83 @@ func Test_ReadOtlpBodyTooLarge(t *testing.T) {
err = parseOtlpRequestBody(body, "application/protobuf", "other", request, 1)
require.Error(t, err)
}

func TestNoSampleRateKeyReturnOne(t *testing.T) {
attrs := map[string]interface{}{
"not_a_sample_rate": 10,
}
sampleRate := getSampleRate(attrs)
assert.Equal(t, int32(1), sampleRate)
}

func TestCanDetectSampleRateCapitalizations(t *testing.T) {
tests := []struct {
name string
attrs map[string]interface{}
}{
{"lowercase", map[string]interface{}{"samplerate": 10}},
{"UPPERCASE", map[string]interface{}{"SAMPLERATE": 10}},
{"camelCase", map[string]interface{}{"sampleRate": 10}},
{"PascalCase", map[string]interface{}{"SampleRate": 10}},
{"MiXeDcAsE", map[string]interface{}{"SaMpLeRaTe": 10}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := getSampleRateKey(tt.attrs)
assert.Equal(t, "sampleRate", key)
})

}
}

func TestGetSampleRateConversions(t *testing.T) {
testCases := []struct {
sampleRate interface{}
expected int32
}{
{sampleRate: nil, expected: 1},
{sampleRate: "0", expected: 1},
JamieDanielson marked this conversation as resolved.
Show resolved Hide resolved
{sampleRate: "1", expected: 1},
{sampleRate: "100", expected: 100},
{sampleRate: "100.0", expected: 100},
{sampleRate: "100.4", expected: 100},
{sampleRate: "100.6", expected: 101},
{sampleRate: "-100", expected: 1},
{sampleRate: "-100.0", expected: 1},
{sampleRate: "-100.6", expected: 1},
{sampleRate: "invalid", expected: 1},
{sampleRate: strconv.Itoa(math.MaxInt32), expected: math.MaxInt32},
{sampleRate: strconv.Itoa(math.MaxInt64), expected: math.MaxInt32},

{sampleRate: 0, expected: 1},
{sampleRate: 1, expected: 1},
{sampleRate: 100, expected: 100},
{sampleRate: 100.0, expected: 100},
{sampleRate: 100.4, expected: 100},
{sampleRate: 100.6, expected: 101},
{sampleRate: -100, expected: 1},
{sampleRate: -100.0, expected: 1},
{sampleRate: -100.6, expected: 1},
{sampleRate: math.MaxInt32, expected: math.MaxInt32},
{sampleRate: math.MaxInt64, expected: math.MaxInt32},

{sampleRate: int32(0), expected: 1},
{sampleRate: int32(1), expected: 1},
{sampleRate: int32(100), expected: 100},
{sampleRate: int32(math.MaxInt32), expected: math.MaxInt32},

{sampleRate: int64(0), expected: 1},
{sampleRate: int64(1), expected: 1},
{sampleRate: int64(100), expected: 100},
{sampleRate: int64(math.MaxInt32), expected: math.MaxInt32},
{sampleRate: int64(math.MaxInt64), expected: math.MaxInt32},
}

for _, tc := range testCases {
attrs := map[string]interface{}{
"sampleRate": tc.sampleRate,
}
assert.Equal(t, tc.expected, getSampleRate(attrs))
assert.Equal(t, 0, len(attrs))
}
}
4 changes: 4 additions & 0 deletions otlp/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,16 @@ func TranslateLogsRequest(ctx context.Context, request *collectorLogs.ExportLogs
AddAttributesToMap(ctx, attrs, log.Attributes)
}

// get sample rate after resource and scope attributes have been added
sampleRate := getSampleRate(attrs)

// Now we need to wrap the eventAttrs in an event so we can specify the timestamp
// which is the StartTime as a time.Time object
timestamp := time.Unix(0, int64(log.TimeUnixNano)).UTC()
events = append(events, Event{
Attributes: attrs,
Timestamp: timestamp,
SampleRate: sampleRate,
})
}
}
Expand Down
8 changes: 8 additions & 0 deletions otlp/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func TestTranslateLogsRequest(t *testing.T) {
assert.Equal(t, "debug", ev.Attributes["severity"])
assert.Equal(t, testServiceName, ev.Attributes["service.name"])
assert.Equal(t, "span_attr_val", ev.Attributes["span_attr"])
assert.Equal(t, int32(100), ev.SampleRate)
assert.Equal(t, "resource_attr_val", ev.Attributes["resource_attr"])
})
}
Expand Down Expand Up @@ -144,6 +145,7 @@ func TestTranslateHttpLogsRequest(t *testing.T) {
assert.Equal(t, "debug", ev.Attributes["severity"])
assert.Equal(t, "my-service", ev.Attributes["service.name"])
assert.Equal(t, "span_attr_val", ev.Attributes["span_attr"])
assert.Equal(t, int32(100), ev.SampleRate)
assert.Equal(t, "resource_attr_val", ev.Attributes["resource_attr"])
assert.Equal(t, "instr_scope_name", ev.Attributes["library.name"])
assert.Equal(t, "instr_scope_version", ev.Attributes["library.version"])
Expand Down Expand Up @@ -546,6 +548,12 @@ func buildExportLogsServiceRequest(traceID []byte, spanID []byte, startTimestamp
Value: &common.AnyValue_StringValue{StringValue: "span_attr_val"},
},
},
{
Key: "sampleRate",
Value: &common.AnyValue{
Value: &common.AnyValue_IntValue{IntValue: 100},
},
},
},
}},
}},
Expand Down
55 changes: 0 additions & 55 deletions otlp/traces.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"context"
"encoding/hex"
"io"
"math"
"strconv"
"time"

collectorTrace "go.opentelemetry.io/proto/otlp/collector/trace/v1"
Expand All @@ -18,7 +16,6 @@ const (
traceIDLongLength = 16
traceIDb64Length = 24
spanIDb64Length = 12
defaultSampleRate = int32(1)
)

// TranslateTraceRequestFromReader translates an OTLP/HTTP request into Honeycomb-friendly structure
Expand Down Expand Up @@ -249,55 +246,3 @@ func getSpanStatusCode(status *trace.Status) (int, bool) {
}
return int(status.Code), status.Code == trace.Status_STATUS_CODE_ERROR
}

func getSampleRate(attrs map[string]interface{}) int32 {
sampleRateKey := getSampleRateKey(attrs)
if sampleRateKey == "" {
return defaultSampleRate
}

sampleRate := defaultSampleRate
sampleRateVal := attrs[sampleRateKey]
switch v := sampleRateVal.(type) {
case string:
if i, err := strconv.Atoi(v); err == nil {
if i < math.MaxInt32 {
sampleRate = int32(i)
} else {
sampleRate = math.MaxInt32
}
}
case int32:
sampleRate = v
case int:
if v < math.MaxInt32 {
sampleRate = int32(v)
} else {
sampleRate = math.MaxInt32
}
case int64:
if v < math.MaxInt32 {
sampleRate = int32(v)
} else {
sampleRate = math.MaxInt32
}
}
// To make sampleRate consistent between Otel and Honeycomb, we coerce all 0 values to 1 here
// A value of 1 means the span was not sampled
// For full explanation, see https://app.asana.com/0/365940753298424/1201973146987622/f
if sampleRate == 0 {
sampleRate = defaultSampleRate
}
delete(attrs, sampleRateKey) // remove attr
return sampleRate
}

func getSampleRateKey(attrs map[string]interface{}) string {
if _, ok := attrs["sampleRate"]; ok {
return "sampleRate"
}
if _, ok := attrs["SampleRate"]; ok {
return "SampleRate"
}
return ""
}
67 changes: 0 additions & 67 deletions otlp/traces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"context"
"encoding/hex"
"io"
"math"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -744,71 +742,6 @@ func TestInvalidBodyReturnsError(t *testing.T) {
assert.Equal(t, ErrFailedParseBody, err)
}

func TestNoSampleRateKeyReturnOne(t *testing.T) {
attrs := map[string]interface{}{
"not_a_sample_rate": 10,
}
sampleRate := getSampleRate(attrs)
assert.Equal(t, int32(1), sampleRate)
}

func TestCanDetectSampleRateCapitalizations(t *testing.T) {
t.Run("lowercase", func(t *testing.T) {
attrs := map[string]interface{}{
"sampleRate": 10,
}
key := getSampleRateKey(attrs)
assert.Equal(t, "sampleRate", key)
})
t.Run("uppercase", func(t *testing.T) {
attrs := map[string]interface{}{
"SampleRate": 10,
}
key := getSampleRateKey(attrs)
assert.Equal(t, "SampleRate", key)
})
}

func TestGetSampleRateConversions(t *testing.T) {
testCases := []struct {
sampleRate interface{}
expected int32
}{
{sampleRate: nil, expected: 1},
{sampleRate: "0", expected: 1},
{sampleRate: "1", expected: 1},
{sampleRate: "100", expected: 100},
{sampleRate: "invalid", expected: 1},
{sampleRate: strconv.Itoa(math.MaxInt32), expected: math.MaxInt32},
{sampleRate: strconv.Itoa(math.MaxInt64), expected: math.MaxInt32},

{sampleRate: 0, expected: 1},
{sampleRate: 1, expected: 1},
{sampleRate: 100, expected: 100},
{sampleRate: math.MaxInt32, expected: math.MaxInt32},
{sampleRate: math.MaxInt64, expected: math.MaxInt32},

{sampleRate: int32(0), expected: 1},
{sampleRate: int32(1), expected: 1},
{sampleRate: int32(100), expected: 100},
{sampleRate: int32(math.MaxInt32), expected: math.MaxInt32},

{sampleRate: int64(0), expected: 1},
{sampleRate: int64(1), expected: 1},
{sampleRate: int64(100), expected: 100},
{sampleRate: int64(math.MaxInt32), expected: math.MaxInt32},
{sampleRate: int64(math.MaxInt64), expected: math.MaxInt32},
}

for _, tc := range testCases {
attrs := map[string]interface{}{
"sampleRate": tc.sampleRate,
}
assert.Equal(t, tc.expected, getSampleRate(attrs))
assert.Equal(t, 0, len(attrs))
}
}

func TestDefaultServiceNameApplied(t *testing.T) {
testCases := []struct {
name string
Expand Down
Loading