Skip to content

Commit

Permalink
Removes query_config_file from mssql config params
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanKurek committed Nov 21, 2023
1 parent 54a7d45 commit 4dc69ee
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 187 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ v0.38.0 (2023-11-21)

- Allow agent to start with `module.git` config if cached before. (@hainenber)

- Adds new optional config parameters `query_config_file` and `query_config` to `mssql` integration to allow for custom metrics (@StefanKurek)
- Adds new optional config parameter `query_config` to `mssql` integration to allow for custom metrics (@StefanKurek)

### Bugfixes

Expand Down
21 changes: 6 additions & 15 deletions component/prometheus/exporter/mssql/mssql.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ package mssql
import (
"errors"
"fmt"
"os"
"time"

"github.com/burningalchemist/sql_exporter/config"
"github.com/grafana/agent/component"
"github.com/grafana/agent/component/prometheus/exporter"
"github.com/grafana/agent/pkg/integrations"
"github.com/grafana/agent/pkg/integrations/mssql"
"github.com/grafana/agent/pkg/util"
"github.com/grafana/river/rivertypes"
config_util "github.com/prometheus/common/config"
"gopkg.in/yaml.v2"
)

func init() {
Expand Down Expand Up @@ -43,7 +44,6 @@ type Arguments struct {
MaxIdleConnections int `river:"max_idle_connections,attr,optional"`
MaxOpenConnections int `river:"max_open_connections,attr,optional"`
Timeout time.Duration `river:"timeout,attr,optional"`
QueryConfigFile string `river:"query_config_file,attr,optional"`
QueryConfig rivertypes.OptionalSecret `river:"query_config,attr,optional"`
}

Expand All @@ -66,18 +66,10 @@ func (a *Arguments) Validate() error {
return errors.New("timeout must be positive")
}

if a.QueryConfigFile != "" {
_, err := os.Stat(a.QueryConfigFile)

if err == nil {
return nil
}

if errors.Is(err, os.ErrNotExist) {
return errors.New("query_config_file must be a valid path of a YAML config file")
} else {
return fmt.Errorf("query_config_file file has issues: %w", err)
}
var collectorConfig config.CollectorConfig
err := yaml.UnmarshalStrict([]byte(a.QueryConfig.Value), &collectorConfig)
if err != nil {
return fmt.Errorf("invalid query_config: %s", err)
}

return nil
Expand All @@ -89,7 +81,6 @@ func (a *Arguments) Convert() *mssql.Config {
MaxIdleConnections: a.MaxIdleConnections,
MaxOpenConnections: a.MaxOpenConnections,
Timeout: a.Timeout,
QueryConfigFile: a.QueryConfigFile,
QueryConfig: util.RawYAML(a.QueryConfig.Value),
}
}
146 changes: 113 additions & 33 deletions component/prometheus/exporter/mssql/mssql_test.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
package mssql

import (
"path/filepath"
"testing"
"time"

"github.com/burningalchemist/sql_exporter/config"
"github.com/grafana/agent/pkg/integrations/mssql"
"github.com/grafana/river"
"github.com/grafana/river/rivertypes"
config_util "github.com/prometheus/common/config"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)

func TestRiverUnmarshal(t *testing.T) {
goodQueryPath, _ := filepath.Abs("../../../../pkg/integrations/mssql/collector_config.yaml")

riverConfig := `
connection_string = "sqlserver://user:pass@localhost:1433"
max_idle_connections = 3
max_open_connections = 3
timeout = "10s"
query_config_file = "` + goodQueryPath + `"`
timeout = "10s"`

var args Arguments
err := river.Unmarshal([]byte(riverConfig), &args)
Expand All @@ -31,12 +29,67 @@ func TestRiverUnmarshal(t *testing.T) {
MaxIdleConnections: 3,
MaxOpenConnections: 3,
Timeout: 10 * time.Second,
QueryConfigFile: goodQueryPath,
}

require.Equal(t, expected, args)
}

func TestRiverUnmarshalWithInlineQueryConfig(t *testing.T) {
riverConfig := `
connection_string = "sqlserver://user:pass@localhost:1433"
max_idle_connections = 3
max_open_connections = 3
timeout = "10s"
query_config = "{ collector_name: mssql_standard, metrics: [ { metric_name: mssql_local_time_seconds, type: gauge, help: 'Local time in seconds since epoch (Unix time).', values: [ unix_time ], query: \"SELECT DATEDIFF(second, '19700101', GETUTCDATE()) AS unix_time\" } ] }"`

var args Arguments
err := river.Unmarshal([]byte(riverConfig), &args)
require.NoError(t, err)
var collectorConfig config.CollectorConfig
err = yaml.UnmarshalStrict([]byte(args.QueryConfig.Value), &collectorConfig)

require.Equal(t, rivertypes.Secret("sqlserver://user:pass@localhost:1433"), args.ConnectionString)
require.Equal(t, 3, args.MaxIdleConnections)
require.Equal(t, 3, args.MaxOpenConnections)
require.Equal(t, 10*time.Second, args.Timeout)
require.Equal(t, "mssql_standard", collectorConfig.Name)
require.Equal(t, 1, len(collectorConfig.Metrics))
require.Equal(t, "mssql_local_time_seconds", collectorConfig.Metrics[0].Name)
require.Equal(t, "gauge", collectorConfig.Metrics[0].TypeString)
require.Equal(t, "Local time in seconds since epoch (Unix time).", collectorConfig.Metrics[0].Help)
require.Equal(t, 1, len(collectorConfig.Metrics[0].Values))
require.Contains(t, collectorConfig.Metrics[0].Values, "unix_time")
require.Equal(t, "SELECT DATEDIFF(second, '19700101', GETUTCDATE()) AS unix_time", collectorConfig.Metrics[0].QueryLiteral)
}

func TestRiverUnmarshalWithInlineQueryConfigYaml(t *testing.T) {
riverConfig := `
connection_string = "sqlserver://user:pass@localhost:1433"
max_idle_connections = 3
max_open_connections = 3
timeout = "10s"
query_config = "collector_name: mssql_standard\nmetrics:\n- metric_name: mssql_local_time_seconds\n type: gauge\n help: 'Local time in seconds since epoch (Unix time).'\n values: [unix_time]\n query: \"SELECT DATEDIFF(second, '19700101', GETUTCDATE()) AS unix_time\""`

var args Arguments
err := river.Unmarshal([]byte(riverConfig), &args)
require.NoError(t, err)
var collectorConfig config.CollectorConfig
err = yaml.UnmarshalStrict([]byte(args.QueryConfig.Value), &collectorConfig)

require.Equal(t, rivertypes.Secret("sqlserver://user:pass@localhost:1433"), args.ConnectionString)
require.Equal(t, 3, args.MaxIdleConnections)
require.Equal(t, 3, args.MaxOpenConnections)
require.Equal(t, 10*time.Second, args.Timeout)
require.Equal(t, "mssql_standard", collectorConfig.Name)
require.Equal(t, 1, len(collectorConfig.Metrics))
require.Equal(t, "mssql_local_time_seconds", collectorConfig.Metrics[0].Name)
require.Equal(t, "gauge", collectorConfig.Metrics[0].TypeString)
require.Equal(t, "Local time in seconds since epoch (Unix time).", collectorConfig.Metrics[0].Help)
require.Equal(t, 1, len(collectorConfig.Metrics[0].Values))
require.Contains(t, collectorConfig.Metrics[0].Values, "unix_time")
require.Equal(t, "SELECT DATEDIFF(second, '19700101', GETUTCDATE()) AS unix_time", collectorConfig.Metrics[0].QueryLiteral)
}

func TestUnmarshalInvalid(t *testing.T) {
invalidRiverConfig := `
connection_string = "sqlserver://user:pass@localhost:1433"
Expand All @@ -48,10 +101,40 @@ func TestUnmarshalInvalid(t *testing.T) {
var invalidArgs Arguments
err := river.Unmarshal([]byte(invalidRiverConfig), &invalidArgs)
require.Error(t, err)
require.EqualError(t, err, "timeout must be positive")
}

func TestUnmarshalInvalidQueryConfigYaml(t *testing.T) {
invalidRiverConfig := `
connection_string = "sqlserver://user:pass@localhost:1433"
max_idle_connections = 1
max_open_connections = 1
timeout = "1s"
query_config = "{ collector_name: mssql_standard, metrics: [ { metric_name: mssql_local_time_seconds, type: gauge, help: 'Local time in seconds since epoch (Unix time).', values: [ unix_time ], query: \"SELECT DATEDIFF(second, '19700101', GETUTCDATE()) AS unix_time\" }"
`

var invalidArgs Arguments
err := river.Unmarshal([]byte(invalidRiverConfig), &invalidArgs)
require.Error(t, err)
require.EqualError(t, err, "invalid query_config: yaml: line 1: did not find expected ',' or ']'")
}

func TestUnmarshalInvalidProperty(t *testing.T) {
invalidRiverConfig := `
connection_string = "sqlserver://user:pass@localhost:1433"
max_idle_connections = 1
max_open_connections = 1
timeout = "1s"
query_config = "collector_name: mssql_standard\nbad_param: true\nmetrics:\n- metric_name: mssql_local_time_seconds\n type: gauge\n help: 'Local time in seconds since epoch (Unix time).'\n values: [unix_time]\n query: \"SELECT DATEDIFF(second, '19700101', GETUTCDATE()) AS unix_time\""
`

var invalidArgs Arguments
err := river.Unmarshal([]byte(invalidRiverConfig), &invalidArgs)
require.Error(t, err)
require.EqualError(t, err, "invalid query_config: unknown fields in collector: bad_param")
}

func TestArgumentsValidate(t *testing.T) {
goodQueryPath, _ := filepath.Abs("../../../../pkg/integrations/mssql/collector_config.yaml")

tests := []struct {
name string
Expand All @@ -65,7 +148,6 @@ func TestArgumentsValidate(t *testing.T) {
MaxIdleConnections: 1,
MaxOpenConnections: 0,
Timeout: 10 * time.Second,
QueryConfigFile: goodQueryPath,
},
wantErr: true,
},
Expand All @@ -76,7 +158,6 @@ func TestArgumentsValidate(t *testing.T) {
MaxIdleConnections: 0,
MaxOpenConnections: 1,
Timeout: 10 * time.Second,
QueryConfigFile: goodQueryPath,
},
wantErr: true,
},
Expand All @@ -87,18 +168,6 @@ func TestArgumentsValidate(t *testing.T) {
MaxIdleConnections: 1,
MaxOpenConnections: 1,
Timeout: 0,
QueryConfigFile: goodQueryPath,
},
wantErr: true,
},
{
name: "invalid query_config_file",
args: Arguments{
ConnectionString: rivertypes.Secret("test"),
MaxIdleConnections: 1,
MaxOpenConnections: 1,
Timeout: 0,
QueryConfigFile: "doesnotexist.YAML",
},
wantErr: true,
},
Expand All @@ -109,7 +178,9 @@ func TestArgumentsValidate(t *testing.T) {
MaxIdleConnections: 1,
MaxOpenConnections: 1,
Timeout: 10 * time.Second,
QueryConfigFile: goodQueryPath,
QueryConfig: rivertypes.OptionalSecret{
Value: `{ collector_name: mssql_standard, metrics: [ { metric_name: mssql_local_time_seconds, type: gauge, help: 'Local time in seconds since epoch (Unix time).', values: [ unix_time ], query: "SELECT DATEDIFF(second, '19700101', GETUTCDATE()) AS unix_time" } ] }`,
},
},
wantErr: false,
},
Expand All @@ -128,22 +199,31 @@ func TestArgumentsValidate(t *testing.T) {
}

