Skip to content

Commit a3556a4

Browse files
[exporter/splunkhec] - Add heartbeat check while startup (#24423)
**Description:** Add heartbeat check while startup. This is different than the `healtcheck_startup`, as the latter doesn't take token or index into account. **Link to tracking Issue:** [<24411>](#24411) **Testing:** Added relevant test cases **Documentation:** <Describe the documentation added.> --------- Co-authored-by: Curtis Robert <[email protected]>
1 parent 5d410bb commit a3556a4

File tree

6 files changed

+143
-2
lines changed

6 files changed

+143
-2
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Use this changelog template to create an entry for release notes.
2+
# If your change doesn't affect end users, such as a test fix or a tooling change,
3+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
4+
5+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
6+
change_type: 'enhancement'
7+
8+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
9+
component: splunkhecexporter
10+
11+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
12+
note: Add heartbeat check while startup and new config param, heartbeat/startup (defaults to false). This is different than the healtcheck_startup, as the latter doesn't take token or index into account.
13+
14+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
15+
issues: [24411]
16+
17+
# (Optional) One or more lines of additional information to render under the primary note.
18+
# These lines will be padded with 2 spaces and then inserted directly into the document.
19+
# Use pipe (|) for multiline entries.
20+
subtext:

exporter/splunkhecexporter/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ The following configuration options can also be configured:
6262
- `otel_to_hec_fields/severity_number` (default = `otel.log.severity.number`): Specifies the name of the field to map the severity number field of log events.
6363
- `otel_to_hec_fields/name` (default = `"otel.log.name`): Specifies the name of the field to map the name field of log events.
6464
- `heartbeat/interval` (no default): Specifies the interval of sending hec heartbeat to the destination. If not specified, heartbeat is not enabled.
65+
- `heartbeat/startup` (default: false): Check heartbeat at start up time. This action enforces a synchronous heartbeat action during the collector start up sequence. The collector will fail to start if the heartbeat returns an error.
6566
- `telemetry/enabled` (default: false): Specifies whether to enable telemetry inside splunk hec exporter.
6667
- `telemetry/override_metrics_names` (default: empty map): Specifies the metrics name to overrides in splunk hec exporter.
6768
- `telemetry/extra_attributes` (default: empty map): Specifies the extra metrics attributes in splunk hec exporter.

exporter/splunkhecexporter/client.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type client struct {
5252
buildInfo component.BuildInfo
5353
heartbeater *heartbeater
5454
bufferPool bufferPool
55+
exporterName string
5556
}
5657

5758
var jsonStreamPool = sync.Pool{
@@ -67,6 +68,7 @@ func newClient(set exporter.CreateSettings, cfg *Config, maxContentLength uint)
6768
telemetrySettings: set.TelemetrySettings,
6869
buildInfo: set.BuildInfo,
6970
bufferPool: newBufferPool(maxContentLength, !cfg.DisableCompression),
71+
exporterName: set.ID.String(),
7072
}
7173
}
7274

@@ -624,12 +626,17 @@ func (c *client) start(_ context.Context, host component.Host) (err error) {
624626
healthCheckURL, _ := c.config.getURL()
625627
healthCheckURL.Path = c.config.HealthPath
626628
if err := checkHecHealth(httpClient, healthCheckURL); err != nil {
627-
return fmt.Errorf("health check failed: %w", err)
629+
return fmt.Errorf("%s: health check failed: %w", c.exporterName, err)
628630
}
629631
}
630632
url, _ := c.config.getURL()
631633
c.hecWorker = &defaultHecWorker{url, httpClient, buildHTTPHeaders(c.config, c.buildInfo)}
632634
c.heartbeater = newHeartbeater(c.config, c.buildInfo, getPushLogFn(c))
635+
if c.config.Heartbeat.Startup {
636+
if err := c.heartbeater.sendHeartbeat(c.config, c.buildInfo, getPushLogFn(c)); err != nil {
637+
return fmt.Errorf("%s: heartbeat on startup failed: %w", c.exporterName, err)
638+
}
639+
}
633640
return nil
634641
}
635642

exporter/splunkhecexporter/client_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,112 @@ func TestInvalidURL(t *testing.T) {
13621362
assert.EqualError(t, err, "Post \"ftp://example.com:134/services/collector\": unsupported protocol scheme \"ftp\"")
13631363
}
13641364

