Skip to content

Commit 784d8e4

Browse files
shalper2XinRanZhAWS
authored andcommitted
Splunkent update conf (open-telemetry#31183)
**Description:** Make changes to configuration of the application to allow the user to specify endpoints corresponding to different Splunk node types. Specifically, this update will allow users to define three separate clients: indexer, cluster master, and search head. This change will allow for the addition of metrics corresponding to these different modes of operation within the Splunk enterprise deployment. **Link to tracking Issue:** [30254](open-telemetry#30254) **Testing:** Unit tests were updated to run against new configuration options. **Documentation:** Updated README to reflect the new changes in configuration.
1 parent 0d66d0f commit 784d8e4

File tree

11 files changed

+407
-143
lines changed

11 files changed

+407
-143
lines changed

.chloggen/splunkent-update-conf.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: 'enhancement'
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: splunkentreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "Updated the config.go and propogated these changes to other receiver components. Change was necessary to differentiate different configurable endpoints."
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: [30254]
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]

receiver/splunkenterprisereceiver/README.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ jobs.
88

99
## Configuration
1010

11-
The following settings are required, omitting them will either cause your receiver to fail to compile or result in 4/5xx return codes during scraping.
11+
The following settings are required, omitting them will either cause your receiver to fail to compile or result in 4/5xx return codes during scraping.
12+
13+
**NOTE:** These must be set for each Splunk instance type (indexer, search head, or cluster master) from which you wish to pull metrics. At present, only one of each type is accepted, per configured receiver instance. This means, for example, that if you have three different "indexer" type instances that you would like to pull metrics from you will need to configure three different `splunkenterprise` receivers for each indexer node you wish to monitor.
1214

1315
* `basicauth` (from [basicauthextension](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/basicauthextension)): A configured stanza for the basicauthextension.
1416
* `auth` (no default): String name referencing your auth extension.
@@ -23,16 +25,38 @@ Example:
2325

2426
```yaml
2527
extensions:
26-
basicauth/client:
28+
basicauth/indexer:
29+
client_auth:
30+
username: admin
31+
password: securityFirst
32+
basicauth/cluster_master:
2733
client_auth:
2834
username: admin
2935
password: securityFirst
3036

3137
receivers:
3238
splunkenterprise:
33-
auth: basicauth/client
34-
endpoint: "https://localhost:8089"
35-
timeout: 45s
39+
indexer:
40+
auth:
41+
authenticator: basicauth/indexer
42+
endpoint: "https://localhost:8089"
43+
timeout: 45s
44+
cluster_master:
45+
auth:
46+
authenticator: basicauth/cluster_master
47+
endpoint: "https://localhost:8089"
48+
timeout: 45s
49+
50+
exporters:
51+
logging:
52+
loglevel: info
53+
54+
service:
55+
extensions: [basicauth/indexer, basicauth/cluster_master]
56+
pipelines:
57+
metrics:
58+
receivers: [splunkenterprise]
59+
exporters: [logging]
3660
```
3761
3862
For a full list of settings exposed by this receiver please look [here](./config.go) with a detailed configuration [here](./testdata/config.yaml).

receiver/splunkenterprisereceiver/client.go

Lines changed: 123 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package splunkenterprisereceiver // import "github.com/open-telemetry/openteleme
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"net/http"
1011
"net/url"
@@ -13,73 +14,172 @@ import (
1314
"go.opentelemetry.io/collector/component"
1415
)
1516

17+
// Indexer type "enum". Included in context sent from scraper functions
18+
const (
19+
typeIdx = "IDX"
20+
typeSh = "SH"
21+
typeCm = "CM"
22+
)
23+
24+
var (
25+
errCtxMissingEndpointType = errors.New("context was passed without the endpoint type included")
26+
errEndpointTypeNotFound = errors.New("requested client is not configured and could not be found in splunkEntClient")
27+
errNoClientFound = errors.New("no client corresponding to the endpoint type was found")
28+
)
29+
30+
// Type wrapper for accessing context value
31+
type endpointType string
32+
33+
// Wrapper around splunkClientMap to avoid awkward reference/dereference stuff that arises when using maps in golang
1634
type splunkEntClient struct {
35+
clients splunkClientMap
36+
}
37+
38+
// The splunkEntClient is made up of a number of splunkClients defined for each configured endpoint
39+
type splunkClientMap map[any]splunkClient
40+
41+
// The client does not carry the endpoint that is configured with it and golang does not support mixed
42+
// type arrays so this struct contains the pair: the client configured for the endpoint and the endpoint
43+
// itself
44+
type splunkClient struct {
1745
client *http.Client
1846
endpoint *url.URL
1947
}
2048

2149
func newSplunkEntClient(cfg *Config, h component.Host, s component.TelemetrySettings) (*splunkEntClient, error) {
22-
client, err := cfg.ClientConfig.ToClient(h, s)
23-
if err != nil {
24-
return nil, err
50+
var err error
51+
var e *url.URL
52+
var c *http.Client
53+
clientMap := make(splunkClientMap)
54+
55+
// if the endpoint is defined, put it in the endpoints map for later use
56+
// we already checked that url.Parse does not fail in cfg.Validate()
57+
if cfg.IdxEndpoint.Endpoint != "" {
58+
e, _ = url.Parse(cfg.IdxEndpoint.Endpoint)
59+
c, err = cfg.IdxEndpoint.ToClient(h, s)
60+
if err != nil {
61+
return nil, err
62+
}
63+
clientMap[typeIdx] = splunkClient{
64+
client: c,
65+
endpoint: e,
66+
}
67+
}
68+
if cfg.SHEndpoint.Endpoint != "" {
69+
e, _ = url.Parse(cfg.SHEndpoint.Endpoint)
70+
c, err = cfg.SHEndpoint.ToClient(h, s)
71+
if err != nil {
72+
return nil, err
73+
}
74+
clientMap[typeSh] = splunkClient{
75+
client: c,
76+
endpoint: e,
77+
}
78+
}
79+
if cfg.CMEndpoint.Endpoint != "" {
80+
e, _ = url.Parse(cfg.CMEndpoint.Endpoint)
81+
c, err = cfg.CMEndpoint.ToClient(h, s)
82+
if err != nil {
83+
return nil, err
84+
}
85+
clientMap[typeCm] = splunkClient{
86+
client: c,
87+
endpoint: e,
88+
}
2589
}
2690

27-
endpoint, _ := url.Parse(cfg.Endpoint)
28-
29-
return &splunkEntClient{
30-
client: client,
31-
endpoint: endpoint,
32-
}, nil
91+
return &splunkEntClient{clients: clientMap}, nil
3392
}
3493

3594
// For running ad hoc searches only
36-
func (c *splunkEntClient) createRequest(ctx context.Context, sr *searchResponse) (*http.Request, error) {
95+
func (c *splunkEntClient) createRequest(ctx context.Context, sr *searchResponse) (req *http.Request, err error) {
96+
// get endpoint type from the context
97+
eptType := ctx.Value(endpointType("type"))
98+
if eptType == nil {
99+
return nil, errCtxMissingEndpointType
100+
}
101+
37102
// Running searches via Splunk's REST API is a two step process: First you submit the job to run
38103
// this returns a jobid which is then used in the second part to retrieve the search results
39104
if sr.Jobid == nil {
105+
var u string
40106
path := "/services/search/jobs/"
41-
url, _ := url.JoinPath(c.endpoint.String(), path)
107+
108+
if e, ok := c.clients[eptType]; ok {
109+
u, err = url.JoinPath(e.endpoint.String(), path)
110+
if err != nil {
111+
return nil, err
112+
}
113+
} else {
114+
return nil, errNoClientFound
115+
}
42116

43117
// reader for the response data
44118
data := strings.NewReader(sr.search)
45119

46120
// return the build request, ready to be run by makeRequest
47-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, data)
121+
req, err = http.NewRequestWithContext(ctx, http.MethodPost, u, data)
48122
if err != nil {
49123
return nil, err
50124
}
51125

52126
return req, nil
53127
}
54128
path := fmt.Sprintf("/services/search/jobs/%s/results", *sr.Jobid)
55-
url, _ := url.JoinPath(c.endpoint.String(), path)
129+
url, _ := url.JoinPath(c.clients[eptType].endpoint.String(), path)
56130

57-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
131+
req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
58132
if err != nil {
59133
return nil, err
60134
}
61135

62136
return req, nil
63137
}
64138

65-
func (c *splunkEntClient) createAPIRequest(ctx context.Context, apiEndpoint string) (*http.Request, error) {
66-
url := c.endpoint.String() + apiEndpoint
139+
// forms an *http.Request for use with Splunk built-in API's (like introspection).
140+
func (c *splunkEntClient) createAPIRequest(ctx context.Context, apiEndpoint string) (req *http.Request, err error) {
141+
var u string
142+
143+
// get endpoint type from the context
144+
eptType := ctx.Value(endpointType("type"))
145+
if eptType == nil {
146+
return nil, errCtxMissingEndpointType
147+
}
148+
149+
if e, ok := c.clients[eptType]; ok {
150+
u = e.endpoint.String() + apiEndpoint
151+
} else {
152+
return nil, errNoClientFound
153+
}
67154

68-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
155+
req, err = http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
69156
if err != nil {
70157
return nil, err
71158
}
72159

73160
return req, nil
74161
}
75162

76-
// Construct and perform a request to the API. Returns the searchResponse passed into the
77-
// function as state
163+
// Perform a request.
78164
func (c *splunkEntClient) makeRequest(req *http.Request) (*http.Response, error) {
79-
res, err := c.client.Do(req)
80-
if err != nil {
81-
return nil, err
165+
// get endpoint type from the context
166+
eptType := req.Context().Value(endpointType("type"))
167+
if eptType == nil {
168+
return nil, errCtxMissingEndpointType
82169
}
170+
if sc, ok := c.clients[eptType]; ok {
171+
res, err := sc.client.Do(req)
172+
if err != nil {
173+
return nil, err
174+
}
175+
return res, nil
176+
}
177+
return nil, errEndpointTypeNotFound
178+
}
83179

84-
return res, nil
180+
// Check if the splunkEntClient contains a configured endpoint for the type of scraper
181+
// Returns true if an entry exists, false if not.
182+
func (c *splunkEntClient) isConfigured(v string) bool {
183+
_, ok := c.clients[v]
184+
return ok
85185
}

receiver/splunkenterprisereceiver/client_test.go

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,9 @@ func (m *mockHost) GetExtensions() map[component.ID]component.Component {
3434

3535
func TestClientCreation(t *testing.T) {
3636
cfg := &Config{
37-
ClientConfig: confighttp.ClientConfig{
37+
IdxEndpoint: confighttp.ClientConfig{
3838
Endpoint: "https://localhost:8089",
39-
Auth: &configauth.Authentication{
40-
AuthenticatorID: component.MustNewIDWithName("basicauth", "client"),
41-
},
39+
Auth: &configauth.Authentication{AuthenticatorID: component.MustNewIDWithName("basicauth", "client")},
4240
},
4341
ScraperControllerSettings: scraperhelper.ScraperControllerSettings{
4442
CollectionInterval: 10 * time.Second,
@@ -58,18 +56,16 @@ func TestClientCreation(t *testing.T) {
5856

5957
testEndpoint, _ := url.Parse("https://localhost:8089")
6058

61-
require.Equal(t, client.endpoint, testEndpoint)
59+
require.Equal(t, testEndpoint, client.clients[typeIdx].endpoint)
6260
}
6361

6462
// test functionality of createRequest which is used for building metrics out of
6563
// ad-hoc searches
6664
func TestClientCreateRequest(t *testing.T) {
6765
cfg := &Config{
68-
ClientConfig: confighttp.ClientConfig{
66+
IdxEndpoint: confighttp.ClientConfig{
6967
Endpoint: "https://localhost:8089",
70-
Auth: &configauth.Authentication{
71-
AuthenticatorID: component.MustNewIDWithName("basicauth", "client"),
72-
},
68+
Auth: &configauth.Authentication{AuthenticatorID: component.MustNewIDWithName("basicauth", "client")},
7369
},
7470
ScraperControllerSettings: scraperhelper.ScraperControllerSettings{
7571
CollectionInterval: 10 * time.Second,
@@ -131,6 +127,7 @@ func TestClientCreateRequest(t *testing.T) {
131127
}
132128

133129
ctx := context.Background()
130+
ctx = context.WithValue(ctx, endpointType("type"), typeIdx)
134131
for _, test := range tests {
135132
t.Run(test.desc, func(t *testing.T) {
136133
req, err := test.client.createRequest(ctx, test.sr)
@@ -147,11 +144,9 @@ func TestClientCreateRequest(t *testing.T) {
147144
// createAPIRequest creates a request for api calls i.e. to introspection endpoint
148145
func TestAPIRequestCreate(t *testing.T) {
149146
cfg := &Config{
150-
ClientConfig: confighttp.ClientConfig{
147+
IdxEndpoint: confighttp.ClientConfig{
151148
Endpoint: "https://localhost:8089",
152-
Auth: &configauth.Authentication{
153-
AuthenticatorID: component.MustNewIDWithName("basicauth", "client"),
154-
},
149+
Auth: &configauth.Authentication{AuthenticatorID: component.MustNewIDWithName("basicauth", "client")},
155150
},
156151
ScraperControllerSettings: scraperhelper.ScraperControllerSettings{
157152
CollectionInterval: 10 * time.Second,
@@ -171,11 +166,12 @@ func TestAPIRequestCreate(t *testing.T) {
171166
require.NoError(t, err)
172167

173168
ctx := context.Background()
169+
ctx = context.WithValue(ctx, endpointType("type"), typeIdx)
174170
req, err := client.createAPIRequest(ctx, "/test/endpoint")
175171
require.NoError(t, err)
176172

177173
// build the expected request
178-
expectedURL := client.endpoint.String() + "/test/endpoint"
174+
expectedURL := client.clients[typeIdx].endpoint.String() + "/test/endpoint"
179175
expected, _ := http.NewRequest(http.MethodGet, expectedURL, nil)
180176

181177
require.Equal(t, expected.URL, req.URL)

0 commit comments

Comments
 (0)