Skip to content

Commit b421eae

Browse files
committed
[webhookeventreceiver] add option to include headers as attributes
1 parent 6a86096 commit b421eae

File tree

6 files changed

+162
-13
lines changed

6 files changed

+162
-13
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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/webhookeventreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add option to include headers as log attributes
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: []
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+
Adds new `convert_headers_to_attributes` option. If set, all headers, with the exception of the
20+
required header (if also enabled) will be added as log attributes. Header names are normalized
21+
to snake_case and then prefixed with the namespace `header`.
22+
23+
# If your change doesn't affect end users or the exported elements of any package,
24+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
25+
# Optional: The change log or logs in which this entry should be included.
26+
# e.g. '[user]' or '[user, api]'
27+
# Include 'user' if the change is relevant to end users.
28+
# Include 'api' if there is a change to a library API.
29+
# Default: '[user]'
30+
change_logs: [user]

receiver/webhookeventreceiver/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The following settings are optional:
3232
* `key` (required if `required_header` config option is set): Represents the key portion of the required header.
3333
* `value` (required if `required_header` config option is set): Represents the value portion of the required header.
3434
* `split_logs_at_newline` (default: false): If true, the receiver will create a separate log record for each line in the request body.
35+
* `convert_headers_to_attributes` (optional): add all request headers (excluding `required_header` if also set) log attributes
3536

3637
### Split logs at newline example
3738

