github.com/grafana/pyroscope@v1.18.0/pkg/ingester/otlp/convert.go (about) 1 package otlp 2 3 import ( 4 "encoding/hex" 5 "fmt" 6 "strings" 7 "time" 8 9 otelProfile "go.opentelemetry.io/proto/otlp/profiles/v1development" 10 11 googleProfile "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 12 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 13 pyromodel "github.com/grafana/pyroscope/pkg/model" 14 "github.com/grafana/pyroscope/pkg/pprof" 15 ) 16 17 const serviceNameKey = "service.name" 18 19 type convertedProfile struct { 20 profile *googleProfile.Profile 21 name *typesv1.LabelPair 22 } 23 24 func at[T any](arr []T, i int32) (T, error) { 25 if i >= 0 && int(i) < len(arr) { 26 return arr[i], nil 27 } 28 var zero T 29 return zero, fmt.Errorf("index %d out of bounds", i) 30 } 31 32 // ConvertOtelToGoogle converts an OpenTelemetry profile to a Google profile. 33 func ConvertOtelToGoogle(src *otelProfile.Profile, dictionary *otelProfile.ProfilesDictionary) (map[string]convertedProfile, error) { 34 svc2Profile := make(map[string]*profileBuilder) 35 for _, sample := range src.Samples { 36 svc, err := serviceNameFromSample(sample, dictionary) 37 if err != nil { 38 return make(map[string]convertedProfile), nil 39 } 40 41 p, ok := svc2Profile[svc] 42 if !ok { 43 p, err = newProfileBuilder(src, dictionary) 44 if err != nil { 45 return nil, err 46 } 47 svc2Profile[svc] = p 48 } 49 if _, err := p.convertSampleBack(sample, dictionary); err != nil { 50 return nil, err 51 } 52 } 53 54 result := make(map[string]convertedProfile) 55 for svc, p := range svc2Profile { 56 result[svc] = convertedProfile{p.dst, p.name} 57 } 58 59 return result, nil 60 } 61 62 type sampleConversionType int 63 64 const ( 65 sampleConversionTypeNone sampleConversionType = 0 66 sampleConversionTypeSamplesToNanos sampleConversionType = 1 67 sampleConversionTypeSumEvents sampleConversionType = 2 68 ) 69 70 type profileBuilder struct { 71 src *otelProfile.Profile 72 dst *googleProfile.Profile 73 stringMap map[string]int64 74 functionMap map[*otelProfile.Function]uint64 75 unsymbolziedFuncNameMap map[string]uint64 76 locationMap map[*otelProfile.Location]uint64 77 mappingMap map[*otelProfile.Mapping]uint64 78 79 sampleProcessingTypes []sampleConversionType 80 name *typesv1.LabelPair 81 } 82 83 func newProfileBuilder(src *otelProfile.Profile, dictionary *otelProfile.ProfilesDictionary) (*profileBuilder, error) { 84 res := &profileBuilder{ 85 src: src, 86 stringMap: make(map[string]int64), 87 functionMap: make(map[*otelProfile.Function]uint64), 88 locationMap: make(map[*otelProfile.Location]uint64), 89 mappingMap: make(map[*otelProfile.Mapping]uint64), 90 unsymbolziedFuncNameMap: make(map[string]uint64), 91 dst: &googleProfile.Profile{ 92 TimeNanos: int64(src.TimeUnixNano), 93 DurationNanos: int64(src.DurationNano), 94 Period: src.Period, 95 }, 96 } 97 res.addstr("") 98 99 if src.SampleType == nil { 100 return nil, fmt.Errorf("sample type is missing") 101 } 102 sampleType, err := res.convertSampleTypeBack(src.SampleType, dictionary) 103 if err != nil { 104 return nil, err 105 } 106 res.dst.SampleType = []*googleProfile.ValueType{sampleType} 107 108 periodType, err := res.convertValueTypeBack(src.PeriodType, dictionary) 109 if err != nil { 110 return nil, err 111 } 112 res.dst.PeriodType = periodType 113 114 var defaultSampleTypeLabel string 115 if src.SampleType != nil { 116 defaultSampleType := src.SampleType 117 defaultSampleTypeLabel, err = at(dictionary.StringTable, defaultSampleType.TypeStrindex) 118 if err != nil { 119 return nil, fmt.Errorf("could not access default sample type label: %w", err) 120 } 121 } else { 122 defaultSampleTypeLabel = "samples" 123 } 124 res.dst.DefaultSampleType = res.addstr(defaultSampleTypeLabel) 125 126 if len(res.dst.SampleType) == 0 { 127 res.dst.SampleType = []*googleProfile.ValueType{{ 128 Type: res.addstr(defaultSampleTypeLabel), 129 Unit: res.addstr("ms"), 130 }} 131 res.dst.DefaultSampleType = res.addstr(defaultSampleTypeLabel) 132 } 133 res.sampleProcessingTypes = make([]sampleConversionType, len(res.dst.SampleType)) 134 for i := 0; i < len(res.dst.SampleType); i++ { 135 profileType := res.profileType(i) 136 if profileType == "samples:count:cpu:nanoseconds" { 137 res.dst.SampleType[i] = &googleProfile.ValueType{ 138 Type: res.addstr("cpu"), 139 Unit: res.addstr("nanoseconds"), 140 } 141 if len(res.dst.SampleType) == 1 { 142 res.name = &typesv1.LabelPair{ 143 Name: pyromodel.LabelNameProfileName, 144 Value: "process_cpu", 145 } 146 } 147 res.sampleProcessingTypes[i] = sampleConversionTypeSamplesToNanos 148 } else if profileType == "events:nanoseconds::" && len(res.dst.SampleType) == 1 { // Identify off-CPU profiles 149 150 res.sampleProcessingTypes[i] = sampleConversionTypeSumEvents 151 res.name = &typesv1.LabelPair{ 152 Name: pyromodel.LabelNameProfileName, 153 Value: pyromodel.ProfileNameOffCpu, 154 } 155 } else { // Custom profile type 156 // Try to extract profile name from the type, e.g. "wall:time:cpu:milliseconds" -> "wall" 157 parts := strings.Split(profileType, `:`) 158 if len(parts) >= 3 { 159 res.name = &typesv1.LabelPair{ 160 Name: pyromodel.LabelNameProfileName, 161 Value: parts[2], 162 } 163 res.sampleProcessingTypes[i] = sampleConversionTypeNone 164 } 165 } 166 } 167 if res.name == nil { 168 res.name = &typesv1.LabelPair{ 169 Name: pyromodel.LabelNameProfileName, 170 Value: "process_cpu", // guess 171 } 172 } 173 174 if res.dst.TimeNanos == 0 { 175 res.dst.TimeNanos = time.Now().UnixNano() 176 } 177 if res.dst.DurationNanos == 0 { 178 res.dst.DurationNanos = (time.Second * 10).Nanoseconds() 179 } 180 return res, nil 181 } 182 183 func (p *profileBuilder) profileType(idx int) string { 184 var ( 185 periodType, periodUnit string 186 ) 187 if p.dst.PeriodType != nil && p.dst.Period != 0 { 188 periodType = p.dst.StringTable[p.dst.PeriodType.Type] 189 periodUnit = p.dst.StringTable[p.dst.PeriodType.Unit] 190 } 191 return fmt.Sprintf("%s:%s:%s:%s", 192 p.dst.StringTable[p.dst.SampleType[idx].Type], 193 p.dst.StringTable[p.dst.SampleType[idx].Unit], 194 periodType, 195 periodUnit, 196 ) 197 } 198 199 func (p *profileBuilder) addstr(s string) int64 { 200 if i, ok := p.stringMap[s]; ok { 201 return i 202 } 203 idx := int64(len(p.dst.StringTable)) 204 p.stringMap[s] = idx 205 p.dst.StringTable = append(p.dst.StringTable, s) 206 return idx 207 } 208 209 func serviceNameFromSample(sample *otelProfile.Sample, dictionary *otelProfile.ProfilesDictionary) (string, error) { 210 return getAttributeValueByKeyOrEmpty(sample.AttributeIndices, dictionary, serviceNameKey) 211 } 212 213 func (p *profileBuilder) convertSampleTypeBack(ost *otelProfile.ValueType, dictionary *otelProfile.ProfilesDictionary) (*googleProfile.ValueType, error) { 214 gst, err := p.convertValueTypeBack(ost, dictionary) 215 if err != nil { 216 return nil, fmt.Errorf("could not process sample type: %w", err) 217 } 218 return gst, nil 219 } 220 221 func (p *profileBuilder) convertValueTypeBack(ovt *otelProfile.ValueType, dictionary *otelProfile.ProfilesDictionary) (*googleProfile.ValueType, error) { 222 if ovt == nil { 223 return nil, nil 224 } 225 typeLabel, err := at(dictionary.StringTable, ovt.TypeStrindex) 226 if err != nil { 227 return nil, fmt.Errorf("could not access type string: %w", err) 228 } 229 unitLabel, err := at(dictionary.StringTable, ovt.UnitStrindex) 230 if err != nil { 231 return nil, fmt.Errorf("could not access unit string: %w", err) 232 } 233 return &googleProfile.ValueType{Type: p.addstr(typeLabel), Unit: p.addstr(unitLabel)}, nil 234 } 235 236 func (p *profileBuilder) convertLocationBack(ol *otelProfile.Location, dictionary *otelProfile.ProfilesDictionary) (uint64, error) { 237 if i, ok := p.locationMap[ol]; ok { 238 return i, nil 239 } 240 lmi := ol.GetMappingIndex() 241 om, err := at(dictionary.MappingTable, lmi) 242 if err != nil { 243 return 0, fmt.Errorf("could not access mapping: %w", err) 244 } 245 246 mappingId, ok := p.mappingMap[om] 247 if !ok { 248 return 0, fmt.Errorf("mapping not found in mappingMap") 249 } 250 gl := &googleProfile.Location{ 251 MappingId: mappingId, 252 Address: ol.Address, 253 Line: make([]*googleProfile.Line, len(ol.Lines)), 254 } 255 256 for i, line := range ol.Lines { 257 gl.Line[i], err = p.convertLineBack(line, dictionary) 258 if err != nil { 259 return 0, fmt.Errorf("could not process line at index %d: %w", i, err) 260 } 261 } 262 263 p.dst.Location = append(p.dst.Location, gl) 264 gl.Id = uint64(len(p.dst.Location)) 265 p.locationMap[ol] = gl.Id 266 return gl.Id, nil 267 } 268 269 // convertLineBack converts an OpenTelemetry Line to a Google Line. 270 func (p *profileBuilder) convertLineBack(ol *otelProfile.Line, dictionary *otelProfile.ProfilesDictionary) (*googleProfile.Line, error) { 271 function, err := at(dictionary.FunctionTable, ol.FunctionIndex) 272 if err != nil { 273 return nil, fmt.Errorf("could not access function: %w", err) 274 } 275 functionId, err := p.convertFunctionBack(function, dictionary) 276 if err != nil { 277 return nil, err 278 } 279 return &googleProfile.Line{FunctionId: functionId, Line: ol.Line}, nil 280 } 281 282 func (p *profileBuilder) convertFunctionBack(of *otelProfile.Function, dictionary *otelProfile.ProfilesDictionary) (uint64, error) { 283 if i, ok := p.functionMap[of]; ok { 284 return i, nil 285 } 286 nameLabel, err := at(dictionary.StringTable, of.NameStrindex) 287 if err != nil { 288 return 0, fmt.Errorf("could not access function name string: %w", err) 289 } 290 systemNameLabel, err := at(dictionary.StringTable, of.SystemNameStrindex) 291 if err != nil { 292 return 0, fmt.Errorf("could not access function system name string: %w", err) 293 } 294 filenameLabel, err := at(dictionary.StringTable, of.FilenameStrindex) 295 if err != nil { 296 return 0, fmt.Errorf("could not access function file name string: %w", err) 297 } 298 gf := &googleProfile.Function{ 299 Name: p.addstr(nameLabel), 300 SystemName: p.addstr(systemNameLabel), 301 Filename: p.addstr(filenameLabel), 302 StartLine: of.StartLine, 303 } 304 p.dst.Function = append(p.dst.Function, gf) 305 gf.Id = uint64(len(p.dst.Function)) 306 p.functionMap[of] = gf.Id 307 return gf.Id, nil 308 } 309 310 func (p *profileBuilder) convertSampleBack(os *otelProfile.Sample, dictionary *otelProfile.ProfilesDictionary) (*googleProfile.Sample, error) { 311 gs := &googleProfile.Sample{ 312 Value: os.Values, 313 } 314 315 // According to spec, samples can come without values, in which case we assume that each timestamp occurrence has value of 1. 316 // See: https://github.com/open-telemetry/opentelemetry-proto/blob/81d6676cdc30dddb0ec1f87d080e6dac07ab214f/opentelemetry/proto/profiles/v1development/profiles.proto#L351-L353 317 if len(gs.Value) == 0 && len(os.TimestampsUnixNano) > 0 { 318 gs.Value = []int64{int64(len(os.TimestampsUnixNano))} 319 } 320 321 if len(gs.Value) == 0 { 322 return nil, fmt.Errorf("sample value is required") 323 } 324 325 for i, typ := range p.sampleProcessingTypes { 326 switch typ { 327 case sampleConversionTypeSamplesToNanos: 328 gs.Value[i] *= p.src.Period 329 case sampleConversionTypeSumEvents: 330 // For off-CPU profiles, aggregate all sample values into a single sum 331 // since pprof cannot represent variable-length sample values 332 sum := int64(0) 333 for _, v := range gs.Value { 334 sum += v 335 } 336 gs.Value = []int64{sum} 337 } 338 } 339 if p.dst.Period != 0 && p.dst.PeriodType != nil && len(gs.Value) != len(p.dst.SampleType) { 340 return nil, fmt.Errorf("sample values length mismatch %d %d", len(gs.Value), len(p.dst.SampleType)) 341 } 342 343 err := p.convertSampleAttributesToLabelsBack(os, dictionary, gs) 344 if err != nil { 345 return nil, err 346 } 347 348 stackIndex := os.GetStackIndex() 349 if stackIndex < 0 || int(stackIndex) >= len(dictionary.StackTable) { 350 return nil, fmt.Errorf("invalid stack index: %d", stackIndex) 351 } 352 stack := dictionary.StackTable[stackIndex] 353 354 // First, gather map of locations pointing to each mapping 355 locationMap := make(map[*otelProfile.Mapping][]*otelProfile.Location) 356 357 for _, locIdx := range stack.LocationIndices { 358 loc, err := at(dictionary.LocationTable, locIdx) 359 if err != nil { 360 return nil, fmt.Errorf("could not access location at index %d: %w", locIdx, err) 361 } 362 mapping, err := at(dictionary.MappingTable, loc.GetMappingIndex()) 363 if err != nil { 364 return nil, fmt.Errorf("could not access mapping at index %d: %w", loc.GetMappingIndex(), err) 365 } 366 locationMap[mapping] = append(locationMap[mapping], loc) 367 } 368 369 // Now, convert each mapping, based on information from all locations that point to it 370 for mapping, locs := range locationMap { 371 _, err := p.convertMappingBack(locs, mapping, dictionary) 372 if err != nil { 373 return nil, err 374 } 375 } 376 377 for _, olocIdx := range stack.LocationIndices { 378 oloc, err := at(dictionary.LocationTable, olocIdx) 379 if err != nil { 380 return nil, fmt.Errorf("could not access location at index %d: %w", olocIdx, err) 381 } 382 loc, err := p.convertLocationBack(oloc, dictionary) 383 if err != nil { 384 return nil, err 385 } 386 gs.LocationId = append(gs.LocationId, loc) 387 } 388 389 p.dst.Sample = append(p.dst.Sample, gs) 390 391 return gs, nil 392 } 393 394 func (p *profileBuilder) convertSampleAttributesToLabelsBack(os *otelProfile.Sample, dictionary *otelProfile.ProfilesDictionary, gs *googleProfile.Sample) error { 395 gs.Label = make([]*googleProfile.Label, 0, len(os.AttributeIndices)) 396 for i, attributeIdx := range os.AttributeIndices { 397 attribute, err := at(dictionary.AttributeTable, attributeIdx) 398 if err != nil { 399 return fmt.Errorf("could not access attribute at index %d: %w", i, err) 400 } 401 if keyStr, err := at(dictionary.StringTable, attribute.KeyStrindex); err == nil && keyStr == serviceNameKey { 402 continue 403 } 404 if attribute.Value.GetStringValue() != "" { 405 keyStr, err := at(dictionary.StringTable, attribute.KeyStrindex) 406 if err != nil { 407 return fmt.Errorf("could not access attribute key: %w", err) 408 } 409 gs.Label = append(gs.Label, &googleProfile.Label{ 410 Key: p.addstr(keyStr), 411 Str: p.addstr(attribute.Value.GetStringValue()), 412 }) 413 } 414 } 415 416 if os.GetLinkIndex() != 0 { 417 link, err := at(dictionary.LinkTable, os.GetLinkIndex()) 418 if err != nil { 419 return fmt.Errorf("could not access link at index %d: %w", os.GetLinkIndex(), err) 420 } 421 gs.Label = append(gs.Label, &googleProfile.Label{ 422 Key: p.addstr(pprof.SpanIDLabelName), 423 Str: p.addstr(hex.EncodeToString(link.GetSpanId())), 424 }) 425 } 426 427 return nil 428 } 429 430 // convertMappingBack converts an OpenTelemetry Mapping to a Google Mapping taking into account availability 431 // of symbol data in all locations that point to this mapping. 432 func (p *profileBuilder) convertMappingBack(ols []*otelProfile.Location, om *otelProfile.Mapping, dictionary *otelProfile.ProfilesDictionary) (uint64, error) { 433 hasLines := true 434 hasFunctions := true 435 hasInlineFrames := true 436 hasFilenames := true 437 438 /* 439 We survey all locations that point to this mapping to determine 440 whether the mapping has functions, filenames, line numbers, inline frames. 441 Note that even if a single location does not have symbol information, 442 the mapping is marked as not having that information. 443 */ 444 for _, ol := range ols { 445 // If at least one location belonging to mapping does not have lines, we must flag whole mapping as not having symbol info. 446 if len(ol.Lines) == 0 { 447 hasLines = false 448 hasFunctions = false 449 hasInlineFrames = false 450 hasFilenames = false 451 } 452 453 // If by this point we know that mapping has no symbol info, we can stop checking other locations. 454 if !hasLines && !hasFunctions && !hasInlineFrames && !hasFilenames { 455 break 456 } 457 458 for i, line := range ol.Lines { 459 hasFunctions = hasFunctions && line.FunctionIndex > 0 460 hasLines = hasLines && line.Line > 0 461 hasInlineFrames = hasInlineFrames && i >= 1 462 463 if line.FunctionIndex > 0 { 464 function, _ := at(dictionary.FunctionTable, line.FunctionIndex) 465 if function != nil { 466 hasFilenames = hasFilenames && function.FilenameStrindex > 0 467 } 468 } 469 } 470 } 471 472 buildID, _ := getAttributeValueByKeyOrEmpty(om.AttributeIndices, dictionary, "process.executable.build_id.gnu") 473 filenameLabel, err := at(dictionary.StringTable, om.FilenameStrindex) 474 if err != nil { 475 return 0, fmt.Errorf("could not access mapping file name string: %w", err) 476 } 477 gm := &googleProfile.Mapping{ 478 MemoryStart: om.MemoryStart, 479 MemoryLimit: om.MemoryLimit, 480 FileOffset: om.FileOffset, 481 Filename: p.addstr(filenameLabel), 482 BuildId: p.addstr(buildID), 483 HasFunctions: hasFunctions, 484 HasFilenames: hasFilenames, 485 HasLineNumbers: hasLines, 486 HasInlineFrames: hasInlineFrames, 487 } 488 p.dst.Mapping = append(p.dst.Mapping, gm) 489 gm.Id = uint64(len(p.dst.Mapping)) 490 p.mappingMap[om] = gm.Id 491 return gm.Id, nil 492 } 493 494 // This function extracts the value of a specific attribute key from a list of attribute indices. 495 // TODO: build a map instead of iterating every time. 496 func getAttributeValueByKeyOrEmpty(attributeIndices []int32, dictionary *otelProfile.ProfilesDictionary, key string) (string, error) { 497 for i, attributeIndex := range attributeIndices { 498 attr, err := at(dictionary.AttributeTable, attributeIndex) 499 if err != nil { 500 return "", fmt.Errorf("attribute not found: %d: %w", i, err) 501 } 502 keyStr, err := at(dictionary.StringTable, attr.KeyStrindex) 503 if err != nil { 504 return "", fmt.Errorf("attribute key string not found %d: %w", i, err) 505 } 506 if keyStr == key { 507 return attr.Value.GetStringValue(), nil 508 } 509 } 510 return "", nil 511 }