@cryptotaxi247 / netdata-1 / commits / 4da3896ab

Add MSSQL collector to go.d.plugin (#21583)

* Add MSSQL collector to go.d.plugin This adds a new Microsoft SQL Server collector with the following features: Instance-level metrics: - User/session connections, blocked processes - Batch requests, SQL compilations/recompilations - Auto-parameterization statistics - SQL errors - Buffer manager (cache hit ratio, page I/O, checkpoints, lazy writes) - Memory manager (total, connection, pending grants) - Page splits Database-level metrics: - Active/total/write transactions - Log flushes and bytes flushed - Data file sizes - Database state (online/offline/restoring/etc.) - Read-only status Lock metrics: - Lock statistics by resource type (deadlocks, waits, timeouts, requests) - Current lock counts from sys.dm_tran_locks Wait statistics: - Total/resource/signal wait times by wait type - Wait counts with category mapping (80+ wait types) SQL Agent jobs: - Job enabled/disabled status Replication monitoring: - Publication status and warnings - Replication latency (avg/best/worst) - Subscription counts and running agents All 40 chart contexts verified against SQL Server 2022 test environment with transactional replication configured. * Add missing metrics to MSSQL Go collector for C parity - Add noLatencySentinel constant for magic number 999999 - Add backup/restore throughput per-database metric - Add max_wait_time per wait type metric - Decode replication status into 6 discrete states - Decode replication warning into 7 individual flags - Fix sentinel handling for worst_latency - Update metadata.yaml and README.md with new metrics * Add additional metrics to MSSQL Go collector Add 4 new metric groups for improved observability parity: - Process memory: resident/virtual memory, utilization, page faults from sys.dm_os_process_memory - OS memory: used/available physical memory and page file from sys.dm_os_sys_memory - I/O stall: per-database read/write latency from sys.dm_io_virtual_file_stats - Log growths: per-database log file growth events from sys.dm_os_performance_counters * Fix MSSQL collector code review findings - Remove dead code: worstLatency sentinel check was never triggered (query returns MAX(ISNULL(worst_latency, 0)) which is always 0) - Add security warning for TrustServerCertificate parameter - Simplify availability list (2016+ already covers 2017/2019/2022)

Costa Tsaousis committed Jan 17, 2026 at 03:41 UTC 4da3896ab28c488a6ca18335445047ac20026d39
11 files changed +3907
src/go/plugin/go.d/collector/init.go
+1
@@ -62,6 +62,7 @@ import (
62 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/memcached"
63 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/mongodb"
64 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/monit"
65 + _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/mssql"
66 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/mysql"
67 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/nats"
68 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/nginx"
src/go/plugin/go.d/collector/mssql/README.md new
+209
@@ -0,0 +1,209 @@
1 +# Microsoft SQL Server collector
2 +
3 +This collector monitors Microsoft SQL Server instances.
4 +
5 +## Requirements
6 +
7 +- SQL Server 2016 or later
8 +- A user with `VIEW SERVER STATE` permission
9 +
10 +## Metrics
11 +
12 +The collector provides the following metric categories:
13 +
14 +### Instance metrics
15 +
16 +- **User connections** - Number of active user connections
17 +- **Blocked processes** - Number of blocked processes
18 +- **Batch requests** - Number of batch requests per second
19 +- **SQL compilations/recompilations** - Query compilation statistics
20 +- **Auto-parameterization attempts** - Auto-param total, safe, and failed attempts
21 +- **SQL errors** - SQL error rate
22 +- **Page splits** - Number of page splits per second
23 +
24 +### Buffer and memory metrics
25 +
26 +- **Buffer cache hit ratio** - Percentage of pages found in buffer pool
27 +- **Page life expectancy** - How long pages stay in buffer pool
28 +- **Page reads/writes** - Buffer I/O operations
29 +- **Checkpoint pages** - Pages flushed during checkpoint
30 +- **Lazy writes** - Lazy writer operations per second
31 +- **Page lookups** - Page lookups per second
32 +- **Total server memory** - Memory used by SQL Server
33 +- **Connection memory** - Memory used for connections
34 +- **Memory grants pending** - Queries waiting for memory
35 +
36 +### Process memory metrics
37 +
38 +- **Resident memory** - SQL Server process resident memory (working set)
39 +- **Virtual memory** - SQL Server process committed virtual memory
40 +- **Memory utilization** - Percentage of committed memory in the working set
41 +- **Page faults** - Number of page faults incurred by the SQL Server process
42 +
43 +### OS memory metrics
44 +
45 +- **Physical memory** - OS physical memory (used and available)
46 +- **Page file** - OS page file (used and available)
47 +
48 +### Per-database metrics
49 +
50 +- **Transactions** - Transaction throughput
51 +- **Active transactions** - Currently active transactions
52 +- **Log bytes flushed** - Transaction log write throughput
53 +- **Log growths** - Number of times the transaction log has been expanded
54 +- **I/O stall time** - Read and write I/O stall (latency) time in milliseconds
55 +- **Data file size** - Size of database data files
56 +- **Backup/restore throughput** - Throughput of backup/restore operations
57 +- **Database state** - Online, restoring, recovering, pending, suspect, emergency, offline
58 +- **Read-only status** - Whether database is read-only or read-write
59 +- **Lock statistics** - Requests, waits, timeouts, deadlocks by lock type
60 +
61 +### Wait statistics
62 +
63 +- **Wait time** - Total, resource, and signal wait times by wait type
64 +- **Maximum wait time** - Maximum wait time by wait type
65 +- **Waiting tasks** - Number of tasks waiting
66 +
67 +### Lock metrics
68 +
69 +- **Lock count** - Current locks by resource type
70 +
71 +### SQL Agent jobs (optional)
72 +
73 +- **Job status** - Enabled/disabled status for each job
74 +
75 +### Replication metrics (optional)
76 +
77 +- **Replication status** - Publication status (started, succeeded, in_progress, idle, retrying, failed)
78 +- **Replication warnings** - Warning flags (expiration, latency, merge warnings)
79 +- **Replication latency** - Average, best, and worst latency in seconds
80 +- **Subscription count** - Number of subscriptions and running agents
81 +
82 +## Configuration
83 +
84 +Edit the `go.d/mssql.conf` configuration file using `edit-config`:
85 +
86 +```bash
87 +cd /etc/netdata
88 +sudo ./edit-config go.d/mssql.conf
89 +```
90 +
91 +### Basic configuration
92 +
93 +```yaml
94 +jobs:
95 + - name: local
96 + dsn: "sqlserver://netdata_user:password@localhost:1433"
97 +```
98 +
99 +### Connection string format
100 +
101 +The DSN follows the [go-mssqldb connection string format](https://github.com/microsoft/go-mssqldb#connection-parameters-and-dsn):
102 +
103 +```
104 +sqlserver://username:password@host:port?param=value
105 +```
106 +
107 +Common parameters:
108 +- `database` - Initial database to connect to
109 +- `encrypt` - Enable encryption (disable/false/true/strict)
110 +- `TrustServerCertificate` - Skip certificate validation (use only in dev/test environments)
111 +- `ApplicationIntent` - ReadOnly for read-only routing
112 +
113 +### Windows Authentication
114 +
115 +```yaml
116 +jobs:
117 + - name: local
118 + dsn: "sqlserver://localhost:1433?trusted_connection=yes"
119 +```
120 +
121 +### Named instance
122 +
123 +```yaml
124 +jobs:
125 + - name: named
126 + dsn: "sqlserver://netdata_user:password@localhost/INSTANCENAME"
127 +```
128 +
129 +### Configuration options
130 +
131 +| Option | Description | Default |
132 +|--------|-------------|---------|
133 +| `dsn` | SQL Server connection string | required |
134 +| `timeout` | Query timeout in seconds | 5 |
135 +| `vnode` | Virtual node name for multi-instance setups | "" |
136 +| `collect_transactions` | Collect per-database transaction metrics | true |
137 +| `collect_waits` | Collect wait statistics | true |
138 +| `collect_locks` | Collect lock metrics | true |
139 +| `collect_jobs` | Collect SQL Agent job status | true |
140 +| `collect_buffer_stats` | Collect buffer manager statistics | true |
141 +| `collect_database_size` | Collect data file sizes | true |
142 +| `collect_user_connections` | Collect user connection counts | true |
143 +| `collect_blocked_processes` | Collect blocked process count | true |
144 +| `collect_sql_errors` | Collect SQL error statistics | true |
145 +| `collect_database_status` | Collect database state and read-only status | true |
146 +| `collect_replication` | Collect replication monitoring metrics | true |
147 +
148 +## Permissions
149 +
150 +Create a monitoring user with the required permissions:
151 +
152 +```sql
153 +-- Create login
154 +CREATE LOGIN netdata_user WITH PASSWORD = 'YourStrongPassword!';
155 +
156 +-- Grant VIEW SERVER STATE (required for DMVs)
157 +GRANT VIEW SERVER STATE TO netdata_user;
158 +
159 +-- Optional: Grant access to msdb for SQL Agent job monitoring
160 +USE msdb;
161 +CREATE USER netdata_user FOR LOGIN netdata_user;
162 +GRANT SELECT ON dbo.sysjobs TO netdata_user;
163 +```
164 +
165 +## Troubleshooting
166 +
167 +### Connection refused
168 +
169 +- Verify SQL Server is running
170 +- Check that TCP/IP is enabled in SQL Server Configuration Manager
171 +- Ensure the firewall allows connections on port 1433
172 +
173 +### Login failed
174 +
175 +- Verify credentials are correct
176 +- Ensure SQL Server is configured for mixed mode authentication
177 +- Check that the user has permission to connect
178 +
179 +### Permission denied on DMVs
180 +
181 +Grant VIEW SERVER STATE permission:
182 +
183 +```sql
184 +GRANT VIEW SERVER STATE TO netdata_user;
185 +```
186 +
187 +### SQL Agent job metrics not collected
188 +
189 +Grant access to the msdb database:
190 +
191 +```sql
192 +USE msdb;
193 +CREATE USER netdata_user FOR LOGIN netdata_user;
194 +GRANT SELECT ON dbo.sysjobs TO netdata_user;
195 +```
196 +
197 +### Replication metrics not collected
198 +
199 +For replication monitoring, grant access to the distribution database:
200 +
201 +```sql
202 +USE distribution;
203 +CREATE USER netdata_user FOR LOGIN netdata_user;
204 +GRANT SELECT ON dbo.MSreplication_monitordata TO netdata_user;
205 +GRANT SELECT ON dbo.MSpublications TO netdata_user;
206 +GRANT SELECT ON dbo.MSsubscriptions TO netdata_user;
207 +```
208 +
209 +Note: The distribution database only exists if replication is configured on the server.
src/go/plugin/go.d/collector/mssql/charts.go new
+894
@@ -0,0 +1,894 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package mssql
4 +
5 +import (
6 + "fmt"
7 + "strings"
8 +
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
10 +)
11 +
12 +const (
13 + prioUserConnections = module.Priority + iota
14 + prioSessionConnections
15 + prioBlockedProcesses
16 +
17 + prioBatchRequests
18 + prioCompilations
19 + prioRecompilations
20 + prioAutoParamAttempts
21 + prioSQLErrors
22 +
23 + prioBufferCacheHitRatio
24 + prioBufferPageIOPS
25 + prioBufferCheckpointPages
26 + prioBufferPageLifeExpectancy
27 + prioBufferLazyWrites
28 + prioBufferPageLookups
29 +
30 + prioPageSplits
31 +
32 + prioMemoryTotal
33 + prioMemoryConnection
34 + prioMemoryPendingGrants
35 + prioMemoryExternalBenefit
36 +
37 + prioProcessMemoryResident
38 + prioProcessMemoryVirtual
39 + prioProcessMemoryUtilization
40 + prioProcessPageFaults
41 +
42 + prioOSMemory
43 + prioOSPageFile
44 +
45 + prioDatabaseActiveTransactions
46 + prioDatabaseTransactions
47 + prioDatabaseWriteTransactions
48 + prioDatabaseLogFlushes
49 + prioDatabaseLogFlushed
50 + prioDatabaseLogGrowths
51 + prioDatabaseIOStall
52 + prioDatabaseDeadlocks
53 + prioDatabaseLockWaits
54 + prioDatabaseLockTimeouts
55 + prioDatabaseLockRequests
56 + prioDatabaseDataFileSize
57 + prioDatabaseState
58 + prioDatabaseReadOnly
59 +
60 + prioLocksByResource
61 +
62 + prioWaitTotalTime
63 + prioWaitResourceTime
64 + prioWaitSignalTime
65 + prioWaitMaxTime
66 + prioWaitCount
67 +
68 + prioJobStatus
69 +
70 + prioReplicationStatus
71 + prioReplicationLatency
72 +)
73 +
74 +// instanceCharts are charts for the SQL Server instance level metrics
75 +var instanceCharts = module.Charts{
76 + userConnectionsChart.Copy(),
77 + sessionConnectionsChart.Copy(),
78 + blockedProcessesChart.Copy(),
79 +
80 + batchRequestsChart.Copy(),
81 + compilationsChart.Copy(),
82 + recompilationsChart.Copy(),
83 + autoParamAttemptsChart.Copy(),
84 + sqlErrorsChart.Copy(),
85 +
86 + bufferCacheHitRatioChart.Copy(),
87 + bufferPageIOPSChart.Copy(),
88 + bufferCheckpointPagesChart.Copy(),
89 + bufferPageLifeExpectancyChart.Copy(),
90 + bufferLazyWritesChart.Copy(),
91 + bufferPageLookupsChart.Copy(),
92 +
93 + pageSplitsChart.Copy(),
94 +
95 + memoryTotalChart.Copy(),
96 + memoryConnectionChart.Copy(),
97 + memoryPendingGrantsChart.Copy(),
98 + memoryExternalBenefitChart.Copy(),
99 +
100 + processMemoryResidentChart.Copy(),
101 + processMemoryVirtualChart.Copy(),
102 + processMemoryUtilizationChart.Copy(),
103 + processPageFaultsChart.Copy(),
104 +
105 + osMemoryChart.Copy(),
106 + osPageFileChart.Copy(),
107 +}
108 +
109 +var (
110 + userConnectionsChart = module.Chart{
111 + ID: "user_connections",
112 + Title: "User connections",
113 + Units: "connections",
114 + Fam: "connections",
115 + Ctx: "mssql.user_connections",
116 + Priority: prioUserConnections,
117 + Dims: module.Dims{
118 + {ID: "user_connections", Name: "user"},
119 + },
120 + }
121 + blockedProcessesChart = module.Chart{
122 + ID: "blocked_processes",
123 + Title: "Blocked processes",
124 + Units: "processes",
125 + Fam: "processes",
126 + Ctx: "mssql.blocked_processes",
127 + Priority: prioBlockedProcesses,
128 + Dims: module.Dims{
129 + {ID: "blocked_processes", Name: "blocked"},
130 + },
131 + }
132 + sessionConnectionsChart = module.Chart{
133 + ID: "session_connections",
134 + Title: "Session connections",
135 + Units: "connections",
136 + Fam: "connections",
137 + Ctx: "mssql.session_connections",
138 + Priority: prioSessionConnections,
139 + Dims: module.Dims{
140 + {ID: "session_connections_user", Name: "user"},
141 + {ID: "session_connections_internal", Name: "internal"},
142 + },
143 + }
144 +
145 + batchRequestsChart = module.Chart{
146 + ID: "batch_requests",
147 + Title: "Batch requests",
148 + Units: "requests/s",
149 + Fam: "queries",
150 + Ctx: "mssql.batch_requests",
151 + Priority: prioBatchRequests,
152 + Dims: module.Dims{
153 + {ID: "batch_requests", Name: "batch", Algo: module.Incremental},
154 + },
155 + }
156 + compilationsChart = module.Chart{
157 + ID: "compilations",
158 + Title: "SQL compilations",
159 + Units: "compilations/s",
160 + Fam: "queries",
161 + Ctx: "mssql.compilations",
162 + Priority: prioCompilations,
163 + Dims: module.Dims{
164 + {ID: "sql_compilations", Name: "compilations", Algo: module.Incremental},
165 + },
166 + }
167 + recompilationsChart = module.Chart{
168 + ID: "recompilations",
169 + Title: "SQL re-compilations",
170 + Units: "recompilations/s",
171 + Fam: "queries",
172 + Ctx: "mssql.recompilations",
173 + Priority: prioRecompilations,
174 + Dims: module.Dims{
175 + {ID: "sql_recompilations", Name: "recompilations", Algo: module.Incremental},
176 + },
177 + }
178 + autoParamAttemptsChart = module.Chart{
179 + ID: "auto_param_attempts",
180 + Title: "Auto-parameterization attempts",
181 + Units: "attempts/s",
182 + Fam: "queries",
183 + Ctx: "mssql.auto_param_attempts",
184 + Priority: prioAutoParamAttempts,
185 + Dims: module.Dims{
186 + {ID: "auto_param_attempts", Name: "total", Algo: module.Incremental},
187 + {ID: "auto_param_safe", Name: "safe", Algo: module.Incremental},
188 + {ID: "auto_param_failed", Name: "failed", Algo: module.Incremental, Mul: -1},
189 + },
190 + }
191 + sqlErrorsChart = module.Chart{
192 + ID: "sql_errors",
193 + Title: "SQL errors",
194 + Units: "errors/s",
195 + Fam: "errors",
196 + Ctx: "mssql.sql_errors",
197 + Priority: prioSQLErrors,
198 + Dims: module.Dims{
199 + {ID: "sql_errors_total", Name: "errors", Algo: module.Incremental},
200 + },
201 + }
202 +
203 + bufferCacheHitRatioChart = module.Chart{
204 + ID: "buffer_cache_hit_ratio",
205 + Title: "Buffer cache hit ratio",
206 + Units: "percentage",
207 + Fam: "buffer",
208 + Ctx: "mssql.buffer_cache_hit_ratio",
209 + Priority: prioBufferCacheHitRatio,
210 + Dims: module.Dims{
211 + {ID: "buffer_cache_hit_ratio", Name: "hit_ratio"},
212 + },
213 + }
214 + bufferPageIOPSChart = module.Chart{
215 + ID: "buffer_page_iops",
216 + Title: "Buffer page I/O",
217 + Units: "pages/s",
218 + Fam: "buffer",
219 + Ctx: "mssql.buffer_page_iops",
220 + Priority: prioBufferPageIOPS,
221 + Dims: module.Dims{
222 + {ID: "buffer_page_reads", Name: "read", Algo: module.Incremental},
223 + {ID: "buffer_page_writes", Name: "written", Algo: module.Incremental, Mul: -1},
224 + },
225 + }
226 + bufferCheckpointPagesChart = module.Chart{
227 + ID: "buffer_checkpoint_pages",
228 + Title: "Buffer checkpoint pages flushed",
229 + Units: "pages/s",
230 + Fam: "buffer",
231 + Ctx: "mssql.buffer_checkpoint_pages",
232 + Priority: prioBufferCheckpointPages,
233 + Dims: module.Dims{
234 + {ID: "buffer_checkpoint_pages", Name: "flushed", Algo: module.Incremental},
235 + },
236 + }
237 + bufferPageLifeExpectancyChart = module.Chart{
238 + ID: "buffer_page_life_expectancy",
239 + Title: "Buffer page life expectancy",
240 + Units: "seconds",
241 + Fam: "buffer",
242 + Ctx: "mssql.buffer_page_life_expectancy",
243 + Priority: prioBufferPageLifeExpectancy,
244 + Dims: module.Dims{
245 + {ID: "buffer_page_life_expectancy", Name: "life_expectancy"},
246 + },
247 + }
248 + bufferLazyWritesChart = module.Chart{
249 + ID: "buffer_lazy_writes",
250 + Title: "Buffer lazy writes",
251 + Units: "writes/s",
252 + Fam: "buffer",
253 + Ctx: "mssql.buffer_lazy_writes",
254 + Priority: prioBufferLazyWrites,
255 + Dims: module.Dims{
256 + {ID: "buffer_lazy_writes", Name: "lazy_writes", Algo: module.Incremental},
257 + },
258 + }
259 + bufferPageLookupsChart = module.Chart{
260 + ID: "buffer_page_lookups",
261 + Title: "Buffer page lookups",
262 + Units: "lookups/s",
263 + Fam: "buffer",
264 + Ctx: "mssql.buffer_page_lookups",
265 + Priority: prioBufferPageLookups,
266 + Dims: module.Dims{
267 + {ID: "buffer_page_lookups", Name: "lookups", Algo: module.Incremental},
268 + },
269 + }
270 +
271 + pageSplitsChart = module.Chart{
272 + ID: "page_splits",
273 + Title: "Page splits",
274 + Units: "splits/s",
275 + Fam: "access",
276 + Ctx: "mssql.page_splits",
277 + Priority: prioPageSplits,
278 + Dims: module.Dims{
279 + {ID: "page_splits", Name: "page", Algo: module.Incremental},
280 + },
281 + }
282 +
283 + memoryTotalChart = module.Chart{
284 + ID: "memory_total",
285 + Title: "Total server memory",
286 + Units: "bytes",
287 + Fam: "memory",
288 + Ctx: "mssql.memory_total",
289 + Priority: prioMemoryTotal,
290 + Dims: module.Dims{
291 + {ID: "memory_total", Name: "memory"},
292 + },
293 + }
294 + memoryConnectionChart = module.Chart{
295 + ID: "memory_connection",
296 + Title: "Connection memory",
297 + Units: "bytes",
298 + Fam: "memory",
299 + Ctx: "mssql.memory_connection",
300 + Priority: prioMemoryConnection,
301 + Dims: module.Dims{
302 + {ID: "memory_connection", Name: "memory"},
303 + },
304 + }
305 + memoryPendingGrantsChart = module.Chart{
306 + ID: "memory_pending_grants",
307 + Title: "Pending memory grants",
308 + Units: "processes",
309 + Fam: "memory",
310 + Ctx: "mssql.memory_pending_grants",
311 + Priority: prioMemoryPendingGrants,
312 + Dims: module.Dims{
313 + {ID: "memory_pending_grants", Name: "pending"},
314 + },
315 + }
316 + memoryExternalBenefitChart = module.Chart{
317 + ID: "memory_external_benefit",
318 + Title: "External benefit of memory",
319 + Units: "benefit",
320 + Fam: "memory",
321 + Ctx: "mssql.memory_external_benefit",
322 + Priority: prioMemoryExternalBenefit,
323 + Dims: module.Dims{
324 + {ID: "memory_external_benefit", Name: "benefit"},
325 + },
326 + }
327 +
328 + processMemoryResidentChart = module.Chart{
329 + ID: "process_memory_resident",
330 + Title: "Process resident memory (working set)",
331 + Units: "bytes",
332 + Fam: "process memory",
333 + Ctx: "mssql.process_memory_resident",
334 + Priority: prioProcessMemoryResident,
335 + Dims: module.Dims{
336 + {ID: "process_memory_resident", Name: "resident"},
337 + },
338 + }
339 + processMemoryVirtualChart = module.Chart{
340 + ID: "process_memory_virtual",
341 + Title: "Process virtual memory committed",
342 + Units: "bytes",
343 + Fam: "process memory",
344 + Ctx: "mssql.process_memory_virtual",
345 + Priority: prioProcessMemoryVirtual,
346 + Dims: module.Dims{
347 + {ID: "process_memory_virtual", Name: "virtual"},
348 + },
349 + }
350 + processMemoryUtilizationChart = module.Chart{
351 + ID: "process_memory_utilization",
352 + Title: "Process memory utilization",
353 + Units: "percentage",
354 + Fam: "process memory",
355 + Ctx: "mssql.process_memory_utilization",
356 + Priority: prioProcessMemoryUtilization,
357 + Dims: module.Dims{
358 + {ID: "process_memory_utilization", Name: "utilization"},
359 + },
360 + }
361 + processPageFaultsChart = module.Chart{
362 + ID: "process_page_faults",
363 + Title: "Process page faults",
364 + Units: "faults",
365 + Fam: "process memory",
366 + Ctx: "mssql.process_page_faults",
367 + Priority: prioProcessPageFaults,
368 + Dims: module.Dims{
369 + {ID: "process_page_faults", Name: "page_faults", Algo: module.Incremental},
370 + },
371 + }
372 +
373 + osMemoryChart = module.Chart{
374 + ID: "os_memory",
375 + Title: "OS physical memory",
376 + Units: "bytes",
377 + Fam: "os memory",
378 + Ctx: "mssql.os_memory",
379 + Priority: prioOSMemory,
380 + Dims: module.Dims{
381 + {ID: "os_memory_used", Name: "used"},
382 + {ID: "os_memory_available", Name: "available"},
383 + },
384 + }
385 + osPageFileChart = module.Chart{
386 + ID: "os_pagefile",
387 + Title: "OS page file",
388 + Units: "bytes",
389 + Fam: "os memory",
390 + Ctx: "mssql.os_pagefile",
391 + Priority: prioOSPageFile,
392 + Dims: module.Dims{
393 + {ID: "os_pagefile_used", Name: "used"},
394 + {ID: "os_pagefile_available", Name: "available"},
395 + },
396 + }
397 +)
398 +
399 +// Database chart templates
400 +var (
401 + databaseActiveTransactionsChartTmpl = module.Chart{
402 + ID: "database_%s_active_transactions",
403 + Title: "Active transactions",
404 + Units: "transactions",
405 + Fam: "db transactions",
406 + Ctx: "mssql.database_active_transactions",
407 + Priority: prioDatabaseActiveTransactions,
408 + Dims: module.Dims{
409 + {ID: "database_%s_active_transactions", Name: "active"},
410 + },
411 + }
412 + databaseTransactionsChartTmpl = module.Chart{
413 + ID: "database_%s_transactions",
414 + Title: "Transactions",
415 + Units: "transactions/s",
416 + Fam: "db transactions",
417 + Ctx: "mssql.database_transactions",
418 + Priority: prioDatabaseTransactions,
419 + Dims: module.Dims{
420 + {ID: "database_%s_transactions", Name: "transactions", Algo: module.Incremental},
421 + },
422 + }
423 + databaseWriteTransactionsChartTmpl = module.Chart{
424 + ID: "database_%s_write_transactions",
425 + Title: "Write transactions",
426 + Units: "transactions/s",
427 + Fam: "db transactions",
428 + Ctx: "mssql.database_write_transactions",
429 + Priority: prioDatabaseWriteTransactions,
430 + Dims: module.Dims{
431 + {ID: "database_%s_write_transactions", Name: "write", Algo: module.Incremental},
432 + },
433 + }
434 + databaseLogFlushesChartTmpl = module.Chart{
435 + ID: "database_%s_log_flushes",
436 + Title: "Log flushes",
437 + Units: "flushes/s",
438 + Fam: "db log",
439 + Ctx: "mssql.database_log_flushes",
440 + Priority: prioDatabaseLogFlushes,
441 + Dims: module.Dims{
442 + {ID: "database_%s_log_flushes", Name: "flushes", Algo: module.Incremental},
443 + },
444 + }
445 + databaseLogFlushedChartTmpl = module.Chart{
446 + ID: "database_%s_log_flushed",
447 + Title: "Log bytes flushed",
448 + Units: "bytes/s",
449 + Fam: "db log",
450 + Ctx: "mssql.database_log_flushed",
451 + Priority: prioDatabaseLogFlushed,
452 + Dims: module.Dims{
453 + {ID: "database_%s_log_flushed", Name: "flushed", Algo: module.Incremental},
454 + },
455 + }
456 + databaseDataFileSizeChartTmpl = module.Chart{
457 + ID: "database_%s_data_file_size",
458 + Title: "Data file size",
459 + Units: "bytes",
460 + Fam: "db size",
461 + Ctx: "mssql.database_data_file_size",
462 + Priority: prioDatabaseDataFileSize,
463 + Dims: module.Dims{
464 + {ID: "database_%s_data_file_size", Name: "size"},
465 + },
466 + }
467 + databaseBackupRestoreThroughputChartTmpl = module.Chart{
468 + ID: "database_%s_backup_restore_throughput",
469 + Title: "Backup/Restore throughput",
470 + Units: "bytes/s",
471 + Fam: "db backup",
472 + Ctx: "mssql.database_backup_restore_throughput",
473 + Priority: prioDatabaseDataFileSize + 1,
474 + Dims: module.Dims{
475 + {ID: "database_%s_backup_restore_throughput", Name: "throughput", Algo: module.Incremental},
476 + },
477 + }
478 + databaseStateChartTmpl = module.Chart{
479 + ID: "database_%s_state",
480 + Title: "Database state",
481 + Units: "state",
482 + Fam: "db status",
483 + Ctx: "mssql.database_state",
484 + Priority: prioDatabaseState,
485 + Dims: module.Dims{
486 + {ID: "database_%s_state_online", Name: "online"},
487 + {ID: "database_%s_state_restoring", Name: "restoring"},
488 + {ID: "database_%s_state_recovering", Name: "recovering"},
489 + {ID: "database_%s_state_pending", Name: "pending"},
490 + {ID: "database_%s_state_suspect", Name: "suspect"},
491 + {ID: "database_%s_state_emergency", Name: "emergency"},
492 + {ID: "database_%s_state_offline", Name: "offline"},
493 + },
494 + }
495 + databaseReadOnlyChartTmpl = module.Chart{
496 + ID: "database_%s_read_only",
497 + Title: "Database read-only status",
498 + Units: "status",
499 + Fam: "db status",
500 + Ctx: "mssql.database_read_only",
501 + Priority: prioDatabaseReadOnly,
502 + Dims: module.Dims{
503 + {ID: "database_%s_read_only", Name: "read_only"},
504 + {ID: "database_%s_read_write", Name: "read_write"},
505 + },
506 + }
507 + databaseLogGrowthsChartTmpl = module.Chart{
508 + ID: "database_%s_log_growths",
509 + Title: "Database log growths",
510 + Units: "growths",
511 + Fam: "db log",
512 + Ctx: "mssql.database_log_growths",
513 + Priority: prioDatabaseLogGrowths,
514 + Dims: module.Dims{
515 + {ID: "database_%s_log_growths", Name: "growths", Algo: module.Incremental},
516 + },
517 + }
518 + databaseIOStallChartTmpl = module.Chart{
519 + ID: "database_%s_io_stall",
520 + Title: "Database I/O stall time",
521 + Units: "ms",
522 + Fam: "db io",
523 + Ctx: "mssql.database_io_stall",
524 + Priority: prioDatabaseIOStall,
525 + Dims: module.Dims{
526 + {ID: "database_%s_io_stall_read", Name: "read", Algo: module.Incremental},
527 + {ID: "database_%s_io_stall_write", Name: "write", Algo: module.Incremental},
528 + },
529 + }
530 +)
531 +
532 +// Lock stats chart templates (per lock resource type, from performance counters)
533 +var (
534 + lockStatsDeadlocksChartTmpl = module.Chart{
535 + ID: "lock_stats_%s_deadlocks",
536 + Title: "Deadlocks by lock resource type",
537 + Units: "deadlocks/s",
538 + Fam: "lock stats",
539 + Ctx: "mssql.lock_stats_deadlocks",
540 + Priority: prioDatabaseDeadlocks,
541 + Dims: module.Dims{
542 + {ID: "lock_stats_%s_deadlocks", Name: "deadlocks", Algo: module.Incremental},
543 + },
544 + }
545 + lockStatsWaitsChartTmpl = module.Chart{
546 + ID: "lock_stats_%s_waits",
547 + Title: "Lock waits by lock resource type",
548 + Units: "waits/s",
549 + Fam: "lock stats",
550 + Ctx: "mssql.lock_stats_waits",
551 + Priority: prioDatabaseLockWaits,
552 + Dims: module.Dims{
553 + {ID: "lock_stats_%s_waits", Name: "waits", Algo: module.Incremental},
554 + },
555 + }
556 + lockStatsTimeoutsChartTmpl = module.Chart{
557 + ID: "lock_stats_%s_timeouts",
558 + Title: "Lock timeouts by lock resource type",
559 + Units: "timeouts/s",
560 + Fam: "lock stats",
561 + Ctx: "mssql.lock_stats_timeouts",
562 + Priority: prioDatabaseLockTimeouts,
563 + Dims: module.Dims{
564 + {ID: "lock_stats_%s_timeouts", Name: "timeouts", Algo: module.Incremental},
565 + },
566 + }
567 + lockStatsRequestsChartTmpl = module.Chart{
568 + ID: "lock_stats_%s_requests",
569 + Title: "Lock requests by lock resource type",
570 + Units: "requests/s",
571 + Fam: "lock stats",
572 + Ctx: "mssql.lock_stats_requests",
573 + Priority: prioDatabaseLockRequests,
574 + Dims: module.Dims{
575 + {ID: "lock_stats_%s_requests", Name: "requests", Algo: module.Incremental},
576 + },
577 + }
578 +)
579 +
580 +// Lock resource chart template
581 +var locksByResourceChartTmpl = module.Chart{
582 + ID: "locks_by_resource_%s",
583 + Title: "Locks by resource type",
584 + Units: "locks",
585 + Fam: "locks",
586 + Ctx: "mssql.locks_by_resource",
587 + Priority: prioLocksByResource,
588 + Dims: module.Dims{
589 + {ID: "locks_%s_count", Name: "locks"},
590 + },
591 +}
592 +
593 +// Wait type chart templates
594 +var (
595 + waitTotalTimeChartTmpl = module.Chart{
596 + ID: "wait_%s_total_time",
597 + Title: "Total wait time",
598 + Units: "ms",
599 + Fam: "waits",
600 + Ctx: "mssql.wait_total_time",
601 + Priority: prioWaitTotalTime,
602 + Dims: module.Dims{
603 + {ID: "wait_%s_total_ms", Name: "duration", Algo: module.Incremental},
604 + },
605 + }
606 + waitResourceTimeChartTmpl = module.Chart{
607 + ID: "wait_%s_resource_time",
608 + Title: "Resource wait time",
609 + Units: "ms",
610 + Fam: "waits",
611 + Ctx: "mssql.wait_resource_time",
612 + Priority: prioWaitResourceTime,
613 + Dims: module.Dims{
614 + {ID: "wait_%s_resource_ms", Name: "duration", Algo: module.Incremental},
615 + },
616 + }
617 + waitSignalTimeChartTmpl = module.Chart{
618 + ID: "wait_%s_signal_time",
619 + Title: "Signal wait time",
620 + Units: "ms",
621 + Fam: "waits",
622 + Ctx: "mssql.wait_signal_time",
623 + Priority: prioWaitSignalTime,
624 + Dims: module.Dims{
625 + {ID: "wait_%s_signal_ms", Name: "duration", Algo: module.Incremental},
626 + },
627 + }
628 + waitCountChartTmpl = module.Chart{
629 + ID: "wait_%s_count",
630 + Title: "Wait count",
631 + Units: "waits/s",
632 + Fam: "waits",
633 + Ctx: "mssql.wait_count",
634 + Priority: prioWaitCount,
635 + Dims: module.Dims{
636 + {ID: "wait_%s_tasks", Name: "waits", Algo: module.Incremental},
637 + },
638 + }
639 + waitMaxTimeChartTmpl = module.Chart{
640 + ID: "wait_%s_max_time",
641 + Title: "Maximum wait time",
642 + Units: "ms",
643 + Fam: "waits",
644 + Ctx: "mssql.wait_max_time",
645 + Priority: prioWaitMaxTime,
646 + Dims: module.Dims{
647 + {ID: "wait_%s_max_ms", Name: "max_time"},
648 + },
649 + }
650 +)
651 +
652 +// Job status chart template
653 +var jobStatusChartTmpl = module.Chart{
654 + ID: "job_%s_status",
655 + Title: "Job status",
656 + Units: "status",
657 + Fam: "jobs",
658 + Ctx: "mssql.job_status",
659 + Priority: prioJobStatus,
660 + Dims: module.Dims{
661 + {ID: "job_%s_enabled", Name: "enabled"},
662 + {ID: "job_%s_disabled", Name: "disabled"},
663 + },
664 +}
665 +
666 +// Replication chart templates
667 +var (
668 + replicationStatusChartTmpl = module.Chart{
669 + ID: "replication_%s_status",
670 + Title: "Replication status",
671 + Units: "status",
672 + Fam: "replication",
673 + Ctx: "mssql.replication_status",
674 + Priority: prioReplicationStatus,
675 + Dims: module.Dims{
676 + {ID: "replication_%s_status_started", Name: "started"},
677 + {ID: "replication_%s_status_succeeded", Name: "succeeded"},
678 + {ID: "replication_%s_status_in_progress", Name: "in_progress"},
679 + {ID: "replication_%s_status_idle", Name: "idle"},
680 + {ID: "replication_%s_status_retrying", Name: "retrying"},
681 + {ID: "replication_%s_status_failed", Name: "failed"},
682 + },
683 + }
684 + replicationWarningChartTmpl = module.Chart{
685 + ID: "replication_%s_warning",
686 + Title: "Replication warnings",
687 + Units: "flags",
688 + Fam: "replication",
689 + Ctx: "mssql.replication_warning",
690 + Priority: prioReplicationStatus + 1,
691 + Dims: module.Dims{
692 + {ID: "replication_%s_warning_expiration", Name: "expiration"},
693 + {ID: "replication_%s_warning_latency", Name: "latency"},
694 + {ID: "replication_%s_warning_mergeexpiration", Name: "merge_expiration"},
695 + {ID: "replication_%s_warning_mergeslowrunduration", Name: "merge_slow_duration"},
696 + {ID: "replication_%s_warning_mergefastrunduration", Name: "merge_fast_duration"},
697 + {ID: "replication_%s_warning_mergefastrunspeed", Name: "merge_fast_speed"},
698 + {ID: "replication_%s_warning_mergeslowrunspeed", Name: "merge_slow_speed"},
699 + },
700 + }
701 + replicationLatencyChartTmpl = module.Chart{
702 + ID: "replication_%s_latency",
703 + Title: "Replication latency",
704 + Units: "seconds",
705 + Fam: "replication",
706 + Ctx: "mssql.replication_latency",
707 + Priority: prioReplicationLatency,
708 + Dims: module.Dims{
709 + {ID: "replication_%s_latency_avg", Name: "average"},
710 + {ID: "replication_%s_latency_best", Name: "best"},
711 + {ID: "replication_%s_latency_worst", Name: "worst"},
712 + },
713 + }
714 + replicationSubscriptionsChartTmpl = module.Chart{
715 + ID: "replication_%s_subscriptions",
716 + Title: "Replication subscriptions",
717 + Units: "subscriptions",
718 + Fam: "replication",
719 + Ctx: "mssql.replication_subscriptions",
720 + Priority: prioReplicationStatus + 1,
721 + Dims: module.Dims{
722 + {ID: "replication_%s_subscriptions", Name: "total"},
723 + {ID: "replication_%s_agents_running", Name: "agents_running"},
724 + },
725 + }
726 +)
727 +
728 +func (c *Collector) addDatabaseCharts(dbName string) {
729 + charts := &module.Charts{
730 + databaseActiveTransactionsChartTmpl.Copy(),
731 + databaseTransactionsChartTmpl.Copy(),
732 + databaseWriteTransactionsChartTmpl.Copy(),
733 + databaseLogFlushesChartTmpl.Copy(),
734 + databaseLogFlushedChartTmpl.Copy(),
735 + databaseLogGrowthsChartTmpl.Copy(),
736 + databaseIOStallChartTmpl.Copy(),
737 + databaseDataFileSizeChartTmpl.Copy(),
738 + databaseBackupRestoreThroughputChartTmpl.Copy(),
739 + databaseStateChartTmpl.Copy(),
740 + databaseReadOnlyChartTmpl.Copy(),
741 + }
742 +
743 + dbID := cleanDatabaseName(dbName)
744 +
745 + for _, chart := range *charts {
746 + chart.ID = fmt.Sprintf(chart.ID, dbID)
747 + chart.Labels = []module.Label{
748 + {Key: "database", Value: dbName},
749 + }
750 + for _, dim := range chart.Dims {
751 + dim.ID = fmt.Sprintf(dim.ID, dbID)
752 + }
753 + }
754 +
755 + if err := c.Charts().Add(*charts...); err != nil {
756 + c.Warning(err)
757 + }
758 +}
759 +
760 +func (c *Collector) addWaitTypeCharts(waitType string, waitCategory string) {
761 + charts := &module.Charts{
762 + waitTotalTimeChartTmpl.Copy(),
763 + waitResourceTimeChartTmpl.Copy(),
764 + waitSignalTimeChartTmpl.Copy(),
765 + waitMaxTimeChartTmpl.Copy(),
766 + waitCountChartTmpl.Copy(),
767 + }
768 +
769 + waitID := cleanWaitTypeName(waitType)
770 +
771 + for _, chart := range *charts {
772 + chart.ID = fmt.Sprintf(chart.ID, waitID)
773 + chart.Labels = []module.Label{
774 + {Key: "wait_type", Value: waitType},
775 + {Key: "wait_category", Value: waitCategory},
776 + }
777 + for _, dim := range chart.Dims {
778 + dim.ID = fmt.Sprintf(dim.ID, waitID)
779 + }
780 + }
781 +
782 + if err := c.Charts().Add(*charts...); err != nil {
783 + c.Warning(err)
784 + }
785 +}
786 +
787 +func (c *Collector) addLockResourceCharts(resourceType string) {
788 + chart := locksByResourceChartTmpl.Copy()
789 +
790 + resID := cleanResourceTypeName(resourceType)
791 +
792 + chart.ID = fmt.Sprintf(chart.ID, resID)
793 + chart.Labels = []module.Label{
794 + {Key: "resource", Value: resourceType},
795 + }
796 + for _, dim := range chart.Dims {
797 + dim.ID = fmt.Sprintf(dim.ID, resID)
798 + }
799 +
800 + if err := c.Charts().Add(chart); err != nil {
801 + c.Warning(err)
802 + }
803 +}
804 +
805 +func (c *Collector) addLockStatsCharts(resourceType string) {
806 + charts := &module.Charts{
807 + lockStatsDeadlocksChartTmpl.Copy(),
808 + lockStatsWaitsChartTmpl.Copy(),
809 + lockStatsTimeoutsChartTmpl.Copy(),
810 + lockStatsRequestsChartTmpl.Copy(),
811 + }
812 +
813 + resID := cleanResourceTypeName(resourceType)
814 +
815 + for _, chart := range *charts {
816 + chart.ID = fmt.Sprintf(chart.ID, resID)
817 + chart.Labels = []module.Label{
818 + {Key: "resource", Value: resourceType},
819 + }
820 + for _, dim := range chart.Dims {
821 + dim.ID = fmt.Sprintf(dim.ID, resID)
822 + }
823 + }
824 +
825 + if err := c.Charts().Add(*charts...); err != nil {
826 + c.Warning(err)
827 + }
828 +}
829 +
830 +func (c *Collector) addJobCharts(jobName string) {
831 + chart := jobStatusChartTmpl.Copy()
832 +
833 + jobID := cleanJobName(jobName)
834 +
835 + chart.ID = fmt.Sprintf(chart.ID, jobID)
836 + chart.Labels = []module.Label{
837 + {Key: "job_name", Value: jobName},
838 + }
839 + for _, dim := range chart.Dims {
840 + dim.ID = fmt.Sprintf(dim.ID, jobID)
841 + }
842 +
843 + if err := c.Charts().Add(chart); err != nil {
844 + c.Warning(err)
845 + }
846 +}
847 +
848 +func (c *Collector) addReplicationCharts(pubDB, publication string) {
849 + charts := &module.Charts{
850 + replicationStatusChartTmpl.Copy(),
851 + replicationWarningChartTmpl.Copy(),
852 + replicationLatencyChartTmpl.Copy(),
853 + replicationSubscriptionsChartTmpl.Copy(),
854 + }
855 +
856 + pubID := cleanPublicationName(pubDB, publication)
857 +
858 + for _, chart := range *charts {
859 + chart.ID = fmt.Sprintf(chart.ID, pubID)
860 + chart.Labels = []module.Label{
861 + {Key: "publisher_db", Value: pubDB},
862 + {Key: "publication", Value: publication},
863 + }
864 + for _, dim := range chart.Dims {
865 + dim.ID = fmt.Sprintf(dim.ID, pubID)
866 + }
867 + }
868 +
869 + if err := c.Charts().Add(*charts...); err != nil {
870 + c.Warning(err)
871 + }
872 +}
873 +
874 +func cleanDatabaseName(name string) string {
875 + return strings.ReplaceAll(strings.ToLower(name), " ", "_")
876 +}
877 +
878 +func cleanWaitTypeName(name string) string {
879 + return strings.ToLower(name)
880 +}
881 +
882 +func cleanResourceTypeName(name string) string {
883 + return strings.ToLower(name)
884 +}
885 +
886 +func cleanJobName(name string) string {
887 + r := strings.NewReplacer(" ", "_", ".", "_", "-", "_")
888 + return strings.ToLower(r.Replace(name))
889 +}
890 +
891 +func cleanPublicationName(pubDB, publication string) string {
892 + r := strings.NewReplacer(" ", "_", ".", "_", "-", "_")
893 + return strings.ToLower(r.Replace(pubDB + "_" + publication))
894 +}
src/go/plugin/go.d/collector/mssql/collect.go new
+839
@@ -0,0 +1,839 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package mssql
4 +
5 +import (
6 + "context"
7 + "database/sql"
8 + "fmt"
9 + "strings"
10 + "time"
11 +)
12 +
13 +// noLatencySentinel is the value SQL Server returns when no latency data is available
14 +const noLatencySentinel = 999999
15 +
16 +func (c *Collector) collect() (map[string]int64, error) {
17 + if c.db == nil {
18 + db, err := c.openConnection()
19 + if err != nil {
20 + return nil, err
21 + }
22 + c.db = db
23 + }
24 +
25 + if c.version == "" {
26 + ver, err := c.queryVersion()
27 + if err != nil {
28 + return nil, fmt.Errorf("failed to query version: %v", err)
29 + }
30 + c.version = ver
31 + c.Debugf("connected to SQL Server version %s", c.version)
32 + }
33 +
34 + mx := make(map[string]int64)
35 +
36 + if err := c.collectInstanceMetrics(mx); err != nil {
37 + return nil, err
38 + }
39 +
40 + if c.CollectTransactions {
41 + if err := c.collectDatabaseMetrics(mx); err != nil {
42 + c.Warning(err)
43 + }
44 + }
45 +
46 + if c.CollectLocks {
47 + if err := c.collectLockMetrics(mx); err != nil {
48 + c.Warning(err)
49 + }
50 + }
51 +
52 + if c.CollectWaits {
53 + if err := c.collectWaitStats(mx); err != nil {
54 + c.Warning(err)
55 + }
56 + }
57 +
58 + if c.CollectJobs {
59 + if err := c.collectJobStatus(mx); err != nil {
60 + c.Warning(err)
61 + }
62 + }
63 +
64 + if c.CollectReplication {
65 + if err := c.collectReplicationStatus(mx); err != nil {
66 + c.Warning(err)
67 + }
68 + }
69 +
70 + return mx, nil
71 +}
72 +
73 +func (c *Collector) openConnection() (*sql.DB, error) {
74 + db, err := sql.Open("sqlserver", c.DSN)
75 + if err != nil {
76 + return nil, fmt.Errorf("error opening connection: %v", err)
77 + }
78 +
79 + db.SetMaxOpenConns(1)
80 + db.SetMaxIdleConns(1)
81 + db.SetConnMaxLifetime(10 * time.Minute)
82 +
83 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
84 + defer cancel()
85 +
86 + if err := db.PingContext(ctx); err != nil {
87 + _ = db.Close()
88 + return nil, fmt.Errorf("error pinging database: %v", err)
89 + }
90 +
91 + return db, nil
92 +}
93 +
94 +func (c *Collector) queryVersion() (string, error) {
95 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
96 + defer cancel()
97 +
98 + var version string
99 + err := c.db.QueryRowContext(ctx, queryVersion).Scan(&version)
100 + if err != nil {
101 + return "", err
102 + }
103 + return version, nil
104 +}
105 +
106 +func (c *Collector) collectInstanceMetrics(mx map[string]int64) error {
107 + if c.CollectUserConnections {
108 + if err := c.collectUserConnections(mx); err != nil {
109 + c.Warning(err)
110 + }
111 + }
112 +
113 + if c.CollectBlockedProcesses {
114 + if err := c.collectBlockedProcesses(mx); err != nil {
115 + c.Warning(err)
116 + }
117 + }
118 +
119 + if err := c.collectBatchRequests(mx); err != nil {
120 + c.Warning(err)
121 + }
122 +
123 + if err := c.collectCompilations(mx); err != nil {
124 + c.Warning(err)
125 + }
126 +
127 + if c.CollectSQLErrors {
128 + if err := c.collectSQLErrors(mx); err != nil {
129 + c.Warning(err)
130 + }
131 + }
132 +
133 + if c.CollectBufferStats {
134 + if err := c.collectBufferManager(mx); err != nil {
135 + c.Warning(err)
136 + }
137 + if err := c.collectMemoryManager(mx); err != nil {
138 + c.Warning(err)
139 + }
140 + if err := c.collectAccessMethods(mx); err != nil {
141 + c.Warning(err)
142 + }
143 + }
144 +
145 + // Process and OS memory metrics (always collected)
146 + if err := c.collectProcessMemory(mx); err != nil {
147 + c.Warning(err)
148 + }
149 + if err := c.collectOSMemory(mx); err != nil {
150 + c.Warning(err)
151 + }
152 +
153 + return nil
154 +}
155 +
156 +func (c *Collector) collectUserConnections(mx map[string]int64) error {
157 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
158 + defer cancel()
159 +
160 + var userConns, sysConns int64
161 + err := c.db.QueryRowContext(ctx, queryUserConnections).Scan(&userConns, &sysConns)
162 + if err != nil {
163 + return fmt.Errorf("user connections query failed: %v", err)
164 + }
165 +
166 + mx["user_connections"] = userConns
167 + // Session connections: user vs internal (system)
168 + mx["session_connections_user"] = userConns
169 + mx["session_connections_internal"] = sysConns
170 + return nil
171 +}
172 +
173 +func (c *Collector) collectBlockedProcesses(mx map[string]int64) error {
174 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
175 + defer cancel()
176 +
177 + var blocked int64
178 + err := c.db.QueryRowContext(ctx, queryBlockedProcesses).Scan(&blocked)
179 + if err != nil {
180 + return fmt.Errorf("blocked processes query failed: %v", err)
181 + }
182 +
183 + mx["blocked_processes"] = blocked
184 + return nil
185 +}
186 +
187 +func (c *Collector) collectBatchRequests(mx map[string]int64) error {
188 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
189 + defer cancel()
190 +
191 + var value int64
192 + err := c.db.QueryRowContext(ctx, queryBatchRequests).Scan(&value)
193 + if err != nil {
194 + return fmt.Errorf("batch requests query failed: %v", err)
195 + }
196 +
197 + mx["batch_requests"] = value
198 + return nil
199 +}
200 +
201 +func (c *Collector) collectCompilations(mx map[string]int64) error {
202 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
203 + defer cancel()
204 +
205 + rows, err := c.db.QueryContext(ctx, queryCompilations)
206 + if err != nil {
207 + return fmt.Errorf("compilations query failed: %v", err)
208 + }
209 + defer rows.Close()
210 +
211 + for rows.Next() {
212 + var counterName string
213 + var value int64
214 + if err := rows.Scan(&counterName, &value); err != nil {
215 + continue
216 + }
217 +
218 + counterName = strings.TrimSpace(counterName)
219 + switch counterName {
220 + case "SQL Compilations/sec":
221 + mx["sql_compilations"] = value
222 + case "SQL Re-Compilations/sec":
223 + mx["sql_recompilations"] = value
224 + case "Auto-Param Attempts/sec":
225 + mx["auto_param_attempts"] = value
226 + case "Safe Auto-Params/sec":
227 + mx["auto_param_safe"] = value
228 + case "Failed Auto-Params/sec":
229 + mx["auto_param_failed"] = value
230 + }
231 + }
232 +
233 + return rows.Err()
234 +}
235 +
236 +func (c *Collector) collectBufferManager(mx map[string]int64) error {
237 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
238 + defer cancel()
239 +
240 + rows, err := c.db.QueryContext(ctx, queryBufferManager)
241 + if err != nil {
242 + return fmt.Errorf("buffer manager query failed: %v", err)
243 + }
244 + defer rows.Close()
245 +
246 + var cacheHitRatio, cacheHitRatioBase int64
247 +
248 + for rows.Next() {
249 + var counterName string
250 + var value int64
251 + if err := rows.Scan(&counterName, &value); err != nil {
252 + continue
253 + }
254 +
255 + counterName = strings.TrimSpace(counterName)
256 + switch counterName {
257 + case "Page reads/sec":
258 + mx["buffer_page_reads"] = value
259 + case "Page writes/sec":
260 + mx["buffer_page_writes"] = value
261 + case "Buffer cache hit ratio":
262 + cacheHitRatio = value
263 + case "Buffer cache hit ratio base":
264 + cacheHitRatioBase = value
265 + case "Checkpoint pages/sec":
266 + mx["buffer_checkpoint_pages"] = value
267 + case "Page life expectancy":
268 + mx["buffer_page_life_expectancy"] = value
269 + case "Lazy writes/sec":
270 + mx["buffer_lazy_writes"] = value
271 + case "Page lookups/sec":
272 + mx["buffer_page_lookups"] = value
273 + }
274 + }
275 +
276 + // Calculate hit ratio as percentage
277 + if cacheHitRatioBase > 0 {
278 + mx["buffer_cache_hit_ratio"] = (cacheHitRatio * 100) / cacheHitRatioBase
279 + }
280 +
281 + return rows.Err()
282 +}
283 +
284 +func (c *Collector) collectMemoryManager(mx map[string]int64) error {
285 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
286 + defer cancel()
287 +
288 + rows, err := c.db.QueryContext(ctx, queryMemoryManager)
289 + if err != nil {
290 + return fmt.Errorf("memory manager query failed: %v", err)
291 + }
292 + defer rows.Close()
293 +
294 + for rows.Next() {
295 + var counterName string
296 + var value int64
297 + if err := rows.Scan(&counterName, &value); err != nil {
298 + continue
299 + }
300 +
301 + counterName = strings.TrimSpace(counterName)
302 + switch counterName {
303 + case "Total Server Memory (KB)":
304 + mx["memory_total"] = value * 1024 // Convert to bytes
305 + case "Connection Memory (KB)":
306 + mx["memory_connection"] = value * 1024
307 + case "Memory Grants Pending":
308 + mx["memory_pending_grants"] = value
309 + case "External benefit of memory":
310 + mx["memory_external_benefit"] = value
311 + }
312 + }
313 +
314 + return rows.Err()
315 +}
316 +
317 +func (c *Collector) collectAccessMethods(mx map[string]int64) error {
318 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
319 + defer cancel()
320 +
321 + var value int64
322 + err := c.db.QueryRowContext(ctx, queryAccessMethods).Scan(&value)
323 + if err != nil {
324 + return fmt.Errorf("access methods query failed: %v", err)
325 + }
326 +
327 + mx["page_splits"] = value
328 + return nil
329 +}
330 +
331 +func (c *Collector) collectDatabaseMetrics(mx map[string]int64) error {
332 + if err := c.collectDatabaseCounters(mx); err != nil {
333 + return err
334 + }
335 + if err := c.collectLockStatsByResourceType(mx); err != nil {
336 + c.Warning(err)
337 + }
338 + if c.CollectDatabaseSize {
339 + if err := c.collectDatabaseSize(mx); err != nil {
340 + c.Warning(err)
341 + }
342 + }
343 + if c.CollectDatabaseStatus {
344 + if err := c.collectDatabaseStatus(mx); err != nil {
345 + c.Warning(err)
346 + }
347 + }
348 + // Collect I/O stall and log growth metrics per database
349 + if err := c.collectIOStall(mx); err != nil {
350 + c.Warning(err)
351 + }
352 + if err := c.collectLogGrowths(mx); err != nil {
353 + c.Warning(err)
354 + }
355 + return nil
356 +}
357 +
358 +func (c *Collector) collectDatabaseCounters(mx map[string]int64) error {
359 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
360 + defer cancel()
361 +
362 + rows, err := c.db.QueryContext(ctx, queryDatabaseCounters)
363 + if err != nil {
364 + return fmt.Errorf("database counters query failed: %v", err)
365 + }
366 + defer rows.Close()
367 +
368 + for rows.Next() {
369 + var dbName, counterName string
370 + var value int64
371 + if err := rows.Scan(&dbName, &counterName, &value); err != nil {
372 + continue
373 + }
374 +
375 + dbName = strings.TrimSpace(dbName)
376 + counterName = strings.TrimSpace(counterName)
377 +
378 + if !c.seenDatabases[dbName] {
379 + c.seenDatabases[dbName] = true
380 + c.addDatabaseCharts(dbName)
381 + }
382 +
383 + dbID := cleanDatabaseName(dbName)
384 + switch counterName {
385 + case "Active Transactions":
386 + mx[fmt.Sprintf("database_%s_active_transactions", dbID)] = value
387 + case "Transactions/sec":
388 + mx[fmt.Sprintf("database_%s_transactions", dbID)] = value
389 + case "Write Transactions/sec":
390 + mx[fmt.Sprintf("database_%s_write_transactions", dbID)] = value
391 + case "Backup/Restore Throughput/sec":
392 + mx[fmt.Sprintf("database_%s_backup_restore_throughput", dbID)] = value
393 + case "Log Bytes Flushed/sec":
394 + mx[fmt.Sprintf("database_%s_log_flushed", dbID)] = value
395 + case "Log Flushes/sec":
396 + mx[fmt.Sprintf("database_%s_log_flushes", dbID)] = value
397 + }
398 + }
399 +
400 + return rows.Err()
401 +}
402 +
403 +func (c *Collector) collectLockStatsByResourceType(mx map[string]int64) error {
404 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
405 + defer cancel()
406 +
407 + rows, err := c.db.QueryContext(ctx, queryDatabaseLocks)
408 + if err != nil {
409 + return fmt.Errorf("lock stats query failed: %v", err)
410 + }
411 + defer rows.Close()
412 +
413 + for rows.Next() {
414 + var resourceType, counterName string
415 + var value int64
416 + if err := rows.Scan(&resourceType, &counterName, &value); err != nil {
417 + continue
418 + }
419 +
420 + resourceType = strings.TrimSpace(resourceType)
421 + counterName = strings.TrimSpace(counterName)
422 +
423 + if !c.seenLockStatsTypes[resourceType] {
424 + c.seenLockStatsTypes[resourceType] = true
425 + c.addLockStatsCharts(resourceType)
426 + }
427 +
428 + // Note: instance_name from Locks counter is the lock resource type, not database name
429 + resID := cleanResourceTypeName(resourceType)
430 + switch counterName {
431 + case "Number of Deadlocks/sec":
432 + mx[fmt.Sprintf("lock_stats_%s_deadlocks", resID)] = value
433 + case "Lock Waits/sec":
434 + mx[fmt.Sprintf("lock_stats_%s_waits", resID)] = value
435 + case "Lock Timeouts/sec":
436 + mx[fmt.Sprintf("lock_stats_%s_timeouts", resID)] = value
437 + case "Lock Requests/sec":
438 + mx[fmt.Sprintf("lock_stats_%s_requests", resID)] = value
439 + }
440 + }
441 +
442 + return rows.Err()
443 +}
444 +
445 +func (c *Collector) collectDatabaseSize(mx map[string]int64) error {
446 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
447 + defer cancel()
448 +
449 + rows, err := c.db.QueryContext(ctx, queryDatabaseSize)
450 + if err != nil {
451 + return fmt.Errorf("database size query failed: %v", err)
452 + }
453 + defer rows.Close()
454 +
455 + for rows.Next() {
456 + var dbName string
457 + var size int64
458 + if err := rows.Scan(&dbName, &size); err != nil {
459 + continue
460 + }
461 +
462 + dbName = strings.TrimSpace(dbName)
463 +
464 + if !c.seenDatabases[dbName] {
465 + c.seenDatabases[dbName] = true
466 + c.addDatabaseCharts(dbName)
467 + }
468 +
469 + dbID := cleanDatabaseName(dbName)
470 + mx[fmt.Sprintf("database_%s_data_file_size", dbID)] = size
471 + }
472 +
473 + return rows.Err()
474 +}
475 +
476 +func (c *Collector) collectLockMetrics(mx map[string]int64) error {
477 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
478 + defer cancel()
479 +
480 + rows, err := c.db.QueryContext(ctx, queryLocksByResource)
481 + if err != nil {
482 + return fmt.Errorf("locks by resource query failed: %v", err)
483 + }
484 + defer rows.Close()
485 +
486 + for rows.Next() {
487 + var resourceType string
488 + var count int64
489 + if err := rows.Scan(&resourceType, &count); err != nil {
490 + continue
491 + }
492 +
493 + resourceType = strings.TrimSpace(resourceType)
494 +
495 + if !c.seenLockTypes[resourceType] {
496 + c.seenLockTypes[resourceType] = true
497 + c.addLockResourceCharts(resourceType)
498 + }
499 +
500 + resID := cleanResourceTypeName(resourceType)
501 + mx[fmt.Sprintf("locks_%s_count", resID)] = count
502 + }
503 +
504 + return rows.Err()
505 +}
506 +
507 +func (c *Collector) collectWaitStats(mx map[string]int64) error {
508 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
509 + defer cancel()
510 +
511 + rows, err := c.db.QueryContext(ctx, queryWaitStats)
512 + if err != nil {
513 + return fmt.Errorf("wait stats query failed: %v", err)
514 + }
515 + defer rows.Close()
516 +
517 + for rows.Next() {
518 + var waitType string
519 + var totalWait, resourceWait, signalWait, maxWait, waitingTasks int64
520 + if err := rows.Scan(&waitType, &totalWait, &resourceWait, &signalWait, &maxWait, &waitingTasks); err != nil {
521 + continue
522 + }
523 +
524 + waitType = strings.TrimSpace(waitType)
525 + waitCategory := getWaitCategory(waitType)
526 +
527 + if !c.seenWaitTypes[waitType] {
528 + c.seenWaitTypes[waitType] = true
529 + c.addWaitTypeCharts(waitType, waitCategory)
530 + }
531 +
532 + waitID := cleanWaitTypeName(waitType)
533 + mx[fmt.Sprintf("wait_%s_total_ms", waitID)] = totalWait
534 + mx[fmt.Sprintf("wait_%s_resource_ms", waitID)] = resourceWait
535 + mx[fmt.Sprintf("wait_%s_signal_ms", waitID)] = signalWait
536 + mx[fmt.Sprintf("wait_%s_max_ms", waitID)] = maxWait
537 + mx[fmt.Sprintf("wait_%s_tasks", waitID)] = waitingTasks
538 + }
539 +
540 + return rows.Err()
541 +}
542 +
543 +func (c *Collector) collectJobStatus(mx map[string]int64) error {
544 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
545 + defer cancel()
546 +
547 + rows, err := c.db.QueryContext(ctx, queryJobs)
548 + if err != nil {
549 + return fmt.Errorf("jobs query failed: %v", err)
550 + }
551 + defer rows.Close()
552 +
553 + for rows.Next() {
554 + var jobName string
555 + var enabled int64
556 + if err := rows.Scan(&jobName, &enabled); err != nil {
557 + continue
558 + }
559 +
560 + jobName = strings.TrimSpace(jobName)
561 +
562 + if !c.seenJobs[jobName] {
563 + c.seenJobs[jobName] = true
564 + c.addJobCharts(jobName)
565 + }
566 +
567 + jobID := cleanJobName(jobName)
568 + if enabled == 1 {
569 + mx[fmt.Sprintf("job_%s_enabled", jobID)] = 1
570 + mx[fmt.Sprintf("job_%s_disabled", jobID)] = 0
571 + } else {
572 + mx[fmt.Sprintf("job_%s_enabled", jobID)] = 0
573 + mx[fmt.Sprintf("job_%s_disabled", jobID)] = 1
574 + }
575 + }
576 +
577 + return rows.Err()
578 +}
579 +
580 +func (c *Collector) collectSQLErrors(mx map[string]int64) error {
581 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
582 + defer cancel()
583 +
584 + var value int64
585 + err := c.db.QueryRowContext(ctx, querySQLErrors).Scan(&value)
586 + if err != nil {
587 + return fmt.Errorf("sql errors query failed: %v", err)
588 + }
589 +
590 + mx["sql_errors_total"] = value
591 + return nil
592 +}
593 +
594 +func (c *Collector) collectDatabaseStatus(mx map[string]int64) error {
595 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
596 + defer cancel()
597 +
598 + rows, err := c.db.QueryContext(ctx, queryDatabaseStatus)
599 + if err != nil {
600 + return fmt.Errorf("database status query failed: %v", err)
601 + }
602 + defer rows.Close()
603 +
604 + for rows.Next() {
605 + var dbName string
606 + var state int64
607 + var isReadOnly bool
608 + if err := rows.Scan(&dbName, &state, &isReadOnly); err != nil {
609 + continue
610 + }
611 +
612 + dbName = strings.TrimSpace(dbName)
613 +
614 + if !c.seenDatabases[dbName] {
615 + c.seenDatabases[dbName] = true
616 + c.addDatabaseCharts(dbName)
617 + }
618 +
619 + dbID := cleanDatabaseName(dbName)
620 +
621 + // Database state values:
622 + // 0 = ONLINE, 1 = RESTORING, 2 = RECOVERING, 3 = RECOVERY_PENDING
623 + // 4 = SUSPECT, 5 = EMERGENCY, 6 = OFFLINE
624 + mx[fmt.Sprintf("database_%s_state_online", dbID)] = boolToInt(state == 0)
625 + mx[fmt.Sprintf("database_%s_state_restoring", dbID)] = boolToInt(state == 1)
626 + mx[fmt.Sprintf("database_%s_state_recovering", dbID)] = boolToInt(state == 2)
627 + mx[fmt.Sprintf("database_%s_state_pending", dbID)] = boolToInt(state == 3)
628 + mx[fmt.Sprintf("database_%s_state_suspect", dbID)] = boolToInt(state == 4)
629 + mx[fmt.Sprintf("database_%s_state_emergency", dbID)] = boolToInt(state == 5)
630 + mx[fmt.Sprintf("database_%s_state_offline", dbID)] = boolToInt(state == 6)
631 +
632 + // Read-only status
633 + mx[fmt.Sprintf("database_%s_read_only", dbID)] = boolToInt(isReadOnly)
634 + mx[fmt.Sprintf("database_%s_read_write", dbID)] = boolToInt(!isReadOnly)
635 + }
636 +
637 + return rows.Err()
638 +}
639 +
640 +func (c *Collector) collectReplicationStatus(mx map[string]int64) error {
641 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
642 + defer cancel()
643 +
644 + // First collect monitor data (status, latency, etc.)
645 + rows, err := c.db.QueryContext(ctx, queryReplicationStatus)
646 + if err != nil {
647 + // Replication may not be configured, don't treat as error
648 + c.Debugf("replication status query failed (may not be configured): %v", err)
649 + return nil
650 + }
651 + defer rows.Close()
652 +
653 + for rows.Next() {
654 + var pubDB, publication string
655 + var status, warning, worstLatency, bestLatency, avgLatency, runningAgents int64
656 + if err := rows.Scan(&pubDB, &publication, &status, &warning, &worstLatency, &bestLatency, &avgLatency, &runningAgents); err != nil {
657 + c.Debugf("replication scan error: %v", err)
658 + continue
659 + }
660 +
661 + pubDB = strings.TrimSpace(pubDB)
662 + publication = strings.TrimSpace(publication)
663 +
664 + pubKey := pubDB + "_" + publication
665 + if !c.seenReplications[pubKey] {
666 + c.seenReplications[pubKey] = true
667 + c.addReplicationCharts(pubDB, publication)
668 + }
669 +
670 + pubID := cleanPublicationName(pubDB, publication)
671 +
672 + // Decode status into 6 discrete states (matching C implementation)
673 + // 1=started, 2=succeeded, 3=in_progress, 4=idle, 5=retrying, 6=failed
674 + mx[fmt.Sprintf("replication_%s_status_started", pubID)] = boolToInt(status == 1)
675 + mx[fmt.Sprintf("replication_%s_status_succeeded", pubID)] = boolToInt(status == 2)
676 + mx[fmt.Sprintf("replication_%s_status_in_progress", pubID)] = boolToInt(status == 3)
677 + mx[fmt.Sprintf("replication_%s_status_idle", pubID)] = boolToInt(status == 4)
678 + mx[fmt.Sprintf("replication_%s_status_retrying", pubID)] = boolToInt(status == 5)
679 + mx[fmt.Sprintf("replication_%s_status_failed", pubID)] = boolToInt(status == 6)
680 +
681 + // Decode warning into 7 individual flags (bitfield)
682 + // Bit 0x01: expiration, 0x02: latency, 0x04: mergeexpiration
683 + // 0x08: mergeslowrunduration, 0x10: mergefastrunduration
684 + // 0x20: mergefastrunspeed, 0x40: mergeslowrunspeed
685 + mx[fmt.Sprintf("replication_%s_warning_expiration", pubID)] = boolToInt(warning&0x01 != 0)
686 + mx[fmt.Sprintf("replication_%s_warning_latency", pubID)] = boolToInt(warning&0x02 != 0)
687 + mx[fmt.Sprintf("replication_%s_warning_mergeexpiration", pubID)] = boolToInt(warning&0x04 != 0)
688 + mx[fmt.Sprintf("replication_%s_warning_mergeslowrunduration", pubID)] = boolToInt(warning&0x08 != 0)
689 + mx[fmt.Sprintf("replication_%s_warning_mergefastrunduration", pubID)] = boolToInt(warning&0x10 != 0)
690 + mx[fmt.Sprintf("replication_%s_warning_mergefastrunspeed", pubID)] = boolToInt(warning&0x20 != 0)
691 + mx[fmt.Sprintf("replication_%s_warning_mergeslowrunspeed", pubID)] = boolToInt(warning&0x40 != 0)
692 +
693 + mx[fmt.Sprintf("replication_%s_latency_avg", pubID)] = avgLatency
694 + // Handle the noLatencySentinel for "no value" (only bestLatency uses sentinel in query)
695 + if bestLatency == noLatencySentinel {
696 + bestLatency = 0
697 + }
698 + mx[fmt.Sprintf("replication_%s_latency_best", pubID)] = bestLatency
699 + mx[fmt.Sprintf("replication_%s_latency_worst", pubID)] = worstLatency
700 + mx[fmt.Sprintf("replication_%s_agents_running", pubID)] = runningAgents
701 + }
702 +
703 + if err := rows.Err(); err != nil {
704 + return err
705 + }
706 +
707 + // Now collect subscription counts from MSpublications/MSsubscriptions
708 + rows2, err := c.db.QueryContext(ctx, querySubscriptionCount)
709 + if err != nil {
710 + c.Debugf("subscription count query failed: %v", err)
711 + return nil
712 + }
713 + defer rows2.Close()
714 +
715 + for rows2.Next() {
716 + var pubDB, publication string
717 + var subCount int64
718 + if err := rows2.Scan(&pubDB, &publication, &subCount); err != nil {
719 + continue
720 + }
721 +
722 + pubDB = strings.TrimSpace(pubDB)
723 + publication = strings.TrimSpace(publication)
724 + pubID := cleanPublicationName(pubDB, publication)
725 + mx[fmt.Sprintf("replication_%s_subscriptions", pubID)] = subCount
726 + }
727 +
728 + return rows2.Err()
729 +}
730 +
731 +func boolToInt(b bool) int64 {
732 + if b {
733 + return 1
734 + }
735 + return 0
736 +}
737 +
738 +func (c *Collector) collectProcessMemory(mx map[string]int64) error {
739 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
740 + defer cancel()
741 +
742 + var resident, virtual, utilization, pageFaults int64
743 + err := c.db.QueryRowContext(ctx, queryProcessMemory).Scan(&resident, &virtual, &utilization, &pageFaults)
744 + if err != nil {
745 + return fmt.Errorf("process memory query failed: %v", err)
746 + }
747 +
748 + mx["process_memory_resident"] = resident
749 + mx["process_memory_virtual"] = virtual
750 + mx["process_memory_utilization"] = utilization
751 + mx["process_page_faults"] = pageFaults
752 + return nil
753 +}
754 +
755 +func (c *Collector) collectOSMemory(mx map[string]int64) error {
756 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
757 + defer cancel()
758 +
759 + var memUsed, memAvailable, pagefileUsed, pagefileAvailable int64
760 + err := c.db.QueryRowContext(ctx, queryOSMemory).Scan(&memUsed, &memAvailable, &pagefileUsed, &pagefileAvailable)
761 + if err != nil {
762 + return fmt.Errorf("OS memory query failed: %v", err)
763 + }
764 +
765 + mx["os_memory_used"] = memUsed
766 + mx["os_memory_available"] = memAvailable
767 + mx["os_pagefile_used"] = pagefileUsed
768 + mx["os_pagefile_available"] = pagefileAvailable
769 + return nil
770 +}
771 +
772 +func (c *Collector) collectIOStall(mx map[string]int64) error {
773 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
774 + defer cancel()
775 +
776 + rows, err := c.db.QueryContext(ctx, queryIOStall)
777 + if err != nil {
778 + return fmt.Errorf("IO stall query failed: %v", err)
779 + }
780 + defer rows.Close()
781 +
782 + for rows.Next() {
783 + var dbName string
784 + var readMs, writeMs, totalMs int64
785 + if err := rows.Scan(&dbName, &readMs, &writeMs, &totalMs); err != nil {
786 + continue
787 + }
788 +
789 + dbName = strings.TrimSpace(dbName)
790 + if dbName == "" {
791 + continue
792 + }
793 +
794 + if !c.seenDatabases[dbName] {
795 + c.seenDatabases[dbName] = true
796 + c.addDatabaseCharts(dbName)
797 + }
798 +
799 + dbID := cleanDatabaseName(dbName)
800 + mx[fmt.Sprintf("database_%s_io_stall_read", dbID)] = readMs
801 + mx[fmt.Sprintf("database_%s_io_stall_write", dbID)] = writeMs
802 + }
803 +
804 + return rows.Err()
805 +}
806 +
807 +func (c *Collector) collectLogGrowths(mx map[string]int64) error {
808 + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration())
809 + defer cancel()
810 +
811 + rows, err := c.db.QueryContext(ctx, queryLogGrowths)
812 + if err != nil {
813 + return fmt.Errorf("log growths query failed: %v", err)
814 + }
815 + defer rows.Close()
816 +
817 + for rows.Next() {
818 + var dbName string
819 + var growths int64
820 + if err := rows.Scan(&dbName, &growths); err != nil {
821 + continue
822 + }
823 +
824 + dbName = strings.TrimSpace(dbName)
825 + if dbName == "" {
826 + continue
827 + }
828 +
829 + if !c.seenDatabases[dbName] {
830 + c.seenDatabases[dbName] = true
831 + c.addDatabaseCharts(dbName)
832 + }
833 +
834 + dbID := cleanDatabaseName(dbName)
835 + mx[fmt.Sprintf("database_%s_log_growths", dbID)] = growths
836 + }
837 +
838 + return rows.Err()
839 +}
src/go/plugin/go.d/collector/mssql/collector.go new
+143
@@ -0,0 +1,143 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package mssql
4 +
5 +import (
6 + "context"
7 + "database/sql"
8 + _ "embed"
9 + "errors"
10 + "time"
11 +
12 + "github.com/netdata/netdata/go/plugins/pkg/confopt"
13 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
14 +
15 + _ "github.com/microsoft/go-mssqldb"
16 +)
17 +
18 +//go:embed "config_schema.json"
19 +var configSchema string
20 +
21 +func init() {
22 + module.Register("mssql", module.Creator{
23 + JobConfigSchema: configSchema,
24 + Defaults: module.Defaults{
25 + UpdateEvery: 10,
26 + },
27 + Create: func() module.Module { return New() },
28 + Config: func() any { return &Config{} },
29 + })
30 +}
31 +
32 +func New() *Collector {
33 + return &Collector{
34 + Config: Config{
35 + DSN: "sqlserver://localhost:1433",
36 + Timeout: confopt.Duration(time.Second * 5),
37 +
38 + CollectTransactions: true,
39 + CollectWaits: true,
40 + CollectLocks: true,
41 + CollectJobs: true,
42 + CollectBufferStats: true,
43 + CollectDatabaseSize: true,
44 + CollectUserConnections: true,
45 + CollectBlockedProcesses: true,
46 + CollectSQLErrors: true,
47 + CollectDatabaseStatus: true,
48 + CollectReplication: true,
49 + },
50 +
51 + charts: instanceCharts.Copy(),
52 +
53 + seenDatabases: make(map[string]bool),
54 + seenWaitTypes: make(map[string]bool),
55 + seenLockTypes: make(map[string]bool),
56 + seenLockStatsTypes: make(map[string]bool),
57 + seenJobs: make(map[string]bool),
58 + seenReplications: make(map[string]bool),
59 + }
60 +}
61 +
62 +type Config struct {
63 + Vnode string `yaml:"vnode,omitempty" json:"vnode"`
64 + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
65 + DSN string `yaml:"dsn" json:"dsn"`
66 + Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"`
67 +
68 + CollectTransactions bool `yaml:"collect_transactions,omitempty" json:"collect_transactions"`
69 + CollectWaits bool `yaml:"collect_waits,omitempty" json:"collect_waits"`
70 + CollectLocks bool `yaml:"collect_locks,omitempty" json:"collect_locks"`
71 + CollectJobs bool `yaml:"collect_jobs,omitempty" json:"collect_jobs"`
72 + CollectBufferStats bool `yaml:"collect_buffer_stats,omitempty" json:"collect_buffer_stats"`
73 + CollectDatabaseSize bool `yaml:"collect_database_size,omitempty" json:"collect_database_size"`
74 + CollectUserConnections bool `yaml:"collect_user_connections,omitempty" json:"collect_user_connections"`
75 + CollectBlockedProcesses bool `yaml:"collect_blocked_processes,omitempty" json:"collect_blocked_processes"`
76 + CollectSQLErrors bool `yaml:"collect_sql_errors,omitempty" json:"collect_sql_errors"`
77 + CollectDatabaseStatus bool `yaml:"collect_database_status,omitempty" json:"collect_database_status"`
78 + CollectReplication bool `yaml:"collect_replication,omitempty" json:"collect_replication"`
79 +}
80 +
81 +type Collector struct {
82 + module.Base
83 + Config `yaml:",inline" json:""`
84 +
85 + charts *module.Charts
86 +
87 + db *sql.DB
88 +
89 + version string
90 +
91 + seenDatabases map[string]bool
92 + seenWaitTypes map[string]bool
93 + seenLockTypes map[string]bool
94 + seenLockStatsTypes map[string]bool
95 + seenJobs map[string]bool
96 + seenReplications map[string]bool
97 +}
98 +
99 +func (c *Collector) Configuration() any {
100 + return c.Config
101 +}
102 +
103 +func (c *Collector) Init(context.Context) error {
104 + if c.DSN == "" {
105 + return errors.New("config: dsn not set")
106 + }
107 + c.Debugf("using DSN [%s]", c.DSN)
108 + return nil
109 +}
110 +
111 +func (c *Collector) Check(context.Context) error {
112 + mx, err := c.collect()
113 + if err != nil {
114 + return err
115 + }
116 + if len(mx) == 0 {
117 + return errors.New("no metrics collected")
118 + }
119 + return nil
120 +}
121 +
122 +func (c *Collector) Charts() *module.Charts {
123 + return c.charts
124 +}
125 +
126 +func (c *Collector) Collect(context.Context) map[string]int64 {
127 + mx, err := c.collect()
128 + if err != nil {
129 + c.Error(err)
130 + return nil
131 + }
132 + return mx
133 +}
134 +
135 +func (c *Collector) Cleanup(context.Context) {
136 + if c.db == nil {
137 + return
138 + }
139 + if err := c.db.Close(); err != nil {
140 + c.Errorf("cleanup: error closing database connection: %v", err)
141 + }
142 + c.db = nil
143 +}
src/go/plugin/go.d/collector/mssql/config_schema.json new
+152
@@ -0,0 +1,152 @@
1 +{
2 + "jsonSchema": {
3 + "$schema": "http://json-schema.org/draft-07/schema#",
4 + "title": "Microsoft SQL Server collector configuration.",
5 + "type": "object",
6 + "properties": {
7 + "update_every": {
8 + "title": "Update every",
9 + "description": "Data collection interval, measured in seconds.",
10 + "type": "integer",
11 + "minimum": 1,
12 + "default": 10
13 + },
14 + "dsn": {
15 + "title": "DSN",
16 + "description": "Microsoft SQL Server [Data Source Name](https://github.com/microsoft/go-mssqldb#connection-parameters-and-dsn).",
17 + "type": "string",
18 + "default": "sqlserver://localhost:1433"
19 + },
20 + "vnode": {
21 + "title": "Virtual Node",
22 + "description": "Associates this data collection job with a [Virtual Node](https://learn.netdata.cloud/docs/netdata-agent/configuration/organize-systems-metrics-and-alerts#virtual-nodes).",
23 + "type": "string",
24 + "default": ""
25 + },
26 + "timeout": {
27 + "title": "Timeout",
28 + "description": "Query timeout, in seconds.",
29 + "type": "number",
30 + "minimum": 0.5,
31 + "default": 5
32 + },
33 + "collect_transactions": {
34 + "title": "Collect transactions",
35 + "description": "Collect transaction metrics from performance counters.",
36 + "type": "boolean",
37 + "default": true
38 + },
39 + "collect_waits": {
40 + "title": "Collect wait statistics",
41 + "description": "Collect wait statistics from sys.dm_os_wait_stats.",
42 + "type": "boolean",
43 + "default": true
44 + },
45 + "collect_locks": {
46 + "title": "Collect locks",
47 + "description": "Collect lock metrics from sys.dm_tran_locks.",
48 + "type": "boolean",
49 + "default": true
50 + },
51 + "collect_jobs": {
52 + "title": "Collect jobs",
53 + "description": "Collect SQL Agent job status.",
54 + "type": "boolean",
55 + "default": true
56 + },
57 + "collect_buffer_stats": {
58 + "title": "Collect buffer stats",
59 + "description": "Collect buffer manager statistics.",
60 + "type": "boolean",
61 + "default": true
62 + },
63 + "collect_database_size": {
64 + "title": "Collect database size",
65 + "description": "Collect data file sizes for each database.",
66 + "type": "boolean",
67 + "default": true
68 + },
69 + "collect_user_connections": {
70 + "title": "Collect user connections",
71 + "description": "Collect user connection counts.",
72 + "type": "boolean",
73 + "default": true
74 + },
75 + "collect_blocked_processes": {
76 + "title": "Collect blocked processes",
77 + "description": "Collect blocked process count.",
78 + "type": "boolean",
79 + "default": true
80 + },
81 + "collect_sql_errors": {
82 + "title": "Collect SQL errors",
83 + "description": "Collect SQL error statistics.",
84 + "type": "boolean",
85 + "default": true
86 + },
87 + "collect_database_status": {
88 + "title": "Collect database status",
89 + "description": "Collect database state (online/offline/etc.) and read-only status.",
90 + "type": "boolean",
91 + "default": true
92 + },
93 + "collect_replication": {
94 + "title": "Collect replication",
95 + "description": "Collect replication monitoring metrics (requires distribution database).",
96 + "type": "boolean",
97 + "default": true
98 + }
99 + },
100 + "required": [
101 + "dsn"
102 + ],
103 + "additionalProperties": false,
104 + "patternProperties": {
105 + "^name$": {}
106 + }
107 + },
108 + "uiSchema": {
109 + "uiOptions": {
110 + "fullPage": true
111 + },
112 + "dsn": {
113 + "ui:placeholder": "sqlserver://user:password@localhost:1433"
114 + },
115 + "vnode": {
116 + "ui:help": "Optional: Group metrics under a virtual node for multi-instance setups."
117 + },
118 + "timeout": {
119 + "ui:help": "Accepts decimals for sub-second granularity (e.g., 0.5 for 500ms)."
120 + },
121 + "ui:flavour": "tabs",
122 + "ui:options": {
123 + "tabs": [
124 + {
125 + "title": "Base",
126 + "fields": [
127 + "update_every",
128 + "dsn",
129 + "timeout",
130 + "vnode"
131 + ]
132 + },
133 + {
134 + "title": "Collection options",
135 + "fields": [
136 + "collect_transactions",
137 + "collect_waits",
138 + "collect_locks",
139 + "collect_jobs",
140 + "collect_buffer_stats",
141 + "collect_database_size",
142 + "collect_user_connections",
143 + "collect_blocked_processes",
144 + "collect_sql_errors",
145 + "collect_database_status",
146 + "collect_replication"
147 + ]
148 + }
149 + ]
150 + }
151 + }
152 +}
src/go/plugin/go.d/collector/mssql/metadata.yaml new
+644
@@ -0,0 +1,644 @@
1 +plugin_name: go.d.plugin
2 +modules:
3 + - &module
4 + meta: &meta
5 + id: collector-go.d.plugin-mssql
6 + plugin_name: go.d.plugin
7 + module_name: mssql
8 + monitored_instance:
9 + name: Microsoft SQL Server
10 + link: https://www.microsoft.com/en-us/sql-server
11 + categories:
12 + - data-collection.database-servers
13 + icon_filename: mssql.svg
14 + related_resources:
15 + integrations:
16 + list:
17 + - plugin_name: apps.plugin
18 + module_name: apps
19 + - plugin_name: cgroups.plugin
20 + module_name: cgroups
21 + info_provided_to_referring_integrations:
22 + description: ""
23 + keywords:
24 + - "db"
25 + - "database"
26 + - "mssql"
27 + - "sql server"
28 + - "microsoft"
29 + most_popular: false
30 + overview:
31 + multi_instance: true
32 + data_collection:
33 + metrics_description: |
34 + This collector monitors the health and performance of Microsoft SQL Server instances.
35 +
36 + It collects metrics from:
37 + - Performance counters (buffer manager, memory manager, SQL statistics)
38 + - Dynamic management views (DMVs) for wait statistics, locks, and sessions
39 + - Per-database transaction and lock statistics
40 + - SQL Server Agent job status (if permissions allow)
41 + method_description: |
42 + It connects to the SQL Server instance via TCP using the go-mssqldb driver and executes queries against:
43 +
44 + - `sys.dm_os_performance_counters` - Performance counter values
45 + - `sys.dm_exec_sessions` - Connection information
46 + - `sys.dm_os_wait_stats` - Wait statistics
47 + - `sys.dm_tran_locks` - Lock information
48 + - `sys.dm_io_virtual_file_stats` - I/O stall (latency) statistics
49 + - `sys.dm_os_process_memory` - SQL Server process memory
50 + - `sys.dm_os_sys_memory` - OS physical memory and page file
51 + - `sys.master_files` - Database file sizes
52 + - `msdb.dbo.sysjobs` - SQL Agent job status (optional)
53 + default_behavior:
54 + auto_detection:
55 + description: |
56 + By default, it tries to connect to SQL Server on localhost:1433 without authentication.
57 + You must configure proper credentials for monitoring.
58 + limits:
59 + description: ""
60 + performance_impact:
61 + description: |
62 + The collector executes lightweight queries against system views.
63 + Most queries complete in milliseconds and have minimal impact on server performance.
64 + additional_permissions:
65 + description: |
66 + The monitoring user requires the VIEW SERVER STATE permission to access DMVs.
67 + For SQL Agent job monitoring, access to the msdb database is required.
68 + supported_platforms:
69 + include: []
70 + exclude: []
71 + setup:
72 + prerequisites:
73 + list:
74 + - title: Create monitoring user
75 + description: |
76 + Create a SQL Server login with VIEW SERVER STATE permission:
77 +
78 + ```sql
79 + -- Create login
80 + CREATE LOGIN netdata_user WITH PASSWORD = 'YourStrongPassword!';
81 +
82 + -- Grant VIEW SERVER STATE (required for DMVs)
83 + GRANT VIEW SERVER STATE TO netdata_user;
84 +
85 + -- Optional: Grant access to msdb for SQL Agent job monitoring
86 + USE msdb;
87 + CREATE USER netdata_user FOR LOGIN netdata_user;
88 + GRANT SELECT ON dbo.sysjobs TO netdata_user;
89 +
90 + -- Optional: Grant access to distribution database for replication monitoring
91 + -- (only if replication is configured)
92 + USE distribution;
93 + CREATE USER netdata_user FOR LOGIN netdata_user;
94 + GRANT SELECT ON dbo.MSreplication_monitordata TO netdata_user;
95 + GRANT SELECT ON dbo.MSpublications TO netdata_user;
96 + GRANT SELECT ON dbo.MSsubscriptions TO netdata_user;
97 + ```
98 +
99 + **Required permissions:**
100 + - `VIEW SERVER STATE` - Access to dynamic management views
101 +
102 + **Optional permissions:**
103 + - `SELECT on msdb.dbo.sysjobs` - SQL Agent job status monitoring
104 + - `SELECT on distribution.dbo.MSreplication_monitordata` - Replication monitoring
105 + - `SELECT on distribution.dbo.MSpublications` - Publication information
106 + - `SELECT on distribution.dbo.MSsubscriptions` - Subscription counts
107 + configuration:
108 + file:
109 + name: go.d/mssql.conf
110 + options:
111 + description: |
112 + The following options can be defined globally: update_every, autodetection_retry.
113 + folding:
114 + title: Config options
115 + enabled: true
116 + list:
117 + - name: update_every
118 + description: Data collection interval (seconds).
119 + default_value: 10
120 + required: false
121 + group: Collection
122 + - name: autodetection_retry
123 + description: Autodetection retry interval (seconds). Set 0 to disable.
124 + default_value: 0
125 + required: false
126 + group: Collection
127 +
128 + - name: dsn
129 + description: "SQL Server DSN (Data Source Name). See [DSN syntax](https://github.com/microsoft/go-mssqldb#connection-parameters-and-dsn)."
130 + default_value: "sqlserver://localhost:1433"
131 + required: true
132 + group: Target
133 + - name: timeout
134 + description: Query timeout (seconds).
135 + default_value: 5
136 + required: false
137 + group: Target
138 +
139 + - name: collect_transactions
140 + description: Collect per-database transaction metrics.
141 + default_value: true
142 + required: false
143 + group: Collection Options
144 + - name: collect_waits
145 + description: Collect wait statistics from sys.dm_os_wait_stats.
146 + default_value: true
147 + required: false
148 + group: Collection Options
149 + - name: collect_locks
150 + description: Collect lock metrics from sys.dm_tran_locks.
151 + default_value: true
152 + required: false
153 + group: Collection Options
154 + - name: collect_jobs
155 + description: Collect SQL Agent job status.
156 + default_value: true
157 + required: false
158 + group: Collection Options
159 + - name: collect_buffer_stats
160 + description: Collect buffer manager and memory statistics.
161 + default_value: true
162 + required: false
163 + group: Collection Options
164 + - name: collect_database_size
165 + description: Collect data file sizes for each database.
166 + default_value: true
167 + required: false
168 + group: Collection Options
169 + - name: collect_user_connections
170 + description: Collect user connection counts.
171 + default_value: true
172 + required: false
173 + group: Collection Options
174 + - name: collect_blocked_processes
175 + description: Collect blocked process count.
176 + default_value: true
177 + required: false
178 + group: Collection Options
179 + - name: collect_sql_errors
180 + description: Collect SQL error statistics.
181 + default_value: true
182 + required: false
183 + group: Collection Options
184 + - name: collect_database_status
185 + description: Collect database state and read-only status.
186 + default_value: true
187 + required: false
188 + group: Collection Options
189 + - name: collect_replication
190 + description: Collect replication monitoring metrics (requires distribution database).
191 + default_value: true
192 + required: false
193 + group: Collection Options
194 +
195 + - name: vnode
196 + description: Associates this data collection job with a [Virtual Node](https://learn.netdata.cloud/docs/netdata-agent/configuration/organize-systems-metrics-and-alerts#virtual-nodes).
197 + default_value: ""
198 + required: false
199 + group: Virtual Node
200 + examples:
201 + folding:
202 + title: Config
203 + enabled: true
204 + list:
205 + - name: Basic configuration
206 + description: Connect to local SQL Server with SQL authentication.
207 + config: |
208 + jobs:
209 + - name: local
210 + dsn: "sqlserver://netdata_user:password@localhost:1433"
211 + - name: Windows Authentication
212 + description: Connect using Windows integrated authentication.
213 + config: |
214 + jobs:
215 + - name: local
216 + dsn: "sqlserver://localhost:1433?trusted_connection=yes"
217 + - name: Named instance
218 + description: Connect to a named SQL Server instance.
219 + config: |
220 + jobs:
221 + - name: named_instance
222 + dsn: "sqlserver://netdata_user:password@localhost/INSTANCENAME"
223 + - name: Remote server
224 + description: Connect to a remote SQL Server.
225 + config: |
226 + jobs:
227 + - name: remote
228 + dsn: "sqlserver://netdata_user:password@192.168.1.100:1433"
229 + - name: Multi-instance
230 + description: |
231 + > **Note**: When you define multiple jobs, their names must be unique.
232 +
233 + Monitoring multiple SQL Server instances.
234 + config: |
235 + jobs:
236 + - name: production
237 + dsn: "sqlserver://netdata_user:password@prod-sql:1433"
238 +
239 + - name: development
240 + dsn: "sqlserver://netdata_user:password@dev-sql:1433"
241 + troubleshooting:
242 + problems:
243 + list:
244 + - name: Connection refused
245 + description: |
246 + Ensure SQL Server is running and accepting TCP connections on the configured port.
247 + Check that the SQL Server Browser service is running if using named instances.
248 + - name: Login failed
249 + description: |
250 + Verify the username and password in the DSN are correct.
251 + Ensure SQL Server is configured for mixed mode authentication if using SQL logins.
252 + - name: Permission denied
253 + description: |
254 + The monitoring user needs VIEW SERVER STATE permission.
255 + Grant it with: `GRANT VIEW SERVER STATE TO netdata_user;`
256 + alerts: []
257 + metrics:
258 + folding:
259 + title: Metrics
260 + enabled: false
261 + description: ""
262 + availability:
263 + - SQL Server 2016+
264 + - Azure SQL Database
265 + scopes:
266 + - name: global
267 + description: These metrics refer to the entire SQL Server instance.
268 + labels: []
269 + metrics:
270 + - name: mssql.user_connections
271 + description: User Connections
272 + unit: connections
273 + chart_type: line
274 + dimensions:
275 + - name: user
276 + - name: mssql.session_connections
277 + description: Session Connections
278 + unit: connections
279 + chart_type: line
280 + dimensions:
281 + - name: user
282 + - name: internal
283 + - name: mssql.blocked_processes
284 + description: Blocked Processes
285 + unit: processes
286 + chart_type: line
287 + dimensions:
288 + - name: blocked
289 + - name: mssql.batch_requests
290 + description: Batch Requests
291 + unit: requests/s
292 + chart_type: line
293 + dimensions:
294 + - name: batch
295 + - name: mssql.compilations
296 + description: SQL Compilations
297 + unit: compilations/s
298 + chart_type: line
299 + dimensions:
300 + - name: compilations
301 + - name: mssql.recompilations
302 + description: SQL Re-Compilations
303 + unit: recompilations/s
304 + chart_type: line
305 + dimensions:
306 + - name: recompilations
307 + - name: mssql.auto_param_attempts
308 + description: Auto-Parameterization Attempts
309 + unit: attempts/s
310 + chart_type: line
311 + dimensions:
312 + - name: total
313 + - name: safe
314 + - name: failed
315 + - name: mssql.sql_errors
316 + description: SQL Errors
317 + unit: errors/s
318 + chart_type: line
319 + dimensions:
320 + - name: errors
321 + - name: mssql.buffer_cache_hit_ratio
322 + description: Buffer Cache Hit Ratio
323 + unit: percentage
324 + chart_type: line
325 + dimensions:
326 + - name: hit_ratio
327 + - name: mssql.buffer_page_life_expectancy
328 + description: Page Life Expectancy
329 + unit: seconds
330 + chart_type: line
331 + dimensions:
332 + - name: life_expectancy
333 + - name: mssql.buffer_page_iops
334 + description: Buffer Page I/O
335 + unit: pages/s
336 + chart_type: line
337 + dimensions:
338 + - name: read
339 + - name: written
340 + - name: mssql.buffer_checkpoint_pages
341 + description: Buffer Checkpoint Pages Flushed
342 + unit: pages/s
343 + chart_type: line
344 + dimensions:
345 + - name: flushed
346 + - name: mssql.buffer_page_lookups
347 + description: Buffer Page Lookups
348 + unit: lookups/s
349 + chart_type: line
350 + dimensions:
351 + - name: lookups
352 + - name: mssql.buffer_lazy_writes
353 + description: Buffer Lazy Writes
354 + unit: writes/s
355 + chart_type: line
356 + dimensions:
357 + - name: lazy_writes
358 + - name: mssql.memory_total
359 + description: Total Server Memory
360 + unit: bytes
361 + chart_type: line
362 + dimensions:
363 + - name: memory
364 + - name: mssql.memory_connection
365 + description: Connection Memory
366 + unit: bytes
367 + chart_type: line
368 + dimensions:
369 + - name: memory
370 + - name: mssql.memory_pending_grants
371 + description: Pending Memory Grants
372 + unit: processes
373 + chart_type: line
374 + dimensions:
375 + - name: pending
376 + - name: mssql.memory_external_benefit
377 + description: External Benefit of Memory
378 + unit: benefit
379 + chart_type: line
380 + dimensions:
381 + - name: benefit
382 + - name: mssql.page_splits
383 + description: Page Splits
384 + unit: splits/s
385 + chart_type: line
386 + dimensions:
387 + - name: page
388 + - name: mssql.process_memory_resident
389 + description: SQL Server Process Resident Memory (Working Set)
390 + unit: bytes
391 + chart_type: line
392 + dimensions:
393 + - name: resident
394 + - name: mssql.process_memory_virtual
395 + description: SQL Server Process Virtual Memory Committed
396 + unit: bytes
397 + chart_type: line
398 + dimensions:
399 + - name: virtual
400 + - name: mssql.process_memory_utilization
401 + description: SQL Server Process Memory Utilization
402 + unit: percentage
403 + chart_type: line
404 + dimensions:
405 + - name: utilization
406 + - name: mssql.process_page_faults
407 + description: SQL Server Process Page Faults
408 + unit: faults
409 + chart_type: line
410 + dimensions:
411 + - name: page_faults
412 + - name: mssql.os_memory
413 + description: OS Physical Memory
414 + unit: bytes
415 + chart_type: stacked
416 + dimensions:
417 + - name: used
418 + - name: available
419 + - name: mssql.os_pagefile
420 + description: OS Page File
421 + unit: bytes
422 + chart_type: stacked
423 + dimensions:
424 + - name: used
425 + - name: available
426 + - name: database
427 + description: These metrics refer to individual databases.
428 + labels:
429 + - name: database
430 + description: Database name
431 + metrics:
432 + - name: mssql.database_active_transactions
433 + description: Active Transactions
434 + unit: transactions
435 + chart_type: line
436 + dimensions:
437 + - name: active
438 + - name: mssql.database_transactions
439 + description: Transactions
440 + unit: transactions/s
441 + chart_type: line
442 + dimensions:
443 + - name: transactions
444 + - name: mssql.database_write_transactions
445 + description: Write Transactions
446 + unit: transactions/s
447 + chart_type: line
448 + dimensions:
449 + - name: write
450 + - name: mssql.database_log_flushes
451 + description: Log Flushes
452 + unit: flushes/s
453 + chart_type: line
454 + dimensions:
455 + - name: flushes
456 + - name: mssql.database_log_flushed
457 + description: Log Bytes Flushed
458 + unit: bytes/s
459 + chart_type: line
460 + dimensions:
461 + - name: flushed
462 + - name: mssql.database_log_growths
463 + description: Log Growths
464 + unit: growths
465 + chart_type: line
466 + dimensions:
467 + - name: growths
468 + - name: mssql.database_io_stall
469 + description: I/O Stall Time
470 + unit: ms
471 + chart_type: line
472 + dimensions:
473 + - name: read
474 + - name: write
475 + - name: mssql.database_data_file_size
476 + description: Data File Size
477 + unit: bytes
478 + chart_type: line
479 + dimensions:
480 + - name: size
481 + - name: mssql.database_backup_restore_throughput
482 + description: Backup/Restore Throughput
483 + unit: bytes/s
484 + chart_type: line
485 + dimensions:
486 + - name: throughput
487 + - name: mssql.database_state
488 + description: Database State
489 + unit: state
490 + chart_type: line
491 + dimensions:
492 + - name: online
493 + - name: restoring
494 + - name: recovering
495 + - name: pending
496 + - name: suspect
497 + - name: emergency
498 + - name: offline
499 + - name: mssql.database_read_only
500 + description: Database Read-Only Status
501 + unit: status
502 + chart_type: line
503 + dimensions:
504 + - name: read_only
505 + - name: read_write
506 + - name: lock stats
507 + description: These metrics refer to lock statistics by lock resource type (from performance counters).
508 + labels:
509 + - name: resource
510 + description: Lock resource type (Database, File, Object, Page, Key, Extent, RID, HoBT, etc.)
511 + metrics:
512 + - name: mssql.lock_stats_deadlocks
513 + description: Deadlocks by Resource Type
514 + unit: deadlocks/s
515 + chart_type: line
516 + dimensions:
517 + - name: deadlocks
518 + - name: mssql.lock_stats_waits
519 + description: Lock Waits by Resource Type
520 + unit: waits/s
521 + chart_type: line
522 + dimensions:
523 + - name: waits
524 + - name: mssql.lock_stats_timeouts
525 + description: Lock Timeouts by Resource Type
526 + unit: timeouts/s
527 + chart_type: line
528 + dimensions:
529 + - name: timeouts
530 + - name: mssql.lock_stats_requests
531 + description: Lock Requests by Resource Type
532 + unit: requests/s
533 + chart_type: line
534 + dimensions:
535 + - name: requests
536 + - name: lock resource
537 + description: These metrics refer to lock resource types (from sys.dm_tran_locks).
538 + labels:
539 + - name: resource
540 + description: Lock resource type (Database, File, Object, Page, Key, etc.)
541 + metrics:
542 + - name: mssql.locks_by_resource
543 + description: Lock Count by Resource Type
544 + unit: locks
545 + chart_type: line
546 + dimensions:
547 + - name: locks
548 + - name: wait type
549 + description: These metrics refer to individual wait types (from sys.dm_os_wait_stats).
550 + labels:
551 + - name: wait_type
552 + description: Wait type name
553 + - name: wait_category
554 + description: Wait category (CPU, Lock, Latch, Buffer IO, etc.)
555 + metrics:
556 + - name: mssql.wait_total_time
557 + description: Total Wait Time
558 + unit: ms
559 + chart_type: line
560 + dimensions:
561 + - name: duration
562 + - name: mssql.wait_resource_time
563 + description: Resource Wait Time
564 + unit: ms
565 + chart_type: line
566 + dimensions:
567 + - name: duration
568 + - name: mssql.wait_signal_time
569 + description: Signal Wait Time
570 + unit: ms
571 + chart_type: line
572 + dimensions:
573 + - name: duration
574 + - name: mssql.wait_max_time
575 + description: Maximum Wait Time
576 + unit: ms
577 + chart_type: line
578 + dimensions:
579 + - name: max_time
580 + - name: mssql.wait_count
581 + description: Wait Count
582 + unit: waits/s
583 + chart_type: line
584 + dimensions:
585 + - name: waits
586 + - name: job
587 + description: These metrics refer to SQL Server Agent jobs.
588 + labels:
589 + - name: job_name
590 + description: Job name
591 + metrics:
592 + - name: mssql.job_status
593 + description: Job Status
594 + unit: status
595 + chart_type: line
596 + dimensions:
597 + - name: enabled
598 + - name: disabled
599 + - name: replication
600 + description: These metrics refer to SQL Server replication publications.
601 + labels:
602 + - name: publisher_db
603 + description: Publisher database name
604 + - name: publication
605 + description: Publication name
606 + metrics:
607 + - name: mssql.replication_status
608 + description: Replication Status
609 + unit: status
610 + chart_type: line
611 + dimensions:
612 + - name: started
613 + - name: succeeded
614 + - name: in_progress
615 + - name: idle
616 + - name: retrying
617 + - name: failed
618 + - name: mssql.replication_warning
619 + description: Replication Warnings
620 + unit: flags
621 + chart_type: line
622 + dimensions:
623 + - name: expiration
624 + - name: latency
625 + - name: merge_expiration
626 + - name: merge_slow_duration
627 + - name: merge_fast_duration
628 + - name: merge_fast_speed
629 + - name: merge_slow_speed
630 + - name: mssql.replication_latency
631 + description: Replication Latency
632 + unit: seconds
633 + chart_type: line
634 + dimensions:
635 + - name: average
636 + - name: best
637 + - name: worst
638 + - name: mssql.replication_subscriptions
639 + description: Replication Subscriptions
640 + unit: subscriptions
641 + chart_type: line
642 + dimensions:
643 + - name: total
644 + - name: agents_running
src/go/plugin/go.d/collector/mssql/mssql_test.go new
+50
@@ -0,0 +1,50 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package mssql
4 +
5 +import (
6 + "context"
7 + "testing"
8 +
9 + "github.com/stretchr/testify/assert"
10 +)
11 +
12 +func TestCollector_Init(t *testing.T) {
13 + c := New()
14 + c.DSN = "sqlserver://localhost:1433"
15 +
16 + assert.NoError(t, c.Init(context.Background()))
17 +}
18 +
19 +func TestCollector_Init_EmptyDSN(t *testing.T) {
20 + c := New()
21 + c.DSN = ""
22 +
23 + assert.Error(t, c.Init(context.Background()))
24 +}
25 +
26 +func TestCollector_Configuration(t *testing.T) {
27 + c := New()
28 +
29 + // Verify defaults
30 + assert.Equal(t, "sqlserver://localhost:1433", c.DSN)
31 + assert.True(t, c.CollectTransactions)
32 + assert.True(t, c.CollectWaits)
33 + assert.True(t, c.CollectLocks)
34 + assert.True(t, c.CollectJobs)
35 + assert.True(t, c.CollectBufferStats)
36 + assert.True(t, c.CollectDatabaseSize)
37 + assert.True(t, c.CollectUserConnections)
38 + assert.True(t, c.CollectBlockedProcesses)
39 + assert.True(t, c.CollectSQLErrors)
40 + assert.True(t, c.CollectDatabaseStatus)
41 + assert.True(t, c.CollectReplication)
42 +}
43 +
44 +func TestCollector_Charts(t *testing.T) {
45 + c := New()
46 +
47 + charts := c.Charts()
48 + assert.NotNil(t, charts)
49 + assert.NotEmpty(t, *charts)
50 +}
src/go/plugin/go.d/collector/mssql/queries.go new
+594
@@ -0,0 +1,594 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package mssql
4 +
5 +// queryVersion retrieves SQL Server version info
6 +const queryVersion = `
7 +SELECT SERVERPROPERTY('ProductVersion') AS version;
8 +`
9 +
10 +// queryUserConnections counts user vs system connections
11 +const queryUserConnections = `
12 +SELECT
13 + SUM(CASE WHEN is_user_process = 1 THEN 1 ELSE 0 END) AS user_connections,
14 + SUM(CASE WHEN is_user_process = 0 THEN 1 ELSE 0 END) AS system_connections
15 +FROM sys.dm_exec_sessions;
16 +`
17 +
18 +// queryBlockedProcesses counts blocked sessions
19 +const queryBlockedProcesses = `
20 +SELECT COUNT(DISTINCT session_id) AS blocked_sessions
21 +FROM sys.dm_exec_requests
22 +WHERE blocking_session_id <> 0;
23 +`
24 +
25 +// queryBatchRequests gets batch request rate from performance counters
26 +const queryBatchRequests = `
27 +SELECT cntr_value
28 +FROM sys.dm_os_performance_counters
29 +WHERE counter_name = 'Batch Requests/sec'
30 + AND object_name LIKE '%SQL Statistics%';
31 +`
32 +
33 +// queryCompilations gets SQL compilation metrics
34 +const queryCompilations = `
35 +SELECT counter_name, cntr_value
36 +FROM sys.dm_os_performance_counters
37 +WHERE object_name LIKE '%SQL Statistics%'
38 + AND counter_name IN (
39 + 'SQL Compilations/sec',
40 + 'SQL Re-Compilations/sec',
41 + 'Auto-Param Attempts/sec',
42 + 'Safe Auto-Params/sec',
43 + 'Failed Auto-Params/sec'
44 + );
45 +`
46 +
47 +// queryBufferManager gets buffer manager metrics
48 +const queryBufferManager = `
49 +SELECT counter_name, cntr_value
50 +FROM sys.dm_os_performance_counters
51 +WHERE object_name LIKE '%Buffer Manager%'
52 + AND counter_name IN (
53 + 'Page reads/sec',
54 + 'Page writes/sec',
55 + 'Buffer cache hit ratio',
56 + 'Buffer cache hit ratio base',
57 + 'Checkpoint pages/sec',
58 + 'Page life expectancy',
59 + 'Lazy writes/sec',
60 + 'Page lookups/sec'
61 + );
62 +`
63 +
64 +// queryMemoryManager gets memory manager metrics
65 +const queryMemoryManager = `
66 +SELECT counter_name, cntr_value
67 +FROM sys.dm_os_performance_counters
68 +WHERE object_name LIKE '%Memory Manager%'
69 + AND counter_name IN (
70 + 'Total Server Memory (KB)',
71 + 'Connection Memory (KB)',
72 + 'Memory Grants Pending',
73 + 'External benefit of memory'
74 + );
75 +`
76 +
77 +// queryAccessMethods gets access method metrics
78 +const queryAccessMethods = `
79 +SELECT cntr_value
80 +FROM sys.dm_os_performance_counters
81 +WHERE object_name LIKE '%Access Methods%'
82 + AND counter_name = 'Page Splits/sec';
83 +`
84 +
85 +// queryDatabaseCounters gets per-database performance counters
86 +const queryDatabaseCounters = `
87 +SELECT
88 + RTRIM(instance_name) AS database_name,
89 + counter_name,
90 + cntr_value
91 +FROM sys.dm_os_performance_counters
92 +WHERE object_name LIKE '%Databases%'
93 + AND instance_name NOT IN ('_Total', 'mssqlsystemresource')
94 + AND counter_name IN (
95 + 'Active Transactions',
96 + 'Transactions/sec',
97 + 'Write Transactions/sec',
98 + 'Backup/Restore Throughput/sec',
99 + 'Log Bytes Flushed/sec',
100 + 'Log Flushes/sec'
101 + );
102 +`
103 +
104 +// queryDatabaseLocks gets per-database lock metrics
105 +const queryDatabaseLocks = `
106 +SELECT
107 + RTRIM(instance_name) AS database_name,
108 + counter_name,
109 + cntr_value
110 +FROM sys.dm_os_performance_counters
111 +WHERE object_name LIKE '%Locks%'
112 + AND instance_name NOT IN ('_Total')
113 + AND counter_name IN (
114 + 'Number of Deadlocks/sec',
115 + 'Lock Waits/sec',
116 + 'Lock Timeouts/sec',
117 + 'Lock Requests/sec'
118 + );
119 +`
120 +
121 +// queryDatabaseSize gets the size of data files for each database
122 +const queryDatabaseSize = `
123 +SELECT
124 + DB_NAME(database_id) AS database_name,
125 + SUM(size) * 8 * 1024 AS size_bytes
126 +FROM sys.master_files
127 +WHERE type = 0 -- data files only
128 + AND database_id > 4 -- exclude system databases
129 +GROUP BY database_id;
130 +`
131 +
132 +// queryLocksByResource gets lock counts grouped by resource type
133 +const queryLocksByResource = `
134 +SELECT
135 + resource_type,
136 + COUNT(*) AS lock_count
137 +FROM sys.dm_tran_locks
138 +WHERE resource_database_id > 4 -- exclude system databases
139 +GROUP BY resource_type;
140 +`
141 +
142 +// queryWaitStats gets wait statistics with category mapping
143 +const queryWaitStats = `
144 +SELECT
145 + ws.[wait_type],
146 + ws.[wait_time_ms] AS total_wait_ms,
147 + ws.[wait_time_ms] - ws.[signal_wait_time_ms] AS resource_wait_ms,
148 + ws.[signal_wait_time_ms] AS signal_wait_ms,
149 + ws.[max_wait_time_ms] AS max_wait_ms,
150 + ws.[waiting_tasks_count] AS waiting_tasks
151 +FROM sys.dm_os_wait_stats AS ws WITH(NOLOCK)
152 +WHERE ws.[waiting_tasks_count] > 0
153 + AND ws.[wait_time_ms] > 100
154 + AND ws.[wait_type] NOT IN (
155 + 'BROKER_EVENTHANDLER', 'BROKER_RECEIVE_WAITFOR', 'BROKER_TASK_STOP',
156 + 'BROKER_TO_FLUSH', 'BROKER_TRANSMITTER', 'CHECKPOINT_QUEUE',
157 + 'CHKPT', 'CLR_AUTO_EVENT', 'CLR_MANUAL_EVENT', 'CLR_SEMAPHORE',
158 + 'DBMIRROR_DBM_EVENT', 'DBMIRROR_EVENTS_QUEUE', 'DBMIRROR_WORKER_QUEUE',
159 + 'DBMIRRORING_CMD', 'DIRTY_PAGE_POLL', 'DISPATCHER_QUEUE_SEMAPHORE',
160 + 'EXECSYNC', 'FSAGENT', 'FT_IFTS_SCHEDULER_IDLE_WAIT',
161 + 'FT_IFTSHC_MUTEX', 'HADR_CLUSAPI_CALL', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION',
162 + 'HADR_LOGCAPTURE_WAIT', 'HADR_NOTIFICATION_DEQUEUE', 'HADR_TIMER_TASK',
163 + 'HADR_WORK_QUEUE', 'KSOURCE_WAKEUP', 'LAZYWRITER_SLEEP',
164 + 'LOGMGR_QUEUE', 'MEMORY_ALLOCATION_EXT', 'ONDEMAND_TASK_QUEUE',
165 + 'PREEMPTIVE_OS_AUTHENTICATIONOPS', 'PREEMPTIVE_OS_GETPROCADDRESS',
166 + 'PREEMPTIVE_XE_CALLBACKEXECUTE', 'PREEMPTIVE_XE_DISPATCHER',
167 + 'PREEMPTIVE_XE_GETTARGETSTATE', 'PREEMPTIVE_XE_SESSIONCOMMIT',
168 + 'PREEMPTIVE_XE_TARGETFINALIZE', 'PREEMPTIVE_XE_TARGETINIT',
169 + 'PWAIT_ALL_COMPONENTS_INITIALIZED', 'PWAIT_DIRECTLOGCONSUMER_GETNEXT',
170 + 'QDS_ASYNC_QUEUE', 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP',
171 + 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'QDS_SHUTDOWN_QUEUE',
172 + 'REDO_THREAD_PENDING_WORK', 'REQUEST_FOR_DEADLOCK_SEARCH',
173 + 'RESOURCE_QUEUE', 'SERVER_IDLE_CHECK', 'SLEEP_BPOOL_FLUSH',
174 + 'SLEEP_DBSTARTUP', 'SLEEP_DCOMSTARTUP', 'SLEEP_MASTERDBREADY',
175 + 'SLEEP_MASTERMDREADY', 'SLEEP_MASTERUPGRADED', 'SLEEP_MSDBSTARTUP',
176 + 'SLEEP_SYSTEMTASK', 'SLEEP_TASK', 'SLEEP_TEMPDBSTARTUP',
177 + 'SNI_HTTP_ACCEPT', 'SP_SERVER_DIAGNOSTICS_SLEEP',
178 + 'SQLTRACE_BUFFER_FLUSH', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
179 + 'SQLTRACE_WAIT_ENTRIES', 'UCS_SESSION_REGISTRATION',
180 + 'WAIT_FOR_RESULTS', 'WAIT_XTP_CKPT_CLOSE', 'WAIT_XTP_HOST_WAIT',
181 + 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG', 'WAIT_XTP_RECOVERY',
182 + 'WAITFOR', 'WAITFOR_TASKSHUTDOWN', 'XE_BUFFERMGR_ALLPROCESSED_EVENT',
183 + 'XE_DISPATCHER_JOIN', 'XE_DISPATCHER_WAIT', 'XE_LIVE_TARGET_TVF',
184 + 'XE_TIMER_EVENT'
185 + )
186 +ORDER BY ws.[wait_time_ms] DESC;
187 +`
188 +
189 +// queryJobs gets SQL Agent job status
190 +const queryJobs = `
191 +SELECT name, enabled
192 +FROM msdb.dbo.sysjobs;
193 +`
194 +
195 +// querySQLErrors gets SQL error counts from performance counters
196 +const querySQLErrors = `
197 +SELECT cntr_value
198 +FROM sys.dm_os_performance_counters
199 +WHERE object_name LIKE '%SQL Errors%'
200 + AND instance_name = '_Total'
201 + AND counter_name = 'Errors/sec';
202 +`
203 +
204 +// queryDatabaseStatus gets database state and read-only status
205 +const queryDatabaseStatus = `
206 +SELECT name, state, is_read_only
207 +FROM sys.databases;
208 +`
209 +
210 +// queryReplicationStatus gets replication publication status (if configured)
211 +// Groups by publication to aggregate across agent types and excludes 'ALL' placeholder
212 +const queryReplicationStatus = `
213 +SELECT
214 + publisher_db,
215 + publication,
216 + MAX(status) AS status,
217 + MAX(warning) AS warning,
218 + MAX(ISNULL(worst_latency, 0)) AS worst_latency,
219 + MIN(CASE WHEN best_latency IS NULL OR best_latency = 0 THEN 999999 ELSE best_latency END) AS best_latency,
220 + AVG(ISNULL(avg_latency, 0)) AS avg_latency,
221 + SUM(CASE WHEN isagentrunningnow = 1 THEN 1 ELSE 0 END) AS running_agents
222 +FROM distribution.dbo.MSreplication_monitordata
223 +WHERE publication != 'ALL'
224 +GROUP BY publisher_db, publication;
225 +`
226 +
227 +// querySubscriptionCount gets subscription counts per publication
228 +const querySubscriptionCount = `
229 +SELECT
230 + p.publisher_db,
231 + p.publication,
232 + COUNT(s.subscription_type) AS subscription_count
233 +FROM distribution.dbo.MSpublications p
234 +LEFT JOIN distribution.dbo.MSsubscriptions s
235 + ON p.publication_id = s.publication_id
236 +GROUP BY p.publisher_db, p.publication;
237 +`
238 +
239 +// queryIOStall gets I/O stall (latency) metrics per database from sys.dm_io_virtual_file_stats
240 +const queryIOStall = `
241 +SELECT
242 + DB_NAME(a.database_id) AS database_name,
243 + SUM(io_stall_read_ms) AS io_stall_read_ms,
244 + SUM(io_stall_write_ms) AS io_stall_write_ms,
245 + SUM(io_stall) AS io_stall_total_ms
246 +FROM sys.dm_io_virtual_file_stats(NULL, NULL) a
247 +INNER JOIN sys.master_files b ON a.database_id = b.database_id AND a.file_id = b.file_id
248 +WHERE a.database_id > 4
249 +GROUP BY a.database_id;
250 +`
251 +
252 +// queryProcessMemory gets SQL Server process memory metrics from sys.dm_os_process_memory
253 +const queryProcessMemory = `
254 +SELECT
255 + physical_memory_in_use_kb * 1024 AS resident_memory_bytes,
256 + virtual_address_space_committed_kb * 1024 AS virtual_memory_bytes,
257 + memory_utilization_percentage,
258 + page_fault_count
259 +FROM sys.dm_os_process_memory;
260 +`
261 +
262 +// queryOSMemory gets OS memory metrics from sys.dm_os_sys_memory
263 +const queryOSMemory = `
264 +SELECT
265 + (total_physical_memory_kb - available_physical_memory_kb) * 1024 AS os_memory_used_bytes,
266 + available_physical_memory_kb * 1024 AS os_memory_available_bytes,
267 + (total_page_file_kb - available_page_file_kb) * 1024 AS os_pagefile_used_bytes,
268 + available_page_file_kb * 1024 AS os_pagefile_available_bytes
269 +FROM sys.dm_os_sys_memory;
270 +`
271 +
272 +// queryLogGrowths gets log growth events per database from performance counters
273 +const queryLogGrowths = `
274 +SELECT
275 + RTRIM(instance_name) AS database_name,
276 + cntr_value
277 +FROM sys.dm_os_performance_counters
278 +WHERE object_name LIKE '%Databases%'
279 + AND counter_name = 'Log Growths'
280 + AND instance_name NOT IN ('_Total', 'mssqlsystemresource');
281 +`
282 +
283 +// waitTypeCategories maps wait types to their categories
284 +var waitTypeCategories = map[string]string{
285 + "ASYNC_IO_COMPLETION": "Other Disk IO",
286 + "ASYNC_NETWORK_IO": "Network IO",
287 + "BACKUPIO": "Other Disk IO",
288 + "BACKUPBUFFER": "Other Disk IO",
289 + "BROKER_DISPATCHER": "Service Broker",
290 + "BROKER_FORWARDER": "Service Broker",
291 + "BROKER_INIT": "Service Broker",
292 + "BROKER_MASTERSTART": "Service Broker",
293 + "BROKER_REGISTERALLENDPOINTS": "Service Broker",
294 + "BROKER_SERVICE": "Service Broker",
295 + "BROKER_SHUTDOWN": "Service Broker",
296 + "CXPACKET": "Parallelism",
297 + "CXCONSUMER": "Parallelism",
298 + "DBMIRROR_DBM_MUTEX": "Mirroring",
299 + "DBMIRROR_SEND": "Mirroring",
300 + "DTC": "Transaction",
301 + "DTC_ABORT_REQUEST": "Transaction",
302 + "DTC_RESOLVE": "Transaction",
303 + "DTC_STATE": "Transaction",
304 + "DTC_TMDOWN_REQUEST": "Transaction",
305 + "DTC_WAITFOR_OUTCOME": "Transaction",
306 + "FT_COMPROWSET_RWLOCK": "Full Text Search",
307 + "FT_IFTS_RWLOCK": "Full Text Search",
308 + "FT_IFTS_SCHEDULER_IDLE_WAIT": "Full Text Search",
309 + "FT_IFTSHC_MUTEX": "Full Text Search",
310 + "FT_MASTER_MERGE": "Full Text Search",
311 + "IO_COMPLETION": "Other Disk IO",
312 + "IO_QUEUE_LIMIT": "Other Disk IO",
313 + "IO_RETRY": "Other Disk IO",
314 + "LATCH_DT": "Latch",
315 + "LATCH_EX": "Latch",
316 + "LATCH_KP": "Latch",
317 + "LATCH_NL": "Latch",
318 + "LATCH_SH": "Latch",
319 + "LATCH_UP": "Latch",
320 + "LCK_M_BU": "Lock",
321 + "LCK_M_IS": "Lock",
322 + "LCK_M_IU": "Lock",
323 + "LCK_M_IX": "Lock",
324 + "LCK_M_RIn_NL": "Lock",
325 + "LCK_M_RIn_S": "Lock",
326 + "LCK_M_RIn_U": "Lock",
327 + "LCK_M_RIn_X": "Lock",
328 + "LCK_M_RS_S": "Lock",
329 + "LCK_M_RS_U": "Lock",
330 + "LCK_M_RX_S": "Lock",
331 + "LCK_M_RX_U": "Lock",
332 + "LCK_M_RX_X": "Lock",
333 + "LCK_M_S": "Lock",
334 + "LCK_M_SCH_M": "Lock",
335 + "LCK_M_SCH_S": "Lock",
336 + "LCK_M_SIU": "Lock",
337 + "LCK_M_SIX": "Lock",
338 + "LCK_M_U": "Lock",
339 + "LCK_M_UIX": "Lock",
340 + "LCK_M_X": "Lock",
341 + "LOGBUFFER": "Tran Log IO",
342 + "LOGMGR": "Tran Log IO",
343 + "LOGMGR_FLUSH": "Tran Log IO",
344 + "LOGMGR_PMM_LOG": "Tran Log IO",
345 + "LOGMGR_RESERVE_APPEND": "Tran Log IO",
346 + "MSQL_DQ": "Network IO",
347 + "MSQL_XP": "Network IO",
348 + "NET_WAITFOR_PACKET": "Network IO",
349 + "OLEDB": "Network IO",
350 + "PAGELATCH_DT": "Buffer Latch",
351 + "PAGELATCH_EX": "Buffer Latch",
352 + "PAGELATCH_KP": "Buffer Latch",
353 + "PAGELATCH_NL": "Buffer Latch",
354 + "PAGELATCH_SH": "Buffer Latch",
355 + "PAGELATCH_UP": "Buffer Latch",
356 + "PAGEIOLATCH_DT": "Buffer IO",
357 + "PAGEIOLATCH_EX": "Buffer IO",
358 + "PAGEIOLATCH_KP": "Buffer IO",
359 + "PAGEIOLATCH_NL": "Buffer IO",
360 + "PAGEIOLATCH_SH": "Buffer IO",
361 + "PAGEIOLATCH_UP": "Buffer IO",
362 + "PARALLEL_BACKUP_QUEUE": "Backup",
363 + "PARALLEL_REDO_DRAIN_WORKER": "Backup",
364 + "PARALLEL_REDO_LOG_CACHE": "Backup",
365 + "PARALLEL_REDO_TRAN_LIST": "Backup",
366 + "PARALLEL_REDO_WORKER_SYNC": "Backup",
367 + "PARALLEL_REDO_WORKER_WAIT_WORK": "Backup",
368 + "PREEMPTIVE_ABR": "Preemptive",
369 + "PREEMPTIVE_AUDIT_ACCESS_EVENTLOG": "Preemptive",
370 + "PREEMPTIVE_AUDIT_ACCESS_SECLOG": "Preemptive",
371 + "PREEMPTIVE_CLOSEBACKUPMEDIA": "Preemptive",
372 + "PREEMPTIVE_CLOSEBACKUPTAPE": "Preemptive",
373 + "PREEMPTIVE_CLOSEBACKUPVDIDEVICE": "Preemptive",
374 + "PREEMPTIVE_CLUSAPI_CLUSTERRESOURCECONTROL": "Preemptive",
375 + "PREEMPTIVE_COM_COCREATEINSTANCE": "Preemptive",
376 + "PREEMPTIVE_COM_COGETCLASSOBJECT": "Preemptive",
377 + "PREEMPTIVE_COM_CREATEACCESSOR": "Preemptive",
378 + "PREEMPTIVE_COM_DELETEROWS": "Preemptive",
379 + "PREEMPTIVE_COM_GETCOMMANDTEXT": "Preemptive",
380 + "PREEMPTIVE_COM_GETDATA": "Preemptive",
381 + "PREEMPTIVE_COM_GETNEXTROWS": "Preemptive",
382 + "PREEMPTIVE_COM_GETRESULT": "Preemptive",
383 + "PREEMPTIVE_COM_GETROWSBYBOOKMARK": "Preemptive",
384 + "PREEMPTIVE_COM_LBFLUSH": "Preemptive",
385 + "PREEMPTIVE_COM_LBLOCKREGION": "Preemptive",
386 + "PREEMPTIVE_COM_LBREADAT": "Preemptive",
387 + "PREEMPTIVE_COM_LBSETSIZE": "Preemptive",
388 + "PREEMPTIVE_COM_LBSTAT": "Preemptive",
389 + "PREEMPTIVE_COM_LBUNLOCKREGION": "Preemptive",
390 + "PREEMPTIVE_COM_LBWRITEAT": "Preemptive",
391 + "PREEMPTIVE_COM_QUERYINTERFACE": "Preemptive",
392 + "PREEMPTIVE_COM_RELEASE": "Preemptive",
393 + "PREEMPTIVE_COM_RELEASEACCESSOR": "Preemptive",
394 + "PREEMPTIVE_COM_RELEASEROWS": "Preemptive",
395 + "PREEMPTIVE_COM_RELEASESESSION": "Preemptive",
396 + "PREEMPTIVE_COM_RESTARTPOSITION": "Preemptive",
397 + "PREEMPTIVE_COM_SEQITHROW": "Preemptive",
398 + "PREEMPTIVE_COM_SETDATAFAILURE": "Preemptive",
399 + "PREEMPTIVE_COM_SETPARAMETERINFO": "Preemptive",
400 + "PREEMPTIVE_COM_SETPARAMETERPROPERTIES": "Preemptive",
401 + "PREEMPTIVE_CONSOLEWRITE": "Preemptive",
402 + "PREEMPTIVE_CREATEPARAM": "Preemptive",
403 + "PREEMPTIVE_DEBUG": "Preemptive",
404 + "PREEMPTIVE_DFSADDLINK": "Preemptive",
405 + "PREEMPTIVE_DFSLINKEXISTCHECK": "Preemptive",
406 + "PREEMPTIVE_DFSLINKHEALTHCHECK": "Preemptive",
407 + "PREEMPTIVE_DFSREMOVELINK": "Preemptive",
408 + "PREEMPTIVE_DFSREMOVEROOT": "Preemptive",
409 + "PREEMPTIVE_DFSROOTFOLDERCHECK": "Preemptive",
410 + "PREEMPTIVE_DFSROOTINIT": "Preemptive",
411 + "PREEMPTIVE_DFSROOTSHARECHECK": "Preemptive",
412 + "PREEMPTIVE_DTC_ABORT": "Preemptive",
413 + "PREEMPTIVE_DTC_ABORTREQUESTDONE": "Preemptive",
414 + "PREEMPTIVE_DTC_BEGINTRANSACTION": "Preemptive",
415 + "PREEMPTIVE_DTC_COMMITREQUESTDONE": "Preemptive",
416 + "PREEMPTIVE_DTC_ENLIST": "Preemptive",
417 + "PREEMPTIVE_DTC_PREPAREREQUESTDONE": "Preemptive",
418 + "PREEMPTIVE_FILESIZEGET": "Preemptive",
419 + "PREEMPTIVE_FSAREATEFILLBACKUP": "Preemptive",
420 + "PREEMPTIVE_FSACREATERESTORE": "Preemptive",
421 + "PREEMPTIVE_FSAFREEHEAP": "Preemptive",
422 + "PREEMPTIVE_FSGETTARGETCOMPLETE": "Preemptive",
423 + "PREEMPTIVE_FSGETTARGETPREPARE": "Preemptive",
424 + "PREEMPTIVE_FSQUERYALLOCATEDRANGES": "Preemptive",
425 + "PREEMPTIVE_GETRMIDENTITY": "Preemptive",
426 + "PREEMPTIVE_LOCKMONITOR": "Preemptive",
427 + "PREEMPTIVE_MSS_RELEASE": "Preemptive",
428 + "PREEMPTIVE_ODBCOPS": "Preemptive",
429 + "PREEMPTIVE_OLE_UNINIT": "Preemptive",
430 + "PREEMPTIVE_OTHER_ABORT": "Preemptive",
431 + "PREEMPTIVE_OTHER_ALERTREGISTER": "Preemptive",
432 + "PREEMPTIVE_OTHER_ALERTSIGNAL": "Preemptive",
433 + "PREEMPTIVE_OTHER_ALERTWAIT": "Preemptive",
434 + "PREEMPTIVE_OTHER_ABORTTRANSACTION": "Preemptive",
435 + "PREEMPTIVE_OTHER_CREATETHREAD": "Preemptive",
436 + "PREEMPTIVE_OTHER_PREPARETOENLIST": "Preemptive",
437 + "PREEMPTIVE_OTHER_RECOVER": "Preemptive",
438 + "PREEMPTIVE_OTHER_SCHEMAUNLOCK": "Preemptive",
439 + "PREEMPTIVE_OTHER_TRANSIMPORT": "Preemptive",
440 + "PREEMPTIVE_OS_ACCEPTSECURITYCONTEXT": "Preemptive",
441 + "PREEMPTIVE_OS_ACQUIRECREDENTIALSHANDLE": "Preemptive",
442 + "PREEMPTIVE_OS_AUTHZGETINFORMATIONFROMCONTEXT": "Preemptive",
443 + "PREEMPTIVE_OS_AUTHZINITIALIZERESOURCEMANAGER": "Preemptive",
444 + "PREEMPTIVE_OS_BACKUPREAD": "Preemptive",
445 + "PREEMPTIVE_OS_CLOSEHANDLE": "Preemptive",
446 + "PREEMPTIVE_OS_CLUSTEROPS": "Preemptive",
447 + "PREEMPTIVE_OS_COMOPS": "Preemptive",
448 + "PREEMPTIVE_OS_COMPLETEAUTHTOKEN": "Preemptive",
449 + "PREEMPTIVE_OS_COPYFILE": "Preemptive",
450 + "PREEMPTIVE_OS_CREATEDIRECTORY": "Preemptive",
451 + "PREEMPTIVE_OS_CREATEFILE": "Preemptive",
452 + "PREEMPTIVE_OS_CRYPTOPS": "Preemptive",
453 + "PREEMPTIVE_OS_DECRYPTMESSAGE": "Preemptive",
454 + "PREEMPTIVE_OS_DELETEFILE": "Preemptive",
455 + "PREEMPTIVE_OS_DELETESECURITYCONTEXT": "Preemptive",
456 + "PREEMPTIVE_OS_DEVICEIOCONTROL": "Preemptive",
457 + "PREEMPTIVE_OS_DEVICEOPS": "Preemptive",
458 + "PREEMPTIVE_OS_DIABORPC": "Preemptive",
459 + "PREEMPTIVE_OS_DOMAINSERVICEOPS": "Preemptive",
460 + "PREEMPTIVE_OS_DSGETDCNAME": "Preemptive",
461 + "PREEMPTIVE_OS_DTCOPS": "Preemptive",
462 + "PREEMPTIVE_OS_ENCRYPTMESSAGE": "Preemptive",
463 + "PREEMPTIVE_OS_FILEOPS": "Preemptive",
464 + "PREEMPTIVE_OS_FINDFILE": "Preemptive",
465 + "PREEMPTIVE_OS_FLUSHFILEBUFFERS": "Preemptive",
466 + "PREEMPTIVE_OS_FORMATMESSAGE": "Preemptive",
467 + "PREEMPTIVE_OS_FREECREDENTIALSHANDLE": "Preemptive",
468 + "PREEMPTIVE_OS_FREELIBRARY": "Preemptive",
469 + "PREEMPTIVE_OS_GENERICOPS": "Preemptive",
470 + "PREEMPTIVE_OS_GETADDRINFO": "Preemptive",
471 + "PREEMPTIVE_OS_GETCOMPRESSEDFILESIZE": "Preemptive",
472 + "PREEMPTIVE_OS_GETDISKFREESPACE": "Preemptive",
473 + "PREEMPTIVE_OS_GETFILEATTRIBUTES": "Preemptive",
474 + "PREEMPTIVE_OS_GETFILESIZE": "Preemptive",
475 + "PREEMPTIVE_OS_GETFINALFILEPATHBYHANDLE": "Preemptive",
476 + "PREEMPTIVE_OS_GETLONGPATHNAME": "Preemptive",
477 + "PREEMPTIVE_OS_GETTEMPPATHNAME": "Preemptive",
478 + "PREEMPTIVE_OS_GETVOLUMEPATHNAMEOFNAME": "Preemptive",
479 + "PREEMPTIVE_OS_INITIALIZESECURITYCONTEXT": "Preemptive",
480 + "PREEMPTIVE_OS_LIBRARYOPS": "Preemptive",
481 + "PREEMPTIVE_OS_LOADLIBRARY": "Preemptive",
482 + "PREEMPTIVE_OS_LOGONUSER": "Preemptive",
483 + "PREEMPTIVE_OS_LOOKUPACCOUNTSID": "Preemptive",
484 + "PREEMPTIVE_OS_MESSAGEQUEUEOPS": "Preemptive",
485 + "PREEMPTIVE_OS_MOVEFILE": "Preemptive",
486 + "PREEMPTIVE_OS_NETGROUPGETUSERS": "Preemptive",
487 + "PREEMPTIVE_OS_NETLOCALGROUPGETMEMBERS": "Preemptive",
488 + "PREEMPTIVE_OS_NETUSERGETGROUPS": "Preemptive",
489 + "PREEMPTIVE_OS_NETUSERGETLOCALGROUPS": "Preemptive",
490 + "PREEMPTIVE_OS_NETUSERMODALSGET": "Preemptive",
491 + "PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY": "Preemptive",
492 + "PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICYFREE": "Preemptive",
493 + "PREEMPTIVE_OS_OPENDIRECTORY": "Preemptive",
494 + "PREEMPTIVE_OS_PABORPC": "Preemptive",
495 + "PREEMPTIVE_OS_PIPEOPS": "Preemptive",
496 + "PREEMPTIVE_OS_PROCESSOPS": "Preemptive",
497 + "PREEMPTIVE_OS_QUERYREGISTRY": "Preemptive",
498 + "PREEMPTIVE_OS_QUERYSECURITYCONTEXTTOKEN": "Preemptive",
499 + "PREEMPTIVE_OS_READFILE": "Preemptive",
500 + "PREEMPTIVE_OS_REMOVEDIRECTORY": "Preemptive",
501 + "PREEMPTIVE_OS_REPORTEVENT": "Preemptive",
502 + "PREEMPTIVE_OS_REVERTTOSELF": "Preemptive",
503 + "PREEMPTIVE_OS_RABORPC": "Preemptive",
504 + "PREEMPTIVE_OS_SABORPC": "Preemptive",
505 + "PREEMPTIVE_OS_SECURITYOPS": "Preemptive",
506 + "PREEMPTIVE_OS_SERVICEOPS": "Preemptive",
507 + "PREEMPTIVE_OS_SETENDOFFILE": "Preemptive",
508 + "PREEMPTIVE_OS_SETFILEPOINTER": "Preemptive",
509 + "PREEMPTIVE_OS_SETFILEVALIDDATA": "Preemptive",
510 + "PREEMPTIVE_OS_SQLTHREADOPS": "Preemptive",
511 + "PREEMPTIVE_OS_VERIFYSIGNATURE": "Preemptive",
512 + "PREEMPTIVE_OS_WAITFORSINGLEOBJECT": "Preemptive",
513 + "PREEMPTIVE_OS_WINSOCKOPS": "Preemptive",
514 + "PREEMPTIVE_OS_WRITEFILE": "Preemptive",
515 + "PREEMPTIVE_OS_WRITEFILEGATHER": "Preemptive",
516 + "PREEMPTIVE_REENLIST": "Preemptive",
517 + "PREEMPTIVE_RESIZELOG": "Preemptive",
518 + "PREEMPTIVE_ROLLFORWARDREDO": "Preemptive",
519 + "PREEMPTIVE_ROLLFORWARDUNDO": "Preemptive",
520 + "PREEMPTIVE_SB_STOPENDPOINT": "Preemptive",
521 + "PREEMPTIVE_SERVER_STARTUP": "Preemptive",
522 + "PREEMPTIVE_SETRMIDENTITY": "Preemptive",
523 + "PREEMPTIVE_SHAREDMEM_GETDATA": "Preemptive",
524 + "PREEMPTIVE_SNIOPEN": "Preemptive",
525 + "PREEMPTIVE_SOSHOST": "Preemptive",
526 + "PREEMPTIVE_SOSTESTING": "Preemptive",
527 + "PREEMPTIVE_SP_SERVER_DIAGNOSTICS": "Preemptive",
528 + "PREEMPTIVE_STARTRM": "Preemptive",
529 + "PREEMPTIVE_STREAMFCB_CHECKPOINT": "Preemptive",
530 + "PREEMPTIVE_STREAMFCB_RECOVER": "Preemptive",
531 + "PREEMPTIVE_STRESSDRIVER": "Preemptive",
532 + "PREEMPTIVE_TESTING": "Preemptive",
533 + "PREEMPTIVE_TRANSIMPORT": "Preemptive",
534 + "PREEMPTIVE_UNMARSHALPROPAGATIONTOKEN": "Preemptive",
535 + "PREEMPTIVE_VSS_CREATESNAPSHOT": "Preemptive",
536 + "PREEMPTIVE_VSS_CREATEVOLUMESNAPSHOT": "Preemptive",
537 + "PREEMPTIVE_XE_CALLBACKEXECUTE": "Preemptive",
538 + "PREEMPTIVE_XE_CX_FILE_OPEN": "Preemptive",
539 + "PREEMPTIVE_XE_CX_HTTP_CALL": "Preemptive",
540 + "PREEMPTIVE_XE_DISPATCHER": "Preemptive",
541 + "PREEMPTIVE_XE_ENGINEINIT": "Preemptive",
542 + "PREEMPTIVE_XE_GETTARGETSTATE": "Preemptive",
543 + "PREEMPTIVE_XE_SESSIONCOMMIT": "Preemptive",
544 + "PREEMPTIVE_XE_TARGETFINALIZE": "Preemptive",
545 + "PREEMPTIVE_XE_TARGETINIT": "Preemptive",
546 + "PREEMPTIVE_XE_TIMERRUN": "Preemptive",
547 + "PREEMPTIVE_XETESTING": "Preemptive",
548 + "PWAIT_HADR_ACTION_COMPLETED": "Replication",
549 + "PWAIT_HADR_CHANGE_NOTIFIER_TERMINATION_SYNC": "Replication",
550 + "PWAIT_HADR_CLUSTER_INTEGRATION": "Replication",
551 + "PWAIT_HADR_FAILOVER_COMPLETED": "Replication",
552 + "PWAIT_HADR_JOIN": "Replication",
553 + "PWAIT_HADR_OFFLINE_COMPLETED": "Replication",
554 + "PWAIT_HADR_ONLINE_COMPLETED": "Replication",
555 + "PWAIT_HADR_POST_ONLINE_COMPLETED": "Replication",
556 + "PWAIT_HADR_SERVER_READY_CONNECTIONS": "Replication",
557 + "PWAIT_HADR_WORKITEM_COMPLETED": "Replication",
558 + "REPLICA_WRITES": "Replication",
559 + "RESOURCE_SEMAPHORE": "Memory",
560 + "RESOURCE_SEMAPHORE_MUTEX": "Memory",
561 + "RESOURCE_SEMAPHORE_QUERY_COMPILE": "Compilation",
562 + "RESOURCE_SEMAPHORE_SMALL_QUERY": "Memory",
563 + "SOS_PHYS_PAGE_CACHE": "Memory",
564 + "SOS_RESERVEDMEMBLOCKLIST": "Memory",
565 + "SOS_SCHEDULER_YIELD": "CPU",
566 + "SOS_VIRTUALMEMORY_LOW": "Memory",
567 + "SOS_WORK_DISPATCHER": "Worker Thread",
568 + "SQLCLR_APPDOMAIN": "SQL CLR",
569 + "SQLCLR_ASSEMBLY": "SQL CLR",
570 + "SQLCLR_DEADLOCK_DETECTION": "SQL CLR",
571 + "SQLCLR_QUANTUM_PUNISHMENT": "SQL CLR",
572 + "THREADPOOL": "Worker Thread",
573 + "TRACEWRITE": "Tracing",
574 + "TRAN_MARKLATCH_DT": "Transaction",
575 + "TRAN_MARKLATCH_EX": "Transaction",
576 + "TRAN_MARKLATCH_KP": "Transaction",
577 + "TRAN_MARKLATCH_NL": "Transaction",
578 + "TRAN_MARKLATCH_SH": "Transaction",
579 + "TRAN_MARKLATCH_UP": "Transaction",
580 + "TRANSACTION_MUTEX": "Transaction",
581 + "WRITELOG": "Tran Log IO",
582 + "XACTLOCKINFO": "Transaction",
583 + "XACT_OWN_TRANSACTION": "Transaction",
584 + "XACT_RECLAIM_SESSION": "Transaction",
585 + "XACT_SNAPSHOT": "Transaction",
586 +}
587 +
588 +// getWaitCategory returns the category for a wait type, or "OTHER" if unknown
589 +func getWaitCategory(waitType string) string {
590 + if cat, ok := waitTypeCategories[waitType]; ok {
591 + return cat
592 + }
593 + return "OTHER"
594 +}
src/go/plugin/go.d/collector/mssql/verify_test.go new
+352
@@ -0,0 +1,352 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +//go:build integration
4 +
5 +package mssql
6 +
7 +import (
8 + "context"
9 + "os"
10 + "sort"
11 + "strings"
12 + "testing"
13 + "time"
14 +
15 + "github.com/netdata/netdata/go/plugins/pkg/confopt"
16 +
17 + "github.com/stretchr/testify/assert"
18 + "github.com/stretchr/testify/require"
19 +)
20 +
21 +// getDSN returns the DSN from MSSQL_DSN environment variable.
22 +// If not set, the test is skipped.
23 +func getDSN(t *testing.T) string {
24 + dsn := os.Getenv("MSSQL_DSN")
25 + if dsn == "" {
26 + t.Skip("MSSQL_DSN environment variable not set")
27 + }
28 + return dsn
29 +}
30 +
31 +// TestIntegration_FullCollection runs a complete integration test against a real SQL Server.
32 +// Run with: MSSQL_DSN="sqlserver://user:pass@host:port" go test -tags=integration -v -run TestIntegration
33 +func TestIntegration_FullCollection(t *testing.T) {
34 + c := New()
35 + c.DSN = getDSN(t)
36 + c.Timeout = confopt.Duration(time.Second * 10)
37 +
38 + // Initialize
39 + require.NoError(t, c.Init(context.Background()), "Init should succeed")
40 +
41 + // Check (first collection)
42 + require.NoError(t, c.Check(context.Background()), "Check should succeed")
43 +
44 + t.Logf("Connected to SQL Server version: %s", c.version)
45 +
46 + // Collect multiple times to verify stability
47 + for i := 1; i <= 3; i++ {
48 + t.Logf("\n=== Collection cycle %d ===", i)
49 +
50 + mx := c.Collect(context.Background())
51 + require.NotNil(t, mx, "Collect should return metrics")
52 + require.NotEmpty(t, mx, "Metrics should not be empty")
53 +
54 + // Verify charts were created
55 + charts := c.Charts()
56 + require.NotNil(t, charts)
57 + t.Logf("Charts count: %d", len(*charts))
58 +
59 + // Group and report metrics
60 + reportMetrics(t, mx)
61 +
62 + // Verify key metrics exist
63 + assertKeyMetrics(t, mx)
64 +
65 + time.Sleep(time.Second)
66 + }
67 +
68 + // Cleanup
69 + c.Cleanup(context.Background())
70 + assert.Nil(t, c.db, "DB connection should be closed after cleanup")
71 +}
72 +
73 +func reportMetrics(t *testing.T, mx map[string]int64) {
74 + // Group metrics by prefix
75 + groups := make(map[string][]string)
76 + for k := range mx {
77 + prefix := getMetricPrefix(k)
78 + groups[prefix] = append(groups[prefix], k)
79 + }
80 +
81 + // Sort and print
82 + var prefixes []string
83 + for p := range groups {
84 + prefixes = append(prefixes, p)
85 + }
86 + sort.Strings(prefixes)
87 +
88 + t.Logf("Total metrics: %d", len(mx))
89 + for _, prefix := range prefixes {
90 + keys := groups[prefix]
91 + sort.Strings(keys)
92 + t.Logf(" [%s]: %d metrics", prefix, len(keys))
93 +
94 + // Show a few sample values
95 + for i, k := range keys {
96 + if i >= 3 {
97 + t.Logf(" ... and %d more", len(keys)-3)
98 + break
99 + }
100 + t.Logf(" %s = %d", k, mx[k])
101 + }
102 + }
103 +}
104 +
105 +func getMetricPrefix(key string) string {
106 + parts := strings.Split(key, "_")
107 + if len(parts) >= 2 {
108 + // Handle special cases
109 + if parts[0] == "database" {
110 + return "database"
111 + }
112 + if parts[0] == "wait" {
113 + return "wait"
114 + }
115 + if parts[0] == "locks" {
116 + return "locks"
117 + }
118 + if parts[0] == "job" {
119 + return "job"
120 + }
121 + return parts[0]
122 + }
123 + return key
124 +}
125 +
126 +func assertKeyMetrics(t *testing.T, mx map[string]int64) {
127 + // Instance metrics
128 + requiredMetrics := []string{
129 + "batch_requests",
130 + "sql_compilations",
131 + "sql_recompilations",
132 + }
133 +
134 + // Optional metrics (may not exist depending on config)
135 + optionalMetrics := []string{
136 + "user_connections",
137 + "blocked_processes",
138 + "buffer_cache_hit_ratio",
139 + "buffer_page_life_expectancy",
140 + "buffer_page_reads",
141 + "buffer_page_writes",
142 + "memory_total",
143 + "page_splits",
144 + }
145 +
146 + for _, m := range requiredMetrics {
147 + _, exists := mx[m]
148 + assert.True(t, exists, "Required metric %s should exist", m)
149 + }
150 +
151 + foundOptional := 0
152 + for _, m := range optionalMetrics {
153 + if _, exists := mx[m]; exists {
154 + foundOptional++
155 + }
156 + }
157 + t.Logf("Found %d/%d optional instance metrics", foundOptional, len(optionalMetrics))
158 +
159 + // Check for database metrics (should have at least system databases)
160 + dbMetrics := 0
161 + for k := range mx {
162 + if strings.HasPrefix(k, "database_") {
163 + dbMetrics++
164 + }
165 + }
166 + assert.Greater(t, dbMetrics, 0, "Should have database metrics")
167 + t.Logf("Found %d database metrics", dbMetrics)
168 +
169 + // Check for wait metrics
170 + waitMetrics := 0
171 + for k := range mx {
172 + if strings.HasPrefix(k, "wait_") {
173 + waitMetrics++
174 + }
175 + }
176 + assert.Greater(t, waitMetrics, 0, "Should have wait metrics")
177 + t.Logf("Found %d wait metrics", waitMetrics)
178 +}
179 +
180 +// TestIntegration_ChartsCreation verifies dynamic chart creation
181 +func TestIntegration_ChartsCreation(t *testing.T) {
182 + c := New()
183 + c.DSN = getDSN(t)
184 + c.Timeout = confopt.Duration(time.Second * 10)
185 +
186 + require.NoError(t, c.Init(context.Background()))
187 + require.NoError(t, c.Check(context.Background()))
188 +
189 + // First collection creates charts
190 + mx1 := c.Collect(context.Background())
191 + require.NotNil(t, mx1)
192 +
193 + charts := c.Charts()
194 + initialChartCount := len(*charts)
195 + t.Logf("Charts after first collection: %d", initialChartCount)
196 +
197 + // List chart IDs
198 + var chartIDs []string
199 + for _, ch := range *charts {
200 + chartIDs = append(chartIDs, ch.ID)
201 + }
202 + sort.Strings(chartIDs)
203 +
204 + t.Log("Chart IDs:")
205 + for _, id := range chartIDs {
206 + t.Logf(" - %s", id)
207 + }
208 +
209 + // Verify we have expected chart categories
210 + hasInstance := false
211 + hasDatabase := false
212 + hasWait := false
213 +
214 + for _, id := range chartIDs {
215 + if strings.Contains(id, "user_connections") || strings.Contains(id, "batch_requests") {
216 + hasInstance = true
217 + }
218 + if strings.Contains(id, "database_") && strings.Contains(id, "_transactions") {
219 + hasDatabase = true
220 + }
221 + if strings.Contains(id, "wait_") {
222 + hasWait = true
223 + }
224 + }
225 +
226 + assert.True(t, hasInstance, "Should have instance charts")
227 + assert.True(t, hasDatabase, "Should have database charts")
228 + assert.True(t, hasWait, "Should have wait charts")
229 +
230 + c.Cleanup(context.Background())
231 +}
232 +
233 +// TestIntegration_MetricValues verifies metric values are sensible
234 +func TestIntegration_MetricValues(t *testing.T) {
235 + c := New()
236 + c.DSN = getDSN(t)
237 + c.Timeout = confopt.Duration(time.Second * 10)
238 +
239 + require.NoError(t, c.Init(context.Background()))
240 + require.NoError(t, c.Check(context.Background()))
241 +
242 + mx := c.Collect(context.Background())
243 + require.NotNil(t, mx)
244 +
245 + // Buffer cache hit ratio should be 0-100
246 + if v, ok := mx["buffer_cache_hit_ratio"]; ok {
247 + assert.GreaterOrEqual(t, v, int64(0), "Cache hit ratio >= 0")
248 + assert.LessOrEqual(t, v, int64(100), "Cache hit ratio <= 100")
249 + t.Logf("Buffer cache hit ratio: %d%%", v)
250 + }
251 +
252 + // Page life expectancy should be positive
253 + if v, ok := mx["buffer_page_life_expectancy"]; ok {
254 + assert.GreaterOrEqual(t, v, int64(0), "PLE should be >= 0")
255 + t.Logf("Page life expectancy: %d seconds", v)
256 + }
257 +
258 + // User connections should be at least 1 (our connection)
259 + if v, ok := mx["user_connections"]; ok {
260 + assert.GreaterOrEqual(t, v, int64(1), "Should have at least 1 connection")
261 + t.Logf("User connections: %d", v)
262 + }
263 +
264 + // Memory should be positive
265 + if v, ok := mx["memory_total"]; ok {
266 + assert.Greater(t, v, int64(0), "Memory should be > 0")
267 + t.Logf("Total memory: %d bytes (%.2f MB)", v, float64(v)/1024/1024)
268 + }
269 +
270 + // Blocked processes should be non-negative
271 + if v, ok := mx["blocked_processes"]; ok {
272 + assert.GreaterOrEqual(t, v, int64(0), "Blocked processes >= 0")
273 + t.Logf("Blocked processes: %d", v)
274 + }
275 +
276 + c.Cleanup(context.Background())
277 +}
278 +
279 +// TestIntegration_ErrorHandling verifies graceful error handling
280 +func TestIntegration_ErrorHandling(t *testing.T) {
281 + // Test with invalid DSN - doesn't need a real server
282 + c := New()
283 + c.DSN = "sqlserver://invalid:invalid@localhost:9999?connection+timeout=2"
284 + c.Timeout = confopt.Duration(time.Second * 3)
285 +
286 + require.NoError(t, c.Init(context.Background()), "Init should succeed (just validates config)")
287 +
288 + // Check should fail with connection error
289 + err := c.Check(context.Background())
290 + assert.Error(t, err, "Check should fail with invalid connection")
291 + t.Logf("Expected error: %v", err)
292 +}
293 +
294 +// TestIntegration_Databases lists all discovered databases
295 +func TestIntegration_Databases(t *testing.T) {
296 + c := New()
297 + c.DSN = getDSN(t)
298 + c.Timeout = confopt.Duration(time.Second * 10)
299 +
300 + require.NoError(t, c.Init(context.Background()))
301 + require.NoError(t, c.Check(context.Background()))
302 +
303 + _ = c.Collect(context.Background())
304 +
305 + t.Log("Discovered databases:")
306 + for db := range c.seenDatabases {
307 + t.Logf(" - %s", db)
308 + }
309 +
310 + // Should have system databases
311 + assert.True(t, c.seenDatabases["master"], "Should see master database")
312 + assert.True(t, c.seenDatabases["tempdb"], "Should see tempdb database")
313 + assert.True(t, c.seenDatabases["msdb"], "Should see msdb database")
314 + assert.True(t, c.seenDatabases["model"], "Should see model database")
315 +
316 + c.Cleanup(context.Background())
317 +}
318 +
319 +// TestIntegration_WaitTypes lists all discovered wait types
320 +func TestIntegration_WaitTypes(t *testing.T) {
321 + c := New()
322 + c.DSN = getDSN(t)
323 + c.Timeout = confopt.Duration(time.Second * 10)
324 +
325 + require.NoError(t, c.Init(context.Background()))
326 + require.NoError(t, c.Check(context.Background()))
327 +
328 + _ = c.Collect(context.Background())
329 +
330 + t.Logf("Discovered wait types: %d", len(c.seenWaitTypes))
331 +
332 + // Group by category
333 + byCategory := make(map[string][]string)
334 + for wt := range c.seenWaitTypes {
335 + cat := getWaitCategory(wt)
336 + byCategory[cat] = append(byCategory[cat], wt)
337 + }
338 +
339 + for cat, types := range byCategory {
340 + sort.Strings(types)
341 + t.Logf(" [%s]: %d types", cat, len(types))
342 + for i, wt := range types {
343 + if i >= 5 {
344 + t.Logf(" ... and %d more", len(types)-5)
345 + break
346 + }
347 + t.Logf(" - %s", wt)
348 + }
349 + }
350 +
351 + c.Cleanup(context.Background())
352 +}
src/go/plugin/go.d/config/go.d/mssql.conf new
+29
@@ -0,0 +1,29 @@
1 +## All available configuration options, their descriptions and default values:
2 +## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/mssql#readme
3 +
4 +#jobs:
5 +# - name: local
6 +# dsn: "sqlserver://netdata_user:password@localhost:1433"
7 +#
8 +# - name: named_instance
9 +# dsn: "sqlserver://netdata_user:password@localhost/INSTANCENAME"
10 +#
11 +# - name: windows_auth
12 +# dsn: "sqlserver://localhost:1433?trusted_connection=yes"
13 +#
14 +# - name: full_example
15 +# dsn: "sqlserver://netdata_user:password@localhost:1433"
16 +# timeout: 5
17 +# vnode: ""
18 +# # Collection options (all default to true)
19 +# collect_transactions: true
20 +# collect_waits: true
21 +# collect_locks: true
22 +# collect_jobs: true
23 +# collect_buffer_stats: true
24 +# collect_database_size: true
25 +# collect_user_connections: true
26 +# collect_blocked_processes: true
27 +# collect_sql_errors: true
28 +# collect_database_status: true
29 +# collect_replication: true