Skip to content

Cortex Exporter Setup Pipeline and Configuration #205

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

Merged
merged 19 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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