@@ -7,11 +7,17 @@ import (
77 "encoding/binary"
88 "errors"
99 "fmt"
10+ "net/url"
11+ "strings"
1012 "structs"
1113 "unsafe"
1214
15+ "go.opentelemetry.io/collector/pdata/pcommon"
16+ semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
17+ commonpb "go.opentelemetry.io/proto/otlp/common/v1"
1318 "google.golang.org/protobuf/proto"
1419
20+ "go.opentelemetry.io/ebpf-profiler/internal/log"
1521 "go.opentelemetry.io/ebpf-profiler/libpf"
1622 "go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe"
1723 processcontextpb "go.opentelemetry.io/ebpf-profiler/processcontext/v1development"
@@ -47,6 +53,12 @@ const (
4753
4854 // Offset of the MonotonicPublishedAtNs field in the header struct
4955 monotonicPublishedAtNsOffset = libpf .Address (unsafe .Offsetof (header {}.MonotonicPublishedAtNs ))
56+
57+ // resourceAttrKey is the environment variable name OpenTelemetry Resource information will be read from.
58+ resourceAttrKey = "OTEL_RESOURCE_ATTRIBUTES"
59+
60+ // svcNameKey is the environment variable name that Service Name information will be read from.
61+ svcNameKey = "OTEL_SERVICE_NAME"
5062)
5163
5264var (
6173)
6274
6375type Info struct {
64- Context * processcontextpb.ProcessContext
65- PublishedAtNs uint64
76+ Resource * pcommon.Resource
77+ ExtraAttributes * pcommon.Map
78+ PublishedAtNs uint64
6679}
6780
6881// header represents the 32-byte memory region header per OTEP #4719.
@@ -200,11 +213,151 @@ func readPayload(rm remotememory.RemoteMemory, hdr header) (Info, error) {
200213 return Info {}, fmt .Errorf ("failed to unmarshal ProcessContext: %w" , err )
201214 }
202215
203- return Info {Context : ctx , PublishedAtNs : hdr .MonotonicPublishedAtNs }, nil
216+ var resource * pcommon.Resource
217+ if ctx .Resource != nil {
218+ r := pcommon .NewResource ()
219+ for _ , attr := range ctx .Resource .Attributes {
220+ convertAnyValue (attr .Value ).MoveTo (r .Attributes ().PutEmpty (attr .Key ))
221+ }
222+ resource = & r
223+ }
224+
225+ var extraAttributes * pcommon.Map
226+ if ctx .ExtraAttributes != nil {
227+ m := pcommon .NewMap ()
228+ for _ , attr := range ctx .ExtraAttributes {
229+ convertAnyValue (attr .Value ).MoveTo (m .PutEmpty (attr .Key ))
230+ }
231+ extraAttributes = & m
232+ }
233+ return Info {Resource : resource , ExtraAttributes : extraAttributes , PublishedAtNs : hdr .MonotonicPublishedAtNs }, nil
204234}
205235
206236func (p * Info ) ClearExtraAttributes () {
207- if p .Context != nil {
208- p .Context .ExtraAttributes = nil
237+ // if p.Context != nil {
238+ // p.Context.ExtraAttributes = nil
239+ // }
240+ }
241+
242+ // convertAnyValue converts a commonpb.AnyValue to a pcommon.Value,
243+ // handling all value types including nested maps and arrays.
244+ func convertAnyValue (src * commonpb.AnyValue ) pcommon.Value {
245+ if src == nil {
246+ return pcommon .NewValueEmpty ()
247+ }
248+ switch v := src .Value .(type ) {
249+ case * commonpb.AnyValue_StringValue :
250+ return pcommon .NewValueStr (v .StringValue )
251+ case * commonpb.AnyValue_BoolValue :
252+ return pcommon .NewValueBool (v .BoolValue )
253+ case * commonpb.AnyValue_IntValue :
254+ return pcommon .NewValueInt (v .IntValue )
255+ case * commonpb.AnyValue_DoubleValue :
256+ return pcommon .NewValueDouble (v .DoubleValue )
257+ case * commonpb.AnyValue_BytesValue :
258+ val := pcommon .NewValueBytes ()
259+ val .Bytes ().FromRaw (v .BytesValue )
260+ return val
261+ case * commonpb.AnyValue_ArrayValue :
262+ val := pcommon .NewValueSlice ()
263+ if v .ArrayValue != nil {
264+ sl := val .Slice ()
265+ sl .EnsureCapacity (len (v .ArrayValue .Values ))
266+ for _ , item := range v .ArrayValue .Values {
267+ convertAnyValue (item ).MoveTo (sl .AppendEmpty ())
268+ }
269+ }
270+ return val
271+ case * commonpb.AnyValue_KvlistValue :
272+ val := pcommon .NewValueMap ()
273+ if v .KvlistValue != nil {
274+ m := val .Map ()
275+ m .EnsureCapacity (len (v .KvlistValue .Values ))
276+ for _ , kv := range v .KvlistValue .Values {
277+ convertAnyValue (kv .Value ).MoveTo (m .PutEmpty (kv .Key ))
278+ }
279+ }
280+ return val
281+ default :
282+ return pcommon .NewValueEmpty ()
283+ }
284+ }
285+
286+ func (p * Info ) addResourceStringAttribute (key string , value string ) {
287+ if p .Resource == nil {
288+ r := pcommon .NewResource ()
289+ p .Resource = & r
290+ }
291+ // Only add the attribute if it is not already present.
292+ if _ , ok := p .Resource .Attributes ().Get (key ); ok {
293+ return
294+ }
295+ p .Resource .Attributes ().PutStr (key , value )
296+ }
297+
298+ // AddEnvVars adds the given env vars to the ProcessContext as resource attributes.
299+ // OTEL_SERVICE_NAME is mapped to the service.name attribute.
300+ // OTEL_RESOURCE_ATTRIBUTES is parsed as comma-separated key=value pairs with
301+ // percent-encoded keys and values per the OTel resource SDK specification.
302+ // OTEL_SERVICE_NAME takes precedence over service.name in OTEL_RESOURCE_ATTRIBUTES.
303+ func (p * Info ) AddEnvVars (envVars map [libpf.String ]libpf.String ) {
304+ // Process OTEL_SERVICE_NAME first so it takes precedence over any
305+ // service.name key inside OTEL_RESOURCE_ATTRIBUTES (addResourceAttribute
306+ // skips keys that are already present).
307+ if value , ok := envVars [libpf .Intern (svcNameKey )]; ok {
308+ p .addResourceStringAttribute (string (semconv .ServiceNameKey ), value .String ())
309+ }
310+ if value , ok := envVars [libpf .Intern (resourceAttrKey )]; ok {
311+ p .parseResourceAttributes (value .String ())
312+ }
313+ }
314+
315+ // parseResourceAttributes parses the OTEL_RESOURCE_ATTRIBUTES env var value
316+ // as comma-separated key=value pairs where keys and values are percent-encoded.
317+ // On any decoding error the entire value is discarded.
318+ func (p * Info ) parseResourceAttributes (raw string ) {
319+ if raw == "" {
320+ return
321+ }
322+ // Parse into a temporary slice first so that on error we discard everything
323+ // per the OTel spec.
324+ type kv struct { key , value string }
325+ var pairs []kv
326+ for pair := range strings .SplitSeq (raw , "," ) {
327+ k , v , ok := strings .Cut (pair , "=" )
328+ if ! ok {
329+ log .Debugf ("OTEL_RESOURCE_ATTRIBUTES: discarding invalid value: missing '=' in %q" , pair )
330+ return
331+ }
332+ key , err := url .PathUnescape (k )
333+ if err != nil {
334+ log .Debugf ("OTEL_RESOURCE_ATTRIBUTES: discarding invalid value: %v" , err )
335+ return
336+ }
337+ value , err := url .PathUnescape (v )
338+ if err != nil {
339+ log .Debugf ("OTEL_RESOURCE_ATTRIBUTES: discarding invalid value: %v" , err )
340+ return
341+ }
342+ pairs = append (pairs , kv {key , value })
343+ }
344+ for _ , pair := range pairs {
345+ p .addResourceStringAttribute (pair .key , pair .value )
346+ }
347+ }
348+
349+ func ResourceToContextKey (resource * pcommon.Resource ) libpf.String {
350+ if resource == nil {
351+ return libpf .NullString
352+ }
353+ // Per semantic conventions, triplet of service.namespace, service.name, service.instance.id
354+ // must be globally unique.
355+ // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/service.md
356+ serviceNamespace , namespaceOk := resource .Attributes ().Get (string (semconv .ServiceNamespaceKey ))
357+ serviceName , nameOk := resource .Attributes ().Get (string (semconv .ServiceNameKey ))
358+ serviceInstanceID , instanceIdOk := resource .Attributes ().Get (string (semconv .ServiceInstanceIDKey ))
359+ if ! namespaceOk || ! nameOk || ! instanceIdOk {
360+ return libpf .NullString
209361 }
362+ return libpf .Intern (fmt .Sprintf ("%s:%s:%s" , serviceNamespace .Str (), serviceName .Str (), serviceInstanceID .Str ()))
210363}
0 commit comments