Skip to content

Commit 3e5c046

Browse files
[processor/geoip] Add GeoIP providers configuration and maxmind factory (#33268)
**Description:** Define and parse a configuration for the geo IP providers section. In addition, the Maxmind GeoIPProvider implementation is included in the providers factories. Example configuration: ```yaml processors: geoip: providers: maxmind: database: "/tmp" ``` Implementation details: - Custom Unmarshal implementation for the processor's component, this is needed to dynamically load each provider's configuration: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/33268/files#diff-aed2c6fd774ef54a3039647190c67e28bd0fc67e008fdd5630b6201c550bd00aR46 . The approach is very similar to how the [hostmetrics receiver loads](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/hostmetricsreceiver/config.go#L44) its scraper configuration. - A new factory for the providers is included in order to retrieve their default configuration and get the actual provider: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/33268/files#diff-2fbb171efac07bbf07c1bcb67ae981eb481e56491add51b6a137fd2c17eec9dcR27 **Link to tracking Issue:** #33269 **Testing:** - Unit tests for the configuration unmarshall + factories - A base mock structure is used through all the test files: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/33268/files#diff-28f4a173f1f4b5ccd3cf4c9f7f7b6bf864ef1567a28291322d7e94a9f63243aeR26 - Integration test to verify the behaviour of the processor + maxmind provider - The generation of the Maxmind databases has been moved to a [public internal package](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/33268/files#diff-83fc0ce7aa1f0495b4f4e5d5aabc2918162fec31ad323cc417b3f8c8eb5a00bcR14) as being used for the unit and integration tests. Should we upload the assembled database files instead? **Documentation:** TODO --------- Co-authored-by: Andrzej Stencel <[email protected]>
1 parent 85f9421 commit 3e5c046

19 files changed

+861
-56
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: geoipprocessor
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add providers configuration and maxmind provider factory
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: [33269]
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: []

processor/geoipprocessor/README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,42 @@
1111
[development]: https://github.com/open-telemetry/opentelemetry-collector#development
1212
<!-- end autogenerated section -->
1313

14-
**This processor is currently under development and is presently a NOP (No Operation) processor. Further features and functionalities will be added in upcoming versions.**
15-
1614
## Description
1715

1816
The geoIP processor `geoipprocessor` enhances resource attributes by appending information about the geographical location of an IP address. To add geographical information, the IP address must be included in the resource attributes using the [`source.address` semantic conventions key attribute](https://github.com/open-telemetry/semantic-conventions/blob/v1.26.0/docs/general/attributes.md#source).
17+
18+
### Geographical location metadata
19+
20+
The following [resource attributes](./internal/convention/attributes.go) will be added if the corresponding information is found:
21+
22+
```
23+
* geo.city_name
24+
* geo.postal_code
25+
* geo.country_name
26+
* geo.country_iso_code
27+
* geo.continent_name
28+
* geo.continent_code
29+
* geo.region_name
30+
* geo.region_iso_code
31+
* geo.timezone
32+
* geo.location.lat
33+
* geo.location.lon
34+
```
35+
36+
## Configuration
37+
38+
The following settings must be configured:
39+
40+
- `providers`: A map containing geographical location information providers. These providers are used to search for the geographical location attributes associated with an IP. Supported providers:
41+
- [maxmind](./internal/provider/maxmindprovider/README.md)
42+
43+
## Examples
44+
45+
```yaml
46+
processors:
47+
# processor name: geoip
48+
geoip:
49+
providers:
50+
maxmind:
51+
database_path: /tmp/mygeodb
52+
```

processor/geoipprocessor/config.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,85 @@
33

44
package geoipprocessor // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/geoipprocessor"
55

6+
import (
7+
"errors"
8+
"fmt"
9+
10+
"go.opentelemetry.io/collector/component"
11+
"go.opentelemetry.io/collector/confmap"
12+
13+
"github.com/open-telemetry/opentelemetry-collector-contrib/processor/geoipprocessor/internal/provider"
14+
)
15+
16+
const (
17+
providersKey = "providers"
18+
)
19+
620
// Config holds the configuration for the GeoIP processor.
7-
type Config struct{}
21+
type Config struct {
22+
// Providers specifies the sources to extract geographical information about a given IP.
23+
Providers map[string]provider.Config `mapstructure:"-"`
24+
}
25+
26+
var (
27+
_ component.Config = (*Config)(nil)
28+
_ confmap.Unmarshaler = (*Config)(nil)
29+
)
830

931
func (cfg *Config) Validate() error {
32+
if len(cfg.Providers) == 0 {
33+
return errors.New("must specify at least one geo IP data provider when using the geoip processor")
34+
}
35+
36+
// validate all provider's configuration
37+
for providerID, providerConfig := range cfg.Providers {
38+
if err := providerConfig.Validate(); err != nil {
39+
return fmt.Errorf("error validating provider %s: %w", providerID, err)
40+
}
41+
}
42+
return nil
43+
}
44+
45+
// Unmarshal a config.Parser into the config struct.
46+
func (cfg *Config) Unmarshal(componentParser *confmap.Conf) error {
47+
if componentParser == nil {
48+
return nil
49+
}
50+
51+
// load the non-dynamic config normally
52+
err := componentParser.Unmarshal(cfg, confmap.WithIgnoreUnused())
53+
if err != nil {
54+
return err
55+
}
56+
57+
// dynamically load the individual providers configs based on the key name
58+
cfg.Providers = map[string]provider.Config{}
59+
60+
// retrieve `providers` configuration section
61+
providersSection, err := componentParser.Sub(providersKey)
62+
if err != nil {
63+
return err
64+
}
65+
66+
// loop through all defined providers and load their configuration
67+
for key := range providersSection.ToStringMap() {
68+
factory, ok := getProviderFactory(key)
69+
if !ok {
70+
return fmt.Errorf("invalid provider key: %s", key)
71+
}
72+
73+
providerCfg := factory.CreateDefaultConfig()
74+
providerSection, err := providersSection.Sub(key)
75+
if err != nil {
76+
return err
77+
}
78+
err = providerSection.Unmarshal(providerCfg)
79+
if err != nil {
80+
return fmt.Errorf("error reading settings for provider type %q: %w", key, err)
81+
}
82+
83+
cfg.Providers[key] = providerCfg
84+
}
85+
1086
return nil
1187
}

processor/geoipprocessor/config_test.go

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,45 @@
44
package geoipprocessor
55

66
import (
7+
"errors"
78
"path/filepath"
89
"testing"
910

1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
1213
"go.opentelemetry.io/collector/component"
1314
"go.opentelemetry.io/collector/confmap/confmaptest"
15+
"go.opentelemetry.io/collector/otelcol/otelcoltest"
1416

1517
"github.com/open-telemetry/opentelemetry-collector-contrib/processor/geoipprocessor/internal/metadata"
18+
"github.com/open-telemetry/opentelemetry-collector-contrib/processor/geoipprocessor/internal/provider"
19+
maxmind "github.com/open-telemetry/opentelemetry-collector-contrib/processor/geoipprocessor/internal/provider/maxmindprovider"
1620
)
1721

1822
func TestLoadConfig(t *testing.T) {
1923
t.Parallel()
2024

2125
tests := []struct {
22-
id component.ID
23-
expected component.Config
24-
errorMessage string
26+
id component.ID
27+
expected component.Config
28+
validateErrorMessage string
29+
unmarshalErrorMessage string
2530
}{
2631
{
27-
id: component.NewID(metadata.Type),
28-
expected: &Config{},
32+
id: component.NewID(metadata.Type),
33+
validateErrorMessage: "must specify at least one geo IP data provider when using the geoip processor",
34+
},
35+
{
36+
id: component.NewIDWithName(metadata.Type, "maxmind"),
37+
expected: &Config{
38+
Providers: map[string]provider.Config{
39+
"maxmind": &maxmind.Config{DatabasePath: "/tmp/db"},
40+
},
41+
},
42+
},
43+
{
44+
id: component.NewIDWithName(metadata.Type, "invalid_providers_config"),
45+
unmarshalErrorMessage: "unexpected sub-config value kind for key:providers value:this should be a map kind:string)",
2946
},
3047
}
3148

@@ -39,10 +56,15 @@ func TestLoadConfig(t *testing.T) {
3956

4057
sub, err := cm.Sub(tt.id.String())
4158
require.NoError(t, err)
59+
60+
if tt.unmarshalErrorMessage != "" {
61+
assert.EqualError(t, sub.Unmarshal(cfg), tt.unmarshalErrorMessage)
62+
return
63+
}
4264
require.NoError(t, sub.Unmarshal(cfg))
4365

44-
if tt.errorMessage != "" {
45-
assert.EqualError(t, component.ValidateConfig(cfg), tt.errorMessage)
66+
if tt.validateErrorMessage != "" {
67+
assert.EqualError(t, component.ValidateConfig(cfg), tt.validateErrorMessage)
4668
return
4769
}
4870

@@ -51,3 +73,70 @@ func TestLoadConfig(t *testing.T) {
5173
})
5274
}
5375
}
76+
77+
func TestLoadConfig_InvalidProviderKey(t *testing.T) {
78+
factories, err := otelcoltest.NopFactories()
79+
require.NoError(t, err)
80+
81+
factory := NewFactory()
82+
factories.Processors[metadata.Type] = factory
83+
_, err = otelcoltest.LoadConfigAndValidate(filepath.Join("testdata", "config-invalidProviderKey.yaml"), factories)
84+
85+
require.Contains(t, err.Error(), "error reading configuration for \"geoip\": invalid provider key: invalidProviderKey")
86+
}
87+
88+
func TestLoadConfig_ValidProviderKey(t *testing.T) {
89+
type dbMockConfig struct {
90+
Database string `mapstructure:"database"`
91+
providerConfigMock
92+
}
93+
baseMockFactory.CreateDefaultConfigF = func() provider.Config {
94+
return &dbMockConfig{providerConfigMock: providerConfigMock{func() error { return nil }}}
95+
}
96+
providerFactories["mock"] = &baseMockFactory
97+
98+
factories, err := otelcoltest.NopFactories()
99+
require.NoError(t, err)
100+
101+
factory := NewFactory()
102+
factories.Processors[metadata.Type] = factory
103+
collectorConfig, err := otelcoltest.LoadConfigAndValidate(filepath.Join("testdata", "config-mockProvider.yaml"), factories)
104+
105+
require.NoError(t, err)
106+
actualDbMockConfig := collectorConfig.Processors[component.NewID(metadata.Type)].(*Config).Providers["mock"].(*dbMockConfig)
107+
require.Equal(t, "/tmp/geodata.csv", actualDbMockConfig.Database)
108+
109+
// assert provider unmarshall configuration error by removing the database fieldfrom the configuration struct
110+
baseMockFactory.CreateDefaultConfigF = func() provider.Config {
111+
return &providerConfigMock{func() error { return nil }}
112+
}
113+
providerFactories["mock"] = &baseMockFactory
114+
115+
factories.Processors[metadata.Type] = factory
116+
_, err = otelcoltest.LoadConfigAndValidate(filepath.Join("testdata", "config-mockProvider.yaml"), factories)
117+
118+
require.ErrorContains(t, err, "has invalid keys: database")
119+
}
120+
121+
func TestLoadConfig_ProviderValidateError(t *testing.T) {
122+
baseMockFactory.CreateDefaultConfigF = func() provider.Config {
123+
sampleConfig := struct {
124+
Database string `mapstructure:"database"`
125+
providerConfigMock
126+
}{
127+
"",
128+
providerConfigMock{func() error { return errors.New("error validating mocked config") }},
129+
}
130+
return &sampleConfig
131+
}
132+
providerFactories["mock"] = &baseMockFactory
133+
134+
factories, err := otelcoltest.NopFactories()
135+
require.NoError(t, err)
136+
137+
factory := NewFactory()
138+
factories.Processors[metadata.Type] = factory
139+
_, err = otelcoltest.LoadConfigAndValidate(filepath.Join("testdata", "config-mockProvider.yaml"), factories)
140+
141+
require.Contains(t, err.Error(), "error validating provider mock")
142+
}

0 commit comments

Comments
 (0)