diff --git a/.chloggen/passing-token-via-context-splunkhec-receiver.yaml b/.chloggen/passing-token-via-context-splunkhec-receiver.yaml new file mode 100644 index 000000000000..3f0d64bd3092 --- /dev/null +++ b/.chloggen/passing-token-via-context-splunkhec-receiver.yaml @@ -0,0 +1,28 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: splunkhecreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Introduce feature gate for passing access token via context in the Splunk HEC receiver + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [37307] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: | + When `receiver.splunkhec.passtokenviacontext` enabled, the Splunk HEC receiver will pass the access token via the context. + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/internal/splunk/common.go b/internal/splunk/common.go index e57ecaf53e15..b26404ec64c2 100644 --- a/internal/splunk/common.go +++ b/internal/splunk/common.go @@ -46,6 +46,9 @@ type AccessTokenPassthroughConfig struct { AccessTokenPassthrough bool `mapstructure:"access_token_passthrough"` } +// LabelType Type wrapper for accessing context value +type LabelType string + // Event represents a metric in Splunk HEC format type Event struct { Time float64 `json:"time,omitempty"` // optional epoch time - set to zero if the event timestamp is missing or unknown (will be added at indexing time) diff --git a/receiver/splunkhecreceiver/factory.go b/receiver/splunkhecreceiver/factory.go index 916f2d8c651c..32321f10869c 100644 --- a/receiver/splunkhecreceiver/factory.go +++ b/receiver/splunkhecreceiver/factory.go @@ -9,6 +9,7 @@ import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/featuregate" "go.opentelemetry.io/collector/receiver" conventions "go.opentelemetry.io/collector/semconv/v1.27.0" @@ -19,6 +20,14 @@ import ( // This file implements factory for Splunk HEC receiver. +var tokenContextGate = featuregate.GlobalRegistry().MustRegister( + "receiver.splunkhec.passtokenviacontext", + featuregate.StageAlpha, + featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/37307"), + featuregate.WithRegisterDescription("When enabled, the Splunk HEC receiver will pass the access token via the context."), + featuregate.WithRegisterFromVersion("0.119.0"), +) + const ( // Default endpoint to bind to. defaultEndpoint = "localhost:8088" diff --git a/receiver/splunkhecreceiver/go.mod b/receiver/splunkhecreceiver/go.mod index 1cfbb94e1900..ae0cf6928779 100644 --- a/receiver/splunkhecreceiver/go.mod +++ b/receiver/splunkhecreceiver/go.mod @@ -22,6 +22,7 @@ require ( go.opentelemetry.io/collector/consumer v1.24.1-0.20250123125445-24f88da7b583 go.opentelemetry.io/collector/consumer/consumertest v0.118.1-0.20250123125445-24f88da7b583 go.opentelemetry.io/collector/exporter/exportertest v0.118.1-0.20250123125445-24f88da7b583 + go.opentelemetry.io/collector/featuregate v1.24.1-0.20250123125445-24f88da7b583 go.opentelemetry.io/collector/pdata v1.24.1-0.20250123125445-24f88da7b583 go.opentelemetry.io/collector/receiver v0.118.1-0.20250123125445-24f88da7b583 go.opentelemetry.io/collector/receiver/receivertest v0.118.1-0.20250123125445-24f88da7b583 @@ -72,7 +73,6 @@ require ( go.opentelemetry.io/collector/extension v0.118.1-0.20250123125445-24f88da7b583 // indirect go.opentelemetry.io/collector/extension/auth v0.118.1-0.20250123125445-24f88da7b583 // indirect go.opentelemetry.io/collector/extension/xextension v0.118.1-0.20250123125445-24f88da7b583 // indirect - go.opentelemetry.io/collector/featuregate v1.24.1-0.20250123125445-24f88da7b583 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.118.1-0.20250123125445-24f88da7b583 // indirect go.opentelemetry.io/collector/pipeline v0.118.1-0.20250123125445-24f88da7b583 // indirect go.opentelemetry.io/collector/receiver/xreceiver v0.118.1-0.20250123125445-24f88da7b583 // indirect diff --git a/receiver/splunkhecreceiver/receiver.go b/receiver/splunkhecreceiver/receiver.go index 2bf5458cc7f7..899455d71b51 100644 --- a/receiver/splunkhecreceiver/receiver.go +++ b/receiver/splunkhecreceiver/receiver.go @@ -302,7 +302,7 @@ func (r *splunkReceiver) handleRawReq(resp http.ResponseWriter, req *http.Reques defer r.gzipReaderPool.Put(reader) } - resourceCustomizer := r.createResourceCustomizer(req) + ctx, resourceCustomizer := r.retrieveAccessToken(ctx, req) query := req.URL.Query() var timestamp pcommon.Timestamp if query.Has(queryTime) { @@ -457,7 +457,7 @@ func (r *splunkReceiver) handleReq(resp http.ResponseWriter, req *http.Request) events = append(events, &msg) } } - resourceCustomizer := r.createResourceCustomizer(req) + ctx, resourceCustomizer := r.retrieveAccessToken(ctx, req) if r.logsConsumer != nil && len(events) > 0 { ld, err := splunkHecToLogData(r.settings.Logger, events, resourceCustomizer, r.config) if err != nil { @@ -494,17 +494,22 @@ func (r *splunkReceiver) handleReq(resp http.ResponseWriter, req *http.Request) } } -func (r *splunkReceiver) createResourceCustomizer(req *http.Request) func(resource pcommon.Resource) { - if r.config.AccessTokenPassthrough { - accessToken := req.Header.Get("Authorization") - if strings.HasPrefix(accessToken, splunk.HECTokenHeader+" ") { - accessTokenValue := accessToken[len(splunk.HECTokenHeader)+1:] - return func(resource pcommon.Resource) { - resource.Attributes().PutStr(splunk.HecTokenLabel, accessTokenValue) - } +func (r *splunkReceiver) retrieveAccessToken(origCtx context.Context, req *http.Request) (context.Context, func(resource pcommon.Resource)) { + if !r.config.AccessTokenPassthrough { + return origCtx, nil + } + + accessToken := req.Header.Get("Authorization") + if strings.HasPrefix(accessToken, splunk.HECTokenHeader+" ") { + accessTokenValue := accessToken[len(splunk.HECTokenHeader)+1:] + if tokenContextGate.IsEnabled() { + return context.WithValue(origCtx, splunk.LabelType(splunk.SFxAccessTokenHeader), accessTokenValue), nil + } + return origCtx, func(resource pcommon.Resource) { + resource.Attributes().PutStr(splunk.HecTokenLabel, accessTokenValue) } } - return nil + return origCtx, nil } func (r *splunkReceiver) failRequest( diff --git a/receiver/splunkhecreceiver/receiver_test.go b/receiver/splunkhecreceiver/receiver_test.go index de4654221649..bd5458c5a503 100644 --- a/receiver/splunkhecreceiver/receiver_test.go +++ b/receiver/splunkhecreceiver/receiver_test.go @@ -484,11 +484,12 @@ func Test_splunkhecReceiver_TLS(t *testing.T) { func Test_splunkhecReceiver_AccessTokenPassthrough(t *testing.T) { tests := []struct { - name string - passthrough bool - tokenProvided string - tokenExpected string - metric bool + name string + passthrough bool + tokenProvided string + tokenExpected string + metric bool + tokenContextGate bool }{ { name: "Log, No token provided and passthrough false", @@ -542,10 +543,18 @@ func Test_splunkhecReceiver_AccessTokenPassthrough(t *testing.T) { tokenExpected: "passthroughToken", metric: true, }, + { + name: "Pass token via context", + passthrough: true, + tokenProvided: "passthroughToken", + metric: true, + tokenContextGate: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + testutil.SetFeatureGateForTest(t, tokenContextGate, tt.tokenContextGate) config := createDefaultConfig().(*Config) config.Endpoint = "localhost:0" config.AccessTokenPassthrough = tt.passthrough @@ -575,8 +584,10 @@ func Test_splunkhecReceiver_AccessTokenPassthrough(t *testing.T) { } msgBytes, _ := json.Marshal(splunkhecMsg) req := httptest.NewRequest(http.MethodPost, "http://localhost", bytes.NewReader(msgBytes)) - if tt.passthrough { - if tt.tokenProvided != "" { + if tt.passthrough && tt.tokenProvided != "" { + if tt.tokenContextGate { + req = req.WithContext(context.WithValue(req.Context(), splunk.LabelType(splunk.SFxAccessTokenHeader), tt.tokenProvided)) + } else { req.Header.Set("Authorization", "Splunk "+tt.tokenProvided) } } @@ -584,7 +595,11 @@ func Test_splunkhecReceiver_AccessTokenPassthrough(t *testing.T) { done := make(chan bool) go func() { tokenReceived := <-accessTokensChan - assert.Equal(t, "Splunk "+tt.tokenExpected, tokenReceived) + if tt.tokenContextGate { + assert.Equal(t, req.Context().Value(splunk.LabelType(splunk.SFxAccessTokenHeader)), tt.tokenProvided) + } else { + assert.Equal(t, "Splunk "+tt.tokenExpected, tokenReceived) + } done <- true }()