master
go 352 lines 9.01 KB
Raw
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 }