@@ -19,6 +19,12 @@ import (
1919 gprofile "github.com/google/pprof/profile"
2020 "github.com/google/uuid"
2121 "github.com/pkg/errors"
22+ profilesv1 "go.opentelemetry.io/proto/otlp/collector/profiles/v1development"
23+ commonv1 "go.opentelemetry.io/proto/otlp/common/v1"
24+ otlpprofiles "go.opentelemetry.io/proto/otlp/profiles/v1development"
25+ resourcev1 "go.opentelemetry.io/proto/otlp/resource/v1"
26+ "google.golang.org/grpc"
27+ "google.golang.org/grpc/credentials/insecure"
2228
2329 googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
2430 pushv1 "github.com/grafana/pyroscope/api/gen/proto/go/push/v1"
@@ -31,6 +37,7 @@ import (
3137
3238const (
3339 profileTypeID = "deadmans_switch:made_up:profilos:made_up:profilos"
40+ otlpProfileTypeID = "otlp_test:otlp_test:count:otlp_test:samples"
3441 canaryExporterServiceName = "pyroscope-canary-exporter"
3542)
3643
@@ -74,6 +81,140 @@ func (ce *canaryExporter) testIngestProfile(ctx context.Context, now time.Time)
7481 return nil
7582}
7683
84+ // generateOTLPProfile creates an OTLP profile with the specified ingestion method label
85+ func (ce * canaryExporter ) generateOTLPProfile (now time.Time , ingestionMethod string ) * profilesv1.ExportProfilesServiceRequest {
86+ // Sanitize the ingestion method label value by replacing "/" with "_"
87+ sanitizedMethod := strings .ReplaceAll (ingestionMethod , "/" , "_" )
88+
89+ // Create the profile dictionary with custom profile type similar to pprof probe
90+ dictionary := & otlpprofiles.ProfilesDictionary {
91+ StringTable : []string {
92+ "" , // 0: empty string
93+ "otlp_test" , // 1
94+ "samples" , // 2
95+ "count" , // 3
96+ "func1" , // 4
97+ "func2" , // 5
98+ "ingestion_method" , // 6
99+ sanitizedMethod , // 7
100+ },
101+ MappingTable : []* otlpprofiles.Mapping {
102+ {}, // 0: empty mapping (required null entry)
103+ },
104+ FunctionTable : []* otlpprofiles.Function {
105+ {NameStrindex : 0 }, // 0: empty
106+ {NameStrindex : 4 }, // 1: func1
107+ {NameStrindex : 5 }, // 2: func2
108+ },
109+ LocationTable : []* otlpprofiles.Location {
110+ {Line : []* otlpprofiles.Line {{FunctionIndex : 1 }}}, // 0: func1
111+ {Line : []* otlpprofiles.Line {{FunctionIndex : 2 }}}, // 1: func2
112+ },
113+ StackTable : []* otlpprofiles.Stack {
114+ {LocationIndices : []int32 {}}, // 0: empty (required null entry)
115+ {LocationIndices : []int32 {1 , 0 }}, // 1: func2, func1 stack
116+ {LocationIndices : []int32 {0 }}, // 2: func1 stack
117+ },
118+ }
119+
120+ // Create profile with two samples matching the original pprof profile
121+ profile := & otlpprofiles.Profile {
122+ TimeUnixNano : uint64 (now .UnixNano ()),
123+ DurationNano : 0 ,
124+ Period : 1 ,
125+ SampleType : & otlpprofiles.ValueType {
126+ TypeStrindex : 1 , // "otlp_test"
127+ UnitStrindex : 3 , // "count"
128+ },
129+ PeriodType : & otlpprofiles.ValueType {
130+ TypeStrindex : 1 , // "otlp_test"
131+ UnitStrindex : 2 , // "samples"
132+ },
133+ Sample : []* otlpprofiles.Sample {
134+ {
135+ // func1>func2 with value 10
136+ StackIndex : 1 , // stack_table[1]
137+ Values : []int64 {10 },
138+ },
139+ {
140+ // func1 with value 20
141+ StackIndex : 2 , // stack_table[2]
142+ Values : []int64 {20 },
143+ },
144+ },
145+ }
146+
147+ // Create the resource attributes
148+ resourceAttrs := []* commonv1.KeyValue {
149+ {
150+ Key : "service.name" ,
151+ Value : & commonv1.AnyValue {Value : & commonv1.AnyValue_StringValue {StringValue : canaryExporterServiceName }},
152+ },
153+ {
154+ Key : "job" ,
155+ Value : & commonv1.AnyValue {Value : & commonv1.AnyValue_StringValue {StringValue : "canary-exporter" }},
156+ },
157+ {
158+ Key : "instance" ,
159+ Value : & commonv1.AnyValue {Value : & commonv1.AnyValue_StringValue {StringValue : ce .hostname }},
160+ },
161+ {
162+ Key : "ingestion_method" ,
163+ Value : & commonv1.AnyValue {Value : & commonv1.AnyValue_StringValue {StringValue : sanitizedMethod }},
164+ },
165+ }
166+
167+ // Create the OTLP request
168+ req := & profilesv1.ExportProfilesServiceRequest {
169+ Dictionary : dictionary ,
170+ ResourceProfiles : []* otlpprofiles.ResourceProfiles {
171+ {
172+ Resource : & resourcev1.Resource {
173+ Attributes : resourceAttrs ,
174+ },
175+ ScopeProfiles : []* otlpprofiles.ScopeProfiles {
176+ {
177+ Scope : & commonv1.InstrumentationScope {
178+ Name : "pyroscope-canary-exporter" ,
179+ },
180+ Profiles : []* otlpprofiles.Profile {profile },
181+ },
182+ },
183+ },
184+ },
185+ }
186+
187+ return req
188+ }
189+
190+ func (ce * canaryExporter ) testIngestOTLPGrpc (ctx context.Context , now time.Time ) error {
191+ // Generate the OTLP profile with the appropriate ingestion method label
192+ req := ce .generateOTLPProfile (now , "otlp/grpc" )
193+
194+ // Create gRPC connection
195+ grpcAddr := strings .TrimPrefix (ce .params .URL , "http://" )
196+ grpcAddr = strings .TrimPrefix (grpcAddr , "https://" )
197+
198+ conn , err := grpc .NewClient (grpcAddr ,
199+ grpc .WithTransportCredentials (insecure .NewCredentials ()))
200+ if err != nil {
201+ return fmt .Errorf ("failed to connect to gRPC server: %w" , err )
202+ }
203+ defer conn .Close ()
204+
205+ // Create OTLP profiles service client
206+ client := profilesv1 .NewProfilesServiceClient (conn )
207+
208+ // Send the profile
209+ _ , err = client .Export (ctx , req )
210+ if err != nil {
211+ return fmt .Errorf ("failed to export OTLP profile via gRPC: %w" , err )
212+ }
213+
214+ level .Info (logger ).Log ("msg" , "successfully ingested OTLP profile via gRPC" )
215+ return nil
216+ }
217+
77218func (ce * canaryExporter ) testSelectMergeProfile (ctx context.Context , now time.Time ) error {
78219 respQuery , err := ce .params .queryClient ().SelectMergeProfile (ctx , connect .NewRequest (& querierv1.SelectMergeProfileRequest {
79220 Start : now .UnixMilli (),
@@ -128,6 +269,65 @@ func (ce *canaryExporter) testSelectMergeProfile(ctx context.Context, now time.T
128269 return nil
129270}
130271
272+ func (ce * canaryExporter ) testSelectMergeOTLPProfile (ctx context.Context , now time.Time ) error {
273+ // Query specifically for OTLP gRPC ingested profiles using the custom profile type
274+ //labelSelector := fmt.Sprintf(`{service_name="%s", job="canary-exporter", instance="%s"}`, canaryExporterServiceName, ce.hostname)
275+
276+ respQuery , err := ce .params .queryClient ().SelectMergeProfile (ctx , connect .NewRequest (& querierv1.SelectMergeProfileRequest {
277+ Start : now .UnixMilli (),
278+ End : now .Add (5 * time .Second ).UnixMilli (),
279+ LabelSelector : ce .createLabelSelector (),
280+ ProfileTypeID : otlpProfileTypeID ,
281+ }))
282+ if err != nil {
283+ return fmt .Errorf ("failed to query OTLP profile: %w" , err )
284+ }
285+
286+ buf , err := respQuery .Msg .MarshalVT ()
287+ if err != nil {
288+ return errors .Wrap (err , "failed to marshal protobuf" )
289+ }
290+
291+ gp , err := gprofile .Parse (bytes .NewReader (buf ))
292+ if err != nil {
293+ return errors .Wrap (err , "failed to parse profile" )
294+ }
295+
296+ // Verify the expected stacktraces from the OTLP profile
297+ expected := map [string ]int64 {
298+ "func2>func1" : 10 ,
299+ "func1" : 20 ,
300+ }
301+ actual := make (map [string ]int64 )
302+
303+ var sb strings.Builder
304+ for _ , s := range gp .Sample {
305+ sb .Reset ()
306+ for _ , loc := range s .Location {
307+ if sb .Len () != 0 {
308+ _ , err := sb .WriteRune ('>' )
309+ if err != nil {
310+ return err
311+ }
312+ }
313+ for _ , line := range loc .Line {
314+ _ , err := sb .WriteString (line .Function .Name )
315+ if err != nil {
316+ return err
317+ }
318+ }
319+ }
320+ actual [sb .String ()] = actual [sb .String ()] + s .Value [0 ]
321+ }
322+
323+ if diff := cmp .Diff (expected , actual ); diff != "" {
324+ return fmt .Errorf ("OTLP profile query mismatch (-expected, +actual):\n %s" , diff )
325+ }
326+
327+ level .Info (logger ).Log ("msg" , "successfully queried OTLP profile via gRPC" )
328+ return nil
329+ }
330+
131331func (ce * canaryExporter ) testProfileTypes (ctx context.Context , now time.Time ) error {
132332 respQuery , err := ce .params .queryClient ().ProfileTypes (ctx , connect .NewRequest (& querierv1.ProfileTypesRequest {
133333 Start : now .UnixMilli (),
0 commit comments