receiver/sqlserverreceiver/factory.go (213 lines of code) (raw):

// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package sqlserverreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/sqlserverreceiver" import ( "context" "database/sql" "errors" "fmt" "time" lru "github.com/hashicorp/golang-lru/v2" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/receiver" "go.opentelemetry.io/collector/scraper" "go.opentelemetry.io/collector/scraper/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") // newCache creates a new cache with the given size. // If the size is less or equal to 0, it will be set to 1. // It will never return an error. func newCache(size int) *lru.Cache[string, int64] { if size <= 0 { size = 1 } // lru will only returns error when the size is less than 0 cache, _ := lru.New[string, int64](size) return cache } // NewFactory creates a factory for SQL Server receiver. func NewFactory() receiver.Factory { return receiver.NewFactory( metadata.Type, createDefaultConfig, receiver.WithMetrics(createMetricsReceiver, metadata.MetricsStability), receiver.WithLogs(createLogsReceiver, metadata.LogsStability)) } func createDefaultConfig() component.Config { cfg := scraperhelper.NewDefaultControllerConfig() cfg.CollectionInterval = 10 * time.Second return &Config{ ControllerConfig: cfg, MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), QuerySample: QuerySample{ Enabled: false, MaxRowsPerQuery: 100, }, TopQueryCollection: TopQueryCollection{ Enabled: false, LookbackTime: uint(2 * cfg.CollectionInterval / time.Second), MaxQuerySampleCount: 1000, TopQueryCount: 200, }, } } func setupQueries(cfg *Config) []string { var queries []string if isDatabaseIOQueryEnabled(&cfg.Metrics) { queries = append(queries, getSQLServerDatabaseIOQuery(cfg.InstanceName)) } if isPerfCounterQueryEnabled(&cfg.Metrics) { queries = append(queries, getSQLServerPerformanceCounterQuery(cfg.InstanceName)) } if cfg.Metrics.SqlserverDatabaseCount.Enabled { queries = append(queries, getSQLServerPropertiesQuery(cfg.InstanceName)) } return queries } func setupLogQueries(cfg *Config) []string { var queries []string if cfg.QuerySample.Enabled { queries = append(queries, getSQLServerQuerySamplesQuery()) } if cfg.TopQueryCollection.Enabled { queries = append(queries, getSQLServerQueryTextAndPlanQuery()) } return queries } // Assumes config has all information necessary to directly connect to the database func getDBConnectionString(config *Config) string { if config.DataSource != "" { return config.DataSource } 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.Settings, cfg *Config) []*sqlServerScraperHelper { if !cfg.isDirectDBConnectionEnabled { 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)) // lru only returns error when the size is less than 0 cache := newCache(1) sqlServerScraper := newSQLServerScraper(id, query, sqlquery.TelemetryConfig{}, dbProviderFunc, sqlquery.NewDbClient, params, cfg, cache) scrapers = append(scrapers, sqlServerScraper) } return scrapers } // SQL Server scraper creation is split out into a separate method for the sake of testing. func setupSQLServerLogsScrapers(params receiver.Settings, cfg *Config) []*sqlServerScraperHelper { if !cfg.isDirectDBConnectionEnabled { params.Logger.Info("No direct connection will be made to the SQL Server: Configuration doesn't include some options.") return nil } queries := setupLogQueries(cfg) if len(queries) == 0 { params.Logger.Info("No direct connection will be made to the SQL Server: No logs 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("logs-query-%d: %s", i, query)) cache := newCache(1) if query == getSQLServerQueryTextAndPlanQuery() { // we have 8 metrics in this query and multiple 2 to allow to cache more queries. cache = newCache(int(cfg.MaxQuerySampleCount * 8 * 2)) } if query == getSQLServerQuerySamplesQuery() { cache = newCache(1) } sqlServerScraper := newSQLServerScraper(id, query, sqlquery.TelemetryConfig{}, dbProviderFunc, sqlquery.NewDbClient, params, cfg, cache) 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.Settings, cfg *Config) ([]scraperhelper.ControllerOption, error) { sqlServerScrapers := setupSQLServerScrapers(params, cfg) var opts []scraperhelper.ControllerOption for _, sqlScraper := range sqlServerScrapers { s, err := scraper.NewMetrics(sqlScraper.ScrapeMetrics, scraper.WithStart(sqlScraper.Start), scraper.WithShutdown(sqlScraper.Shutdown)) if err != nil { return nil, err } opt := scraperhelper.AddScraper(metadata.Type, s) opts = append(opts, opt) } return opts, nil } // 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 setupLogsScrapers(params receiver.Settings, cfg *Config) ([]scraperhelper.ControllerOption, error) { sqlServerScrapers := setupSQLServerLogsScrapers(params, cfg) var opts []scraperhelper.ControllerOption for _, sqlScraper := range sqlServerScrapers { s, err := scraper.NewLogs(sqlScraper.ScrapeLogs, scraper.WithStart(sqlScraper.Start), scraper.WithShutdown(sqlScraper.Shutdown)) if err != nil { return nil, err } opt := scraperhelper.AddFactoryWithConfig( scraper.NewFactory(metadata.Type, nil, scraper.WithLogs(func(context.Context, scraper.Settings, component.Config) (scraper.Logs, error) { return s, nil }, component.StabilityLevelAlpha)), nil) opts = append(opts, opt) } return opts, nil } func isDatabaseIOQueryEnabled(metrics *metadata.MetricsConfig) bool { if metrics == nil { return false } return metrics.SqlserverDatabaseLatency.Enabled || metrics.SqlserverDatabaseOperations.Enabled || metrics.SqlserverDatabaseIo.Enabled } func isPerfCounterQueryEnabled(metrics *metadata.MetricsConfig) bool { if metrics == nil { return false } return metrics.SqlserverBatchRequestRate.Enabled || metrics.SqlserverBatchSQLCompilationRate.Enabled || metrics.SqlserverBatchSQLRecompilationRate.Enabled || metrics.SqlserverDatabaseBackupOrRestoreRate.Enabled || metrics.SqlserverDatabaseExecutionErrors.Enabled || metrics.SqlserverDatabaseFullScanRate.Enabled || metrics.SqlserverDatabaseTempdbSpace.Enabled || metrics.SqlserverDatabaseTempdbVersionStoreSize.Enabled || metrics.SqlserverDeadlockRate.Enabled || metrics.SqlserverIndexSearchRate.Enabled || metrics.SqlserverLockTimeoutRate.Enabled || metrics.SqlserverLockWaitRate.Enabled || metrics.SqlserverLoginRate.Enabled || metrics.SqlserverLogoutRate.Enabled || metrics.SqlserverMemoryGrantsPendingCount.Enabled || metrics.SqlserverMemoryUsage.Enabled || metrics.SqlserverPageBufferCacheFreeListStallsRate.Enabled || metrics.SqlserverPageBufferCacheHitRatio.Enabled || metrics.SqlserverPageLookupRate.Enabled || metrics.SqlserverProcessesBlocked.Enabled || metrics.SqlserverReplicaDataRate.Enabled || metrics.SqlserverResourcePoolDiskThrottledReadRate.Enabled || metrics.SqlserverResourcePoolDiskThrottledWriteRate.Enabled || metrics.SqlserverTableCount.Enabled || metrics.SqlserverTransactionDelay.Enabled || metrics.SqlserverTransactionMirrorWriteRate.Enabled || metrics.SqlserverUserConnectionCount.Enabled }