Skip to content

Commit

Permalink
[headerssetter] Extend headerssetter with header actions. (#16818)
Browse files Browse the repository at this point in the history
  • Loading branch information
kovrus committed Jan 26, 2023
1 parent e03dcde commit 8fe7e79
Show file tree
Hide file tree
Showing 11 changed files with 490 additions and 41 deletions.
24 changes: 24 additions & 0 deletions .chloggen/add-actions-to-headerssetter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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: headerssetter

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Extend the headers setter extension with header modification actions.

# One or more tracking issues related to the change
issues: [16581, 7596]

# (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: |
Please update configurations to use the action that suits your requirements:
- `insert`: Inserts the new header if it does not exist.
- `update`: Updates the header value if it exists.
- `upsert`: Inserts a header if it does not exist and updates the header
if it exists.
- `delete`: Deletes the header.
The default action is `upsert`, however, in future versions, we'll require this
to be explicitly set.
33 changes: 23 additions & 10 deletions extension/headerssetterextension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,40 @@ header to the value extracted from the context.

The following settings are required:

- `headers`: a list of header configuration objects that specify headers and
their value sources. Each configuration object has the following properties:
- `key`: the header name
- `value`: the header value is looked up from the `value` property of the
extension configuration
- `from_context`: the header value is looked up from the request metadata,
such as HTTP headers, using the property value as the key (likely a header name)
- `headers`: a list of header configuration objects that specify headers and
their value sources. Each configuration object has the following properties:
- `key`: The header name.
- `action` (default: `upsert`): An action to perform on the header. Supported actions are:
- `insert`: Inserts the new header if it does not exist.
- `update`: Updates the header value if it exists.
- `upsert`: Inserts a header if it does not exist and updates the header
if it exists.
- `delete`: Deletes the header.
- `value`: The header value is looked up from the `value` property of the
extension configuration.
- `from_context`: The header value is looked up from the request metadata,
such as HTTP headers, using the property value as the key (likely a header
name).

The `value` and `from_context` properties are mutually exclusive.


#### Configuration Example

```yaml
extensions:
headers_setter:
headers:
- key: X-Scope-OrgID
- action: insert
key: X-Scope-OrgID
from_context: tenant_id
- key: User-ID
- action: upsert
key: User-ID
value: user_id
- action: update
key: User-ID
value: user_id
- action: delete
key: Some-Header

receivers:
otlp:
Expand Down
38 changes: 30 additions & 8 deletions extension/headerssetterextension/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,30 @@ type Config struct {
}

type HeaderConfig struct {
Key *string `mapstructure:"key"`
Value *string `mapstructure:"value"`
FromContext *string `mapstructure:"from_context"`
Action actionValue `mapstructure:"action"`
Key *string `mapstructure:"key"`
Value *string `mapstructure:"value"`
FromContext *string `mapstructure:"from_context"`
}

// actionValue is the enum to capture the four types of actions to perform on a header
type actionValue string

const (
// INSERT inserts the new header if it does not exist
INSERT actionValue = "insert"

// UPDATE updates the header value if it exists
UPDATE actionValue = "update"

// UPSERT inserts a header if it does not exist and updates the header
// if it exists
UPSERT actionValue = "upsert"

// DELETE deletes the header
DELETE actionValue = "delete"
)

// Validate checks if the extension configuration is valid
func (cfg *Config) Validate() error {
if cfg.HeadersConfig == nil || len(cfg.HeadersConfig) == 0 {
Expand All @@ -44,11 +63,14 @@ func (cfg *Config) Validate() error {
if header.Key == nil || *header.Key == "" {
return errMissingHeader
}
if header.FromContext == nil && header.Value == nil {
return errMissingSource
}
if header.FromContext != nil && header.Value != nil {
return errConflictingSources

if header.Action != DELETE {
if header.FromContext == nil && header.Value == nil {
return errMissingSource
}
if header.FromContext != nil && header.Value != nil {
return errConflictingSources
}
}
}
return nil
Expand Down
79 changes: 70 additions & 9 deletions extension/headerssetterextension/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,40 @@ func TestLoadConfig(t *testing.T) {
t.Parallel()

tests := []struct {
id component.ID
expected component.Config
id component.ID
expected component.Config
expectedError error
}{
{
id: component.NewID(typeStr),
expected: NewFactory().CreateDefaultConfig(),
id: component.NewIDWithName(typeStr, ""),
expectedError: errMissingHeadersConfig,
},
{
id: component.NewIDWithName(typeStr, "1"),
expected: &Config{
HeadersConfig: []HeaderConfig{
{
Key: stringp("X-Scope-OrgID"),
Action: INSERT,
FromContext: stringp("tenant_id"),
Value: nil,
},
{
Key: stringp("User-ID"),
Action: UPDATE,
FromContext: stringp("user_id"),
Value: nil,
},

{
Key: stringp("User-ID"),
FromContext: nil,
Value: stringp("user_id"),
},
{
Key: stringp("User-ID"),
Action: DELETE,
},
},
},
},
Expand All @@ -60,8 +73,15 @@ func TestLoadConfig(t *testing.T) {
factory := NewFactory()
cfg := factory.CreateDefaultConfig()
sub, err := cm.Sub(tt.id.String())

require.NoError(t, err)
require.NoError(t, component.UnmarshalConfig(sub, cfg))

if tt.expectedError != nil {
assert.Error(t, component.ValidateConfig(cfg), tt.expectedError)
return
}
assert.NoError(t, component.ValidateConfig(cfg))
assert.Equal(t, tt.expected, cfg)
})
}
Expand All @@ -77,8 +97,9 @@ func TestValidateConfig(t *testing.T) {
"header value from config property",
[]HeaderConfig{
{
Key: stringp("name"),
Value: stringp("from config"),
Key: stringp("name"),
Action: INSERT,
Value: stringp("from config"),
},
},
nil,
Expand All @@ -88,6 +109,7 @@ func TestValidateConfig(t *testing.T) {
[]HeaderConfig{
{
Key: stringp("name"),
Action: INSERT,
FromContext: stringp("from config"),
},
},
Expand All @@ -96,14 +118,20 @@ func TestValidateConfig(t *testing.T) {
{
"missing header name for from value",
[]HeaderConfig{
{Value: stringp("test")},
{
Action: INSERT,
Value: stringp("test"),
},
},
errMissingHeader,
},
{
"missing header name for from context",
[]HeaderConfig{
{FromContext: stringp("test")},
{
Action: INSERT,
FromContext: stringp("test"),
},
},
errMissingHeader,
},
Expand All @@ -112,6 +140,7 @@ func TestValidateConfig(t *testing.T) {
[]HeaderConfig{
{
Key: stringp("name"),
Action: INSERT,
Value: stringp("from config"),
FromContext: stringp("from context"),
},
Expand All @@ -122,11 +151,43 @@ func TestValidateConfig(t *testing.T) {
"header value source is missing",
[]HeaderConfig{
{
Key: stringp("name"),
Key: stringp("name"),
Action: INSERT,
},
},
errMissingSource,
},
{
"delete header action",
[]HeaderConfig{
{
Key: stringp("name"),
Action: DELETE,
},
},
nil,
},
{
"insert header action",
[]HeaderConfig{
{
Key: stringp("name"),
Action: INSERT,
Value: stringp("from config"),
},
},
nil,
},
{
"missing header action",
[]HeaderConfig{
{
Key: stringp("name"),
Value: stringp("from config"),
},
},
nil,
},
{
"headers configuration is missing",
nil,
Expand Down
28 changes: 23 additions & 5 deletions extension/headerssetterextension/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@ import (
"net/http"

"go.opentelemetry.io/collector/extension/auth"
"go.uber.org/zap"
"google.golang.org/grpc/credentials"

"github.com/open-telemetry/opentelemetry-collector-contrib/extension/headerssetterextension/internal/action"
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/headerssetterextension/internal/source"
)

type Header struct {
key string
action action.Action
source source.Source
}

func newHeadersSetterExtension(cfg *Config) (auth.Client, error) {
func newHeadersSetterExtension(cfg *Config, logger *zap.Logger) (auth.Client, error) {
if cfg == nil {
return nil, errors.New("extension configuration is not provided")
}
Expand All @@ -48,7 +50,23 @@ func newHeadersSetterExtension(cfg *Config) (auth.Client, error) {
Key: *header.FromContext,
}
}
headers = append(headers, Header{key: *header.Key, source: s})

var a action.Action
switch header.Action {
case INSERT:
a = action.Insert{Key: *header.Key}
case UPSERT:
a = action.Upsert{Key: *header.Key}
case UPDATE:
a = action.Update{Key: *header.Key}
case DELETE:
a = action.Delete{Key: *header.Key}
default:
a = action.Upsert{Key: *header.Key}
logger.Warn("The action was not provided, using 'upsert'." +
" In future versions, we'll require this to be explicitly set")
}
headers = append(headers, Header{action: a, source: s})
}

return auth.NewClient(
Expand Down Expand Up @@ -84,7 +102,7 @@ func (h *headersPerRPC) GetRequestMetadata(
if err != nil {
return nil, fmt.Errorf("failed to determine the source: %w", err)
}
metadata[header.key] = value
header.action.ApplyOnMetadata(metadata, value)
}
return metadata, nil
}
Expand Down Expand Up @@ -115,7 +133,7 @@ func (h *headersRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
if err != nil {
return nil, fmt.Errorf("failed to determine the source: %w", err)
}
req2.Header.Set(header.key, value)
header.action.ApplyOnHeaders(req2.Header, value)
}
return h.base.RoundTrip(req2)
}
Loading

0 comments on commit 8fe7e79

Please sign in to comment.