1365+
func TestHeartbeatStartupFailed(t *testing.T) {
1366+
rr := make(chan receivedRequest)
1367+
capture := CapturingData{receivedRequest: rr, statusCode: 403}
1368+
listener, err := net.Listen("tcp", "127.0.0.1:0")
1369+
if err != nil {
1370+
panic(err)
1371+
}
1372+
s := &http.Server{
1373+
Handler: &capture,
1374+
ReadHeaderTimeout: 20 * time.Second,
1375+
}
1376+
defer s.Close()
1377+
go func() {
1378+
if e := s.Serve(listener); e != http.ErrServerClosed {
1379+
require.NoError(t, e)
1380+
}
1381+
}()
1382+
factory := NewFactory()
1383+
cfg := factory.CreateDefaultConfig().(*Config)
1384+
cfg.HTTPClientSettings.Endpoint = "http://" + listener.Addr().String() + "/services/collector"
1385+
// Disable QueueSettings to ensure that we execute the request when calling ConsumeTraces
1386+
// otherwise we will not see the error.
1387+
cfg.QueueSettings.Enabled = false
1388+
// Disable retries to not wait too much time for the return error.
1389+
cfg.RetrySettings.Enabled = false
1390+
cfg.DisableCompression = true
1391+
cfg.Token = "1234-1234"
1392+
cfg.Heartbeat.Startup = true
1393+
1394+
params := exportertest.NewNopCreateSettings()
1395+
exporter, err := factory.CreateTracesExporter(context.Background(), params, cfg)
1396+
assert.NoError(t, err)
1397+
// The exporter's name is "" while generating default params
1398+
assert.EqualError(t, exporter.Start(context.Background(), componenttest.NewNopHost()), ": heartbeat on startup failed: HTTP 403 \"Forbidden\"")
1399+
}
1400+
1401+
func TestHeartbeatStartupPass_Disabled(t *testing.T) {
1402+
rr := make(chan receivedRequest)
1403+
capture := CapturingData{receivedRequest: rr, statusCode: 403}
1404+
listener, err := net.Listen("tcp", "127.0.0.1:0")
1405+
if err != nil {
1406+
panic(err)
1407+
}
1408+
s := &http.Server{
1409+
Handler: &capture,
1410+
ReadHeaderTimeout: 20 * time.Second,
1411+
}
1412+
defer s.Close()
1413+
go func() {
1414+
if e := s.Serve(listener); e != http.ErrServerClosed {
1415+
require.NoError(t, e)
1416+
}
1417+
}()
1418+
factory := NewFactory()
1419+
cfg := factory.CreateDefaultConfig().(*Config)
1420+
cfg.HTTPClientSettings.Endpoint = "http://" + listener.Addr().String() + "/services/collector"
1421+
// Disable QueueSettings to ensure that we execute the request when calling ConsumeTraces
1422+
// otherwise we will not see the error.
1423+
cfg.QueueSettings.Enabled = false
1424+
// Disable retries to not wait too much time for the return error.
1425+
cfg.RetrySettings.Enabled = false
1426+
cfg.DisableCompression = true
1427+
cfg.Token = "1234-1234"
1428+
cfg.Heartbeat.Startup = false
1429+
1430+
params := exportertest.NewNopCreateSettings()
1431+
exporter, err := factory.CreateTracesExporter(context.Background(), params, cfg)
1432+
assert.NoError(t, err)
1433+
assert.NoError(t, exporter.Start(context.Background(), componenttest.NewNopHost()))
1434+
}
1435+
1436+
func TestHeartbeatStartupPass(t *testing.T) {
1437+
rr := make(chan receivedRequest)
1438+
capture := CapturingData{receivedRequest: rr, statusCode: 200}
1439+
listener, err := net.Listen("tcp", "127.0.0.1:0")
1440+
if err != nil {
1441+
panic(err)
1442+
}
1443+
s := &http.Server{
1444+
Handler: &capture,
1445+
ReadHeaderTimeout: 20 * time.Second,
1446+
}
1447+
defer s.Close()
1448+
go func() {
1449+
if e := s.Serve(listener); e != http.ErrServerClosed {
1450+
require.NoError(t, e)
1451+
}
1452+
}()
1453+
factory := NewFactory()
1454+
cfg := factory.CreateDefaultConfig().(*Config)
1455+
cfg.HTTPClientSettings.Endpoint = "http://" + listener.Addr().String() + "/services/collector"
1456+
// Disable QueueSettings to ensure that we execute the request when calling ConsumeTraces
1457+
// otherwise we will not see the error.
1458+
cfg.QueueSettings.Enabled = false
1459+
// Disable retries to not wait too much time for the return error.
1460+
cfg.RetrySettings.Enabled = false
1461+
cfg.DisableCompression = true
1462+
cfg.Token = "1234-1234"
1463+
cfg.Heartbeat.Startup = true
1464+
1465+
params := exportertest.NewNopCreateSettings()
1466+
exporter, err := factory.CreateTracesExporter(context.Background(), params, cfg)
1467+
assert.NoError(t, err)
1468+
assert.NoError(t, exporter.Start(context.Background(), componenttest.NewNopHost()))
1469+
}
1470+
13651471
type badJSON struct {
13661472
Foo float64 `json:"foo"`
13671473
}

exporter/splunkhecexporter/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ type HecHeartbeat struct {
4444
// heartbeat is not enabled.
4545
// A heartbeat is an event sent to _internal index with metadata for the current collector/host.
4646
Interval time.Duration `mapstructure:"interval"`
47+
48+
// Startup is used to send heartbeat events on exporter's startup.
49+
Startup bool `mapstructure:"startup"`
4750
}
4851

4952
// HecTelemetry defines the telemetry configuration for the exporter

exporter/splunkhecexporter/heartbeat.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func newHeartbeater(config *Config, buildInfo component.BuildInfo, pushLogFn fun
9797
case <-hbter.hbDoneChan:
9898
return
9999
case <-ticker.C:
100-
err := pushLogFn(context.Background(), generateHeartbeatLog(config.HecToOtelAttrs, buildInfo))
100+
err := hbter.sendHeartbeat(config, buildInfo, pushLogFn)
101101
if config.Telemetry.Enabled {
102102
observe(heartbeatsSent, heartbeatsFailed, tagMutators, err)
103103
}
@@ -111,6 +111,10 @@ func (h *heartbeater) shutdown() {
111111
close(h.hbDoneChan)
112112
}
113113

114+
func (h *heartbeater) sendHeartbeat(config *Config, buildInfo component.BuildInfo, pushLogFn func(ctx context.Context, ld plog.Logs) error) error {
115+
return pushLogFn(context.Background(), generateHeartbeatLog(config.HecToOtelAttrs, buildInfo))
116+
}
117+
114118
// there is only use case for open census metrics recording for now. Extend to use open telemetry in the future.
115119
func observe(heartbeatsSent *stats.Int64Measure, heartbeatsFailed *stats.Int64Measure, tagMutators []tag.Mutator, err error) {
116120
var counter *stats.Int64Measure

0 commit comments

Comments
 (0)