Skip to content

Commit 378fe0a

Browse files
JonathanWamsleydjaglowski
authored andcommitted
[receiver/mongodbatlasreceiver] add metrics project config (open-telemetry#28866)
**Description:** <Describe what has changed.> <!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> This feature adds a Project Config for the metrics to filter by Project name and or clusters. **Link to tracking Issue:** <Issue number if applicable> open-telemetry#28865 **Testing:** <Describe what testing was performed and which tests were added.> - Added test for cluster filtering - Tested project name alone, project name with IncludeClusters and project name with ExcludeClusters on a live environment with success. **Documentation:** <Describe the documentation added.> Added optional project config fields to README --------- Co-authored-by: Daniel Jaglowski <[email protected]>
1 parent f44ccbd commit 378fe0a

File tree

6 files changed

+239
-23
lines changed

6 files changed

+239
-23
lines changed
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: receiver/mongodbatlasreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: adds project config to mongodbatlas metrics to filter by project name and clusters.
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: [28865]
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: []

receiver/mongodbatlasreceiver/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ MongoDB Atlas [Documentation](https://www.mongodb.com/docs/atlas/reference/api/l
4242
- `granularity` (default `PT1M` - See [MongoDB Atlas Documentation](https://docs.atlas.mongodb.com/reference/api/process-measurements/))
4343
- `collection_interval` (default `3m`) This receiver collects metrics on an interval. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`.
4444
- `storage` (optional) The component ID of a storage extension which can be used when polling for `alerts` or `events` . The storage extension prevents duplication of data after a collector restart by remembering which data were previously collected.
45+
- `projects` (optional for metrics) a slice of projects this receiver collects metrics from instead of all projects in an organization
46+
- `name` Name of the project to discover metrics from
47+
- `include_clusters` (default empty, exclusive with `exclude_clusters`)
48+
- `exclude_clusters` (default empty, exclusive with `include_clusters`)
49+
- If both `include_clusters` and `exclude_clusters` are empty, then all clusters in the project will be included
4550
- `retry_on_failure`
4651
- `enabled` (default true)
4752
- `initial_interval` (default 5s)

receiver/mongodbatlasreceiver/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Config struct {
2828
PrivateKey configopaque.String `mapstructure:"private_key"`
2929
Granularity string `mapstructure:"granularity"`
3030
MetricsBuilderConfig metadata.MetricsBuilderConfig `mapstructure:",squash"`
31+
Projects []*ProjectConfig `mapstructure:"projects"`
3132
Alerts AlertConfig `mapstructure:"alerts"`
3233
Events *EventsConfig `mapstructure:"events"`
3334
Logs LogConfig `mapstructure:"logs"`
@@ -133,6 +134,12 @@ var (
133134
func (c *Config) Validate() error {
134135
var errs error
135136

137+
for _, project := range c.Projects {
138+
if len(project.ExcludeClusters) != 0 && len(project.IncludeClusters) != 0 {
139+
errs = multierr.Append(errs, errClusterConfig)
140+
}
141+
}
142+
136143
errs = multierr.Append(errs, c.Alerts.validate())
137144
errs = multierr.Append(errs, c.Logs.validate())
138145
if c.Events != nil {

receiver/mongodbatlasreceiver/config_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,47 @@ func TestValidate(t *testing.T) {
116116
},
117117
expectedErr: errNoCert.Error(),
118118
},
119+
{
120+
name: "Valid Metrics Config",
121+
input: Config{
122+
Projects: []*ProjectConfig{
123+
{
124+
Name: "Project1",
125+
},
126+
},
127+
ScraperControllerSettings: scraperhelper.NewDefaultScraperControllerSettings(metadata.Type),
128+
},
129+
},
130+
{
131+
name: "Valid Metrics Config with multiple projects with an inclusion or exclusion",
132+
input: Config{
133+
Projects: []*ProjectConfig{
134+
{
135+
Name: "Project1",
136+
IncludeClusters: []string{"Cluster1"},
137+
},
138+
{
139+
Name: "Project2",
140+
ExcludeClusters: []string{"Cluster1"},
141+
},
142+
},
143+
ScraperControllerSettings: scraperhelper.NewDefaultScraperControllerSettings(metadata.Type),
144+
},
145+
},
146+
{
147+
name: "invalid Metrics Config",
148+
input: Config{
149+
Projects: []*ProjectConfig{
150+
{
151+
Name: "Project1",
152+
IncludeClusters: []string{"Cluster1"},
153+
ExcludeClusters: []string{"Cluster2"},
154+
},
155+
},
156+
ScraperControllerSettings: scraperhelper.NewDefaultScraperControllerSettings(metadata.Type),
157+
},
158+
expectedErr: errClusterConfig.Error(),
159+
},
119160
{
120161
name: "Valid Logs Config",
121162
input: Config{

receiver/mongodbatlasreceiver/receiver.go

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ type timeconstraints struct {
3737

3838
func newMongoDBAtlasReceiver(settings receiver.CreateSettings, cfg *Config) *mongodbatlasreceiver {
3939
client := internal.NewMongoDBAtlasClient(cfg.PublicKey, string(cfg.PrivateKey), cfg.RetrySettings, settings.Logger)
40+
for _, p := range cfg.Projects {
41+
p.populateIncludesAndExcludes()
42+
}
43+
4044
return &mongodbatlasreceiver{
4145
log: settings.Logger,
4246
cfg: cfg,
@@ -77,47 +81,113 @@ func (s *mongodbatlasreceiver) shutdown(context.Context) error {
7781
return s.client.Shutdown()
7882
}
7983

84+
// poll decides whether to poll all projects or a specific project based on the configuration.
8085
func (s *mongodbatlasreceiver) poll(ctx context.Context, time timeconstraints) error {
86+
if len(s.cfg.Projects) == 0 {
87+
return s.pollAllProjects(ctx, time)
88+
}
89+
return s.pollProjects(ctx, time)
90+
}
91+
92+
// pollAllProjects handles polling across all projects within the organizations.
93+
func (s *mongodbatlasreceiver) pollAllProjects(ctx context.Context, time timeconstraints) error {
8194
orgs, err := s.client.Organizations(ctx)
8295
if err != nil {
8396
return fmt.Errorf("error retrieving organizations: %w", err)
8497
}
8598
for _, org := range orgs {
86-
projects, err := s.client.Projects(ctx, org.ID)
99+
proj, err := s.client.Projects(ctx, org.ID)
87100
if err != nil {
88-
return fmt.Errorf("error retrieving projects: %w", err)
101+
s.log.Error("error retrieving projects", zap.String("orgID", org.ID), zap.Error(err))
102+
continue
89103
}
90-
for _, project := range projects {
91-
nodeClusterMap, providerMap, err := s.getNodeClusterNameMap(ctx, project.ID)
92-
if err != nil {
93-
return fmt.Errorf("error collecting clusters from project %s: %w", project.ID, err)
104+
for _, project := range proj {
105+
// Since there is no specific ProjectConfig for these projects, pass nil.
106+
if err := s.processProject(ctx, time, org.Name, project, nil); err != nil {
107+
s.log.Error("error processing project", zap.String("projectID", project.ID), zap.Error(err))
94108
}
109+
}
110+
}
111+
return nil
112+
}
95113

96-
processes, err := s.client.Processes(ctx, project.ID)
97-
if err != nil {
98-
return fmt.Errorf("error retrieving MongoDB Atlas processes for project %s: %w", project.ID, err)
99-
}
100-
for _, process := range processes {
101-
clusterName := nodeClusterMap[process.UserAlias]
102-
providerValues := providerMap[clusterName]
114+
// pollProject handles polling for specific projects as configured.
115+
func (s *mongodbatlasreceiver) pollProjects(ctx context.Context, time timeconstraints) error {
116+
for _, projectCfg := range s.cfg.Projects {
117+
project, err := s.client.GetProject(ctx, projectCfg.Name)
118+
if err != nil {
119+
s.log.Error("error retrieving project", zap.String("projectName", projectCfg.Name), zap.Error(err))
120+
continue
121+
}
103122

104-
if err := s.extractProcessMetrics(ctx, time, org.Name, project, process, clusterName, providerValues); err != nil {
105-
return fmt.Errorf("error when polling process metrics from MongoDB Atlas for process %s: %w", process.ID, err)
106-
}
123+
org, err := s.client.GetOrganization(ctx, project.OrgID)
124+
if err != nil {
125+
s.log.Error("error retrieving organization from project", zap.String("projectName", projectCfg.Name), zap.Error(err))
126+
continue
127+
}
107128

108-
if err := s.extractProcessDatabaseMetrics(ctx, time, org.Name, project, process, clusterName, providerValues); err != nil {
109-
return fmt.Errorf("error when polling process database metrics from MongoDB Atlas for process %s: %w", process.ID, err)
110-
}
129+
if err := s.processProject(ctx, time, org.Name, project, projectCfg); err != nil {
130+
s.log.Error("error processing project", zap.String("projectID", project.ID), zap.Error(err))
131+
}
132+
}
133+
return nil
134+
}
111135

112-
if err := s.extractProcessDiskMetrics(ctx, time, org.Name, project, process, clusterName, providerValues); err != nil {
113-
return fmt.Errorf("error when polling process disk metrics from MongoDB Atlas for process %s: %w", process.ID, err)
114-
}
115-
}
136+
func (s *mongodbatlasreceiver) processProject(ctx context.Context, time timeconstraints, orgName string, project *mongodbatlas.Project, projectCfg *ProjectConfig) error {
137+
nodeClusterMap, providerMap, err := s.getNodeClusterNameMap(ctx, project.ID)
138+
if err != nil {
139+
return fmt.Errorf("error collecting clusters from project %s: %w", project.ID, err)
140+
}
141+
142+
processes, err := s.client.Processes(ctx, project.ID)
143+
if err != nil {
144+
return fmt.Errorf("error retrieving MongoDB Atlas processes for project %s: %w", project.ID, err)
145+
}
146+
147+
for _, process := range processes {
148+
clusterName := nodeClusterMap[process.UserAlias]
149+
providerValues := providerMap[clusterName]
150+
151+
if !shouldProcessCluster(projectCfg, clusterName) {
152+
// Skip processing for this cluster
153+
continue
154+
}
155+
156+
if err := s.extractProcessMetrics(ctx, time, orgName, project, process, clusterName, providerValues); err != nil {
157+
return fmt.Errorf("error when polling process metrics from MongoDB Atlas for process %s: %w", process.ID, err)
158+
}
159+
160+
if err := s.extractProcessDatabaseMetrics(ctx, time, orgName, project, process, clusterName, providerValues); err != nil {
161+
return fmt.Errorf("error when polling process database metrics from MongoDB Atlas for process %s: %w", process.ID, err)
162+
}
163+
164+
if err := s.extractProcessDiskMetrics(ctx, time, orgName, project, process, clusterName, providerValues); err != nil {
165+
return fmt.Errorf("error when polling process disk metrics from MongoDB Atlas for process %s: %w", process.ID, err)
116166
}
117167
}
168+
118169
return nil
119170
}
120171

172+
// shouldProcessCluster checks whether a given cluster should be processed based on the project configuration.
173+
func shouldProcessCluster(projectCfg *ProjectConfig, clusterName string) bool {
174+
if projectCfg == nil {
175+
// If there is no project config, process all clusters.
176+
return true
177+
}
178+
179+
_, isIncluded := projectCfg.includesByClusterName[clusterName]
180+
_, isExcluded := projectCfg.excludesByClusterName[clusterName]
181+
182+
// Return false immediately if the cluster is excluded.
183+
if isExcluded {
184+
return false
185+
}
186+
187+
// If IncludeClusters is empty, or the cluster is explicitly included, return true.
188+
return len(projectCfg.IncludeClusters) == 0 || isIncluded
189+
}
190+
121191
type providerValues struct {
122192
RegionName string
123193
ProviderName string

receiver/mongodbatlasreceiver/receiver_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,69 @@ func TestTimeConstraints(t *testing.T) {
7171
t.Run(testCase.name, testCase.run)
7272
}
7373
}
74+
75+
func TestShouldProcessCluster(t *testing.T) {
76+
tests := []struct {
77+
name string
78+
projectCfg *ProjectConfig
79+
clusterName string
80+
want bool
81+
}{
82+
{
83+
name: "included cluster should be processed",
84+
projectCfg: &ProjectConfig{
85+
IncludeClusters: []string{"Cluster1"},
86+
},
87+
clusterName: "Cluster1",
88+
want: true,
89+
},
90+
{
91+
name: "cluster not included should not be processed",
92+
projectCfg: &ProjectConfig{
93+
IncludeClusters: []string{"Cluster1"},
94+
},
95+
clusterName: "Cluster2",
96+
want: false,
97+
},
98+
{
99+
name: "excluded cluster should not be processed",
100+
projectCfg: &ProjectConfig{
101+
ExcludeClusters: []string{"Cluster2"},
102+
},
103+
clusterName: "Cluster2",
104+
want: false,
105+
},
106+
{
107+
name: "cluster not excluded should processed assuming it exists in the project",
108+
projectCfg: &ProjectConfig{
109+
ExcludeClusters: []string{"Cluster1"},
110+
},
111+
clusterName: "Cluster2",
112+
want: true,
113+
},
114+
{
115+
name: "cluster should be processed when no includes or excludes are set",
116+
projectCfg: &ProjectConfig{},
117+
clusterName: "Cluster1",
118+
want: true,
119+
},
120+
{
121+
name: "cluster should be processed when no includes or excludes are set and cluster name is empty",
122+
projectCfg: nil,
123+
clusterName: "Cluster1",
124+
want: true,
125+
},
126+
}
127+
128+
for _, tt := range tests {
129+
t.Run(tt.name, func(t *testing.T) {
130+
if tt.projectCfg != nil {
131+
tt.projectCfg.populateIncludesAndExcludes()
132+
}
133+
134+
if got := shouldProcessCluster(tt.projectCfg, tt.clusterName); got != tt.want {
135+
t.Errorf("shouldProcessCluster() = %v, want %v", got, tt.want)
136+
}
137+
})
138+
}
139+
}

0 commit comments

Comments
 (0)