| 1 | //go:build cgo |
| 2 | |
| 3 | package as400 |
| 4 | |
| 5 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 6 | |
| 7 | import ( |
| 8 | "context" |
| 9 | "fmt" |
| 10 | "strconv" |
| 11 | "strings" |
| 12 | |
| 13 | as400proto "github.com/netdata/netdata/go/plugins/plugin/ibm.d/protocols/as400" |
| 14 | ) |
| 15 | |
| 16 | func cleanName(name string) string { |
| 17 | r := strings.NewReplacer( |
| 18 | " ", "_", |
| 19 | ".", "_", |
| 20 | "-", "_", |
| 21 | "/", "_", |
| 22 | ":", "_", |
| 23 | "=", "_", |
| 24 | ) |
| 25 | return strings.ToLower(r.Replace(name)) |
| 26 | } |
| 27 | |
| 28 | func (c *Collector) logOnce(key string, format string, args ...any) { |
| 29 | if c.disabled[key] { |
| 30 | return |
| 31 | } |
| 32 | c.Warningf(format, args...) |
| 33 | c.disabled[key] = true |
| 34 | } |
| 35 | |
| 36 | func (c *Collector) logErrorOnce(key string, format string, args ...any) { |
| 37 | c.muErrorLog.Lock() |
| 38 | defer c.muErrorLog.Unlock() |
| 39 | |
| 40 | if c.errorLogged[key] { |
| 41 | return |
| 42 | } |
| 43 | msg := fmt.Sprintf(format, args...) |
| 44 | c.Errorf("[%s] %s", key, msg) |
| 45 | c.errorLogged[key] = true |
| 46 | } |
| 47 | |
| 48 | func (c *Collector) clearErrorOnce(key string) { |
| 49 | c.muErrorLog.Lock() |
| 50 | defer c.muErrorLog.Unlock() |
| 51 | |
| 52 | delete(c.errorLogged, key) |
| 53 | } |
| 54 | |
| 55 | func (c *Collector) isDisabled(key string) bool { |
| 56 | return c.disabled[key] |
| 57 | } |
| 58 | |
| 59 | func isSQLFeatureError(err error) bool { |
| 60 | return as400proto.IsFeatureError(err) |
| 61 | } |
| 62 | |
| 63 | func isSQLTemporaryError(err error) bool { |
| 64 | return as400proto.IsTemporaryError(err) |
| 65 | } |
| 66 | |
| 67 | func (c *Collector) collectSingleMetric(ctx context.Context, metricKey string, query string, handler func(value string)) error { |
| 68 | if c.isDisabled(metricKey) { |
| 69 | return nil |
| 70 | } |
| 71 | |
| 72 | err := c.doQuery(ctx, metricKey, query, func(column, value string, lineEnd bool) { |
| 73 | if value != "" { |
| 74 | handler(value) |
| 75 | } |
| 76 | }) |
| 77 | |
| 78 | if err != nil && isSQLFeatureError(err) { |
| 79 | c.logOnce(metricKey, "metric %s not available on this IBM i version: %v", metricKey, err) |
| 80 | c.disabled[metricKey] = true |
| 81 | return nil |
| 82 | } |
| 83 | |
| 84 | return err |
| 85 | } |
| 86 | |
| 87 | func (c *Collector) detectIBMiVersion(ctx context.Context) error { |
| 88 | var version, release string |
| 89 | versionDetected := false |
| 90 | |
| 91 | err := c.doQuery(ctx, "detect_ibmi_version_primary", queryIBMiVersion, func(column, value string, lineEnd bool) { |
| 92 | switch column { |
| 93 | case "OS_NAME": |
| 94 | // ignore |
| 95 | case "OS_VERSION": |
| 96 | version = strings.TrimSpace(value) |
| 97 | case "OS_RELEASE": |
| 98 | release = strings.TrimSpace(value) |
| 99 | } |
| 100 | }) |
| 101 | |
| 102 | if err == nil && version != "" && release != "" { |
| 103 | versionDetected = true |
| 104 | c.osVersion = fmt.Sprintf("%s.%s", version, release) |
| 105 | c.Debugf("detected IBM i version from ENV_SYS_INFO: %s", c.osVersion) |
| 106 | } else if err != nil { |
| 107 | c.Debugf("ENV_SYS_INFO query failed: %v, trying fallback method", err) |
| 108 | |
| 109 | err = c.doQuery(ctx, "detect_ibmi_version_fallback", queryIBMiVersionDataArea, func(column, value string, lineEnd bool) { |
| 110 | if column == "VERSION" { |
| 111 | dataAreaValue := strings.TrimSpace(value) |
| 112 | if len(dataAreaValue) >= 6 { |
| 113 | c.osVersion = dataAreaValue |
| 114 | versionDetected = true |
| 115 | c.Debugf("detected IBM i version from data area: %s", c.osVersion) |
| 116 | } |
| 117 | } |
| 118 | }) |
| 119 | |
| 120 | if err != nil { |
| 121 | c.Warningf("failed to detect IBM i version from data area: %v", err) |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | if !versionDetected { |
| 126 | c.osVersion = "7.4" |
| 127 | c.Warningf("could not detect IBM i version, using default: %s", c.osVersion) |
| 128 | } |
| 129 | |
| 130 | c.parseIBMiVersion() |
| 131 | return nil |
| 132 | } |
| 133 | |
| 134 | func (c *Collector) detectAvailableFeatures(ctx context.Context) { |
| 135 | c.disabled["active_job_info"] = true |
| 136 | c.disabled["ifs_object_statistics"] = true |
| 137 | } |
| 138 | |
| 139 | func trimDriverMessage(err error) string { |
| 140 | if err == nil { |
| 141 | return "" |
| 142 | } |
| 143 | msg := err.Error() |
| 144 | msg = strings.TrimRight(msg, "\x00") |
| 145 | msg = strings.TrimSpace(msg) |
| 146 | return msg |
| 147 | } |
| 148 | |
| 149 | func sanitizeQuery(q string) string { |
| 150 | if q == "" { |
| 151 | return "" |
| 152 | } |
| 153 | q = strings.Join(strings.Fields(q), " ") |
| 154 | const limit = 240 |
| 155 | if len(q) > limit { |
| 156 | return q[:limit-3] + "..." |
| 157 | } |
| 158 | return q |
| 159 | } |
| 160 | |
| 161 | func (c *Collector) logQueryErrorOnce(key, query string, err error) { |
| 162 | msg := trimDriverMessage(err) |
| 163 | if query != "" { |
| 164 | query = sanitizeQuery(query) |
| 165 | c.logErrorOnce(key, "query failed (%s): %s", query, msg) |
| 166 | return |
| 167 | } |
| 168 | c.logErrorOnce(key, "%s", msg) |
| 169 | } |
| 170 | |
| 171 | func (c *Collector) collectSystemInfo(ctx context.Context) { |
| 172 | c.systemName = "Unknown" |
| 173 | |
| 174 | _ = c.collectSingleMetric(ctx, "serial_number", querySerialNumber, func(value string) { |
| 175 | c.serialNumber = strings.TrimSpace(value) |
| 176 | c.Debugf("detected serial number: %s", c.serialNumber) |
| 177 | }) |
| 178 | |
| 179 | _ = c.collectSingleMetric(ctx, "system_name_metric", querySystemName, func(value string) { |
| 180 | name := strings.TrimSpace(value) |
| 181 | if name != "" { |
| 182 | c.systemName = name |
| 183 | c.Debugf("detected system name: %s", c.systemName) |
| 184 | } |
| 185 | }) |
| 186 | |
| 187 | _ = c.collectSingleMetric(ctx, "system_model", querySystemModel, func(value string) { |
| 188 | c.model = strings.TrimSpace(value) |
| 189 | c.Debugf("detected system model: %s", c.model) |
| 190 | }) |
| 191 | |
| 192 | err := c.doQuery(ctx, "technology_refresh_level", queryTechnologyRefresh, func(column, value string, lineEnd bool) { |
| 193 | if column == "TR_LEVEL" && value != "" { |
| 194 | trLevel := strings.TrimSpace(value) |
| 195 | if trLevel != "" { |
| 196 | c.technologyRefresh = fmt.Sprintf("TR%s", trLevel) |
| 197 | c.Debugf("detected Technology Refresh level: %s", c.technologyRefresh) |
| 198 | } |
| 199 | } |
| 200 | }) |
| 201 | if err != nil { |
| 202 | c.Debugf("failed to detect Technology Refresh level: %v", err) |
| 203 | } |
| 204 | |
| 205 | if c.systemName == "" { |
| 206 | c.systemName = "Unknown" |
| 207 | } |
| 208 | if c.serialNumber == "" { |
| 209 | c.serialNumber = "Unknown" |
| 210 | } |
| 211 | if c.model == "" { |
| 212 | c.model = "Unknown" |
| 213 | } |
| 214 | if c.osVersion == "" { |
| 215 | c.osVersion = "Unknown" |
| 216 | } |
| 217 | if c.technologyRefresh == "" { |
| 218 | c.technologyRefresh = "Unknown" |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | func (c *Collector) parseIBMiVersion() { |
| 223 | if c.osVersion == "" || c.osVersion == "Unknown" { |
| 224 | return |
| 225 | } |
| 226 | |
| 227 | versionStr := strings.ToUpper(strings.TrimSpace(c.osVersion)) |
| 228 | if idx := strings.IndexAny(versionStr, " L"); idx > 0 { |
| 229 | versionStr = versionStr[:idx] |
| 230 | } |
| 231 | versionStr = strings.ReplaceAll(versionStr, " ", "") |
| 232 | |
| 233 | if strings.HasPrefix(versionStr, "V") { |
| 234 | versionStr = versionStr[1:] |
| 235 | if idx := strings.Index(versionStr, "R"); idx > 0 { |
| 236 | if major, err := strconv.Atoi(versionStr[:idx]); err == nil { |
| 237 | c.versionMajor = major |
| 238 | } |
| 239 | remainder := versionStr[idx+1:] |
| 240 | if mIdx := strings.Index(remainder, "M"); mIdx > 0 { |
| 241 | if release, err := strconv.Atoi(remainder[:mIdx]); err == nil { |
| 242 | c.versionRelease = release |
| 243 | } |
| 244 | modStr := remainder[mIdx+1:] |
| 245 | modNum := "" |
| 246 | for _, ch := range modStr { |
| 247 | if ch >= '0' && ch <= '9' { |
| 248 | modNum += string(ch) |
| 249 | } else { |
| 250 | break |
| 251 | } |
| 252 | } |
| 253 | if modNum != "" { |
| 254 | if mod, err := strconv.Atoi(modNum); err == nil { |
| 255 | c.versionMod = mod |
| 256 | } |
| 257 | } |
| 258 | } else { |
| 259 | if release, err := strconv.Atoi(remainder); err == nil { |
| 260 | c.versionRelease = release |
| 261 | } |
| 262 | } |
| 263 | } |
| 264 | } else { |
| 265 | parts := strings.Split(versionStr, ".") |
| 266 | if len(parts) >= 2 { |
| 267 | if major, err := strconv.Atoi(parts[0]); err == nil { |
| 268 | c.versionMajor = major |
| 269 | } |
| 270 | if release, err := strconv.Atoi(parts[1]); err == nil { |
| 271 | c.versionRelease = release |
| 272 | } |
| 273 | if len(parts) >= 3 { |
| 274 | if mod, err := strconv.Atoi(parts[2]); err == nil { |
| 275 | c.versionMod = mod |
| 276 | } |
| 277 | } |
| 278 | } else if len(parts) == 1 { |
| 279 | if major, err := strconv.Atoi(parts[0]); err == nil { |
| 280 | c.versionMajor = major |
| 281 | } |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | c.Debugf("parsed IBM i version: major=%d, release=%d, mod=%d", c.versionMajor, c.versionRelease, c.versionMod) |
| 286 | } |
| 287 | |
| 288 | func (c *Collector) logVersionInformation() { |
| 289 | if c.versionMajor > 0 { |
| 290 | c.Infof("IBM i %d.%d detected - collector will attempt all configured features with graceful error handling", c.versionMajor, c.versionRelease) |
| 291 | |
| 292 | if c.versionMajor >= 7 { |
| 293 | if c.versionRelease >= 5 { |
| 294 | c.Infof("IBM i 7.5+ typically supports all collector features") |
| 295 | } else if c.versionRelease >= 3 { |
| 296 | c.Infof("IBM i 7.3+ typically supports ACTIVE_JOB_INFO and IFS_OBJECT_STATISTICS") |
| 297 | } else if c.versionRelease >= 2 { |
| 298 | c.Infof("IBM i 7.2+ typically supports MESSAGE_QUEUE_INFO and JOB_QUEUE_ENTRIES") |
| 299 | } else { |
| 300 | c.Infof("IBM i 7.1+ has basic SQL services - some advanced features may not be available") |
| 301 | } |
| 302 | } else { |
| 303 | c.Infof("IBM i %d.x has limited SQL services - many features may not be available", c.versionMajor) |
| 304 | } |
| 305 | |
| 306 | c.Infof("Note: admin configuration takes precedence - all enabled features will be attempted regardless of version") |
| 307 | } else { |
| 308 | c.Infof("IBM i version unknown - collector will attempt all configured features with graceful error handling") |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | func (c *Collector) setConfigurationDefaults() { |
| 313 | c.CollectDiskMetrics = c.CollectDiskMetrics.WithDefault(true) |
| 314 | c.CollectSubsystemMetrics = c.CollectSubsystemMetrics.WithDefault(true) |
| 315 | c.CollectHTTPServerMetrics = c.CollectHTTPServerMetrics.WithDefault(true) |
| 316 | c.CollectPlanCacheMetrics = c.CollectPlanCacheMetrics.WithDefault(true) |
| 317 | |
| 318 | if len(c.ActiveJobs) == 0 { |
| 319 | c.CollectActiveJobs = c.CollectActiveJobs.WithDefault(false) |
| 320 | } else { |
| 321 | c.CollectActiveJobs = c.CollectActiveJobs.WithDefault(true) |
| 322 | } |
| 323 | |
| 324 | if c.MessageQueues == nil { |
| 325 | c.MessageQueues = append([]string{}, "QSYS/QSYSOPR", "QSYS/QSYSMSG", "QSYS/QHST") |
| 326 | } |
| 327 | |
| 328 | c.Infof("Configuration after defaults: DiskMetrics=%t, SubsystemMetrics=%t, ActiveJobs=%t (configured=%d), HTTPServer=%t, PlanCache=%t", |
| 329 | c.CollectDiskMetrics.IsEnabled(), |
| 330 | c.CollectSubsystemMetrics.IsEnabled(), |
| 331 | c.CollectActiveJobs.IsEnabled(), |
| 332 | len(c.ActiveJobs), |
| 333 | c.CollectHTTPServerMetrics.IsEnabled(), |
| 334 | c.CollectPlanCacheMetrics.IsEnabled()) |
| 335 | } |
| 336 | |
| 337 | func (c *Collector) systemStatusQuery() string { |
| 338 | if c.ResetStatistics { |
| 339 | return querySystemStatusReset |
| 340 | } |
| 341 | return querySystemStatusNoReset |
| 342 | } |
| 343 | |
| 344 | func (c *Collector) memoryPoolQuery() string { |
| 345 | if c.ResetStatistics { |
| 346 | return queryMemoryPoolsReset |
| 347 | } |
| 348 | return queryMemoryPoolsNoReset |
| 349 | } |
| 350 | |
| 351 | func (c *Collector) systemActivityQuery() string { |
| 352 | if c.ResetStatistics { |
| 353 | return querySystemActivityReset |
| 354 | } |
| 355 | return querySystemActivityNoReset |
| 356 | } |
| 357 | |
| 358 | func (c *Collector) supportsMessageQueueTableFunction() bool { |
| 359 | if c.versionMajor == 0 { |
| 360 | return false |
| 361 | } |
| 362 | if c.versionMajor > 7 { |
| 363 | return true |
| 364 | } |
| 365 | return c.versionMajor == 7 && c.versionRelease >= 4 |
| 366 | } |