func TestConvert(t *testing.T) {
goodQueryPath, _ := filepath.Abs("../../../../pkg/integrations/mssql/collector_config.yaml")
riverConfig := `
connection_string = "sqlserver://user:pass@localhost:1433"
query_config_file = "` + goodQueryPath + `"`
var args Arguments
err := river.Unmarshal([]byte(riverConfig), &args)
require.NoError(t, err)
strQueryConfig := `collector_name: mssql_standard
metrics:
- metric_name: mssql_local_time_seconds
type: gauge
help: 'Local time in seconds since epoch (Unix time).'
values: [unix_time]
query: "SELECT DATEDIFF(second, '19700101', GETUTCDATE()) AS unix_time"`

args := Arguments{
ConnectionString: rivertypes.Secret("sqlserver://user:pass@localhost:1433"),
MaxIdleConnections: 1,
MaxOpenConnections: 1,
Timeout: 10 * time.Second,
QueryConfig: rivertypes.OptionalSecret{
Value: strQueryConfig,
},
}
res := args.Convert()

expected := mssql.Config{
ConnectionString: config_util.Secret("sqlserver://user:pass@localhost:1433"),
MaxIdleConnections: DefaultArguments.MaxIdleConnections,
MaxOpenConnections: DefaultArguments.MaxOpenConnections,
Timeout: DefaultArguments.Timeout,
QueryConfigFile: goodQueryPath,
MaxIdleConnections: 1,
MaxOpenConnections: 1,
Timeout: 10 * time.Second,
QueryConfig: []byte(strQueryConfig),
}
require.Equal(t, expected, *res)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,22 @@ Omitted fields take their default values.
| `max_idle_connections` | `int` | Maximum number of idle connections to any one target. | `3` | no |
| `max_open_connections` | `int` | Maximum number of open connections to any one target. | `3` | no |
| `timeout` | `duration` | The query timeout in seconds. | `"10s"` | no |
| `query_config_file` | `string` | MSSQL query to prometheus metric configuration file path. | | no |
| `query_config` | `string` | MSSQL query to prometheus metric configuration as an inline string. | | no |
| `query_config` | `string` | MSSQL query to Prometheus metric configuration as an inline string. | | no |

[The sql_exporter examples](https://github.com/burningalchemist/sql_exporter/blob/master/examples/azure-sql-mi/sql_exporter.yml#L21) show the format of the `connection_string` argument:

```conn
sqlserver://USERNAME_HERE:PASSWORD_HERE@SQLMI_HERE_ENDPOINT.database.windows.net:1433?encrypt=true&hostNameInCertificate=%2A.SQL_MI_DOMAIN_HERE.database.windows.net&trustservercertificate=true
```

Either `query_config_file` or `query_config` can be specified.
The `query_config_file` argument points to a YAML file defining which MSSQL queries map to custom Prometheus metrics.
The `query_config` argument must be a YAML document as string defining which MSSQL queries map to custom Prometheus metrics.
If specified, the `query_config` argument must be a YAML document as string defining which MSSQL queries map to custom Prometheus metrics.
`query_config` is typically loaded by using the exports of another component. For example,

- `local.file.LABEL.content`
- `remote.http.LABEL.content`
- `remote.s3.LABEL.content`

See [sql_exporter](https://github.com/burningalchemist/sql_exporter#collectors) for details on how to create a configuration file.
See [sql_exporter](https://github.com/burningalchemist/sql_exporter#collectors) for details on how to create a configuration.

## Blocks

Expand Down Expand Up @@ -116,9 +113,9 @@ Replace the following:
[scrape]: {{< relref "./prometheus.scrape.md" >}}

## Custom metrics
You can use the optional `query_config_file` or `query_config` parameters to retrieve custom Prometheus metrics for a MSSQL instance.
You can use the optional `query_config` parameter to retrieve custom Prometheus metrics for a MSSQL instance.

If either of these are defined, they will use the new configuration to query your MSSQL instance and create whatever Prometheus metrics are defined.
If this is defined, the new configuration will be used to query your MSSQL instance and create whatever Prometheus metrics are defined.
If you want additional metrics on top of the default metrics, the default configuration must be used as a base.

The default configuration used by this integration is as follows:
Expand Down
12 changes: 3 additions & 9 deletions docs/sources/static/configuration/integrations/mssql-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,17 @@ Full reference of options:
# The timeout for scraping metrics from the MSSQL instance.
[timeout: <duration> | default = "10s"]

# Path to a YAML configuration file with custom Prometheus metrics and MSSQL queries.
# This field has precedence to the config defined in the query_config block.
# See https://github.com/burningalchemist/sql_exporter#collectors for more details how to specify your configuration file.
[query_config_file: <string>]

# Embedded MSSQL query configuration for custom MSSQL Prometheus metrics.
# You can specify your metrics and queries here instead of in an external configuration file.
# Embedded MSSQL query configuration for specifying custom MSSQL Prometheus metrics.
# See https://github.com/burningalchemist/sql_exporter#collectors for more details how to specify your metric configurations.
query_config:
[- <metrics> ... ]
[- <queries> ... ]]
```
## Custom metrics
You can use the optional `query_config_file` or `query_config` parameters to retrieve custom Prometheus metrics for a MSSQL instance.
You can use the optional `query_config` parameter to retrieve custom Prometheus metrics for a MSSQL instance.

If either of these are defined, they will use the new configuration to query your MSSQL instance and create whatever Prometheus metrics are defined.
If this is defined, the new configuration will be used to query your MSSQL instance and create whatever Prometheus metrics are defined.
If you want additional metrics on top of the default metrics, the default configuration must be used as a base.

The default configuration used by this integration is as follows:
Expand Down
Loading

0 comments on commit 4dc69ee

Please sign in to comment.