Skip to content

Commit 7f89604

Browse files
authored
[exporter/coralogix] Fix missing header causing authentication errors (#40335)
#### Link to tracking issue Fixes #40330 #### Testing - Added new unit tests - Manual test with the Coralogix backend Workaround tested with: ```yaml receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 exporters: coralogix: traces: headers: "Authorization": "Bearer ${env:CORALOGIX_PRIVATE_KEY}" metrics: headers: "Authorization": "Bearer ${env:CORALOGIX_PRIVATE_KEY}" logs: headers: "Authorization": "Bearer ${env:CORALOGIX_PRIVATE_KEY}" application_name: otel application_name_attributes: - service.name - k8s.namespace.name - service.namespace sending_queue: enabled: true sizer: "items" num_consumers: 10 queue_size: 25000 batch: flush_timeout: 1s min_size: 1024 max_size: 2048 domain: eu2.coralogix.com subsystem_name: k8s-saas subsystem_name_attributes: - subsystem.name private_key: ${env:CORALOGIX_PRIVATE_KEY} service: pipelines: traces: receivers: [otlp] exporters: [coralogix] metrics: receivers: [otlp] exporters: [coralogix] logs: receivers: [otlp] exporters: [coralogix] ``` Signed-off-by: Israel Blancas <[email protected]>
1 parent c184403 commit 7f89604

File tree

9 files changed

+275
-9
lines changed

9 files changed

+275
-9
lines changed

.chloggen/40330.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: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: coralogixexporter
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Fix Authorization header not being set in metadata.
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: [40330]
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]

