master
go 313 lines 8.81 KB
Raw
1 //go:build cgo
2
3 package db2
4
5 import (
6 "context"
7 "database/sql"
8 "errors"
9 "strconv"
10 "strings"
11 "time"
12
13 "github.com/netdata/netdata/go/plugins/pkg/confopt"
14 )
15
16 func safeDSN(dsn string) string {
17 if dsn == "" {
18 return "<empty>"
19 }
20
21 masked := dsn
22
23 if strings.Contains(masked, "PWD=") {
24 start := strings.Index(masked, "PWD=")
25 if start != -1 {
26 end := strings.Index(masked[start:], ";")
27 if end == -1 {
28 masked = masked[:start] + "PWD=***"
29 } else {
30 masked = masked[:start] + "PWD=***" + masked[start+end:]
31 }
32 }
33 }
34
35 if strings.Contains(masked, "pwd=") {
36 start := strings.Index(masked, "pwd=")
37 if start != -1 {
38 end := strings.Index(masked[start:], ";")
39 if end == -1 {
40 masked = masked[:start] + "pwd=***"
41 } else {
42 masked = masked[:start] + "pwd=***" + masked[start+end:]
43 }
44 }
45 }
46
47 if strings.Contains(masked, "AUTHENTICATION=") {
48 start := strings.Index(masked, "AUTHENTICATION=")
49 if start != -1 {
50 end := strings.Index(masked[start:], ";")
51 if end == -1 {
52 masked = masked[:start] + "AUTHENTICATION=***"
53 } else {
54 masked = masked[:start] + "AUTHENTICATION=***" + masked[start+end:]
55 }
56 }
57 }
58
59 return masked
60 }
61
62 func (c *Collector) logOnce(key string, format string, args ...any) {
63 if c.disabledMetrics[key] || c.disabledFeatures[key] {
64 return
65 }
66
67 c.Warningf(format, args...)
68 c.disabledMetrics[key] = true
69 }
70
71 func (c *Collector) isDisabled(key string) bool {
72 return c.disabledMetrics[key] || c.disabledFeatures[key]
73 }
74
75 func isSQLFeatureError(err error) bool {
76 if err == nil {
77 return false
78 }
79 errStr := strings.ToUpper(err.Error())
80 return strings.Contains(errStr, "SQL0204N") ||
81 strings.Contains(errStr, "SQL0206N") ||
82 strings.Contains(errStr, "SQL0443N") ||
83 strings.Contains(errStr, "SQL0551N") ||
84 strings.Contains(errStr, "SQL0707N") ||
85 strings.Contains(errStr, "SQLCODE=-204") ||
86 strings.Contains(errStr, "SQLCODE=-206") ||
87 strings.Contains(errStr, "SQLCODE=-443") ||
88 strings.Contains(errStr, "SQLCODE=-551") ||
89 strings.Contains(errStr, "UNSUPPORTED COLUMN TYPE")
90 }
91
92 func (c *Collector) collectSingleMetric(ctx context.Context, metricKey string, query string, handler func(value string)) error {
93 if c.isDisabled(metricKey) {
94 return nil
95 }
96
97 db := c.client.DB()
98 if db == nil {
99 return errors.New("db2 collector: database connection not initialised")
100 }
101
102 timeout := time.Duration(c.Timeout)
103 if timeout <= 0 {
104 timeout = 5 * time.Second
105 }
106
107 queryCtx, cancel := context.WithTimeout(ctx, timeout)
108 defer cancel()
109
110 var nullValue sql.NullString
111 if err := db.QueryRowContext(queryCtx, query).Scan(&nullValue); err != nil {
112 if isSQLFeatureError(err) {
113 c.logOnce(metricKey, "metric %s not available on this DB2 edition/version: %v", metricKey, err)
114 c.disabledMetrics[metricKey] = true
115 return nil
116 }
117 return err
118 }
119
120 if nullValue.Valid && nullValue.String != "" {
121 handler(nullValue.String)
122 }
123
124 return nil
125 }
126
127 func (c *Collector) detectDB2Edition(ctx context.Context) error {
128 err := c.collectSingleMetric(ctx, "version_detection_luw", queryDetectVersionLUW, func(value string) {
129 c.edition = "LUW"
130 c.version = value
131 c.parseDB2Version()
132 c.Debugf("detected DB2 LUW edition, version: %s", value)
133 })
134 if err == nil && c.edition != "" {
135 c.logVersionInformation()
136 return nil
137 }
138
139 err = c.collectSingleMetric(ctx, "version_detection_i", queryDetectVersionI, func(value string) {
140 c.edition = "i"
141 c.version = "DB2 for i"
142 c.Debugf("detected DB2 for i (AS/400) edition")
143 })
144 if err == nil && c.edition != "" {
145 c.logVersionInformation()
146 return nil
147 }
148
149 err = c.collectSingleMetric(ctx, "version_detection_zos", queryDetectVersionZOS, func(value string) {
150 c.edition = "z/OS"
151 c.version = value
152 c.parseDB2Version()
153 c.Debugf("detected DB2 for z/OS edition")
154 })
155 if err == nil && c.edition != "" {
156 c.logVersionInformation()
157 return nil
158 }
159
160 c.edition = "LUW"
161 c.version = "Unknown"
162 c.Warningf("could not detect DB2 edition, defaulting to LUW")
163 c.parseDB2Version()
164 c.logVersionInformation()
165 return nil
166 }
167
168 func (c *Collector) parseDB2Version() {
169 if c.version == "" || c.version == "Unknown" {
170 return
171 }
172
173 versionStr := c.version
174 if strings.Contains(versionStr, "v") {
175 parts := strings.Split(versionStr, "v")
176 if len(parts) > 1 {
177 versionStr = parts[1]
178 }
179 } else if strings.Contains(versionStr, "V") && strings.Contains(versionStr, "R") {
180 versionStr = strings.Replace(versionStr, "V", "", 1)
181 versionStr = strings.Replace(versionStr, "R", ".", 1)
182 versionStr = strings.Replace(versionStr, "M", ".", 1)
183 }
184
185 parts := strings.Split(versionStr, ".")
186 if len(parts) >= 2 {
187 if major, err := strconv.Atoi(parts[0]); err == nil {
188 c.versionMajor = major
189 }
190 if minor, err := strconv.Atoi(parts[1]); err == nil {
191 c.versionMinor = minor
192 }
193 } else if len(parts) == 1 {
194 if major, err := strconv.Atoi(parts[0]); err == nil {
195 c.versionMajor = major
196 }
197 }
198
199 c.Debugf("parsed DB2 version: major=%d, minor=%d", c.versionMajor, c.versionMinor)
200 }
201
202 func (c *Collector) setConfigurationDefaults() {
203 if c.CollectDatabaseMetrics.IsAuto() {
204 defaultValue := c.edition == "LUW" || c.edition == "Cloud"
205 c.CollectDatabaseMetrics = confopt.AutoBoolFromBool(defaultValue)
206 c.Debugf("CollectDatabaseMetrics not configured, defaulting to %v (edition: %s)", defaultValue, c.edition)
207 }
208
209 if c.CollectBufferpoolMetrics.IsAuto() {
210 defaultValue := c.edition != "i"
211 c.CollectBufferpoolMetrics = confopt.AutoBoolFromBool(defaultValue)
212 c.Debugf("CollectBufferpoolMetrics not configured, defaulting to %v (edition: %s)", defaultValue, c.edition)
213 }
214
215 if c.CollectTablespaceMetrics.IsAuto() {
216 defaultValue := true
217 c.CollectTablespaceMetrics = confopt.AutoBoolFromBool(defaultValue)
218 c.Debugf("CollectTablespaceMetrics not configured, defaulting to %v", defaultValue)
219 }
220
221 if c.CollectConnectionMetrics.IsAuto() {
222 defaultValue := c.edition == "LUW" || (c.edition == "Cloud" && !c.isDisabled("connection_instances"))
223 c.CollectConnectionMetrics = confopt.AutoBoolFromBool(defaultValue)
224 c.Debugf("CollectConnectionMetrics not configured, defaulting to %v (edition: %s)", defaultValue, c.edition)
225 }
226
227 if c.CollectLockMetrics.IsAuto() {
228 defaultValue := true
229 c.CollectLockMetrics = confopt.AutoBoolFromBool(defaultValue)
230 c.Debugf("CollectLockMetrics not configured, defaulting to %v", defaultValue)
231 }
232
233 if c.CollectTableMetrics.IsAuto() {
234 defaultValue := false
235 c.CollectTableMetrics = confopt.AutoBoolFromBool(defaultValue)
236 c.Debugf("CollectTableMetrics not configured, defaulting to %v", defaultValue)
237 }
238
239 if c.CollectIndexMetrics.IsAuto() {
240 defaultValue := false
241 c.CollectIndexMetrics = confopt.AutoBoolFromBool(defaultValue)
242 c.Debugf("CollectIndexMetrics not configured, defaulting to %v", defaultValue)
243 }
244
245 c.Infof("Configuration after defaults: DB=%t Bufferpool=%t Tablespace=%t Connection=%t",
246 c.CollectDatabaseMetrics.IsEnabled(),
247 c.CollectBufferpoolMetrics.IsEnabled(),
248 c.CollectTablespaceMetrics.IsEnabled(),
249 c.CollectConnectionMetrics.IsEnabled())
250 c.Infof("Lock=%t Table=%t Index=%t Memory=%v Wait=%v TableIO=%v",
251 c.CollectLockMetrics.IsEnabled(),
252 c.CollectTableMetrics.IsEnabled(),
253 c.CollectIndexMetrics.IsEnabled(),
254 c.CollectMemoryMetrics,
255 c.CollectWaitMetrics,
256 c.CollectTableIOMetrics)
257 }
258
259 func (c *Collector) logVersionInformation() {
260 c.parseDB2Version()
261
262 c.Infof("DB2 %s edition detected - collector will attempt all configured features with graceful error handling", c.edition)
263
264 switch c.edition {
265 case "i":
266 c.Infof("DB2 for i (AS/400) typically has limited SYSIBMADM view support")
267 case "z/OS":
268 c.Infof("DB2 for z/OS typically has different monitoring capabilities than LUW")
269 case "Cloud":
270 c.Infof("Db2 on Cloud typically restricts some system-level metrics compared to standard DB2 LUW")
271 case "LUW":
272 if c.versionMajor > 0 {
273 c.Infof("DB2 LUW %c.%d detected", c.versionMajor, c.versionMinor)
274 } else {
275 c.Infof("DB2 LUW version unknown - assuming broad feature availability")
276 }
277 default:
278 c.Infof("Unknown DB2 edition '%s' - assuming LUW-like capabilities", c.edition)
279 }
280
281 c.Infof("Note: admin configuration takes precedence - all enabled features will be attempted")
282 }
283
284 func (c *Collector) detectColumnOrganizedSupport(ctx context.Context) {
285 if c.edition == "i" {
286 c.Infof("Column-organized tables not available on DB2 for i")
287 return
288 }
289
290 if c.versionMajor > 0 && c.versionMajor < 10 {
291 c.Infof("Column-organized tables require DB2 10.5+, current version %c.%d", c.versionMajor, c.versionMinor)
292 return
293 }
294
295 if c.versionMajor == 10 && c.versionMinor < 5 {
296 c.Infof("Column-organized tables require DB2 10.5+, current version %c.%d", c.versionMajor, c.versionMinor)
297 return
298 }
299
300 c.Debugf("Column-organized buffer pool metrics expected to be available (DB2 %c.%d)", c.versionMajor, c.versionMinor)
301 }
302
303 func cleanName(name string) string {
304 replacer := strings.NewReplacer(
305 " ", "_",
306 ".", "_",
307 "-", "_",
308 "/", "_",
309 ":", "_",
310 "=", "_",
311 )
312 return strings.ToLower(replacer.Replace(name))
313 }