receiver/webhookeventreceiver/config.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ var (
2020

2121
// Config defines configuration for the Generic Webhook receiver.
2222
type Config struct {
23-
confighttp.ServerConfig `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct
24-
ReadTimeout string `mapstructure:"read_timeout"` // wait time for reading request headers in ms. Default is 500ms.
25-
WriteTimeout string `mapstructure:"write_timeout"` // wait time for writing request response in ms. Default is 500ms.
26-
Path string `mapstructure:"path"` // path for data collection. Default is /events
27-
HealthPath string `mapstructure:"health_path"` // path for health check api. Default is /health_check
28-
RequiredHeader RequiredHeader `mapstructure:"required_header"` // optional setting to set a required header for all requests to have
29-
SplitLogsAtNewLine bool `mapstructure:"split_logs_at_newline"` // optional setting to split logs into multiple log records
23+
confighttp.ServerConfig `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct
24+
ReadTimeout string `mapstructure:"read_timeout"` // wait time for reading request headers in ms. Default is 500ms.
25+
WriteTimeout string `mapstructure:"write_timeout"` // wait time for writing request response in ms. Default is 500ms.
26+
Path string `mapstructure:"path"` // path for data collection. Default is /events
27+
HealthPath string `mapstructure:"health_path"` // path for health check api. Default is /health_check
28+
RequiredHeader RequiredHeader `mapstructure:"required_header"` // optional setting to set a required header for all requests to have
29+
SplitLogsAtNewLine bool `mapstructure:"split_logs_at_newline"` // optional setting to split logs into multiple log records
30+
ConvertHeadersToAttributes bool `mapstructure:"convert_headers_to_attributes"` // optional to convert all headers to attributes
3031
}
3132

3233
type RequiredHeader struct {

receiver/webhookeventreceiver/receiver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func (er *eventReceiver) handleReq(w http.ResponseWriter, r *http.Request, _ htt
191191

192192
// send body into a scanner and then convert the request body into a log
193193
sc := bufio.NewScanner(bodyReader)
194-
ld, numLogs := reqToLog(sc, r.URL.Query(), er.cfg, er.settings)
194+
ld, numLogs := reqToLog(sc, r.Header, r.URL.Query(), er.cfg, er.settings)
195195
consumerErr := er.logConsumer.ConsumeLogs(ctx, ld)
196196

197197
_ = bodyReader.Close()

receiver/webhookeventreceiver/req_to_log.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ package webhookeventreceiver // import "github.com/open-telemetry/opentelemetry-
55

66
import (
77
"bufio"
8+
"net/http"
9+
"net/textproto"
810
"net/url"
11+
"strings"
912
"time"
1013

1114
"go.opentelemetry.io/collector/pdata/pcommon"
@@ -16,6 +19,7 @@ import (
1619
)
1720

1821
func reqToLog(sc *bufio.Scanner,
22+
headers http.Header,
1923
query url.Values,
2024
cfg *Config,
2125
settings receiver.Settings,
@@ -45,6 +49,9 @@ func reqToLog(sc *bufio.Scanner,
4549
scopeLog.Scope().SetVersion(settings.BuildInfo.Version)
4650
scopeLog.Scope().Attributes().PutStr("source", settings.ID.String())
4751
scopeLog.Scope().Attributes().PutStr("receiver", metadata.Type.String())
52+
if cfg.ConvertHeadersToAttributes {
53+
appendHeaders(cfg, scopeLog, headers)
54+
}
4855

4956
for sc.Scan() {
5057
logRecord := scopeLog.LogRecords().AppendEmpty()
@@ -64,3 +71,17 @@ func appendMetadata(resourceLog plog.ResourceLogs, query url.Values) {
6471
}
6572
}
6673
}
74+
75+
// append headers as attributes
76+
func appendHeaders(config *Config, scopeLog plog.ScopeLogs, headers http.Header) {
77+
for k := range headers {
78+
// Skip the required header used for authentication
79+
if k == textproto.CanonicalMIMEHeaderKey(config.RequiredHeader.Key) {
80+
continue
81+
}
82+
// store headers with "header" namespace and normalize key to snake_case
83+
normalizedHeader := strings.ReplaceAll(k, "-", "_")
84+
normalizedHeader = strings.ToLower(normalizedHeader)
85+
scopeLog.Scope().Attributes().PutStr("header."+normalizedHeader, strings.Join(headers.Values(k), ";"))
86+
}
87+
}

receiver/webhookeventreceiver/req_to_log_test.go

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"bytes"
99
"io"
1010
"log"
11+
"net/http"
12+
"net/textproto"
1113
"net/url"
1214
"testing"
1315

@@ -23,10 +25,12 @@ func TestReqToLog(t *testing.T) {
2325
defaultConfig := createDefaultConfig().(*Config)
2426

2527
tests := []struct {
26-
desc string
27-
sc *bufio.Scanner
28-
query url.Values
29-
tt func(t *testing.T, reqLog plog.Logs, reqLen int, settings receiver.Settings)
28+
desc string
29+
sc *bufio.Scanner
30+
headers http.Header
31+
query url.Values
32+
config *Config
33+
tt func(t *testing.T, reqLog plog.Logs, reqLen int, settings receiver.Settings)
3034
}{
3135
{
3236
desc: "Valid query valid event",
@@ -112,11 +116,103 @@ func TestReqToLog(t *testing.T) {
112116
require.Equal(t, 2, scopeLogsScope.Attributes().Len())
113117
},
114118
},
119+
{
120+
desc: "Headers not added by default",
121+
headers: http.Header{
122+
textproto.CanonicalMIMEHeaderKey("X-Foo"): []string{"1"},
123+
textproto.CanonicalMIMEHeaderKey("X-Bar"): []string{"2"},
124+
},
125+
sc: func() *bufio.Scanner {
126+
reader := io.NopCloser(bytes.NewReader([]byte("this is a: log")))
127+
return bufio.NewScanner(reader)
128+
}(),
129+
tt: func(t *testing.T, reqLog plog.Logs, reqLen int, _ receiver.Settings) {
130+
require.Equal(t, 1, reqLen)
131+
132+
attributes := reqLog.ResourceLogs().At(0).Resource().Attributes()
133+
require.Equal(t, 0, attributes.Len())
134+
135+
scopeLogsScope := reqLog.ResourceLogs().At(0).ScopeLogs().At(0).Scope()
136+
require.Equal(t, 2, scopeLogsScope.Attributes().Len()) // expect no additional attributes even though headers are set
137+
},
138+
},
139+
{
140+
desc: "Headers added if ConvertHeadersToAttributes enabled",
141+
headers: http.Header{
142+
textproto.CanonicalMIMEHeaderKey("X-Foo"): []string{"1"},
143+
textproto.CanonicalMIMEHeaderKey("X-Bar"): []string{"2"},
144+
},
145+
config: &Config{
146+
Path: defaultPath,
147+
HealthPath: defaultHealthPath,
148+
ReadTimeout: defaultReadTimeout,
149+
WriteTimeout: defaultWriteTimeout,
150+
ConvertHeadersToAttributes: true,
151+
},
152+
sc: func() *bufio.Scanner {
153+
reader := io.NopCloser(bytes.NewReader([]byte("this is a: log")))
154+
return bufio.NewScanner(reader)
155+
}(),
156+
tt: func(t *testing.T, reqLog plog.Logs, reqLen int, _ receiver.Settings) {
157+
require.Equal(t, 1, reqLen)
158+
159+
attributes := reqLog.ResourceLogs().At(0).Resource().Attributes()
160+
require.Equal(t, 0, attributes.Len())
161+
162+
scopeLogsScope := reqLog.ResourceLogs().At(0).ScopeLogs().At(0).Scope()
163+
require.Equal(t, 4, scopeLogsScope.Attributes().Len()) // expect no additional attributes even though headers are set
164+
v, exists := scopeLogsScope.Attributes().Get("header.x_foo")
165+
require.True(t, exists)
166+
require.Equal(t, "1", v.AsString())
167+
v, exists = scopeLogsScope.Attributes().Get("header.x_bar")
168+
require.True(t, exists)
169+
require.Equal(t, "2", v.AsString())
170+
},
171+
},
172+
{
173+
desc: "Required header skipped",
174+
headers: http.Header{
175+
textproto.CanonicalMIMEHeaderKey("X-Foo"): []string{"1"},
176+
textproto.CanonicalMIMEHeaderKey("X-Bar"): []string{"2"},
177+
textproto.CanonicalMIMEHeaderKey("X-Required-Header"): []string{"password"},
178+
},
179+
config: &Config{
180+
Path: defaultPath,
181+
HealthPath: defaultHealthPath,
182+
ReadTimeout: defaultReadTimeout,
183+
WriteTimeout: defaultWriteTimeout,
184+
RequiredHeader: RequiredHeader{Key: "X-Required-Header", Value: "password"},
185+
ConvertHeadersToAttributes: true,
186+
},
187+
sc: func() *bufio.Scanner {
188+
reader := io.NopCloser(bytes.NewReader([]byte("this is a: log")))
189+
return bufio.NewScanner(reader)
190+
}(),
191+
tt: func(t *testing.T, reqLog plog.Logs, reqLen int, _ receiver.Settings) {
192+
require.Equal(t, 1, reqLen)
193+
194+
attributes := reqLog.ResourceLogs().At(0).Resource().Attributes()
195+
require.Equal(t, 0, attributes.Len())
196+
197+
scopeLogsScope := reqLog.ResourceLogs().At(0).ScopeLogs().At(0).Scope()
198+
require.Equal(t, 4, scopeLogsScope.Attributes().Len()) // expect no additional attributes even though headers are set
199+
_, exists := scopeLogsScope.Attributes().Get("header.x_foo")
200+
require.True(t, exists)
201+
_, exists = scopeLogsScope.Attributes().Get("header.x_bar")
202+
require.True(t, exists)
203+
_, exists = scopeLogsScope.Attributes().Get("header.x_required_header")
204+
require.False(t, exists)
205+
},
206+
},
115207
}
116208

117209
for _, test := range tests {
118210
t.Run(test.desc, func(t *testing.T) {
119-
reqLog, reqLen := reqToLog(test.sc, test.query, defaultConfig, receivertest.NewNopSettings(metadata.Type))
211+
testConfig := defaultConfig
212+
if test.config != nil {
213+
testConfig = test.config
214+
}
215+
reqLog, reqLen := reqToLog(test.sc, test.headers, test.query, testConfig, receivertest.NewNopSettings(metadata.Type))
120216
test.tt(t, reqLog, reqLen, receivertest.NewNopSettings(metadata.Type))
121217
})
122218
}

0 commit comments

Comments
 (0)