diff --git a/api/connect-openapi/gen/ingester/v1/ingester.openapi.yaml b/api/connect-openapi/gen/ingester/v1/ingester.openapi.yaml index a0c99129ea..d2b70f4f04 100644 --- a/api/connect-openapi/gen/ingester/v1/ingester.openapi.yaml +++ b/api/connect-openapi/gen/ingester/v1/ingester.openapi.yaml @@ -1048,6 +1048,56 @@ components: title: labels title: BlockInfo additionalProperties: false + types.v1.Exemplar: + type: object + properties: + timestamp: + type: + - integer + - string + examples: + - "1730000023000" + title: timestamp + format: int64 + description: Milliseconds since epoch when the profile was captured. + profileId: + type: string + examples: + - 7c9e6679-7425-40de-944b-e07fc1f90ae7 + title: profile_id + description: Unique identifier for the profile (UUID). + spanId: + type: string + examples: + - 00f067aa0ba902b7 + title: span_id + description: Span ID if this profile was split by span during ingestion. + value: + type: + - integer + - string + examples: + - "2450000000" + title: value + format: int64 + description: Total sample value for this profile (e.g., CPU nanoseconds, bytes allocated). + labels: + type: array + items: + $ref: '#/components/schemas/types.v1.LabelPair' + title: labels + description: |- + Series labels that are NOT included in the group_by query parameter. + These labels complete the full series identity of this exemplar's profile. + For example, if group_by=["service"], this would contain labels like "pod", + "namespace", "region" that were omitted from grouping. This allows identifying + which specific series instance this profile sample came from. + title: Exemplar + additionalProperties: false + description: |- + Exemplar represents metadata for an individual profile sample. + Exemplars allow users to drill down from aggregated timeline views + to specific profile instances. types.v1.GetProfileStatsRequest: type: object title: GetProfileStatsRequest @@ -1229,6 +1279,12 @@ components: items: $ref: '#/components/schemas/types.v1.ProfileAnnotation' title: annotations + exemplars: + type: array + items: + $ref: '#/components/schemas/types.v1.Exemplar' + title: exemplars + description: Exemplars are samples of individual profiles that contributed to this aggregated point title: Point additionalProperties: false types.v1.ProfileAnnotation: diff --git a/api/connect-openapi/gen/querier/v1/querier.openapi.yaml b/api/connect-openapi/gen/querier/v1/querier.openapi.yaml index c50ae05f0e..7dfdf8ea07 100644 --- a/api/connect-openapi/gen/querier/v1/querier.openapi.yaml +++ b/api/connect-openapi/gen/querier/v1/querier.openapi.yaml @@ -1345,6 +1345,16 @@ components: description: Select stack traces that match the provided selector. nullable: true $ref: '#/components/schemas/types.v1.StackTraceSelector' + profileIdSelector: + type: array + examples: + - - 7c9e6679-7425-40de-944b-e07fc1f90ae7 + items: + type: string + examples: + - - 7c9e6679-7425-40de-944b-e07fc1f90ae7 + title: profile_id_selector + description: List of Profile UUIDs to query title: SelectMergeStacktracesRequest additionalProperties: false querier.v1.SelectMergeStacktracesResponse: @@ -1426,6 +1436,10 @@ components: format: int64 description: Select the top N series by total value. nullable: true + exemplarType: + title: exemplar_type + description: Type of exemplars to include in the response. + $ref: '#/components/schemas/types.v1.ExemplarType' title: SelectSeriesRequest additionalProperties: false querier.v1.SelectSeriesResponse: @@ -1491,6 +1505,64 @@ components: title: labels_set title: SeriesResponse additionalProperties: false + types.v1.Exemplar: + type: object + properties: + timestamp: + type: + - integer + - string + examples: + - "1730000023000" + title: timestamp + format: int64 + description: Milliseconds since epoch when the profile was captured. + profileId: + type: string + examples: + - 7c9e6679-7425-40de-944b-e07fc1f90ae7 + title: profile_id + description: Unique identifier for the profile (UUID). + spanId: + type: string + examples: + - 00f067aa0ba902b7 + title: span_id + description: Span ID if this profile was split by span during ingestion. + value: + type: + - integer + - string + examples: + - "2450000000" + title: value + format: int64 + description: Total sample value for this profile (e.g., CPU nanoseconds, bytes allocated). + labels: + type: array + items: + $ref: '#/components/schemas/types.v1.LabelPair' + title: labels + description: |- + Series labels that are NOT included in the group_by query parameter. + These labels complete the full series identity of this exemplar's profile. + For example, if group_by=["service"], this would contain labels like "pod", + "namespace", "region" that were omitted from grouping. This allows identifying + which specific series instance this profile sample came from. + title: Exemplar + additionalProperties: false + description: |- + Exemplar represents metadata for an individual profile sample. + Exemplars allow users to drill down from aggregated timeline views + to specific profile instances. + types.v1.ExemplarType: + type: string + title: ExemplarType + enum: + - EXEMPLAR_TYPE_UNSPECIFIED + - EXEMPLAR_TYPE_NONE + - EXEMPLAR_TYPE_INDIVIDUAL + - EXEMPLAR_TYPE_SPAN types.v1.GetProfileStatsRequest: type: object title: GetProfileStatsRequest @@ -1672,6 +1744,12 @@ components: items: $ref: '#/components/schemas/types.v1.ProfileAnnotation' title: annotations + exemplars: + type: array + items: + $ref: '#/components/schemas/types.v1.Exemplar' + title: exemplars + description: Exemplars are samples of individual profiles that contributed to this aggregated point title: Point additionalProperties: false types.v1.ProfileAnnotation: diff --git a/api/connect-openapi/gen/query/v1/query.openapi.yaml b/api/connect-openapi/gen/query/v1/query.openapi.yaml index 53be2622f6..7cfffaaec0 100644 --- a/api/connect-openapi/gen/query/v1/query.openapi.yaml +++ b/api/connect-openapi/gen/query/v1/query.openapi.yaml @@ -678,6 +678,9 @@ components: - string title: limit format: int64 + exemplarType: + title: exemplar_type + $ref: '#/components/schemas/types.v1.ExemplarType' title: TimeSeriesQuery additionalProperties: false query.v1.TimeSeriesReport: @@ -725,6 +728,64 @@ components: format: byte title: TreeReport additionalProperties: false + types.v1.Exemplar: + type: object + properties: + timestamp: + type: + - integer + - string + examples: + - "1730000023000" + title: timestamp + format: int64 + description: Milliseconds since epoch when the profile was captured. + profileId: + type: string + examples: + - 7c9e6679-7425-40de-944b-e07fc1f90ae7 + title: profile_id + description: Unique identifier for the profile (UUID). + spanId: + type: string + examples: + - 00f067aa0ba902b7 + title: span_id + description: Span ID if this profile was split by span during ingestion. + value: + type: + - integer + - string + examples: + - "2450000000" + title: value + format: int64 + description: Total sample value for this profile (e.g., CPU nanoseconds, bytes allocated). + labels: + type: array + items: + $ref: '#/components/schemas/types.v1.LabelPair' + title: labels + description: |- + Series labels that are NOT included in the group_by query parameter. + These labels complete the full series identity of this exemplar's profile. + For example, if group_by=["service"], this would contain labels like "pod", + "namespace", "region" that were omitted from grouping. This allows identifying + which specific series instance this profile sample came from. + title: Exemplar + additionalProperties: false + description: |- + Exemplar represents metadata for an individual profile sample. + Exemplars allow users to drill down from aggregated timeline views + to specific profile instances. + types.v1.ExemplarType: + type: string + title: ExemplarType + enum: + - EXEMPLAR_TYPE_UNSPECIFIED + - EXEMPLAR_TYPE_NONE + - EXEMPLAR_TYPE_INDIVIDUAL + - EXEMPLAR_TYPE_SPAN types.v1.GoPGO: type: object properties: @@ -795,6 +856,12 @@ components: items: $ref: '#/components/schemas/types.v1.ProfileAnnotation' title: annotations + exemplars: + type: array + items: + $ref: '#/components/schemas/types.v1.Exemplar' + title: exemplars + description: Exemplars are samples of individual profiles that contributed to this aggregated point title: Point additionalProperties: false types.v1.ProfileAnnotation: diff --git a/api/connect-openapi/gen/storegateway/v1/storegateway.openapi.yaml b/api/connect-openapi/gen/storegateway/v1/storegateway.openapi.yaml index 4dc89947f0..ed59a8f9df 100644 --- a/api/connect-openapi/gen/storegateway/v1/storegateway.openapi.yaml +++ b/api/connect-openapi/gen/storegateway/v1/storegateway.openapi.yaml @@ -867,6 +867,56 @@ components: title: labels title: BlockInfo additionalProperties: false + types.v1.Exemplar: + type: object + properties: + timestamp: + type: + - integer + - string + examples: + - "1730000023000" + title: timestamp + format: int64 + description: Milliseconds since epoch when the profile was captured. + profileId: + type: string + examples: + - 7c9e6679-7425-40de-944b-e07fc1f90ae7 + title: profile_id + description: Unique identifier for the profile (UUID). + spanId: + type: string + examples: + - 00f067aa0ba902b7 + title: span_id + description: Span ID if this profile was split by span during ingestion. + value: + type: + - integer + - string + examples: + - "2450000000" + title: value + format: int64 + description: Total sample value for this profile (e.g., CPU nanoseconds, bytes allocated). + labels: + type: array + items: + $ref: '#/components/schemas/types.v1.LabelPair' + title: labels + description: |- + Series labels that are NOT included in the group_by query parameter. + These labels complete the full series identity of this exemplar's profile. + For example, if group_by=["service"], this would contain labels like "pod", + "namespace", "region" that were omitted from grouping. This allows identifying + which specific series instance this profile sample came from. + title: Exemplar + additionalProperties: false + description: |- + Exemplar represents metadata for an individual profile sample. + Exemplars allow users to drill down from aggregated timeline views + to specific profile instances. types.v1.GoPGO: type: object properties: @@ -1021,6 +1071,12 @@ components: items: $ref: '#/components/schemas/types.v1.ProfileAnnotation' title: annotations + exemplars: + type: array + items: + $ref: '#/components/schemas/types.v1.Exemplar' + title: exemplars + description: Exemplars are samples of individual profiles that contributed to this aggregated point title: Point additionalProperties: false types.v1.ProfileAnnotation: diff --git a/api/gen/proto/go/querier/v1/querier.pb.go b/api/gen/proto/go/querier/v1/querier.pb.go index 3b4cf211a2..4ba89f377c 100644 --- a/api/gen/proto/go/querier/v1/querier.pb.go +++ b/api/gen/proto/go/querier/v1/querier.pb.go @@ -313,8 +313,10 @@ type SelectMergeStacktracesRequest struct { Format ProfileFormat `protobuf:"varint,6,opt,name=format,proto3,enum=querier.v1.ProfileFormat" json:"format,omitempty"` // Select stack traces that match the provided selector. StackTraceSelector *v1.StackTraceSelector `protobuf:"bytes,7,opt,name=stack_trace_selector,json=stackTraceSelector,proto3,oneof" json:"stack_trace_selector,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // List of Profile UUIDs to query + ProfileIdSelector []string `protobuf:"bytes,8,rep,name=profile_id_selector,json=profileIdSelector,proto3" json:"profile_id_selector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SelectMergeStacktracesRequest) Reset() { @@ -396,6 +398,13 @@ func (x *SelectMergeStacktracesRequest) GetStackTraceSelector() *v1.StackTraceSe return nil } +func (x *SelectMergeStacktracesRequest) GetProfileIdSelector() []string { + if x != nil { + return x.ProfileIdSelector + } + return nil +} + type SelectMergeStacktracesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Flamegraph *FlameGraph `protobuf:"bytes,1,opt,name=flamegraph,proto3" json:"flamegraph,omitempty"` @@ -1006,7 +1015,9 @@ type SelectSeriesRequest struct { // Select stack traces that match the provided selector. StackTraceSelector *v1.StackTraceSelector `protobuf:"bytes,8,opt,name=stack_trace_selector,json=stackTraceSelector,proto3,oneof" json:"stack_trace_selector,omitempty"` // Select the top N series by total value. - Limit *int64 `protobuf:"varint,9,opt,name=limit,proto3,oneof" json:"limit,omitempty"` + Limit *int64 `protobuf:"varint,9,opt,name=limit,proto3,oneof" json:"limit,omitempty"` + // Type of exemplars to include in the response. + ExemplarType v1.ExemplarType `protobuf:"varint,10,opt,name=exemplar_type,json=exemplarType,proto3,enum=types.v1.ExemplarType" json:"exemplar_type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1104,6 +1115,13 @@ func (x *SelectSeriesRequest) GetLimit() int64 { return 0 } +func (x *SelectSeriesRequest) GetExemplarType() v1.ExemplarType { + if x != nil { + return x.ExemplarType + } + return v1.ExemplarType(0) +} + type SelectSeriesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Series []*v1.Series `protobuf:"bytes,1,rep,name=series,proto3" json:"series,omitempty"` @@ -1448,7 +1466,7 @@ const file_querier_v1_querier_proto_rawDesc = "" + "\x03end\x18\x04 \x01(\x03B\x14\xbaG\x11:\x0f\x12\r1676289600000R\x03end\"A\n" + "\x0eSeriesResponse\x12/\n" + "\n" + - "labels_set\x18\x02 \x03(\v2\x10.types.v1.LabelsR\tlabelsSet\"\xeb\x03\n" + + "labels_set\x18\x02 \x03(\v2\x10.types.v1.LabelsR\tlabelsSet\"\xcc\x04\n" + "\x1dSelectMergeStacktracesRequest\x12Y\n" + "\x0eprofile_typeID\x18\x01 \x01(\tB2\xbaG/:-\x12+process_cpu:cpu:nanoseconds:cpu:nanosecondsR\rprofileTypeID\x12J\n" + "\x0elabel_selector\x18\x02 \x01(\tB#\xbaG :\x1e\x12\x1c'{namespace=\"my-namespace\"}'R\rlabelSelector\x12*\n" + @@ -1456,7 +1474,8 @@ const file_querier_v1_querier_proto_rawDesc = "" + "\x03end\x18\x04 \x01(\x03B\x14\xbaG\x11:\x0f\x12\r1676289600000R\x03end\x12 \n" + "\tmax_nodes\x18\x05 \x01(\x03H\x00R\bmaxNodes\x88\x01\x01\x121\n" + "\x06format\x18\x06 \x01(\x0e2\x19.querier.v1.ProfileFormatR\x06format\x12S\n" + - "\x14stack_trace_selector\x18\a \x01(\v2\x1c.types.v1.StackTraceSelectorH\x01R\x12stackTraceSelector\x88\x01\x01B\f\n" + + "\x14stack_trace_selector\x18\a \x01(\v2\x1c.types.v1.StackTraceSelectorH\x01R\x12stackTraceSelector\x88\x01\x01\x12_\n" + + "\x13profile_id_selector\x18\b \x03(\tB/\xbaG,:*\x12(['7c9e6679-7425-40de-944b-e07fc1f90ae7']R\x11profileIdSelectorB\f\n" + "\n" + "_max_nodesB\x17\n" + "\x15_stack_trace_selector\"l\n" + @@ -1513,7 +1532,7 @@ const file_querier_v1_querier_proto_rawDesc = "" + "\x14stack_trace_selector\x18\x06 \x01(\v2\x1c.types.v1.StackTraceSelectorH\x01R\x12stackTraceSelector\x88\x01\x01B\f\n" + "\n" + "_max_nodesB\x17\n" + - "\x15_stack_trace_selector\"\xbe\x04\n" + + "\x15_stack_trace_selector\"\xfb\x04\n" + "\x13SelectSeriesRequest\x12Y\n" + "\x0eprofile_typeID\x18\x01 \x01(\tB2\xbaG/:-\x12+process_cpu:cpu:nanoseconds:cpu:nanosecondsR\rprofileTypeID\x12J\n" + "\x0elabel_selector\x18\x02 \x01(\tB#\xbaG :\x1e\x12\x1c'{namespace=\"my-namespace\"}'R\rlabelSelector\x12*\n" + @@ -1523,7 +1542,9 @@ const file_querier_v1_querier_proto_rawDesc = "" + "\x04step\x18\x06 \x01(\x01R\x04step\x12J\n" + "\vaggregation\x18\a \x01(\x0e2#.types.v1.TimeSeriesAggregationTypeH\x00R\vaggregation\x88\x01\x01\x12S\n" + "\x14stack_trace_selector\x18\b \x01(\v2\x1c.types.v1.StackTraceSelectorH\x01R\x12stackTraceSelector\x88\x01\x01\x12\x19\n" + - "\x05limit\x18\t \x01(\x03H\x02R\x05limit\x88\x01\x01B\x0e\n" + + "\x05limit\x18\t \x01(\x03H\x02R\x05limit\x88\x01\x01\x12;\n" + + "\rexemplar_type\x18\n" + + " \x01(\x0e2\x16.types.v1.ExemplarTypeR\fexemplarTypeB\x0e\n" + "\f_aggregationB\x17\n" + "\x15_stack_trace_selectorB\b\n" + "\x06_limit\"@\n" + @@ -1625,14 +1646,15 @@ var file_querier_v1_querier_proto_goTypes = []any{ (*v1.Labels)(nil), // 22: types.v1.Labels (*v1.StackTraceSelector)(nil), // 23: types.v1.StackTraceSelector (v1.TimeSeriesAggregationType)(0), // 24: types.v1.TimeSeriesAggregationType - (*v1.Series)(nil), // 25: types.v1.Series - (*v1.LabelValuesRequest)(nil), // 26: types.v1.LabelValuesRequest - (*v1.LabelNamesRequest)(nil), // 27: types.v1.LabelNamesRequest - (*v1.GetProfileStatsRequest)(nil), // 28: types.v1.GetProfileStatsRequest - (*v1.LabelValuesResponse)(nil), // 29: types.v1.LabelValuesResponse - (*v1.LabelNamesResponse)(nil), // 30: types.v1.LabelNamesResponse - (*v11.Profile)(nil), // 31: google.v1.Profile - (*v1.GetProfileStatsResponse)(nil), // 32: types.v1.GetProfileStatsResponse + (v1.ExemplarType)(0), // 25: types.v1.ExemplarType + (*v1.Series)(nil), // 26: types.v1.Series + (*v1.LabelValuesRequest)(nil), // 27: types.v1.LabelValuesRequest + (*v1.LabelNamesRequest)(nil), // 28: types.v1.LabelNamesRequest + (*v1.GetProfileStatsRequest)(nil), // 29: types.v1.GetProfileStatsRequest + (*v1.LabelValuesResponse)(nil), // 30: types.v1.LabelValuesResponse + (*v1.LabelNamesResponse)(nil), // 31: types.v1.LabelNamesResponse + (*v11.Profile)(nil), // 32: google.v1.Profile + (*v1.GetProfileStatsResponse)(nil), // 33: types.v1.GetProfileStatsResponse } var file_querier_v1_querier_proto_depIdxs = []int32{ 21, // 0: querier.v1.ProfileTypesResponse.profile_types:type_name -> types.v1.ProfileType @@ -1650,36 +1672,37 @@ var file_querier_v1_querier_proto_depIdxs = []int32{ 23, // 12: querier.v1.SelectMergeProfileRequest.stack_trace_selector:type_name -> types.v1.StackTraceSelector 24, // 13: querier.v1.SelectSeriesRequest.aggregation:type_name -> types.v1.TimeSeriesAggregationType 23, // 14: querier.v1.SelectSeriesRequest.stack_trace_selector:type_name -> types.v1.StackTraceSelector - 25, // 15: querier.v1.SelectSeriesResponse.series:type_name -> types.v1.Series - 19, // 16: querier.v1.AnalyzeQueryResponse.query_scopes:type_name -> querier.v1.QueryScope - 20, // 17: querier.v1.AnalyzeQueryResponse.query_impact:type_name -> querier.v1.QueryImpact - 1, // 18: querier.v1.QuerierService.ProfileTypes:input_type -> querier.v1.ProfileTypesRequest - 26, // 19: querier.v1.QuerierService.LabelValues:input_type -> types.v1.LabelValuesRequest - 27, // 20: querier.v1.QuerierService.LabelNames:input_type -> types.v1.LabelNamesRequest - 3, // 21: querier.v1.QuerierService.Series:input_type -> querier.v1.SeriesRequest - 5, // 22: querier.v1.QuerierService.SelectMergeStacktraces:input_type -> querier.v1.SelectMergeStacktracesRequest - 7, // 23: querier.v1.QuerierService.SelectMergeSpanProfile:input_type -> querier.v1.SelectMergeSpanProfileRequest - 14, // 24: querier.v1.QuerierService.SelectMergeProfile:input_type -> querier.v1.SelectMergeProfileRequest - 15, // 25: querier.v1.QuerierService.SelectSeries:input_type -> querier.v1.SelectSeriesRequest - 9, // 26: querier.v1.QuerierService.Diff:input_type -> querier.v1.DiffRequest - 28, // 27: querier.v1.QuerierService.GetProfileStats:input_type -> types.v1.GetProfileStatsRequest - 17, // 28: querier.v1.QuerierService.AnalyzeQuery:input_type -> querier.v1.AnalyzeQueryRequest - 2, // 29: querier.v1.QuerierService.ProfileTypes:output_type -> querier.v1.ProfileTypesResponse - 29, // 30: querier.v1.QuerierService.LabelValues:output_type -> types.v1.LabelValuesResponse - 30, // 31: querier.v1.QuerierService.LabelNames:output_type -> types.v1.LabelNamesResponse - 4, // 32: querier.v1.QuerierService.Series:output_type -> querier.v1.SeriesResponse - 6, // 33: querier.v1.QuerierService.SelectMergeStacktraces:output_type -> querier.v1.SelectMergeStacktracesResponse - 8, // 34: querier.v1.QuerierService.SelectMergeSpanProfile:output_type -> querier.v1.SelectMergeSpanProfileResponse - 31, // 35: querier.v1.QuerierService.SelectMergeProfile:output_type -> google.v1.Profile - 16, // 36: querier.v1.QuerierService.SelectSeries:output_type -> querier.v1.SelectSeriesResponse - 10, // 37: querier.v1.QuerierService.Diff:output_type -> querier.v1.DiffResponse - 32, // 38: querier.v1.QuerierService.GetProfileStats:output_type -> types.v1.GetProfileStatsResponse - 18, // 39: querier.v1.QuerierService.AnalyzeQuery:output_type -> querier.v1.AnalyzeQueryResponse - 29, // [29:40] is the sub-list for method output_type - 18, // [18:29] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 25, // 15: querier.v1.SelectSeriesRequest.exemplar_type:type_name -> types.v1.ExemplarType + 26, // 16: querier.v1.SelectSeriesResponse.series:type_name -> types.v1.Series + 19, // 17: querier.v1.AnalyzeQueryResponse.query_scopes:type_name -> querier.v1.QueryScope + 20, // 18: querier.v1.AnalyzeQueryResponse.query_impact:type_name -> querier.v1.QueryImpact + 1, // 19: querier.v1.QuerierService.ProfileTypes:input_type -> querier.v1.ProfileTypesRequest + 27, // 20: querier.v1.QuerierService.LabelValues:input_type -> types.v1.LabelValuesRequest + 28, // 21: querier.v1.QuerierService.LabelNames:input_type -> types.v1.LabelNamesRequest + 3, // 22: querier.v1.QuerierService.Series:input_type -> querier.v1.SeriesRequest + 5, // 23: querier.v1.QuerierService.SelectMergeStacktraces:input_type -> querier.v1.SelectMergeStacktracesRequest + 7, // 24: querier.v1.QuerierService.SelectMergeSpanProfile:input_type -> querier.v1.SelectMergeSpanProfileRequest + 14, // 25: querier.v1.QuerierService.SelectMergeProfile:input_type -> querier.v1.SelectMergeProfileRequest + 15, // 26: querier.v1.QuerierService.SelectSeries:input_type -> querier.v1.SelectSeriesRequest + 9, // 27: querier.v1.QuerierService.Diff:input_type -> querier.v1.DiffRequest + 29, // 28: querier.v1.QuerierService.GetProfileStats:input_type -> types.v1.GetProfileStatsRequest + 17, // 29: querier.v1.QuerierService.AnalyzeQuery:input_type -> querier.v1.AnalyzeQueryRequest + 2, // 30: querier.v1.QuerierService.ProfileTypes:output_type -> querier.v1.ProfileTypesResponse + 30, // 31: querier.v1.QuerierService.LabelValues:output_type -> types.v1.LabelValuesResponse + 31, // 32: querier.v1.QuerierService.LabelNames:output_type -> types.v1.LabelNamesResponse + 4, // 33: querier.v1.QuerierService.Series:output_type -> querier.v1.SeriesResponse + 6, // 34: querier.v1.QuerierService.SelectMergeStacktraces:output_type -> querier.v1.SelectMergeStacktracesResponse + 8, // 35: querier.v1.QuerierService.SelectMergeSpanProfile:output_type -> querier.v1.SelectMergeSpanProfileResponse + 32, // 36: querier.v1.QuerierService.SelectMergeProfile:output_type -> google.v1.Profile + 16, // 37: querier.v1.QuerierService.SelectSeries:output_type -> querier.v1.SelectSeriesResponse + 10, // 38: querier.v1.QuerierService.Diff:output_type -> querier.v1.DiffResponse + 33, // 39: querier.v1.QuerierService.GetProfileStats:output_type -> types.v1.GetProfileStatsResponse + 18, // 40: querier.v1.QuerierService.AnalyzeQuery:output_type -> querier.v1.AnalyzeQueryResponse + 30, // [30:41] is the sub-list for method output_type + 19, // [19:30] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_querier_v1_querier_proto_init() } diff --git a/api/gen/proto/go/querier/v1/querier_vtproto.pb.go b/api/gen/proto/go/querier/v1/querier_vtproto.pb.go index 517ac6c999..2e0837e93c 100644 --- a/api/gen/proto/go/querier/v1/querier_vtproto.pb.go +++ b/api/gen/proto/go/querier/v1/querier_vtproto.pb.go @@ -148,6 +148,11 @@ func (m *SelectMergeStacktracesRequest) CloneVT() *SelectMergeStacktracesRequest r.StackTraceSelector = proto.Clone(rhs).(*v1.StackTraceSelector) } } + if rhs := m.ProfileIdSelector; rhs != nil { + tmpContainer := make([]string, len(rhs)) + copy(tmpContainer, rhs) + r.ProfileIdSelector = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -392,6 +397,7 @@ func (m *SelectSeriesRequest) CloneVT() *SelectSeriesRequest { r.Start = m.Start r.End = m.End r.Step = m.Step + r.ExemplarType = m.ExemplarType if rhs := m.GroupBy; rhs != nil { tmpContainer := make([]string, len(rhs)) copy(tmpContainer, rhs) @@ -706,6 +712,15 @@ func (this *SelectMergeStacktracesRequest) EqualVT(that *SelectMergeStacktracesR } else if !proto.Equal(this.StackTraceSelector, that.StackTraceSelector) { return false } + if len(this.ProfileIdSelector) != len(that.ProfileIdSelector) { + return false + } + for i, vx := range this.ProfileIdSelector { + vy := that.ProfileIdSelector[i] + if vx != vy { + return false + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -1056,6 +1071,9 @@ func (this *SelectSeriesRequest) EqualVT(that *SelectSeriesRequest) bool { if p, q := this.Limit, that.Limit; (p == nil && q != nil) || (p != nil && (q == nil || *p != *q)) { return false } + if this.ExemplarType != that.ExemplarType { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -1968,6 +1986,15 @@ func (m *SelectMergeStacktracesRequest) MarshalToSizedBufferVT(dAtA []byte) (int i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.ProfileIdSelector) > 0 { + for iNdEx := len(m.ProfileIdSelector) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.ProfileIdSelector[iNdEx]) + copy(dAtA[i:], m.ProfileIdSelector[iNdEx]) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.ProfileIdSelector[iNdEx]))) + i-- + dAtA[i] = 0x42 + } + } if m.StackTraceSelector != nil { if vtmsg, ok := interface{}(m.StackTraceSelector).(interface { MarshalToSizedBufferVT([]byte) (int, error) @@ -2605,6 +2632,11 @@ func (m *SelectSeriesRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.ExemplarType != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.ExemplarType)) + i-- + dAtA[i] = 0x50 + } if m.Limit != nil { i = protohelpers.EncodeVarint(dAtA, i, uint64(*m.Limit)) i-- @@ -3098,6 +3130,12 @@ func (m *SelectMergeStacktracesRequest) SizeVT() (n int) { } n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if len(m.ProfileIdSelector) > 0 { + for _, s := range m.ProfileIdSelector { + l = len(s) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } n += len(m.unknownFields) return n } @@ -3367,6 +3405,9 @@ func (m *SelectSeriesRequest) SizeVT() (n int) { if m.Limit != nil { n += 1 + protohelpers.SizeOfVarint(uint64(*m.Limit)) } + if m.ExemplarType != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.ExemplarType)) + } n += len(m.unknownFields) return n } @@ -4132,6 +4173,38 @@ func (m *SelectMergeStacktracesRequest) UnmarshalVT(dAtA []byte) error { } } iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ProfileIdSelector", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ProfileIdSelector = append(m.ProfileIdSelector, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -5780,6 +5853,25 @@ func (m *SelectSeriesRequest) UnmarshalVT(dAtA []byte) error { } } m.Limit = &v + case 10: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ExemplarType", wireType) + } + m.ExemplarType = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ExemplarType |= v1.ExemplarType(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/api/gen/proto/go/query/v1/query.pb.go b/api/gen/proto/go/query/v1/query.pb.go index d0d08d352a..a055e32c50 100644 --- a/api/gen/proto/go/query/v1/query.pb.go +++ b/api/gen/proto/go/query/v1/query.pb.go @@ -1125,6 +1125,7 @@ type TimeSeriesQuery struct { Step float64 `protobuf:"fixed64,1,opt,name=step,proto3" json:"step,omitempty"` GroupBy []string `protobuf:"bytes,2,rep,name=group_by,json=groupBy,proto3" json:"group_by,omitempty"` Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` + ExemplarType v11.ExemplarType `protobuf:"varint,4,opt,name=exemplar_type,json=exemplarType,proto3,enum=types.v1.ExemplarType" json:"exemplar_type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1180,6 +1181,13 @@ func (x *TimeSeriesQuery) GetLimit() int64 { return 0 } +func (x *TimeSeriesQuery) GetExemplarType() v11.ExemplarType { + if x != nil { + return x.ExemplarType + } + return v11.ExemplarType(0) +} + type TimeSeriesReport struct { state protoimpl.MessageState `protogen:"open.v1"` Query *TimeSeriesQuery `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` @@ -1527,11 +1535,12 @@ const file_query_v1_query_proto_rawDesc = "" + "labelNames\"~\n" + "\x12SeriesLabelsReport\x121\n" + "\x05query\x18\x01 \x01(\v2\x1b.query.v1.SeriesLabelsQueryR\x05query\x125\n" + - "\rseries_labels\x18\x02 \x03(\v2\x10.types.v1.LabelsR\fseriesLabels\"V\n" + + "\rseries_labels\x18\x02 \x03(\v2\x10.types.v1.LabelsR\fseriesLabels\"\x93\x01\n" + "\x0fTimeSeriesQuery\x12\x12\n" + "\x04step\x18\x01 \x01(\x01R\x04step\x12\x19\n" + "\bgroup_by\x18\x02 \x03(\tR\agroupBy\x12\x14\n" + - "\x05limit\x18\x03 \x01(\x03R\x05limit\"v\n" + + "\x05limit\x18\x03 \x01(\x03R\x05limit\x12;\n" + + "\rexemplar_type\x18\x04 \x01(\x0e2\x16.types.v1.ExemplarTypeR\fexemplarType\"v\n" + "\x10TimeSeriesReport\x12/\n" + "\x05query\x18\x01 \x01(\v2\x19.query.v1.TimeSeriesQueryR\x05query\x121\n" + "\vtime_series\x18\x02 \x03(\v2\x10.types.v1.SeriesR\n" + @@ -1620,8 +1629,9 @@ var file_query_v1_query_proto_goTypes = []any{ (*PprofReport)(nil), // 24: query.v1.PprofReport (*v1.BlockMeta)(nil), // 25: metastore.v1.BlockMeta (*v11.Labels)(nil), // 26: types.v1.Labels - (*v11.Series)(nil), // 27: types.v1.Series - (*v11.StackTraceSelector)(nil), // 28: types.v1.StackTraceSelector + (v11.ExemplarType)(0), // 27: types.v1.ExemplarType + (*v11.Series)(nil), // 28: types.v1.Series + (*v11.StackTraceSelector)(nil), // 29: types.v1.StackTraceSelector } var file_query_v1_query_proto_depIdxs = []int32{ 9, // 0: query.v1.QueryRequest.query:type_name -> query.v1.Query @@ -1654,21 +1664,22 @@ var file_query_v1_query_proto_depIdxs = []int32{ 15, // 27: query.v1.LabelValuesReport.query:type_name -> query.v1.LabelValuesQuery 17, // 28: query.v1.SeriesLabelsReport.query:type_name -> query.v1.SeriesLabelsQuery 26, // 29: query.v1.SeriesLabelsReport.series_labels:type_name -> types.v1.Labels - 19, // 30: query.v1.TimeSeriesReport.query:type_name -> query.v1.TimeSeriesQuery - 27, // 31: query.v1.TimeSeriesReport.time_series:type_name -> types.v1.Series - 28, // 32: query.v1.TreeQuery.stack_trace_selector:type_name -> types.v1.StackTraceSelector - 21, // 33: query.v1.TreeReport.query:type_name -> query.v1.TreeQuery - 28, // 34: query.v1.PprofQuery.stack_trace_selector:type_name -> types.v1.StackTraceSelector - 23, // 35: query.v1.PprofReport.query:type_name -> query.v1.PprofQuery - 3, // 36: query.v1.QueryFrontendService.Query:input_type -> query.v1.QueryRequest - 6, // 37: query.v1.QueryBackendService.Invoke:input_type -> query.v1.InvokeRequest - 4, // 38: query.v1.QueryFrontendService.Query:output_type -> query.v1.QueryResponse - 10, // 39: query.v1.QueryBackendService.Invoke:output_type -> query.v1.InvokeResponse - 38, // [38:40] is the sub-list for method output_type - 36, // [36:38] is the sub-list for method input_type - 36, // [36:36] is the sub-list for extension type_name - 36, // [36:36] is the sub-list for extension extendee - 0, // [0:36] is the sub-list for field type_name + 27, // 30: query.v1.TimeSeriesQuery.exemplar_type:type_name -> types.v1.ExemplarType + 19, // 31: query.v1.TimeSeriesReport.query:type_name -> query.v1.TimeSeriesQuery + 28, // 32: query.v1.TimeSeriesReport.time_series:type_name -> types.v1.Series + 29, // 33: query.v1.TreeQuery.stack_trace_selector:type_name -> types.v1.StackTraceSelector + 21, // 34: query.v1.TreeReport.query:type_name -> query.v1.TreeQuery + 29, // 35: query.v1.PprofQuery.stack_trace_selector:type_name -> types.v1.StackTraceSelector + 23, // 36: query.v1.PprofReport.query:type_name -> query.v1.PprofQuery + 3, // 37: query.v1.QueryFrontendService.Query:input_type -> query.v1.QueryRequest + 6, // 38: query.v1.QueryBackendService.Invoke:input_type -> query.v1.InvokeRequest + 4, // 39: query.v1.QueryFrontendService.Query:output_type -> query.v1.QueryResponse + 10, // 40: query.v1.QueryBackendService.Invoke:output_type -> query.v1.InvokeResponse + 39, // [39:41] is the sub-list for method output_type + 37, // [37:39] is the sub-list for method input_type + 37, // [37:37] is the sub-list for extension type_name + 37, // [37:37] is the sub-list for extension extendee + 0, // [0:37] is the sub-list for field type_name } func init() { file_query_v1_query_proto_init() } diff --git a/api/gen/proto/go/query/v1/query_vtproto.pb.go b/api/gen/proto/go/query/v1/query_vtproto.pb.go index 08be94451f..2186c9c91b 100644 --- a/api/gen/proto/go/query/v1/query_vtproto.pb.go +++ b/api/gen/proto/go/query/v1/query_vtproto.pb.go @@ -398,6 +398,7 @@ func (m *TimeSeriesQuery) CloneVT() *TimeSeriesQuery { r := new(TimeSeriesQuery) r.Step = m.Step r.Limit = m.Limit + r.ExemplarType = m.ExemplarType if rhs := m.GroupBy; rhs != nil { tmpContainer := make([]string, len(rhs)) copy(tmpContainer, rhs) @@ -1076,6 +1077,9 @@ func (this *TimeSeriesQuery) EqualVT(that *TimeSeriesQuery) bool { if this.Limit != that.Limit { return false } + if this.ExemplarType != that.ExemplarType { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -2381,6 +2385,11 @@ func (m *TimeSeriesQuery) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.ExemplarType != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.ExemplarType)) + i-- + dAtA[i] = 0x20 + } if m.Limit != 0 { i = protohelpers.EncodeVarint(dAtA, i, uint64(m.Limit)) i-- @@ -3072,6 +3081,9 @@ func (m *TimeSeriesQuery) SizeVT() (n int) { if m.Limit != 0 { n += 1 + protohelpers.SizeOfVarint(uint64(m.Limit)) } + if m.ExemplarType != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.ExemplarType)) + } n += len(m.unknownFields) return n } @@ -5448,6 +5460,25 @@ func (m *TimeSeriesQuery) UnmarshalVT(dAtA []byte) error { break } } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ExemplarType", wireType) + } + m.ExemplarType = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ExemplarType |= v11.ExemplarType(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/api/gen/proto/go/types/v1/types.pb.go b/api/gen/proto/go/types/v1/types.pb.go index 14d95762be..acbd83ecb9 100644 --- a/api/gen/proto/go/types/v1/types.pb.go +++ b/api/gen/proto/go/types/v1/types.pb.go @@ -68,6 +68,58 @@ func (TimeSeriesAggregationType) EnumDescriptor() ([]byte, []int) { return file_types_v1_types_proto_rawDescGZIP(), []int{0} } +type ExemplarType int32 + +const ( + ExemplarType_EXEMPLAR_TYPE_UNSPECIFIED ExemplarType = 0 + ExemplarType_EXEMPLAR_TYPE_NONE ExemplarType = 1 + ExemplarType_EXEMPLAR_TYPE_INDIVIDUAL ExemplarType = 2 + ExemplarType_EXEMPLAR_TYPE_SPAN ExemplarType = 3 +) + +// Enum value maps for ExemplarType. +var ( + ExemplarType_name = map[int32]string{ + 0: "EXEMPLAR_TYPE_UNSPECIFIED", + 1: "EXEMPLAR_TYPE_NONE", + 2: "EXEMPLAR_TYPE_INDIVIDUAL", + 3: "EXEMPLAR_TYPE_SPAN", + } + ExemplarType_value = map[string]int32{ + "EXEMPLAR_TYPE_UNSPECIFIED": 0, + "EXEMPLAR_TYPE_NONE": 1, + "EXEMPLAR_TYPE_INDIVIDUAL": 2, + "EXEMPLAR_TYPE_SPAN": 3, + } +) + +func (x ExemplarType) Enum() *ExemplarType { + p := new(ExemplarType) + *p = x + return p +} + +func (x ExemplarType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ExemplarType) Descriptor() protoreflect.EnumDescriptor { + return file_types_v1_types_proto_enumTypes[1].Descriptor() +} + +func (ExemplarType) Type() protoreflect.EnumType { + return &file_types_v1_types_proto_enumTypes[1] +} + +func (x ExemplarType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ExemplarType.Descriptor instead. +func (ExemplarType) EnumDescriptor() ([]byte, []int) { + return file_types_v1_types_proto_rawDescGZIP(), []int{1} +} + type LabelPair struct { state protoimpl.MessageState `protogen:"open.v1"` // Label name @@ -307,8 +359,10 @@ type Point struct { state protoimpl.MessageState `protogen:"open.v1"` Value float64 `protobuf:"fixed64,1,opt,name=value,proto3" json:"value,omitempty"` // Milliseconds unix timestamp - Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - Annotations []*ProfileAnnotation `protobuf:"bytes,3,rep,name=annotations,proto3" json:"annotations,omitempty"` + Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Annotations []*ProfileAnnotation `protobuf:"bytes,3,rep,name=annotations,proto3" json:"annotations,omitempty"` + // Exemplars are samples of individual profiles that contributed to this aggregated point + Exemplars []*Exemplar `protobuf:"bytes,4,rep,name=exemplars,proto3" json:"exemplars,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -364,6 +418,13 @@ func (x *Point) GetAnnotations() []*ProfileAnnotation { return nil } +func (x *Point) GetExemplars() []*Exemplar { + if x != nil { + return x.Exemplars + } + return nil +} + // Annotations provide additional metadata for a profile. // // The main differences between labels and annotations are: @@ -1041,6 +1102,94 @@ func (x *GetProfileStatsResponse) GetNewestProfileTime() int64 { return 0 } +// Exemplar represents metadata for an individual profile sample. +// Exemplars allow users to drill down from aggregated timeline views +// to specific profile instances. +type Exemplar struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Milliseconds since epoch when the profile was captured. + Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // Unique identifier for the profile (UUID). + ProfileId string `protobuf:"bytes,2,opt,name=profile_id,json=profileId,proto3" json:"profile_id,omitempty"` + // Span ID if this profile was split by span during ingestion. + SpanId string `protobuf:"bytes,3,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` + // Total sample value for this profile (e.g., CPU nanoseconds, bytes allocated). + Value uint64 `protobuf:"varint,4,opt,name=value,proto3" json:"value,omitempty"` + // Series labels that are NOT included in the group_by query parameter. + // These labels complete the full series identity of this exemplar's profile. + // For example, if group_by=["service"], this would contain labels like "pod", + // "namespace", "region" that were omitted from grouping. This allows identifying + // which specific series instance this profile sample came from. + Labels []*LabelPair `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Exemplar) Reset() { + *x = Exemplar{} + mi := &file_types_v1_types_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Exemplar) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Exemplar) ProtoMessage() {} + +func (x *Exemplar) ProtoReflect() protoreflect.Message { + mi := &file_types_v1_types_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Exemplar.ProtoReflect.Descriptor instead. +func (*Exemplar) Descriptor() ([]byte, []int) { + return file_types_v1_types_proto_rawDescGZIP(), []int{17} +} + +func (x *Exemplar) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *Exemplar) GetProfileId() string { + if x != nil { + return x.ProfileId + } + return "" +} + +func (x *Exemplar) GetSpanId() string { + if x != nil { + return x.SpanId + } + return "" +} + +func (x *Exemplar) GetValue() uint64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *Exemplar) GetLabels() []*LabelPair { + if x != nil { + return x.Labels + } + return nil +} + var File_types_v1_types_proto protoreflect.FileDescriptor const file_types_v1_types_proto_rawDesc = "" + @@ -1065,11 +1214,12 @@ const file_types_v1_types_proto_rawDesc = "" + "\x06labels\x18\x01 \x03(\v2\x13.types.v1.LabelPairR\x06labels\"^\n" + "\x06Series\x12+\n" + "\x06labels\x18\x01 \x03(\v2\x13.types.v1.LabelPairR\x06labels\x12'\n" + - "\x06points\x18\x02 \x03(\v2\x0f.types.v1.PointR\x06points\"z\n" + + "\x06points\x18\x02 \x03(\v2\x0f.types.v1.PointR\x06points\"\xac\x01\n" + "\x05Point\x12\x14\n" + "\x05value\x18\x01 \x01(\x01R\x05value\x12\x1c\n" + "\ttimestamp\x18\x02 \x01(\x03R\ttimestamp\x12=\n" + - "\vannotations\x18\x03 \x03(\v2\x1b.types.v1.ProfileAnnotationR\vannotations\";\n" + + "\vannotations\x18\x03 \x03(\v2\x1b.types.v1.ProfileAnnotationR\vannotations\x120\n" + + "\texemplars\x18\x04 \x03(\v2\x12.types.v1.ExemplarR\texemplars\";\n" + "\x11ProfileAnnotation\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value\"\xad\x01\n" + @@ -1110,10 +1260,23 @@ const file_types_v1_types_proto_rawDesc = "" + "\x17GetProfileStatsResponse\x12#\n" + "\rdata_ingested\x18\x01 \x01(\bR\fdataIngested\x12.\n" + "\x13oldest_profile_time\x18\x02 \x01(\x03R\x11oldestProfileTime\x12.\n" + - "\x13newest_profile_time\x18\x03 \x01(\x03R\x11newestProfileTime*k\n" + + "\x13newest_profile_time\x18\x03 \x01(\x03R\x11newestProfileTime\"\x92\x02\n" + + "\bExemplar\x122\n" + + "\ttimestamp\x18\x01 \x01(\x03B\x14\xbaG\x11:\x0f\x12\r1730000023000R\ttimestamp\x12J\n" + + "\n" + + "profile_id\x18\x02 \x01(\tB+\xbaG(:&\x12$7c9e6679-7425-40de-944b-e07fc1f90ae7R\tprofileId\x120\n" + + "\aspan_id\x18\x03 \x01(\tB\x17\xbaG\x14:\x12\x12\x1000f067aa0ba902b7R\x06spanId\x12'\n" + + "\x05value\x18\x04 \x01(\x04B\x11\xbaG\x0e:\f\x12\n" + + "2450000000R\x05value\x12+\n" + + "\x06labels\x18\x05 \x03(\v2\x13.types.v1.LabelPairR\x06labels*k\n" + "\x19TimeSeriesAggregationType\x12$\n" + " TIME_SERIES_AGGREGATION_TYPE_SUM\x10\x00\x12(\n" + - "$TIME_SERIES_AGGREGATION_TYPE_AVERAGE\x10\x01B\x9b\x01\n" + + "$TIME_SERIES_AGGREGATION_TYPE_AVERAGE\x10\x01*{\n" + + "\fExemplarType\x12\x1d\n" + + "\x19EXEMPLAR_TYPE_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12EXEMPLAR_TYPE_NONE\x10\x01\x12\x1c\n" + + "\x18EXEMPLAR_TYPE_INDIVIDUAL\x10\x02\x12\x16\n" + + "\x12EXEMPLAR_TYPE_SPAN\x10\x03B\x9b\x01\n" + "\fcom.types.v1B\n" + "TypesProtoP\x01Z>github.com/grafana/pyroscope/api/gen/proto/go/types/v1;typesv1\xa2\x02\x03TXX\xaa\x02\bTypes.V1\xca\x02\bTypes\\V1\xe2\x02\x14Types\\V1\\GPBMetadata\xea\x02\tTypes::V1b\x06proto3" @@ -1129,42 +1292,46 @@ func file_types_v1_types_proto_rawDescGZIP() []byte { return file_types_v1_types_proto_rawDescData } -var file_types_v1_types_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_types_v1_types_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_types_v1_types_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_types_v1_types_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_types_v1_types_proto_goTypes = []any{ (TimeSeriesAggregationType)(0), // 0: types.v1.TimeSeriesAggregationType - (*LabelPair)(nil), // 1: types.v1.LabelPair - (*ProfileType)(nil), // 2: types.v1.ProfileType - (*Labels)(nil), // 3: types.v1.Labels - (*Series)(nil), // 4: types.v1.Series - (*Point)(nil), // 5: types.v1.Point - (*ProfileAnnotation)(nil), // 6: types.v1.ProfileAnnotation - (*LabelValuesRequest)(nil), // 7: types.v1.LabelValuesRequest - (*LabelValuesResponse)(nil), // 8: types.v1.LabelValuesResponse - (*LabelNamesRequest)(nil), // 9: types.v1.LabelNamesRequest - (*LabelNamesResponse)(nil), // 10: types.v1.LabelNamesResponse - (*BlockInfo)(nil), // 11: types.v1.BlockInfo - (*BlockCompaction)(nil), // 12: types.v1.BlockCompaction - (*StackTraceSelector)(nil), // 13: types.v1.StackTraceSelector - (*Location)(nil), // 14: types.v1.Location - (*GoPGO)(nil), // 15: types.v1.GoPGO - (*GetProfileStatsRequest)(nil), // 16: types.v1.GetProfileStatsRequest - (*GetProfileStatsResponse)(nil), // 17: types.v1.GetProfileStatsResponse + (ExemplarType)(0), // 1: types.v1.ExemplarType + (*LabelPair)(nil), // 2: types.v1.LabelPair + (*ProfileType)(nil), // 3: types.v1.ProfileType + (*Labels)(nil), // 4: types.v1.Labels + (*Series)(nil), // 5: types.v1.Series + (*Point)(nil), // 6: types.v1.Point + (*ProfileAnnotation)(nil), // 7: types.v1.ProfileAnnotation + (*LabelValuesRequest)(nil), // 8: types.v1.LabelValuesRequest + (*LabelValuesResponse)(nil), // 9: types.v1.LabelValuesResponse + (*LabelNamesRequest)(nil), // 10: types.v1.LabelNamesRequest + (*LabelNamesResponse)(nil), // 11: types.v1.LabelNamesResponse + (*BlockInfo)(nil), // 12: types.v1.BlockInfo + (*BlockCompaction)(nil), // 13: types.v1.BlockCompaction + (*StackTraceSelector)(nil), // 14: types.v1.StackTraceSelector + (*Location)(nil), // 15: types.v1.Location + (*GoPGO)(nil), // 16: types.v1.GoPGO + (*GetProfileStatsRequest)(nil), // 17: types.v1.GetProfileStatsRequest + (*GetProfileStatsResponse)(nil), // 18: types.v1.GetProfileStatsResponse + (*Exemplar)(nil), // 19: types.v1.Exemplar } var file_types_v1_types_proto_depIdxs = []int32{ - 1, // 0: types.v1.Labels.labels:type_name -> types.v1.LabelPair - 1, // 1: types.v1.Series.labels:type_name -> types.v1.LabelPair - 5, // 2: types.v1.Series.points:type_name -> types.v1.Point - 6, // 3: types.v1.Point.annotations:type_name -> types.v1.ProfileAnnotation - 12, // 4: types.v1.BlockInfo.compaction:type_name -> types.v1.BlockCompaction - 1, // 5: types.v1.BlockInfo.labels:type_name -> types.v1.LabelPair - 14, // 6: types.v1.StackTraceSelector.call_site:type_name -> types.v1.Location - 15, // 7: types.v1.StackTraceSelector.go_pgo:type_name -> types.v1.GoPGO - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 2, // 0: types.v1.Labels.labels:type_name -> types.v1.LabelPair + 2, // 1: types.v1.Series.labels:type_name -> types.v1.LabelPair + 6, // 2: types.v1.Series.points:type_name -> types.v1.Point + 7, // 3: types.v1.Point.annotations:type_name -> types.v1.ProfileAnnotation + 19, // 4: types.v1.Point.exemplars:type_name -> types.v1.Exemplar + 13, // 5: types.v1.BlockInfo.compaction:type_name -> types.v1.BlockCompaction + 2, // 6: types.v1.BlockInfo.labels:type_name -> types.v1.LabelPair + 15, // 7: types.v1.StackTraceSelector.call_site:type_name -> types.v1.Location + 16, // 8: types.v1.StackTraceSelector.go_pgo:type_name -> types.v1.GoPGO + 2, // 9: types.v1.Exemplar.labels:type_name -> types.v1.LabelPair + 10, // [10:10] is the sub-list for method output_type + 10, // [10:10] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_types_v1_types_proto_init() } @@ -1177,8 +1344,8 @@ func file_types_v1_types_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_v1_types_proto_rawDesc), len(file_types_v1_types_proto_rawDesc)), - NumEnums: 1, - NumMessages: 17, + NumEnums: 2, + NumMessages: 18, NumExtensions: 0, NumServices: 0, }, diff --git a/api/gen/proto/go/types/v1/types_vtproto.pb.go b/api/gen/proto/go/types/v1/types_vtproto.pb.go index c9bd53bbd3..c84f97c44d 100644 --- a/api/gen/proto/go/types/v1/types_vtproto.pb.go +++ b/api/gen/proto/go/types/v1/types_vtproto.pb.go @@ -128,6 +128,13 @@ func (m *Point) CloneVT() *Point { } r.Annotations = tmpContainer } + if rhs := m.Exemplars; rhs != nil { + tmpContainer := make([]*Exemplar, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Exemplars = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -394,6 +401,33 @@ func (m *GetProfileStatsResponse) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *Exemplar) CloneVT() *Exemplar { + if m == nil { + return (*Exemplar)(nil) + } + r := new(Exemplar) + r.Timestamp = m.Timestamp + r.ProfileId = m.ProfileId + r.SpanId = m.SpanId + r.Value = m.Value + if rhs := m.Labels; rhs != nil { + tmpContainer := make([]*LabelPair, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Labels = tmpContainer + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *Exemplar) CloneMessageVT() proto.Message { + return m.CloneVT() +} + func (this *LabelPair) EqualVT(that *LabelPair) bool { if this == that { return true @@ -562,6 +596,23 @@ func (this *Point) EqualVT(that *Point) bool { } } } + if len(this.Exemplars) != len(that.Exemplars) { + return false + } + for i, vx := range this.Exemplars { + vy := that.Exemplars[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &Exemplar{} + } + if q == nil { + q = &Exemplar{} + } + if !p.EqualVT(q) { + return false + } + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -909,6 +960,51 @@ func (this *GetProfileStatsResponse) EqualMessageVT(thatMsg proto.Message) bool } return this.EqualVT(that) } +func (this *Exemplar) EqualVT(that *Exemplar) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.Timestamp != that.Timestamp { + return false + } + if this.ProfileId != that.ProfileId { + return false + } + if this.SpanId != that.SpanId { + return false + } + if this.Value != that.Value { + return false + } + if len(this.Labels) != len(that.Labels) { + return false + } + for i, vx := range this.Labels { + vy := that.Labels[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &LabelPair{} + } + if q == nil { + q = &LabelPair{} + } + if !p.EqualVT(q) { + return false + } + } + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *Exemplar) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*Exemplar) + if !ok { + return false + } + return this.EqualVT(that) +} func (m *LabelPair) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -1163,6 +1259,18 @@ func (m *Point) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.Exemplars) > 0 { + for iNdEx := len(m.Exemplars) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Exemplars[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x22 + } + } if len(m.Annotations) > 0 { for iNdEx := len(m.Annotations) - 1; iNdEx >= 0; iNdEx-- { size, err := m.Annotations[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) @@ -1788,6 +1896,75 @@ func (m *GetProfileStatsResponse) MarshalToSizedBufferVT(dAtA []byte) (int, erro return len(dAtA) - i, nil } +func (m *Exemplar) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Exemplar) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *Exemplar) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Labels) > 0 { + for iNdEx := len(m.Labels) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Labels[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x2a + } + } + if m.Value != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.Value)) + i-- + dAtA[i] = 0x20 + } + if len(m.SpanId) > 0 { + i -= len(m.SpanId) + copy(dAtA[i:], m.SpanId) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.SpanId))) + i-- + dAtA[i] = 0x1a + } + if len(m.ProfileId) > 0 { + i -= len(m.ProfileId) + copy(dAtA[i:], m.ProfileId) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.ProfileId))) + i-- + dAtA[i] = 0x12 + } + if m.Timestamp != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.Timestamp)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func (m *LabelPair) SizeVT() (n int) { if m == nil { return 0 @@ -1896,6 +2073,12 @@ func (m *Point) SizeVT() (n int) { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } } + if len(m.Exemplars) > 0 { + for _, e := range m.Exemplars { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } n += len(m.unknownFields) return n } @@ -2132,6 +2315,36 @@ func (m *GetProfileStatsResponse) SizeVT() (n int) { return n } +func (m *Exemplar) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Timestamp != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.Timestamp)) + } + l = len(m.ProfileId) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.SpanId) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Value != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.Value)) + } + if len(m.Labels) > 0 { + for _, e := range m.Labels { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} + func (m *LabelPair) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -2787,6 +3000,40 @@ func (m *Point) UnmarshalVT(dAtA []byte) error { return err } iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Exemplars", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Exemplars = append(m.Exemplars, &Exemplar{}) + if err := m.Exemplars[len(m.Exemplars)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -4143,3 +4390,190 @@ func (m *GetProfileStatsResponse) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *Exemplar) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Exemplar: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Exemplar: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) + } + m.Timestamp = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Timestamp |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ProfileId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ProfileId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SpanId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.SpanId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) + } + m.Value = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Value |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Labels", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Labels = append(m.Labels, &LabelPair{}) + if err := m.Labels[len(m.Labels)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/api/openapiv2/gen/phlare.swagger.json b/api/openapiv2/gen/phlare.swagger.json index 1fe415d5f5..bae1ad7d9d 100644 --- a/api/openapiv2/gen/phlare.swagger.json +++ b/api/openapiv2/gen/phlare.swagger.json @@ -961,6 +961,48 @@ } } }, + "v1Exemplar": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "int64", + "description": "Milliseconds since epoch when the profile was captured." + }, + "profileId": { + "type": "string", + "description": "Unique identifier for the profile (UUID)." + }, + "spanId": { + "type": "string", + "description": "Span ID if this profile was split by span during ingestion." + }, + "value": { + "type": "string", + "format": "uint64", + "description": "Total sample value for this profile (e.g., CPU nanoseconds, bytes allocated)." + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1LabelPair" + }, + "description": "Series labels that are NOT included in the group_by query parameter.\nThese labels complete the full series identity of this exemplar's profile.\nFor example, if group_by=[\"service\"], this would contain labels like \"pod\",\n\"namespace\", \"region\" that were omitted from grouping. This allows identifying\nwhich specific series instance this profile sample came from." + } + }, + "description": "Exemplar represents metadata for an individual profile sample.\nExemplars allow users to drill down from aggregated timeline views\nto specific profile instances." + }, + "v1ExemplarType": { + "type": "string", + "enum": [ + "EXEMPLAR_TYPE_UNSPECIFIED", + "EXEMPLAR_TYPE_NONE", + "EXEMPLAR_TYPE_INDIVIDUAL", + "EXEMPLAR_TYPE_SPAN" + ], + "default": "EXEMPLAR_TYPE_UNSPECIFIED" + }, "v1FeatureFlag": { "type": "object", "properties": { @@ -1653,6 +1695,14 @@ "type": "object", "$ref": "#/definitions/v1ProfileAnnotation" } + }, + "exemplars": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1Exemplar" + }, + "title": "Exemplars are samples of individual profiles that contributed to this aggregated point" } } }, @@ -2131,6 +2181,13 @@ "stackTraceSelector": { "$ref": "#/definitions/v1StackTraceSelector", "description": "Select stack traces that match the provided selector." + }, + "profileIdSelector": { + "type": "array", + "items": { + "type": "string" + }, + "title": "List of Profile UUIDs to query" } } }, @@ -2413,6 +2470,9 @@ "limit": { "type": "string", "format": "int64" + }, + "exemplarType": { + "$ref": "#/definitions/v1ExemplarType" } } }, diff --git a/api/querier/v1/querier.proto b/api/querier/v1/querier.proto index e44d78cdc0..a27d01248e 100644 --- a/api/querier/v1/querier.proto +++ b/api/querier/v1/querier.proto @@ -112,6 +112,8 @@ message SelectMergeStacktracesRequest { ProfileFormat format = 6; // Select stack traces that match the provided selector. optional types.v1.StackTraceSelector stack_trace_selector = 7; + // List of Profile UUIDs to query + repeated string profile_id_selector = 8 [(gnostic.openapi.v3.property).example = {yaml: "['7c9e6679-7425-40de-944b-e07fc1f90ae7']"}]; } enum ProfileFormat { @@ -217,6 +219,8 @@ message SelectSeriesRequest { optional types.v1.StackTraceSelector stack_trace_selector = 8; // Select the top N series by total value. optional int64 limit = 9; + // Type of exemplars to include in the response. + types.v1.ExemplarType exemplar_type = 10; } message SelectSeriesResponse { diff --git a/api/query/v1/query.proto b/api/query/v1/query.proto index 70e00e1d8b..ce30905b41 100644 --- a/api/query/v1/query.proto +++ b/api/query/v1/query.proto @@ -161,6 +161,7 @@ message TimeSeriesQuery { double step = 1; repeated string group_by = 2; int64 limit = 3; + types.v1.ExemplarType exemplar_type = 4; } message TimeSeriesReport { diff --git a/api/types/v1/types.proto b/api/types/v1/types.proto index d090d71cf5..9149906e7a 100644 --- a/api/types/v1/types.proto +++ b/api/types/v1/types.proto @@ -36,6 +36,8 @@ message Point { // Milliseconds unix timestamp int64 timestamp = 2; repeated ProfileAnnotation annotations = 3; + // Exemplars are samples of individual profiles that contributed to this aggregated point + repeated Exemplar exemplars = 4; } // Annotations provide additional metadata for a profile. @@ -100,6 +102,13 @@ enum TimeSeriesAggregationType { TIME_SERIES_AGGREGATION_TYPE_AVERAGE = 1; } +enum ExemplarType { + EXEMPLAR_TYPE_UNSPECIFIED = 0; + EXEMPLAR_TYPE_NONE = 1; + EXEMPLAR_TYPE_INDIVIDUAL = 2; + EXEMPLAR_TYPE_SPAN = 3; +} + // StackTraceSelector is used for filtering stack traces by locations. message StackTraceSelector { // Stack trace of the call site. Root at call_site[0]. @@ -133,3 +142,27 @@ message GetProfileStatsResponse { // Milliseconds since epoch. int64 newest_profile_time = 3; } + +// Exemplar represents metadata for an individual profile sample. +// Exemplars allow users to drill down from aggregated timeline views +// to specific profile instances. +message Exemplar { + // Milliseconds since epoch when the profile was captured. + int64 timestamp = 1 [(gnostic.openapi.v3.property).example = {yaml: "1730000023000"}]; + + // Unique identifier for the profile (UUID). + string profile_id = 2 [(gnostic.openapi.v3.property).example = {yaml: "7c9e6679-7425-40de-944b-e07fc1f90ae7"}]; + + // Span ID if this profile was split by span during ingestion. + string span_id = 3 [(gnostic.openapi.v3.property).example = {yaml: "00f067aa0ba902b7"}]; + + // Total sample value for this profile (e.g., CPU nanoseconds, bytes allocated). + uint64 value = 4 [(gnostic.openapi.v3.property).example = {yaml: "2450000000"}]; + + // Series labels that are NOT included in the group_by query parameter. + // These labels complete the full series identity of this exemplar's profile. + // For example, if group_by=["service"], this would contain labels like "pod", + // "namespace", "region" that were omitted from grouping. This allows identifying + // which specific series instance this profile sample came from. + repeated LabelPair labels = 5; +} diff --git a/docs/sources/reference-server-api/index.md b/docs/sources/reference-server-api/index.md index 46c76fcb8f..23bfa03ae8 100644 --- a/docs/sources/reference-server-api/index.md +++ b/docs/sources/reference-server-api/index.md @@ -126,6 +126,7 @@ A request body with the following fields is required: |`left.format` | | | |`left.labelSelector` | Label selector string | `{namespace="my-namespace"}` | |`left.maxNodes` | Limit the nodes returned to only show the node with the max_node's biggest total | | +|`left.profileIdSelector` | List of Profile UUIDs to query | `["7c9e6679-7425-40de-944b-e07fc1f90ae7"]` | |`left.profileTypeID` | Profile Type ID string in the form ::::. | `process_cpu:cpu:nanoseconds:cpu:nanoseconds` | |`left.stackTraceSelector.callSite[].name` | | | |`left.stackTraceSelector.goPgo.aggregateCallees` | Aggregate callees causes the leaf location line number to be ignored, thus aggregating all callee samples (but not callers). | | @@ -135,6 +136,7 @@ A request body with the following fields is required: |`right.format` | | | |`right.labelSelector` | Label selector string | `{namespace="my-namespace"}` | |`right.maxNodes` | Limit the nodes returned to only show the node with the max_node's biggest total | | +|`right.profileIdSelector` | List of Profile UUIDs to query | `["7c9e6679-7425-40de-944b-e07fc1f90ae7"]` | |`right.profileTypeID` | Profile Type ID string in the form ::::. | `process_cpu:cpu:nanoseconds:cpu:nanoseconds` | |`right.stackTraceSelector.callSite[].name` | | | |`right.stackTraceSelector.goPgo.aggregateCallees` | Aggregate callees causes the leaf location line number to be ignored, thus aggregating all callee samples (but not callers). | | @@ -148,12 +150,18 @@ curl \ "left": { "end": '$(date +%s)000', "labelSelector": "{namespace=\"my-namespace\"}", + "profileIdSelector": [ + "7c9e6679-7425-40de-944b-e07fc1f90ae7" + ], "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", "start": '$(expr $(date +%s) - 3600 )000' }, "right": { "end": '$(date +%s)000', "labelSelector": "{namespace=\"my-namespace\"}", + "profileIdSelector": [ + "7c9e6679-7425-40de-944b-e07fc1f90ae7" + ], "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", "start": '$(expr $(date +%s) - 3600 )000' } @@ -168,12 +176,18 @@ body = { "left": { "end": int(datetime.datetime.now().timestamp() * 1000), "labelSelector": "{namespace=\"my-namespace\"}", + "profileIdSelector": [ + "7c9e6679-7425-40de-944b-e07fc1f90ae7" + ], "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) }, "right": { "end": int(datetime.datetime.now().timestamp() * 1000), "labelSelector": "{namespace=\"my-namespace\"}", + "profileIdSelector": [ + "7c9e6679-7425-40de-944b-e07fc1f90ae7" + ], "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) } @@ -416,6 +430,7 @@ A request body with the following fields is required: |`format` | | | |`labelSelector` | Label selector string | `{namespace="my-namespace"}` | |`maxNodes` | Limit the nodes returned to only show the node with the max_node's biggest total | | +|`profileIdSelector` | List of Profile UUIDs to query | `["7c9e6679-7425-40de-944b-e07fc1f90ae7"]` | |`profileTypeID` | Profile Type ID string in the form ::::. | `process_cpu:cpu:nanoseconds:cpu:nanoseconds` | |`stackTraceSelector.callSite[].name` | | | |`stackTraceSelector.goPgo.aggregateCallees` | Aggregate callees causes the leaf location line number to be ignored, thus aggregating all callee samples (but not callers). | | @@ -428,6 +443,9 @@ curl \ -d '{ "end": '$(date +%s)000', "labelSelector": "{namespace=\"my-namespace\"}", + "profileIdSelector": [ + "7c9e6679-7425-40de-944b-e07fc1f90ae7" + ], "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", "start": '$(expr $(date +%s) - 3600 )000' }' \ @@ -440,6 +458,9 @@ import datetime body = { "end": int(datetime.datetime.now().timestamp() * 1000), "labelSelector": "{namespace=\"my-namespace\"}", + "profileIdSelector": [ + "7c9e6679-7425-40de-944b-e07fc1f90ae7" + ], "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) } @@ -462,6 +483,7 @@ A request body with the following fields is required: |`start` | Milliseconds since epoch. | `1676282400000` | |`end` | Milliseconds since epoch. | `1676289600000` | |`aggregation` | | | +|`exemplarType` | | | |`groupBy` | | `["pod"]` | |`labelSelector` | Label selector string | `{namespace="my-namespace"}` | |`limit` | Select the top N series by total value. | |