Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ func newStartedApp(
if mockTransmission != nil {
upstreamTransmission = mockTransmission
} else {
upstreamTransmission = transmit.NewDirectTransmission(types.TransmitTypeUpstream, http.DefaultTransport.(*http.Transport), 500, 100*time.Millisecond, 100*time.Millisecond, true)
upstreamTransmission = transmit.NewDirectTransmission(types.TransmitTypeUpstream, http.DefaultTransport.(*http.Transport), 500, 100*time.Millisecond, 100*time.Millisecond, true, nil)
}

// Always create real peer transmission using DirectTransmission
Expand All @@ -354,7 +354,7 @@ func newStartedApp(
Timeout: 3 * time.Second,
}).Dial,
}
peerTransmissionWrapper := transmit.NewDirectTransmission(types.TransmitTypePeer, peerTransport, int(cfg.GetTracesConfigVal.MaxBatchSize), 100*time.Millisecond, 100*time.Millisecond, false)
peerTransmissionWrapper := transmit.NewDirectTransmission(types.TransmitTypePeer, peerTransport, int(cfg.GetTracesConfigVal.MaxBatchSize), 100*time.Millisecond, 100*time.Millisecond, false, nil)

var g inject.Graph
err = g.Provide(
Expand Down
4 changes: 3 additions & 1 deletion cmd/refinery/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func main() {
time.Duration(c.GetTracesConfig().GetBatchTimeout()),
30*time.Second,
true,
c.GetAdditionalHeaders(), // Custom headers for upstream Honeycomb API
)
peerTransmission := transmit.NewDirectTransmission(
types.TransmitTypePeer,
Expand All @@ -184,6 +185,7 @@ func main() {
time.Duration(c.GetTracesConfig().GetBatchTimeout()),
10*time.Second,
c.GetCompressPeerCommunication(),
nil, // No custom headers for peer-to-peer traffic
)

// we need to include all the metrics types so we can inject them in case they're needed
Expand All @@ -206,7 +208,7 @@ func main() {

if c.GetOTelTracingConfig().Enabled {
// let's set up some OTel tracing
tracer, shutdown = otelutil.SetupTracing(c.GetOTelTracingConfig(), resourceLib, resourceVer)
tracer, shutdown = otelutil.SetupTracing(c.GetOTelTracingConfig(), resourceLib, resourceVer, c.GetAdditionalHeaders())

// add telemetry callback so husky can enrich spans with attributes
husky.AddTelemetryAttributeFunc = func(ctx context.Context, key string, value any) {
Expand Down
15 changes: 14 additions & 1 deletion config.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Honeycomb Refinery Configuration Documentation

This is the documentation for the configuration file for Honeycomb's Refinery.
It was automatically generated on 2026-02-04 at 18:27:42 UTC.
It was automatically generated on 2026-02-13 at 14:32:49 UTC.

## The Config file

Expand Down Expand Up @@ -151,6 +151,19 @@ This setting is the destination to which Refinery sends all events that it decid
- Environment variable: `REFINERY_HONEYCOMB_API`
- Command line switch: `--honeycomb-api`

### `AdditionalHeaders`

AdditionalHeaders is a map of additional HTTP headers to add to all upstream Honeycomb API requests.

These headers will be added to all HTTP requests made to the upstream Honeycomb API endpoint, including trace data, OTel metrics, OTel traces, and logs.
This is useful for scenarios where requests need to pass through an mTLS proxy that requires additional headers like FORWARD_TO_URL.
Both keys and values must be strings.
Reserved Honeycomb header prefixes ("x-honeycomb-" and "x-hny-") cannot be set.

- Not eligible for live reload.
- Type: `map`
- Example: `FORWARD_TO_URL:https://api.honeycomb.io`

## OpAMP Configuration

`OpAMP` contains OpAMP configuration options.
Expand Down
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ type Config interface {
// the upstream Honeycomb API server
GetHoneycombAPI() string

// GetAdditionalHeaders returns custom HTTP headers to add to all upstream
// Honeycomb API calls, both processed and generated telemetry
GetAdditionalHeaders() map[string]string

GetTracesConfig() TracesConfig

// GetLoggerType returns the type of the logger to use. Valid types are in
Expand Down
65 changes: 65 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1400,3 +1400,68 @@ func BenchmarkIsLegacyAPIKey(b *testing.B) {
})
}
}

func TestAdditionalHeaders(t *testing.T) {
cm := makeYAML(
"General.ConfigurationVersion", 2,
"Network.AdditionalHeaders", map[string]string{
"FORWARD_TO_URL": "https://proxy.example.com",
"X-Custom-Header": "custom-value",
},
)
rm := makeYAML("ConfigVersion", 2)
config, rules := createTempConfigs(t, cm, rm)
c, err := getConfig([]string{"--no-validate", "--config", config, "--rules_config", rules})
assert.NoError(t, err)

expected := map[string]string{
"FORWARD_TO_URL": "https://proxy.example.com",
"X-Custom-Header": "custom-value",
}
assert.Equal(t, expected, c.GetAdditionalHeaders())
}

func TestAdditionalHeadersEmpty(t *testing.T) {
cm := makeYAML("General.ConfigurationVersion", 2)
rm := makeYAML("ConfigVersion", 2)
config, rules := createTempConfigs(t, cm, rm)
c, err := getConfig([]string{"--no-validate", "--config", config, "--rules_config", rules})
assert.NoError(t, err)

// Should return empty map (or nil) when not configured
headers := c.GetAdditionalHeaders()
assert.Empty(t, headers)
}

func TestAdditionalHeadersReservedHeadersRejected(t *testing.T) {
testCases := []struct {
name string
header string
}{
{"x-honeycomb-team", "X-Honeycomb-Team"},
{"x-hny-team", "X-Hny-Team"},
{"x-honeycomb-dataset", "X-Honeycomb-Dataset"},
{"x-honeycomb-samplerate", "X-Honeycomb-Samplerate"},
{"x-honeycomb-event-time", "X-Honeycomb-Event-Time"},
{"lowercase honeycomb team", "x-honeycomb-team"},
{"arbitrary x-honeycomb prefix", "X-Honeycomb-Custom"},
{"arbitrary x-hny prefix", "X-Hny-Custom"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cm := makeYAML(
"General.ConfigurationVersion", 2,
"Network.AdditionalHeaders", map[string]string{
tc.header: "should-fail",
},
)
rm := makeYAML("RulesVersion", 2)
config, rules := createTempConfigs(t, cm, rm)
// Reserved headers validation now happens during schema validation
_, err := getConfig([]string{"--config", config, "--rules_config", rules})
assert.Error(t, err)
assert.Contains(t, err.Error(), "reserved")
})
}
}
16 changes: 12 additions & 4 deletions config/file_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ type OpAMPConfig struct {
}

type NetworkConfig struct {
ListenAddr string `yaml:"ListenAddr" default:"0.0.0.0:8080" cmdenv:"HTTPListenAddr"`
PeerListenAddr string `yaml:"PeerListenAddr" default:"0.0.0.0:8081" cmdenv:"PeerListenAddr"`
HoneycombAPI string `yaml:"HoneycombAPI" default:"https://api.honeycomb.io" cmdenv:"HoneycombAPI"`
HTTPIdleTimeout Duration `yaml:"HTTPIdleTimeout"`
ListenAddr string `yaml:"ListenAddr" default:"0.0.0.0:8080" cmdenv:"HTTPListenAddr"`
PeerListenAddr string `yaml:"PeerListenAddr" default:"0.0.0.0:8081" cmdenv:"PeerListenAddr"`
HoneycombAPI string `yaml:"HoneycombAPI" default:"https://api.honeycomb.io" cmdenv:"HoneycombAPI"`
HTTPIdleTimeout Duration `yaml:"HTTPIdleTimeout"`
AdditionalHeaders map[string]string `yaml:"AdditionalHeaders" default:"{}"`
}

type AccessKeyConfig struct {
Expand Down Expand Up @@ -877,6 +878,13 @@ func (f *fileConfig) GetHoneycombAPI() string {
return f.mainConfig.Network.HoneycombAPI
}

func (f *fileConfig) GetAdditionalHeaders() map[string]string {
f.mux.RLock()
defer f.mux.RUnlock()

return f.mainConfig.Network.AdditionalHeaders
}

func (f *fileConfig) GetLoggerLevel() Level {
f.mux.RLock()
defer f.mux.RUnlock()
Expand Down
19 changes: 19 additions & 0 deletions config/metadata/configMeta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,25 @@ groups:
description: >
This setting is the destination to which Refinery sends all events
that it decides to keep.

- name: AdditionalHeaders
type: map
valuetype: map
example: "FORWARD_TO_URL:https://api.honeycomb.io"
reload: false
validations:
- type: elementType
arg: string
- type: noReservedHeaders
summary: is a map of additional HTTP headers to add to all upstream Honeycomb API requests.
description: >
These headers will be added to all HTTP requests made to the upstream
Honeycomb API endpoint, including trace data, OTel metrics, OTel
traces, and logs. This is useful for scenarios where requests need
to pass through an mTLS proxy that requires additional headers like
FORWARD_TO_URL. Both keys and values must be strings. Reserved
Honeycomb header prefixes ("x-honeycomb-" and "x-hny-") cannot be set.

- name: OpAMP
title: "OpAMP Configuration"
description: >
Expand Down
11 changes: 11 additions & 0 deletions config/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type MockConfig struct {
SampleCache SampleCacheConfig
StressRelief StressReliefConfig
AdditionalAttributes map[string]string
AdditionalHeaders map[string]string
TraceIdFieldNames []string
ParentIdFieldNames []string
CfgMetadata []ConfigMetadata
Expand Down Expand Up @@ -460,6 +461,16 @@ func (f *MockConfig) GetAdditionalAttributes() map[string]string {
return f.AdditionalAttributes
}

func (f *MockConfig) GetAdditionalHeaders() map[string]string {
f.Mux.RLock()
defer f.Mux.RUnlock()

if f.AdditionalHeaders == nil {
return make(map[string]string)
}
return f.AdditionalHeaders
}

func (f *MockConfig) GetOpAMPConfig() OpAMPConfig {
f.Mux.RLock()
defer f.Mux.RUnlock()
Expand Down
12 changes: 12 additions & 0 deletions config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"time"

"github.com/honeycombio/refinery/internal/headers"
"golang.org/x/exp/slices"
)

Expand Down Expand Up @@ -488,6 +489,17 @@ func (m *Metadata) Validate(data map[string]any, currentVersion ...string) Valid
}
}
}
case "noReservedHeaders":
if mapVal, ok := v.(map[string]any); ok {
for kk := range mapVal {
if headers.IsReserved(kk) {
results = append(results, ValidationResult{
Message: fmt.Sprintf("field %s contains reserved Honeycomb header %q which cannot be overridden; headers starting with X-Honeycomb-* or X-Hny-* are reserved", k, kk),
Severity: Error,
})
}
}
}
case "required", "requiredInGroup", "requiredWith", "conflictsWith":
// these are handled below
default:
Expand Down
16 changes: 15 additions & 1 deletion config_complete.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
## Honeycomb Refinery Configuration ##
######################################
#
# created on 2026-02-04 at 18:27:42 UTC from ../../config.yaml using a template generated on 2026-02-04 at 18:27:39 UTC
# created on 2026-02-13 at 14:32:49 UTC from ../../config.yaml using a template generated on 2026-02-06 at 15:01:38 UTC

# This file contains a configuration for the Honeycomb Refinery. It is in YAML
# format, organized into named groups, each of which contains a set of
Expand Down Expand Up @@ -127,6 +127,20 @@ Network:
## Eligible for live reload.
# HoneycombAPI: "https://api.honeycomb.io"

## AdditionalHeaders is a map of additional HTTP headers to add to all
## upstream Honeycomb API requests.
##
## These headers will be added to all HTTP requests made to the upstream
## Honeycomb API endpoint, including trace data, OTel metrics, OTel
## traces, and logs. This is useful for scenarios where requests need to
## pass through an mTLS proxy that requires additional headers like
## FORWARD_TO_URL. Both keys and values must be strings. Reserved
## Honeycomb headers cannot be overridden.
##
## Eligible for live reload.
# AdditionalHeaders:
# FORWARD_TO_URL: https://api.honeycomb.io

#########################
## OpAMP Configuration ##
#########################
Expand Down
23 changes: 23 additions & 0 deletions internal/headers/headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package headers

import "strings"

// reservedPrefixes contains HTTP header prefixes that cannot be overridden
// via AdditionalHeaders because they are reserved for Honeycomb API communication.
var reservedPrefixes = []string{
"x-honeycomb-",
"x-hny-",
}

// IsReserved checks if a header name is a reserved Honeycomb header.
// Any header starting with "X-Honeycomb-" or "X-Hny-" is reserved.
// The check is case-insensitive.
func IsReserved(name string) bool {
lower := strings.ToLower(name)
for _, prefix := range reservedPrefixes {
if strings.HasPrefix(lower, prefix) {
return true
}
}
return false
}
12 changes: 7 additions & 5 deletions internal/otelutil/otel_tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func StartSpanMulti(ctx context.Context, tracer trace.Tracer, name string, field
return tracer.Start(ctx, name, trace.WithAttributes(Attributes(fields)...))
}

func SetupTracing(cfg config.OTelTracingConfig, resourceLibrary string, resourceVersion string) (tracer trace.Tracer, shutdown func()) {
func SetupTracing(cfg config.OTelTracingConfig, resourceLibrary string, resourceVersion string, additionalHeaders map[string]string) (tracer trace.Tracer, shutdown func()) {
if !cfg.Enabled {
pr := noop.NewTracerProvider()
return pr.Tracer(resourceLibrary, trace.WithInstrumentationVersion(resourceVersion)), func() {}
Expand All @@ -122,12 +122,14 @@ func SetupTracing(cfg config.OTelTracingConfig, resourceLibrary string, resource

var sampleRatio float64 = 1.0 / float64(sampleRate)

// set up honeycomb specific headers if an API key is provided
// Add custom headers first, then Honeycomb headers (which take precedence)
headers := make(map[string]string)
for k, v := range additionalHeaders {
headers[k] = v
}
// Honeycomb headers override any custom headers
if cfg.APIKey != "" {
headers = map[string]string{
types.APIKeyHeader: cfg.APIKey,
}
headers[types.APIKeyHeader] = cfg.APIKey

if config.IsLegacyAPIKey(cfg.APIKey) {
headers[types.DatasetHeader] = cfg.Dataset
Expand Down
25 changes: 24 additions & 1 deletion logger/honeycomb.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ import (
"github.com/honeycombio/refinery/config"
)

// headerInjectingTransport wraps an http.RoundTripper and adds custom headers
// to all outgoing requests.
type headerInjectingTransport struct {
base http.RoundTripper
headers map[string]string
}

func (t *headerInjectingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Add custom headers before making the request
for k, v := range t.headers {
req.Header.Set(k, v)
}
return t.base.RoundTrip(req)
}

// HoneycombLogger is a Logger implementation that sends all logs to a Honeycomb
// dataset.
type HoneycombLogger struct {
Expand Down Expand Up @@ -45,12 +60,20 @@ func (h *HoneycombLogger) Start() error {
if h.loggerConfig.APIKey == "" {
loggerTx = &transmission.DiscardSender{}
} else {
// Wrap transport with header injector if there are additional headers
var transport http.RoundTripper = h.UpstreamTransport
if additionalHeaders := h.Config.GetAdditionalHeaders(); len(additionalHeaders) > 0 {
transport = &headerInjectingTransport{
base: h.UpstreamTransport,
headers: additionalHeaders,
}
}
loggerTx = &transmission.Honeycomb{
// logs are often sent in flurries; flush every half second
MaxBatchSize: 100,
BatchTimeout: 500 * time.Millisecond,
UserAgentAddition: "refinery/" + h.Version + " (metrics)",
Transport: h.UpstreamTransport,
Transport: transport,
PendingWorkCapacity: libhoney.DefaultPendingWorkCapacity,
EnableMsgpackEncoding: true,
}
Expand Down
2 changes: 1 addition & 1 deletion metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Honeycomb Refinery Metrics Documentation

This document contains the description of various metrics used in Refinery.
It was automatically generated on 2026-02-04 at 18:27:42 UTC.
It was automatically generated on 2026-02-06 at 15:01:40 UTC.

Note: This document does not include metrics defined in the dynsampler-go dependency, as those metrics are generated dynamically at runtime. As a result, certain metrics may be missing or incomplete in this document, but they will still be available during execution with their full names.

Expand Down
Loading