exporter/coralogixexporter/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
| Stability | [alpha]: profiles |
77
| | [beta]: traces, metrics, logs |
88
| Distributions | [contrib] |
9+
| Warnings | [Authentication issues in v0.127.0](#warnings) |
910
| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Aexporter%2Fcoralogix%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Aexporter%2Fcoralogix) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Aexporter%2Fcoralogix%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Aexporter%2Fcoralogix) |
1011
| Code coverage | [![codecov](https://codecov.io/github/open-telemetry/opentelemetry-collector-contrib/graph/main/badge.svg?component=exporter_coralogix)](https://app.codecov.io/gh/open-telemetry/opentelemetry-collector-contrib/tree/main/?components%5B0%5D=exporter_coralogix&displayType=list) |
1112
| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | [@povilasv](https://www.github.com/povilasv), [@iblancasa](https://www.github.com/iblancasa), [@douglascamata](https://www.github.com/douglascamata) |
@@ -325,6 +326,28 @@ service:
325326
exporters: [coralogix]
326327
```
327328

329+
## Warnings
330+
### Authentication issues in v0.127.0
331+
332+
Version 0.127.0 introduced a regression in the Coralogix exporter. As a consequence, it requires an updated authentication configuration to ensure proper telemetry data transmission to Coralogix. If you're using this version, please modify your configuration to include the authentication headers as shown below:
333+
334+
```yaml
335+
coralogix:
336+
traces:
337+
headers:
338+
"Authorization": "Bearer ${env:CORALOGIX_PRIVATE_KEY}"
339+
metrics:
340+
headers:
341+
"Authorization": "Bearer ${env:CORALOGIX_PRIVATE_KEY}"
342+
logs:
343+
headers:
344+
"Authorization": "Bearer ${env:CORALOGIX_PRIVATE_KEY}"
345+
```
346+
347+
This configuration ensures proper authentication with the Coralogix backend.
348+
Prior versions (v0.126.0 and earlier) and subsequent versions (v0.128.0 and later) are not affected by this authentication issue.
349+
350+
328351
### Need help?
329352

330353
Our world-class customer success team is available 24/7 to walk you through the setup for this exporter and answer any questions that may come up.

exporter/coralogixexporter/logs_client_test.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"go.uber.org/zap/zapcore"
2626
"go.uber.org/zap/zaptest/observer"
2727
"google.golang.org/grpc"
28+
"google.golang.org/grpc/metadata"
2829
)
2930

3031
func TestNewLogsExporter(t *testing.T) {
@@ -211,9 +212,27 @@ type mockLogsServer struct {
211212
plogotlp.UnimplementedGRPCServer
212213
recvCount int
213214
partialSuccess *plogotlp.ExportPartialSuccess
215+
t testing.TB
214216
}
215217

216-
func (m *mockLogsServer) Export(_ context.Context, req plogotlp.ExportRequest) (plogotlp.ExportResponse, error) {
218+
func (m *mockLogsServer) Export(ctx context.Context, req plogotlp.ExportRequest) (plogotlp.ExportResponse, error) {
219+
md, ok := metadata.FromIncomingContext(ctx)
220+
if !ok {
221+
m.t.Error("No metadata found in context")
222+
return plogotlp.NewExportResponse(), errors.New("no metadata found")
223+
}
224+
225+
authHeader := md.Get("authorization")
226+
if len(authHeader) == 0 {
227+
m.t.Error("No Authorization header found")
228+
return plogotlp.NewExportResponse(), errors.New("no authorization header")
229+
}
230+
231+
if authHeader[0] != "Bearer test-key" {
232+
m.t.Errorf("Expected Authorization header 'Bearer test-key', got %s", authHeader[0])
233+
return plogotlp.NewExportResponse(), errors.New("invalid authorization header")
234+
}
235+
217236
m.recvCount += req.Logs().LogRecordCount()
218237
resp := plogotlp.NewExportResponse()
219238
if m.partialSuccess != nil {
@@ -229,7 +248,7 @@ func startMockOtlpLogsServer(tb testing.TB) (endpoint string, stopFn func(), srv
229248
tb.Fatalf("failed to listen: %v", err)
230249
}
231250
grpcServer := grpc.NewServer()
232-
srv = &mockLogsServer{}
251+
srv = &mockLogsServer{t: tb}
233252
plogotlp.RegisterGRPCServer(grpcServer, srv)
234253
go func() {
235254
_ = grpcServer.Serve(ln)

exporter/coralogixexporter/metadata.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ status:
88
distributions: [contrib]
99
codeowners:
1010
active: [povilasv, iblancasa, douglascamata]
11+
warnings: ["Authentication issues in v0.127.0"]
1112

1213
tests:
1314
config:

exporter/coralogixexporter/metrics_client_test.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"go.uber.org/zap/zapcore"
2626
"go.uber.org/zap/zaptest/observer"
2727
"google.golang.org/grpc"
28+
"google.golang.org/grpc/metadata"
2829
)
2930

3031
func TestNewMetricsExporter(t *testing.T) {
@@ -213,10 +214,28 @@ type mockMetricsServer struct {
213214
pmetricotlp.UnimplementedGRPCServer
214215
recvCount int
215216
partialSuccess *pmetricotlp.ExportPartialSuccess
217+
t testing.TB
216218
}
217219

218-
func (m *mockMetricsServer) Export(_ context.Context, req pmetricotlp.ExportRequest) (pmetricotlp.ExportResponse, error) {
219-
m.recvCount += req.Metrics().MetricCount()
220+
func (m *mockMetricsServer) Export(ctx context.Context, req pmetricotlp.ExportRequest) (pmetricotlp.ExportResponse, error) {
221+
md, ok := metadata.FromIncomingContext(ctx)
222+
if !ok {
223+
m.t.Error("No metadata found in context")
224+
return pmetricotlp.NewExportResponse(), errors.New("no metadata found")
225+
}
226+
227+
authHeader := md.Get("authorization")
228+
if len(authHeader) == 0 {
229+
m.t.Error("No Authorization header found")
230+
return pmetricotlp.NewExportResponse(), errors.New("no authorization header")
231+
}
232+
233+
if authHeader[0] != "Bearer test-key" {
234+
m.t.Errorf("Expected Authorization header 'Bearer test-key', got %s", authHeader[0])
235+
return pmetricotlp.NewExportResponse(), errors.New("invalid authorization header")
236+
}
237+
238+
m.recvCount += req.Metrics().DataPointCount()
220239
resp := pmetricotlp.NewExportResponse()
221240
if m.partialSuccess != nil {
222241
resp.PartialSuccess().SetErrorMessage(m.partialSuccess.ErrorMessage())
@@ -231,7 +250,7 @@ func startMockOtlpMetricsServer(tb testing.TB) (endpoint string, stopFn func(),
231250
tb.Fatalf("failed to listen: %v", err)
232251
}
233252
grpcServer := grpc.NewServer()
234-
srv = &mockMetricsServer{}
253+
srv = &mockMetricsServer{t: tb}
235254
pmetricotlp.RegisterGRPCServer(grpcServer, srv)
236255
go func() {
237256
_ = grpcServer.Serve(ln)

exporter/coralogixexporter/profiles_client_test.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"go.uber.org/zap/zapcore"
2727
"go.uber.org/zap/zaptest/observer"
2828
"google.golang.org/grpc"
29+
"google.golang.org/grpc/metadata"
2930
)
3031

3132
func TestNewProfilesExporter(t *testing.T) {
@@ -214,9 +215,27 @@ type mockProfilesServer struct {
214215
pprofileotlp.UnimplementedGRPCServer
215216
recvCount int
216217
partialSuccess *pprofileotlp.ExportPartialSuccess
218+
t testing.TB
217219
}
218220

219-
func (m *mockProfilesServer) Export(_ context.Context, req pprofileotlp.ExportRequest) (pprofileotlp.ExportResponse, error) {
221+
func (m *mockProfilesServer) Export(ctx context.Context, req pprofileotlp.ExportRequest) (pprofileotlp.ExportResponse, error) {
222+
md, ok := metadata.FromIncomingContext(ctx)
223+
if !ok {
224+
m.t.Error("No metadata found in context")
225+
return pprofileotlp.NewExportResponse(), errors.New("no metadata found")
226+
}
227+
228+
authHeader := md.Get("authorization")
229+
if len(authHeader) == 0 {
230+
m.t.Error("No Authorization header found")
231+
return pprofileotlp.NewExportResponse(), errors.New("no authorization header")
232+
}
233+
234+
if authHeader[0] != "Bearer test-key" {
235+
m.t.Errorf("Expected Authorization header 'Bearer test-key', got %s", authHeader[0])
236+
return pprofileotlp.NewExportResponse(), errors.New("invalid authorization header")
237+
}
238+
220239
m.recvCount += req.Profiles().ResourceProfiles().Len()
221240
resp := pprofileotlp.NewExportResponse()
222241
if m.partialSuccess != nil {
@@ -232,7 +251,7 @@ func startMockOtlpProfilesServer(tb testing.TB) (endpoint string, stopFn func(),
232251
tb.Fatalf("failed to listen: %v", err)
233252
}
234253
grpcServer := grpc.NewServer()
235-
srv = &mockProfilesServer{}
254+
srv = &mockProfilesServer{t: tb}
236255
pprofileotlp.RegisterGRPCServer(grpcServer, srv)
237256
go func() {
238257
_ = grpcServer.Serve(ln)

exporter/coralogixexporter/signals.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ func (e *signalExporter) startSignalExporter(ctx context.Context, host component
120120
signalConfigWrapper.config.Headers = make(map[string]configopaque.String)
121121
}
122122
signalConfigWrapper.config.Headers["Authorization"] = configopaque.String("Bearer " + string(e.config.PrivateKey))
123+
124+
for k, v := range signalConfigWrapper.config.Headers {
125+
e.metadata.Set(k, string(v))
126+
}
123127
}
124128

125129
e.callOptions = []grpc.CallOption{

exporter/coralogixexporter/signals_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
package coralogixexporter
55

66
import (
7+
"context"
78
"errors"
89
"testing"
910
"time"
1011

1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
14+
"go.opentelemetry.io/collector/component/componenttest"
15+
"go.opentelemetry.io/collector/config/configgrpc"
16+
"go.opentelemetry.io/collector/config/configopaque"
1317
"go.opentelemetry.io/collector/consumer/consumererror"
1418
"go.opentelemetry.io/collector/exporter/exporterhelper"
1519
"go.opentelemetry.io/collector/exporter/exportertest"
@@ -160,3 +164,134 @@ func TestProcessError(t *testing.T) {
160164
})
161165
}
162166
}
167+
168+
func TestSignalExporter_AuthorizationHeader(t *testing.T) {
169+
privateKey := "test-private-key"
170+
cfg := &Config{
171+
Domain: "test.domain.com",
172+
PrivateKey: configopaque.String(privateKey),
173+
Logs: configgrpc.ClientConfig{
174+
Headers: map[string]configopaque.String{},
175+
},
176+
}
177+
178+
exp, err := newSignalExporter(cfg, exportertest.NewNopSettings(exportertest.NopType), "", nil)
179+
require.NoError(t, err)
180+
181+
wrapper := &signalConfigWrapper{config: &cfg.Logs}
182+
err = exp.startSignalExporter(context.Background(), componenttest.NewNopHost(), wrapper)
183+
require.NoError(t, err)
184+
defer func() {
185+
require.NoError(t, exp.shutdown(context.Background()))
186+
}()
187+
188+
authHeader, ok := wrapper.config.Headers["Authorization"]
189+
require.True(t, ok, "Authorization header should be present")
190+
assert.Equal(t, configopaque.String("Bearer "+privateKey), authHeader, "Authorization header should be in Bearer format")
191+
192+
mdValue := exp.metadata.Get("Authorization")
193+
require.Len(t, mdValue, 1, "Authorization header should be present in metadata")
194+
assert.Equal(t, "Bearer "+privateKey, mdValue[0], "Authorization header in metadata should be in Bearer format")
195+
}
196+
197+
func TestSignalExporter_CustomHeadersAndAuthorization(t *testing.T) {
198+
tests := []struct {
199+
name string
200+
config configgrpc.ClientConfig
201+
}{
202+
{
203+
name: "logs",
204+
config: configgrpc.ClientConfig{
205+
Headers: map[string]configopaque.String{
206+
"Custom-Header": "custom-value",
207+
"X-Test": "test-value",
208+
},
209+
},
210+
},
211+
{
212+
name: "traces",
213+
config: configgrpc.ClientConfig{
214+
Headers: map[string]configopaque.String{
215+
"Custom-Header": "custom-value",
216+
"X-Test": "test-value",
217+
},
218+
},
219+
},
220+
{
221+
name: "metrics",
222+
config: configgrpc.ClientConfig{
223+
Headers: map[string]configopaque.String{
224+
"Custom-Header": "custom-value",
225+
"X-Test": "test-value",
226+
},
227+
},
228+
},
229+
{
230+
name: "profiles",
231+
config: configgrpc.ClientConfig{
232+
Headers: map[string]configopaque.String{
233+
"Custom-Header": "custom-value",
234+
"X-Test": "test-value",
235+
},
236+
},
237+
},
238+
}
239+
240+
for _, tt := range tests {
241+
t.Run(tt.name, func(t *testing.T) {
242+
privateKey := "test-private-key"
243+
cfg := &Config{
244+
Domain: "test.domain.com",
245+
PrivateKey: configopaque.String(privateKey),
246+
}
247+
248+
switch tt.name {
249+
case "logs":
250+
cfg.Logs = tt.config
251+
case "traces":
252+
cfg.Traces = tt.config
253+
case "metrics":
254+
cfg.Metrics = tt.config
255+
case "profiles":
256+
cfg.Profiles = tt.config
257+
}
258+
259+
exp, err := newSignalExporter(cfg, exportertest.NewNopSettings(exportertest.NopType), "", nil)
260+
require.NoError(t, err)
261+
262+
wrapper := &signalConfigWrapper{config: &tt.config}
263+
err = exp.startSignalExporter(context.Background(), componenttest.NewNopHost(), wrapper)
264+
require.NoError(t, err)
265+
defer func() {
266+
require.NoError(t, exp.shutdown(context.Background()))
267+
}()
268+
269+
headers := wrapper.config.Headers
270+
require.Len(t, headers, 3)
271+
272+
authHeader, ok := headers["Authorization"]
273+
require.True(t, ok)
274+
assert.Equal(t, configopaque.String("Bearer "+privateKey), authHeader)
275+
276+
customHeader, ok := headers["Custom-Header"]
277+
require.True(t, ok)
278+
assert.Equal(t, configopaque.String("custom-value"), customHeader)
279+
280+
testHeader, ok := headers["X-Test"]
281+
require.True(t, ok)
282+
assert.Equal(t, configopaque.String("test-value"), testHeader)
283+
284+
mdAuth := exp.metadata.Get("Authorization")
285+
require.Len(t, mdAuth, 1)
286+
assert.Equal(t, "Bearer "+privateKey, mdAuth[0])
287+
288+
mdCustom := exp.metadata.Get("Custom-Header")
289+
require.Len(t, mdCustom, 1)
290+
assert.Equal(t, "custom-value", mdCustom[0])
291+
292+
mdTest := exp.metadata.Get("X-Test")
293+
require.Len(t, mdTest, 1)
294+
assert.Equal(t, "test-value", mdTest[0])
295+
})
296+
}
297+
}

0 commit comments

Comments
 (0)