Skip to content

Commit c0ce352

Browse files
authored
[receiver/signalfx] Accept otlp metrics (#31008)
**Description:** Accept OTLP payloads on /v2/datapoint api of the SignalFx receiver **Link to tracking Issue:** #26298
1 parent 8ccadc3 commit c0ce352

File tree

3 files changed

+353
-73
lines changed

3 files changed

+353
-73
lines changed

.chloggen/signalfx-recv-otlp.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: receiver/signalfx
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Accept otlp protobuf requests when content-type is "application/x-protobuf;format=otlp"
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [31052]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

receiver/signalfxreceiver/receiver.go

Lines changed: 86 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"go.opentelemetry.io/collector/component"
2121
"go.opentelemetry.io/collector/consumer"
2222
"go.opentelemetry.io/collector/pdata/plog"
23+
"go.opentelemetry.io/collector/pdata/pmetric"
24+
"go.opentelemetry.io/collector/pdata/pmetric/pmetricotlp"
2325
"go.opentelemetry.io/collector/receiver"
2426
"go.opentelemetry.io/collector/receiver/receiverhelper"
2527
"go.uber.org/zap"
@@ -32,9 +34,11 @@ import (
3234
const (
3335
defaultServerTimeout = 20 * time.Second
3436

35-
responseOK = "OK"
36-
responseInvalidMethod = "Only \"POST\" method is supported"
37-
responseInvalidContentType = "\"Content-Type\" must be \"application/x-protobuf\""
37+
responseOK = "OK"
38+
responseInvalidMethod = "Only \"POST\" method is supported"
39+
responseEventsInvalidContentType = "\"Content-Type\" must be \"application/x-protobuf\""
40+
41+
responseInvalidContentType = "\"Content-Type\" must be either \"application/x-protobuf\" or \"application/x-protobuf;format=otlp\""
3842
responseInvalidEncoding = "\"Content-Encoding\" must be \"gzip\" or empty"
3943
responseErrGzipReader = "Error on gzip body"
4044
responseErrReadBody = "Failed to read message body"
@@ -45,22 +49,24 @@ const (
4549

4650
// Centralizing some HTTP and related string constants.
4751
protobufContentType = "application/x-protobuf"
52+
otlpProtobufContentType = "application/x-protobuf;format=otlp"
4853
gzipEncoding = "gzip"
4954
httpContentTypeHeader = "Content-Type"
5055
httpContentEncodingHeader = "Content-Encoding"
5156
)
5257

5358
var (
54-
okRespBody = initJSONResponse(responseOK)
55-
invalidMethodRespBody = initJSONResponse(responseInvalidMethod)
56-
invalidContentRespBody = initJSONResponse(responseInvalidContentType)
57-
invalidEncodingRespBody = initJSONResponse(responseInvalidEncoding)
58-
errGzipReaderRespBody = initJSONResponse(responseErrGzipReader)
59-
errReadBodyRespBody = initJSONResponse(responseErrReadBody)
60-
errUnmarshalBodyRespBody = initJSONResponse(responseErrUnmarshalBody)
61-
errNextConsumerRespBody = initJSONResponse(responseErrNextConsumer)
62-
errLogsNotConfigured = initJSONResponse(responseErrLogsNotConfigured)
63-
errMetricsNotConfigured = initJSONResponse(responseErrMetricsNotConfigured)
59+
okRespBody = initJSONResponse(responseOK)
60+
invalidMethodRespBody = initJSONResponse(responseInvalidMethod)
61+
invalidContentRespBody = initJSONResponse(responseInvalidContentType)
62+
invalidEventsContentRespBody = initJSONResponse(responseEventsInvalidContentType)
63+
invalidEncodingRespBody = initJSONResponse(responseInvalidEncoding)
64+
errGzipReaderRespBody = initJSONResponse(responseErrGzipReader)
65+
errReadBodyRespBody = initJSONResponse(responseErrReadBody)
66+
errUnmarshalBodyRespBody = initJSONResponse(responseErrUnmarshalBody)
67+
errNextConsumerRespBody = initJSONResponse(responseErrNextConsumer)
68+
errLogsNotConfigured = initJSONResponse(responseErrLogsNotConfigured)
69+
errMetricsNotConfigured = initJSONResponse(responseErrMetricsNotConfigured)
6470

6571
translator = &signalfx.ToTranslator{}
6672
)
@@ -166,16 +172,6 @@ func (r *sfxReceiver) Shutdown(context.Context) error {
166172
}
167173

168174
func (r *sfxReceiver) readBody(ctx context.Context, resp http.ResponseWriter, req *http.Request) ([]byte, bool) {
169-
if req.Method != http.MethodPost {
170-
r.failRequest(ctx, resp, http.StatusBadRequest, invalidMethodRespBody, nil)
171-
return nil, false
172-
}
173-
174-
if req.Header.Get(httpContentTypeHeader) != protobufContentType {
175-
r.failRequest(ctx, resp, http.StatusUnsupportedMediaType, invalidContentRespBody, nil)
176-
return nil, false
177-
}
178-
179175
encoding := req.Header.Get(httpContentEncodingHeader)
180176
if encoding != "" && encoding != gzipEncoding {
181177
r.failRequest(ctx, resp, http.StatusUnsupportedMediaType, invalidEncodingRespBody, nil)
@@ -221,40 +217,64 @@ func (r *sfxReceiver) handleDatapointReq(resp http.ResponseWriter, req *http.Req
221217
return
222218
}
223219

224-
body, ok := r.readBody(ctx, resp, req)
225-
if !ok {
220+
if req.Method != http.MethodPost {
221+
r.failRequest(ctx, resp, http.StatusBadRequest, invalidMethodRespBody, nil)
226222
return
227223
}
228224

229-
msg := &sfxpb.DataPointUploadMessage{}
230-
if err := msg.Unmarshal(body); err != nil {
231-
r.failRequest(ctx, resp, http.StatusBadRequest, errUnmarshalBodyRespBody, err)
225+
otlpFormat := false
226+
switch req.Header.Get(httpContentTypeHeader) {
227+
case protobufContentType:
228+
case otlpProtobufContentType:
229+
otlpFormat = true
230+
default:
231+
r.failRequest(ctx, resp, http.StatusUnsupportedMediaType, invalidContentRespBody, nil)
232232
return
233233
}
234234

235-
if len(msg.Datapoints) == 0 {
236-
r.obsrecv.EndMetricsOp(ctx, metadata.Type.String(), 0, nil)
237-
_, _ = resp.Write(okRespBody)
235+
body, ok := r.readBody(ctx, resp, req)
236+
if !ok {
238237
return
239238
}
240239

241-
md, err := translator.ToMetrics(msg.Datapoints)
242-
if err != nil {
243-
r.settings.Logger.Debug("SignalFx conversion error", zap.Error(err))
244-
}
240+
r.settings.Logger.Debug("Handling metrics data")
245241

246-
if r.config.AccessTokenPassthrough {
247-
if accessToken := req.Header.Get(splunk.SFxAccessTokenHeader); accessToken != "" {
248-
for i := 0; i < md.ResourceMetrics().Len(); i++ {
249-
rm := md.ResourceMetrics().At(i)
250-
res := rm.Resource()
251-
res.Attributes().PutStr(splunk.SFxAccessTokenLabel, accessToken)
252-
}
242+
var md pmetric.Metrics
243+
244+
if otlpFormat {
245+
r.settings.Logger.Debug("Received request is in OTLP format")
246+
otlpreq := pmetricotlp.NewExportRequest()
247+
if err := otlpreq.UnmarshalProto(body); err != nil {
248+
r.settings.Logger.Debug("OTLP data unmarshalling failed", zap.Error(err))
249+
r.failRequest(ctx, resp, http.StatusBadRequest, errUnmarshalBodyRespBody, err)
250+
return
253251
}
252+
md = otlpreq.Metrics()
253+
} else {
254+
msg := &sfxpb.DataPointUploadMessage{}
255+
err := msg.Unmarshal(body)
256+
if err != nil {
257+
r.failRequest(ctx, resp, http.StatusBadRequest, errUnmarshalBodyRespBody, err)
258+
return
259+
}
260+
261+
md, err = translator.ToMetrics(msg.Datapoints)
262+
if err != nil {
263+
r.settings.Logger.Debug("SignalFx conversion error", zap.Error(err))
264+
}
265+
}
266+
267+
dataPointCount := md.DataPointCount()
268+
if dataPointCount == 0 {
269+
r.obsrecv.EndMetricsOp(ctx, metadata.Type.String(), 0, nil)
270+
_, _ = resp.Write(okRespBody)
271+
return
254272
}
255273

256-
err = r.metricsConsumer.ConsumeMetrics(ctx, md)
257-
r.obsrecv.EndMetricsOp(ctx, metadata.Type.String(), len(msg.Datapoints), err)
274+
r.addAccessTokenLabel(md, req)
275+
276+
err := r.metricsConsumer.ConsumeMetrics(ctx, md)
277+
r.obsrecv.EndMetricsOp(ctx, metadata.Type.String(), dataPointCount, err)
258278

259279
r.writeResponse(ctx, resp, err)
260280
}
@@ -267,6 +287,16 @@ func (r *sfxReceiver) handleEventReq(resp http.ResponseWriter, req *http.Request
267287
return
268288
}
269289

290+
if req.Method != http.MethodPost {
291+
r.failRequest(ctx, resp, http.StatusBadRequest, invalidMethodRespBody, nil)
292+
return
293+
}
294+
295+
if req.Header.Get(httpContentTypeHeader) != protobufContentType {
296+
r.failRequest(ctx, resp, http.StatusUnsupportedMediaType, invalidEventsContentRespBody, nil)
297+
return
298+
}
299+
270300
body, ok := r.readBody(ctx, resp, req)
271301
if !ok {
272302
return
@@ -336,6 +366,18 @@ func (r *sfxReceiver) failRequest(
336366
)
337367
}
338368

369+
func (r *sfxReceiver) addAccessTokenLabel(md pmetric.Metrics, req *http.Request) {
370+
if r.config.AccessTokenPassthrough {
371+
if accessToken := req.Header.Get(splunk.SFxAccessTokenHeader); accessToken != "" {
372+
for i := 0; i < md.ResourceMetrics().Len(); i++ {
373+
rm := md.ResourceMetrics().At(i)
374+
res := rm.Resource()
375+
res.Attributes().PutStr(splunk.SFxAccessTokenLabel, accessToken)
376+
}
377+
}
378+
}
379+
}
380+
339381
func initJSONResponse(s string) []byte {
340382
respBody, err := json.Marshal(s)
341383
if err != nil {

0 commit comments

Comments
 (0)