Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[processor/geoip] Add GeoIP providers configuration and maxmind factory #33268

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dd89cf6
feat: add geoipprovider internal package
rogercoll May 28, 2024
18c1d24
add providers to configuration
rogercoll May 28, 2024
ca50fa1
feat: add unmarshal implementation for geoip providers
rogercoll May 29, 2024
49c277b
add provider's mock and configuration load test case
rogercoll May 29, 2024
4450aa8
feat: add list of providers to the geoip processor
rogercoll May 29, 2024
a7381ae
chore: add changelog entry
rogercoll May 30, 2024
1d8dad2
fix format and linter
rogercoll May 30, 2024
48f3d8d
Merge branch 'main' into geoipprocessor_provider
rogercoll May 30, 2024
686b9e7
fix .chloggen/geoipprocessor_provider.yaml
rogercoll May 31, 2024
cb2b2dc
Merge branch 'main' into geoipprocessor_provider
rogercoll Jun 27, 2024
9bbfd46
chore: replace CreateSettings for Settings
rogercoll Jun 27, 2024
81137cc
feat: validate all providers configuration
rogercoll Jun 27, 2024
87b0161
feat: add maxmind provider factory
rogercoll Jun 28, 2024
31ba472
add configuration unmarshal test cases
rogercoll Jun 28, 2024
0cec847
chore: tidy mod files
rogercoll Jun 28, 2024
be9e2df
chore: update changelog to reflect maxmind
rogercoll Jun 28, 2024
74bde08
chore: fix imports order
rogercoll Jun 28, 2024
05d0be9
docs: add configuration section
rogercoll Jun 28, 2024
f20bfc4
chore: add comments to factory methods
rogercoll Jun 28, 2024
f7f9902
docs: reference added attributes
rogercoll Jun 28, 2024
21e80e8
Merge branch 'main' into geoipprocessor_provider
rogercoll Jun 28, 2024
f8c2991
chore: remove additional require mod section
rogercoll Jun 28, 2024
bda01e5
rename providers config key variable
rogercoll Jun 28, 2024
cd5293d
Update processor/geoipprocessor/README.md
rogercoll Jul 2, 2024
e196b02
Merge branch 'main' into geoipprocessor_provider
rogercoll Jul 2, 2024
bb6ba71
chore: remove deprecated test helpers
rogercoll Jul 2, 2024
0bec9c9
Merge branch 'main' into geoipprocessor_provider
rogercoll Jul 2, 2024
e392379
chore: tidy indirect deps
rogercoll Jul 2, 2024
e5fb50a
Merge branch 'main' into geoipprocessor_provider
rogercoll Jul 2, 2024
eec083e
Merge branch 'main' into geoipprocessor_provider
rogercoll Jul 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .chloggen/geoipprocessor_provider.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: 'enhancement'

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: geoipprocessor

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add providers configuration and maxmind provider factory

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [33269]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
38 changes: 36 additions & 2 deletions processor/geoipprocessor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,42 @@
[development]: https://github.com/open-telemetry/opentelemetry-collector#development
<!-- end autogenerated section -->

**This processor is currently under development and is presently a NOP (No Operation) processor. Further features and functionalities will be added in upcoming versions.**

## Description

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).

### Geographical location metadata

The following [resource attributes](./internal/convention/attributes.go) will be added if the corresponding information is found:

```
* geo.city_name
* geo.postal_code
* geo.country_name
* geo.country_iso_code
* geo.continent_name
* geo.continent_code
* geo.region_name
* geo.region_iso_code
* geo.timezone
* geo.location.lat
* geo.location.lon
```

## Configuration

The following settings must be configured:

