Skip to content

Commit

Permalink
Cortex Exporter Setup Pipeline and Configuration (#205)
Browse files Browse the repository at this point in the history
* Add Config struct definition, test files, and testing data

* Add tests for Validate method

* Implement Validate method

* Add Config utility files and testing data

* Add helper function to create YAML files for testing

* Add NewConfig function, Options interface, and tests

* Add Viper tags to Config struct and two With functions

* Add test for WithFilepath

* Add WithClient and test for WithClient

* Remove default for http client and adjust tests

* Add check for conflicting authentication types and update tests

* Update README.md

* Run make precommit

* Add example Config struct

* Add NewRawExporter for creating an Exporter and TestNewRawExporter

* Add NewExportPipeline for creating a controller and TestNewExportPipeline

* Add InstallNewPipeline and TestInstallNewPipeline

* Update README and run make precommit
  • Loading branch information
ercl committed Aug 12, 2020
1 parent df49040 commit 020b7b5
Show file tree
Hide file tree
Showing 13 changed files with 1,310 additions and 1 deletion.
108 changes: 108 additions & 0 deletions exporters/metric/cortex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,111 @@

Work in progress exporter to send data from the OpenTelemetry Go SDK to Cortex using the Prometheus
Remote Write API.

## Setting up an Exporter

Users can setup the Exporter with the `InstallNewPipeline` function. It requires a
`Config` struct and returns a push Controller that will periodically collect and push
data.

Example:
```go
pusher, err := cortex.InstallNewPipeline(config)
if err != nil {
return err
}

// Make instruments and record data
```

## Configuration

The Exporter needs certain information, such as the endpoint URL and push interval
duration, to function properly. This information is stored in a `Config` struct, which is
passed into the Exporter during the setup pipeline.

### Creating the Config struct

Users can either create the struct manually or use a `utils` submodule in the package to
read settings from a YAML file into a new Config struct using `Viper`. Here are the
supported YAML properties as well as the Config struct that they map to.

```yaml
# The URL of the endpoint to send samples to.
url: <string>

# Timeout for requests to the remote write endpoint.
[ remote_timeout: <duration> | default = 30s ]

# Name of the remote write config, which if specified must be unique among remote write configs. The name will be used in metrics and logging in place of a generated value to help users distinguish between remote write configs.
[ name: <string>]

# Sets the `Authorization` header on every remote write request with the
# configured username and password.
# password and password_file are mutually exclusive.
basic_auth:
[ username: <string>]
[ password: <string>]
[ password_file: <string> ]

# Sets the `Authorization` header on every remote write request with
# the configured bearer token. It is mutually exclusive with `bearer_token_file`.
[ bearer_token: <string> ]

# Sets the `Authorization` header on every remote write request with the bearer token
# read from the configured file. It is mutually exclusive with `bearer_token`.
[ bearer_token_file: /path/to/bearer/token/file ]

# Configures the remote write request's TLS settings.
tls_config:
# CA certificate to validate API server certificate with.
[ ca_file: <filename>]

# Certificate and key files for client cert authentication to the server.
[ cert_file: <filename> ]
[ key_file: <filename> ]

# ServerName extension to indicate the name of the server.
# https://tools.ietf.org/html/rfc4366#section-3.1
[ server_name: <string> ]

# Disable validation of the server certificate.
[ insecure_skip_verify: <boolean> ]

# Optional proxy URL.
[ proxy_url: <string>]
```

```go
type Config struct {
Endpoint string `mapstructure:"url"`
RemoteTimeout time.Duration `mapstructure:"remote_timeout"`
Name string `mapstructure:"name"`
BasicAuth map[string]string `mapstructure:"basic_auth"`
BearerToken string `mapstructure:"bearer_token"`
BearerTokenFile string `mapstructure:"bearer_token_file"`
TLSConfig map[string]string `mapstructure:"tls_config"`
ProxyURL string `mapstructure:"proxy_url"`
PushInterval time.Duration `mapstructure:"push_interval"`
Headers map[string]string `mapstructure:"headers"`
Client *http.Client
}
```

The struct is used during the setup pipeline:

```go
// Create Config struct using utils module.
config, err := utils.NewConfig("config.yml")
if err != nil {
return err
}

// Setup the exporter.
pusher, err := cortex.InstallNewPipeline(config)
if err != nil {
return err
}

// Add instruments and start collecting data.
```
81 changes: 81 additions & 0 deletions exporters/metric/cortex/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cortex

import (
"fmt"
"net/http"
"time"
)

var (
// ErrTwoPasswords occurs when the YAML file contains both `password` and
// `password_file`.
ErrTwoPasswords = fmt.Errorf("Cannot have two passwords in the YAML file")

// ErrTwoBearerTokens occurs when the YAML file contains both `bearer_token` and
// `bearer_token_file`.
ErrTwoBearerTokens = fmt.Errorf("Cannot have two bearer tokens in the YAML file")

// ErrConflictingAuthorization occurs when the YAML file contains both BasicAuth and
// bearer token authorization
ErrConflictingAuthorization = fmt.Errorf("Cannot have both basic auth and bearer token authorization")
)

// Config contains properties the Exporter uses to export metrics data to Cortex.
type Config struct {
Endpoint string `mapstructure:"url"`
RemoteTimeout time.Duration `mapstructure:"remote_timeout"`
Name string `mapstructure:"name"`
BasicAuth map[string]string `mapstructure:"basic_auth"`
BearerToken string `mapstructure:"bearer_token"`
BearerTokenFile string `mapstructure:"bearer_token_file"`
TLSConfig map[string]string `mapstructure:"tls_config"`
ProxyURL string `mapstructure:"proxy_url"`
PushInterval time.Duration `mapstructure:"push_interval"`
Headers map[string]string `mapstructure:"headers"`
Client *http.Client
}

// Validate checks a Config struct for missing required properties and property conflicts.
// Additionally, it adds default values to missing properties when there is a default.
func (c *Config) Validate() error {
// Check for mutually exclusive properties.
if c.BasicAuth != nil {
if c.BearerToken != "" || c.BearerTokenFile != "" {
return ErrConflictingAuthorization
}
if c.BasicAuth["password"] != "" && c.BasicAuth["password_file"] != "" {
return ErrTwoPasswords
}
}
if c.BearerToken != "" && c.BearerTokenFile != "" {
return ErrTwoBearerTokens
}

// Add default values for missing properties.
if c.Endpoint == "" {
c.Endpoint = "/api/prom/push"
}
if c.RemoteTimeout == 0 {
c.RemoteTimeout = 30 * time.Second
}
// Default time interval between pushes for the push controller is 10s.
if c.PushInterval == 0 {
c.PushInterval = 10 * time.Second
}

return nil
}
112 changes: 112 additions & 0 deletions exporters/metric/cortex/config_data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cortex_test

import (
"time"

"go.opentelemetry.io/contrib/exporters/metric/cortex"
)

// Config struct with default values. This is used to verify the output of Validate().
var validatedStandardConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
RemoteTimeout: 30 * time.Second,
PushInterval: 10 * time.Second,
}

