Skip to content

Commit

Permalink
[chore][receiver/sqlserver] Enable receiver to run on non-Windows sys…
Browse files Browse the repository at this point in the history
…tems (#32542)

**Description:** <Describe what has changed.>
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
This change enables the SQL Server receiver to directly connect to SQL
Server instances, and to run on operating systems other than Windows.

This is part of
#31915,
but doesn't add any functionality for the end user. No metrics are
currently being gathered from SQL server instances, to make the PR
(hopefully) simpler to review.

**Link to tracking Issue:** <Issue number if applicable>

#31915

#30297

**Testing:** <Describe what testing was performed and which tests were
added.>
Tests added are passing.
  • Loading branch information
crobert-1 authored Apr 19, 2024
1 parent 71dcf7a commit 6d8b65d
Show file tree
Hide file tree
Showing 13 changed files with 1,032 additions and 78 deletions.
79 changes: 79 additions & 0 deletions receiver/sqlserverreceiver/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@
package sqlserverreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/sqlserverreceiver"

import (
"database/sql"
"errors"
"fmt"
"time"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/receiver"
"go.opentelemetry.io/collector/receiver/scraperhelper"

"github.com/open-telemetry/opentelemetry-collector-contrib/internal/sqlquery"
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/sqlserverreceiver/internal/metadata"
)

var errConfigNotSQLServer = errors.New("config was not a sqlserver receiver config")

// NewFactory creates a factory for SQL Server receiver.
func NewFactory() receiver.Factory {
return receiver.NewFactory(
Expand All @@ -30,8 +36,81 @@ func createDefaultConfig() component.Config {
}
}

func setupQueries(cfg *Config) []string {
var queries []string
// TODO: Only add query if metrics are enabled
queries = append(queries, getSQLServerDatabaseIOQuery(cfg.InstanceName))
return queries
}

func directDBConnectionEnabled(config *Config) bool {
return config.Server != "" &&
config.Username != "" &&
string(config.Password) != ""
}

// Assumes config has all information necessary to directly connect to the database
func getDBConnectionString(config *Config) string {
return fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d", config.Server, config.Username, string(config.Password), config.Port)
}

// SQL Server scraper creation is split out into a separate method for the sake of testing.
func setupSQLServerScrapers(params receiver.CreateSettings, cfg *Config) []*sqlServerScraperHelper {
if !directDBConnectionEnabled(cfg) {
params.Logger.Info("No direct connection will be made to the SQL Server: Configuration doesn't include some options.")
return nil
}

queries := setupQueries(cfg)
if len(queries) == 0 {
params.Logger.Info("No direct connection will be made to the SQL Server: No metrics are enabled requiring it.")
return nil
}

// TODO: Test if this needs to be re-defined for each scraper
// This should be tested when there is more than one query being made.
dbProviderFunc := func() (*sql.DB, error) {
return sql.Open("sqlserver", getDBConnectionString(cfg))
}

var scrapers []*sqlServerScraperHelper
for i, query := range queries {
id := component.NewIDWithName(metadata.Type, fmt.Sprintf("query-%d: %s", i, query))

sqlServerScraper := newSQLServerScraper(id, query,
cfg.InstanceName,
cfg.ControllerConfig,
params.Logger,
sqlquery.TelemetryConfig{},
dbProviderFunc,
sqlquery.NewDbClient,
metadata.NewMetricsBuilder(cfg.MetricsBuilderConfig, params))

scrapers = append(scrapers, sqlServerScraper)
}

return scrapers
}

// Note: This method will fail silently if there is no work to do. This is an acceptable use case
// as this receiver can still get information on Windows from performance counters without a direct
// connection. Messages will be logged at the INFO level in such cases.
func setupScrapers(params receiver.CreateSettings, cfg *Config) ([]scraperhelper.ScraperControllerOption, error) {
sqlServerScrapers := setupSQLServerScrapers(params, cfg)

var opts []scraperhelper.ScraperControllerOption
for _, sqlScraper := range sqlServerScrapers {
scraper, err := scraperhelper.NewScraper(metadata.Type.String(), sqlScraper.Scrape,
scraperhelper.WithStart(sqlScraper.Start),
scraperhelper.WithShutdown(sqlScraper.Shutdown))

if err != nil {
return nil, err
}

opt := scraperhelper.AddScraper(scraper)
opts = append(opts, opt)
}

return opts, nil
}
25 changes: 20 additions & 5 deletions receiver/sqlserverreceiver/factory_others.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,34 @@ package sqlserverreceiver // import "github.com/open-telemetry/opentelemetry-col

import (
"context"
"errors"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/consumer"
"go.opentelemetry.io/collector/receiver"
"go.opentelemetry.io/collector/receiver/scraperhelper"
)

// createMetricsReceiver creates a metrics receiver based on provided config.
func createMetricsReceiver(
_ context.Context,
_ receiver.CreateSettings,
_ component.Config,
_ consumer.Metrics,
params receiver.CreateSettings,
receiverCfg component.Config,
metricsConsumer consumer.Metrics,
) (receiver.Metrics, error) {
return nil, errors.New("the sqlserver receiver is only supported on Windows")
cfg, ok := receiverCfg.(*Config)
if !ok {
return nil, errConfigNotSQLServer
}

opts, err := setupScrapers(params, cfg)
if err != nil {
return nil, err
}

return scraperhelper.NewScraperControllerReceiver(
&cfg.ControllerConfig,
params,
metricsConsumer,
opts...,
)
}
62 changes: 55 additions & 7 deletions receiver/sqlserverreceiver/factory_others_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,64 @@ import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/consumer/consumertest"
"go.opentelemetry.io/collector/receiver/receivertest"
)

func TestCreateMetricsReceiver(t *testing.T) {
factory := NewFactory()
cfg := factory.CreateDefaultConfig()
mReceiver, err := factory.CreateMetricsReceiver(context.Background(), receivertest.NewNopCreateSettings(), cfg, consumertest.NewNop())
func TestCreateMetricsReceiverOtherOS(t *testing.T) {
testCases := []struct {
desc string
testFunc func(*testing.T)
}{
{
desc: "Test direct connection with instance name",
testFunc: func(t *testing.T) {
factory := NewFactory()
cfg := factory.CreateDefaultConfig().(*Config)
cfg.Username = "sa"
cfg.Password = "password"
cfg.Server = "0.0.0.0"
cfg.Port = 1433
cfg.InstanceName = "instanceName"
require.NoError(t, cfg.Validate())

assert.EqualError(t, err, "the sqlserver receiver is only supported on Windows")
assert.Nil(t, mReceiver)
require.True(t, directDBConnectionEnabled(cfg))
require.Equal(t, "server=0.0.0.0;user id=sa;password=password;port=1433", getDBConnectionString(cfg))

params := receivertest.NewNopCreateSettings()
scrapers, err := setupScrapers(params, cfg)
require.NoError(t, err)
require.NotEmpty(t, scrapers)

sqlScrapers := setupSQLServerScrapers(params, cfg)
require.NotEmpty(t, sqlScrapers)

databaseIOScraperFound := false
for _, scraper := range sqlScrapers {
if scraper.sqlQuery == getSQLServerDatabaseIOQuery(cfg.InstanceName) {
databaseIOScraperFound = true
break
}
}

require.True(t, databaseIOScraperFound)

r, err := factory.CreateMetricsReceiver(
context.Background(),
receivertest.NewNopCreateSettings(),
cfg,
consumertest.NewNop(),
)
require.NoError(t, err)
require.NoError(t, r.Start(context.Background(), componenttest.NewNopHost()))
require.NoError(t, r.Shutdown(context.Background()))
},
},
}

for _, tc := range testCases {
t.Run(tc.desc, tc.testFunc)
}
}
93 changes: 92 additions & 1 deletion receiver/sqlserverreceiver/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
package sqlserverreceiver

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/consumer/consumertest"
"go.opentelemetry.io/collector/receiver/receivertest"
"go.opentelemetry.io/collector/receiver/scraperhelper"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/sqlserverreceiver/internal/metadata"
)

func TestNewFactory(t *testing.T) {
func TestCreateMetricsReceiver(t *testing.T) {
testCases := []struct {
desc string
testFunc func(*testing.T)
Expand Down Expand Up @@ -42,6 +46,93 @@ func TestNewFactory(t *testing.T) {
require.Equal(t, expectedCfg, factory.CreateDefaultConfig())
},
},
{
desc: "creates a new factory and CreateMetricsReceiver returns error with incorrect config",
testFunc: func(t *testing.T) {
factory := NewFactory()
_, err := factory.CreateMetricsReceiver(
context.Background(),
receivertest.NewNopCreateSettings(),
nil,
consumertest.NewNop(),
)
require.ErrorIs(t, err, errConfigNotSQLServer)
},
},
{
desc: "creates a new factory and CreateMetricsReceiver returns no error",
testFunc: func(t *testing.T) {
factory := NewFactory()
cfg := factory.CreateDefaultConfig()
r, err := factory.CreateMetricsReceiver(
context.Background(),
receivertest.NewNopCreateSettings(),
cfg,
consumertest.NewNop(),
)
require.NoError(t, err)
scrapers := setupSQLServerScrapers(receivertest.NewNopCreateSettings(), cfg.(*Config))
require.Empty(t, scrapers)
require.NoError(t, r.Start(context.Background(), componenttest.NewNopHost()))
require.NoError(t, r.Shutdown(context.Background()))
},
},
{
desc: "Test direct connection",
testFunc: func(t *testing.T) {
factory := NewFactory()
cfg := factory.CreateDefaultConfig().(*Config)
cfg.Username = "sa"
cfg.Password = "password"
cfg.Server = "0.0.0.0"
cfg.Port = 1433
require.NoError(t, cfg.Validate())

require.True(t, directDBConnectionEnabled(cfg))
require.Equal(t, "server=0.0.0.0;user id=sa;password=password;port=1433", getDBConnectionString(cfg))

params := receivertest.NewNopCreateSettings()
scrapers, err := setupScrapers(params, cfg)
require.NoError(t, err)
require.NotEmpty(t, scrapers)

sqlScrapers := setupSQLServerScrapers(params, cfg)
require.NotEmpty(t, sqlScrapers)

databaseIOScraperFound := false
for _, scraper := range sqlScrapers {
if scraper.sqlQuery == getSQLServerDatabaseIOQuery(cfg.InstanceName) {
databaseIOScraperFound = true
break
}
}

require.True(t, databaseIOScraperFound)
cfg.InstanceName = "instanceName"
sqlScrapers = setupSQLServerScrapers(params, cfg)
require.NotEmpty(t, sqlScrapers)

databaseIOScraperFound = false
for _, scraper := range sqlScrapers {
if scraper.sqlQuery == getSQLServerDatabaseIOQuery(cfg.InstanceName) {
databaseIOScraperFound = true
break
}
}

require.True(t, databaseIOScraperFound)

r, err := factory.CreateMetricsReceiver(
context.Background(),
receivertest.NewNopCreateSettings(),
cfg,
consumertest.NewNop(),
)
require.NoError(t, err)
require.NoError(t, r.Start(context.Background(), componenttest.NewNopHost()))
require.NoError(t, r.Shutdown(context.Background()))
},
},
}

for _, tc := range testCases {
Expand Down
15 changes: 11 additions & 4 deletions receiver/sqlserverreceiver/factory_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package sqlserverreceiver // import "github.com/open-telemetry/opentelemetry-col

import (
"context"
"errors"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/consumer"
Expand All @@ -17,8 +16,6 @@ import (
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/sqlserverreceiver/internal/metadata"
)

var errConfigNotSQLServer = errors.New("config was not a sqlserver receiver config")

// createMetricsReceiver creates a metrics receiver based on provided config.
func createMetricsReceiver(
_ context.Context,
Expand All @@ -39,7 +36,17 @@ func createMetricsReceiver(
return nil, err
}

var opts []scraperhelper.ScraperControllerOption
opts, err = setupScrapers(params, cfg)
if err != nil {
return nil, err
}
opts = append(opts, scraperhelper.AddScraper(scraper))

return scraperhelper.NewScraperControllerReceiver(
&cfg.ControllerConfig, params, metricsConsumer, scraperhelper.AddScraper(scraper),
&cfg.ControllerConfig,
params,
metricsConsumer,
opts...,
)
}
Loading

0 comments on commit 6d8b65d

Please sign in to comment.