- `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:
- [maxmind](./internal/provider/maxmindprovider/README.md)

#### Examples
rogercoll marked this conversation as resolved.
Show resolved Hide resolved

```yaml
processors:
# processor name: geoip
geoip:
providers:
maxmind:
database_path: /tmp/mygeodb
```
78 changes: 77 additions & 1 deletion processor/geoipprocessor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,85 @@

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

import (
"errors"
"fmt"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/confmap"

"github.com/open-telemetry/opentelemetry-collector-contrib/processor/geoipprocessor/internal/provider"
)

const (
providersKey = "providers"
)

// Config holds the configuration for the GeoIP processor.
type Config struct{}
type Config struct {
// Providers specifies the sources to extract geographical information about a given IP.
Providers map[string]provider.Config `mapstructure:"-"`
}

var (
_ component.Config = (*Config)(nil)
_ confmap.Unmarshaler = (*Config)(nil)
)

func (cfg *Config) Validate() error {
if len(cfg.Providers) == 0 {
return errors.New("must specify at least one geo IP data provider when using the geoip processor")
}

// validate all provider's configuration
for providerID, providerConfig := range cfg.Providers {
if err := providerConfig.Validate(); err != nil {
return fmt.Errorf("error validating provider %s: %w", providerID, err)
}
}
return nil
}

// Unmarshal a config.Parser into the config struct.
func (cfg *Config) Unmarshal(componentParser *confmap.Conf) error {
if componentParser == nil {
return nil
}

// load the non-dynamic config normally
err := componentParser.Unmarshal(cfg, confmap.WithIgnoreUnused())
if err != nil {
return err
}

// dynamically load the individual providers configs based on the key name
cfg.Providers = map[string]provider.Config{}

// retrieve `providers` configuration section
providersSection, err := componentParser.Sub(providersKey)
if err != nil {
return err
}

// loop through all defined providers and load their configuration
for key := range providersSection.ToStringMap() {
factory, ok := getProviderFactory(key)
if !ok {
return fmt.Errorf("invalid provider key: %s", key)
}

providerCfg := factory.CreateDefaultConfig()
providerSection, err := providersSection.Sub(key)
if err != nil {
return err
}
err = providerSection.Unmarshal(providerCfg)
if err != nil {
return fmt.Errorf("error reading settings for provider type %q: %w", key, err)
}

cfg.Providers[key] = providerCfg
}

return nil
}
138 changes: 131 additions & 7 deletions processor/geoipprocessor/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,48 @@
package geoipprocessor

import (
"errors"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/confmaptest"
"go.opentelemetry.io/collector/confmap/provider/fileprovider"
"go.opentelemetry.io/collector/otelcol"
"go.opentelemetry.io/collector/otelcol/otelcoltest"

"github.com/open-telemetry/opentelemetry-collector-contrib/processor/geoipprocessor/internal/metadata"
"github.com/open-telemetry/opentelemetry-collector-contrib/processor/geoipprocessor/internal/provider"
maxmind "github.com/open-telemetry/opentelemetry-collector-contrib/processor/geoipprocessor/internal/provider/maxmindprovider"
)

func TestLoadConfig(t *testing.T) {
t.Parallel()

tests := []struct {
id component.ID
expected component.Config
errorMessage string
id component.ID
expected component.Config
validateErrorMessage string
unmarshalErrorMessage string
}{
{
id: component.NewID(metadata.Type),
expected: &Config{},
id: component.NewID(metadata.Type),
validateErrorMessage: "must specify at least one geo IP data provider when using the geoip processor",
},
{
id: component.NewIDWithName(metadata.Type, "maxmind"),
expected: &Config{
Providers: map[string]provider.Config{
"maxmind": &maxmind.Config{DatabasePath: "/tmp/db"},
},
},
},
{
id: component.NewIDWithName(metadata.Type, "invalid_providers_config"),
unmarshalErrorMessage: "unexpected sub-config value kind for key:providers value:this should be a map kind:string)",
},
}

Expand All @@ -39,10 +59,15 @@ func TestLoadConfig(t *testing.T) {

sub, err := cm.Sub(tt.id.String())
require.NoError(t, err)

if tt.unmarshalErrorMessage != "" {
assert.EqualError(t, sub.Unmarshal(cfg), tt.unmarshalErrorMessage)
return
}
require.NoError(t, sub.Unmarshal(cfg))

if tt.errorMessage != "" {
assert.EqualError(t, component.ValidateConfig(cfg), tt.errorMessage)
if tt.validateErrorMessage != "" {
assert.EqualError(t, component.ValidateConfig(cfg), tt.validateErrorMessage)
return
}

Expand All @@ -51,3 +76,102 @@ func TestLoadConfig(t *testing.T) {
})
}
}

func TestLoadConfig_InvalidProviderKey(t *testing.T) {
factories, err := otelcoltest.NopFactories()
require.NoError(t, err)

factory := NewFactory()
factories.Processors[metadata.Type] = factory
_, err = otelcoltest.LoadConfigAndValidateWithSettings(factories, otelcol.ConfigProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: []string{filepath.Join("testdata", "config-invalidProviderKey.yaml")},
ProviderFactories: []confmap.ProviderFactory{
fileprovider.NewFactory(),
},
},
},
)

require.Contains(t, err.Error(), "error reading configuration for \"geoip\": invalid provider key: invalidProviderKey")
}

func TestLoadConfig_ValidProviderKey(t *testing.T) {
type dbMockConfig struct {
Database string `mapstructure:"database"`
providerConfigMock
}
baseMockFactory.CreateDefaultConfigF = func() provider.Config {
return &dbMockConfig{providerConfigMock: providerConfigMock{func() error { return nil }}}
}
providerFactories["mock"] = &baseMockFactory

factories, err := otelcoltest.NopFactories()
require.NoError(t, err)

factory := NewFactory()
factories.Processors[metadata.Type] = factory
collectorConfig, err := otelcoltest.LoadConfigAndValidateWithSettings(factories, otelcol.ConfigProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: []string{filepath.Join("testdata", "config-mockProvider.yaml")},
ProviderFactories: []confmap.ProviderFactory{
fileprovider.NewFactory(),
},
},
},
)

require.NoError(t, err)
actualDbMockConfig := collectorConfig.Processors[component.NewID(metadata.Type)].(*Config).Providers["mock"].(*dbMockConfig)
require.Equal(t, "/tmp/geodata.csv", actualDbMockConfig.Database)

// assert provider unmarshall configuration error by removing the database fieldfrom the configuration struct
baseMockFactory.CreateDefaultConfigF = func() provider.Config {
return &providerConfigMock{func() error { return nil }}
}
providerFactories["mock"] = &baseMockFactory

factories.Processors[metadata.Type] = factory
_, err = otelcoltest.LoadConfigAndValidateWithSettings(factories, otelcol.ConfigProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: []string{filepath.Join("testdata", "config-mockProvider.yaml")},
ProviderFactories: []confmap.ProviderFactory{
fileprovider.NewFactory(),
},
},
},
)

require.ErrorContains(t, err, "has invalid keys: database")
}

func TestLoadConfig_ProviderValidateError(t *testing.T) {
baseMockFactory.CreateDefaultConfigF = func() provider.Config {
sampleConfig := struct {
Database string `mapstructure:"database"`
providerConfigMock
}{
"",
providerConfigMock{func() error { return errors.New("error validating mocked config") }},
}
return &sampleConfig
}
providerFactories["mock"] = &baseMockFactory

factories, err := otelcoltest.NopFactories()
require.NoError(t, err)

factory := NewFactory()
factories.Processors[metadata.Type] = factory
_, err = otelcoltest.LoadConfigAndValidateWithSettings(factories, otelcol.ConfigProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: []string{filepath.Join("testdata", "config-mockProvider.yaml")},
ProviderFactories: []confmap.ProviderFactory{
fileprovider.NewFactory(),
},
},
},
)

require.Contains(t, err.Error(), "error validating provider mock")
}
Loading