// Config struct with default values other than the remote timeout. This is used to verify
// the output of Validate().
var validatedCustomTimeoutConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
RemoteTimeout: 10 * time.Second,
PushInterval: 10 * time.Second,
}

// Example Config struct with a custom remote timeout.
var exampleRemoteTimeoutConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
PushInterval: 10 * time.Second,
RemoteTimeout: 10 * time.Second,
}

// Example Config struct without a remote timeout.
var exampleNoRemoteTimeoutConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
PushInterval: 10 * time.Second,
}

// Example Config struct without a push interval.
var exampleNoPushIntervalConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
RemoteTimeout: 30 * time.Second,
}

// Example Config struct without a http client.
var exampleNoClientConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
RemoteTimeout: 30 * time.Second,
PushInterval: 10 * time.Second,
}

// Example Config struct without an endpoint.
var exampleNoEndpointConfig = cortex.Config{
Name: "Config",
RemoteTimeout: 30 * time.Second,
PushInterval: 10 * time.Second,
}

// Example Config struct with two bearer tokens.
var exampleTwoBearerTokenConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
RemoteTimeout: 30 * time.Second,
PushInterval: 10 * time.Second,
BearerToken: "bearer_token",
BearerTokenFile: "bearer_token_file",
}

// Example Config struct with two passwords.
var exampleTwoPasswordConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
RemoteTimeout: 30 * time.Second,
PushInterval: 10 * time.Second,
BasicAuth: map[string]string{
"username": "user",
"password": "password",
"password_file": "passwordFile",
},
}

// Example Config struct with both basic auth and bearer token authentication.
var exampleTwoAuthConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
RemoteTimeout: 30 * time.Second,
PushInterval: 10 * time.Second,
BasicAuth: map[string]string{
"username": "user",
"password": "password",
"password_file": "passwordFile",
},
BearerToken: "bearer_token",
}
92 changes: 92 additions & 0 deletions exporters/metric/cortex/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cortex_test

import (
"testing"

"github.com/stretchr/testify/require"

"go.opentelemetry.io/contrib/exporters/metric/cortex"
)

// TestValidate checks whether Validate() returns the correct error and sets the correct
// default values.
func TestValidate(t *testing.T) {
tests := []struct {
testName string
config *cortex.Config
expectedConfig *cortex.Config
expectedError error
}{
{
testName: "Config with Conflicting Bearer Tokens",
config: &exampleTwoBearerTokenConfig,
expectedConfig: nil,
expectedError: cortex.ErrTwoBearerTokens,
},
{
testName: "Config with Conflicting Passwords",
config: &exampleTwoPasswordConfig,
expectedConfig: nil,
expectedError: cortex.ErrTwoPasswords,
},
{
testName: "Config with Custom Timeout",
config: &exampleRemoteTimeoutConfig,
expectedConfig: &validatedCustomTimeoutConfig,
expectedError: nil,
},
{
testName: "Config with no Endpoint",
config: &exampleNoEndpointConfig,
expectedConfig: &validatedStandardConfig,
expectedError: nil,
},
{
testName: "Config with no Remote Timeout",
config: &exampleNoRemoteTimeoutConfig,
expectedConfig: &validatedStandardConfig,
expectedError: nil,
},
{
testName: "Config with no Push Interval",
config: &exampleNoPushIntervalConfig,
expectedConfig: &validatedStandardConfig,
expectedError: nil,
},
{
testName: "Config with no Client",
config: &exampleNoClientConfig,
expectedConfig: &validatedStandardConfig,
expectedError: nil,
},
{
testName: "Config with both BasicAuth and BearerTokens",
config: &exampleTwoAuthConfig,
expectedConfig: nil,
expectedError: cortex.ErrConflictingAuthorization,
},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
err := test.config.Validate()
require.Equal(t, err, test.expectedError)
if err == nil {
require.Equal(t, test.config, test.expectedConfig)
}
})
}
}
Loading

0 comments on commit 020b7b5

Please sign in to comment.