bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/scollector/collectors/oracle.go (about) 1 package collectors 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os" 8 "strconv" 9 "strings" 10 "time" 11 12 "bosun.org/cmd/scollector/conf" 13 "bosun.org/metadata" 14 "bosun.org/opentsdb" 15 "bosun.org/slog" 16 "bosun.org/util" 17 ) 18 19 func init() { 20 os.Setenv("NLS_LANG", "AMERICAN_AMERICA.AL32UTF8") 21 22 registerInit(func(c *conf.Conf) { 23 for _, o := range c.Oracles { 24 name := o.ClusterName 25 for _, inst := range o.Instances { 26 i := inst 27 collectors = append(collectors, &IntervalCollector{ 28 F: func() (opentsdb.MultiDataPoint, error) { 29 return c_oracle(name, i) 30 }, 31 name: fmt.Sprintf("oracle-%s", name), 32 }) 33 } 34 } 35 }) 36 } 37 38 func c_oracle(name string, inst conf.OracleInstance) (opentsdb.MultiDataPoint, error) { 39 var md opentsdb.MultiDataPoint 40 41 pr, pw := io.Pipe() 42 errc := make(chan error, 1) 43 44 p := &sqlplusParser{ 45 md: &md, 46 prefix: "oracle.", 47 common: opentsdb.TagSet{ 48 "oracle_cluster": name, 49 }, 50 } 51 52 args := []string{"-S", inst.ConnectionString} 53 if role := inst.Role; role != "" { 54 args = append(args, "as") 55 args = append(args, role) 56 } 57 58 go func() { 59 errc <- util.ReadCommandTimeout(10*time.Second, p.ParseAndAdd, pr, "sqlplus", args...) 60 }() 61 62 sqlplusWrite(pw) 63 64 return md, <-errc 65 } 66 67 type sqlplusQueryRowParser struct { 68 query string 69 parse func(row string, md *opentsdb.MultiDataPoint, prefix string, common opentsdb.TagSet) error 70 } 71 72 var ( 73 sqlplusParserFieldCountErr = errors.New("number of field doesn't match") 74 ) 75 76 var sqlplusParsers = []sqlplusQueryRowParser{ 77 { 78 "select name from v$database;\n", 79 func(row string, md *opentsdb.MultiDataPoint, prefix string, common opentsdb.TagSet) error { 80 // get database name and add it to common tag set 81 common.Merge(opentsdb.TagSet{"oracle_database": row}) 82 return nil 83 }, 84 }, 85 { 86 "select instance_name from v$instance;\n", 87 func(row string, md *opentsdb.MultiDataPoint, prefix string, common opentsdb.TagSet) error { 88 // get instance name and add it to common tag set 89 common.Merge(opentsdb.TagSet{"oracle_instance": row}) 90 return nil 91 }, 92 }, 93 { 94 "select METRIC_NAME || ',' || INTSIZE_CSEC || ',' || VALUE || ',' || METRIC_UNIT from v$sysmetric;\n", 95 func(row string, md *opentsdb.MultiDataPoint, prefix string, common opentsdb.TagSet) error { 96 fields := strings.Split(row, ",") 97 if len(fields) != 4 { 98 return sqlplusParserFieldCountErr 99 } 100 101 v, err := sqlplusValueConv(fields[2]) 102 if err != nil { 103 return err 104 } 105 106 name := sqlplusMetricNameConv(fields[0]) 107 108 period, err := strconv.Atoi(fields[1]) 109 if err != nil { 110 return err 111 } 112 period = (period/100 + 4) / 5 * 5 // handle rounding error 113 114 name = prefix + name + "_" + strconv.Itoa(period) + "s" 115 116 Add(md, name, v, common, metadata.Gauge, metadata.Unit(fields[3]), fields[0]) 117 return nil 118 }, 119 }, 120 { 121 "select NAME || ',' || VALUE from v$sysstat where NAME not like '%this session%';\n", 122 func(row string, md *opentsdb.MultiDataPoint, prefix string, common opentsdb.TagSet) error { 123 fields := strings.Split(row, ",") 124 if len(fields) != 2 { 125 return sqlplusParserFieldCountErr 126 } 127 128 v, err := sqlplusValueConv(fields[1]) 129 if err != nil { 130 return err 131 } 132 133 f0 := fields[0] 134 name := sqlplusMetricNameConv(f0) 135 136 rate := metadata.RateType(metadata.Counter) 137 if f0 == "logons current" || strings.HasSuffix(f0, "cursors current") || 138 strings.HasPrefix(f0, "gc current") { 139 rate = metadata.Gauge 140 } 141 142 Add(md, prefix+name, v, common, rate, metadata.None, f0) 143 return nil 144 }, 145 }, 146 { 147 "select TABLESPACE_NAME || ',' || USED_PERCENT from dba_tablespace_usage_metrics;\n", 148 func(row string, md *opentsdb.MultiDataPoint, prefix string, common opentsdb.TagSet) error { 149 fields := strings.Split(row, ",") 150 if len(fields) != 2 { 151 return sqlplusParserFieldCountErr 152 } 153 154 v, err := sqlplusValueConv(fields[1]) 155 if err != nil { 156 return err 157 } 158 159 ts := common.Copy().Merge(opentsdb.TagSet{"tablespace_name": fields[0]}) 160 Add(md, prefix+"tablespace_usage", v, ts, metadata.Gauge, metadata.Pct, "tablespace usage with autoextend and disk space be considered in") 161 162 return nil 163 }, 164 }, 165 { 166 "select NAME || ',' || TYPE || ',' || TOTAL_MB || ',' || FREE_MB || ',' || USABLE_FILE_MB || ',' || OFFLINE_DISKS from v$asm_diskgroup_stat;\n", 167 func(row string, md *opentsdb.MultiDataPoint, prefix string, common opentsdb.TagSet) error { 168 fields := strings.Split(row, ",") 169 if len(fields) != 6 { 170 return sqlplusParserFieldCountErr 171 } 172 173 ts := common.Copy().Merge(opentsdb.TagSet{ 174 "diskgroup_name": fields[0], 175 "diskgroup_type": fields[1], 176 }) 177 178 v1, err1 := sqlplusValueConv(fields[2]) 179 v2, err2 := sqlplusValueConv(fields[3]) 180 v3, err3 := sqlplusValueConv(fields[4]) 181 v4, err4 := sqlplusValueConv(fields[5]) 182 183 for _, err := range []error{err1, err2, err3, err4} { 184 if err != nil { 185 return err 186 } 187 } 188 189 rate := metadata.RateType(metadata.Gauge) 190 191 Add(md, prefix+"asm_diskgroup.total_mb", v1, ts, rate, metadata.None, "asm disk group total space counted in megabytes") 192 Add(md, prefix+"asm_diskgroup.free_mb", v2, ts, rate, metadata.None, "asm disk group free space counted in megabytes") 193 Add(md, prefix+"asm_diskgroup.usable_file_mb", v3, ts, rate, metadata.None, "asm disk group usable space for storing files counted in megabytes") 194 Add(md, prefix+"asm_diskgroup.offline_disks", v4, ts, rate, metadata.None, "asm disk group offline disk count") 195 196 return nil 197 }, 198 }, 199 { 200 "select FAILGROUP || ',' || READS || ',' || WRITES || ',' || READ_ERRS || ',' || WRITE_ERRS || ',' || READ_TIME || ',' || WRITE_TIME || ',' || BYTES_READ || ',' || BYTES_WRITTEN from v$asm_disk_iostat;\n", 201 func(row string, md *opentsdb.MultiDataPoint, prefix string, common opentsdb.TagSet) error { 202 fields := strings.Split(row, ",") 203 if len(fields) != 9 { 204 return sqlplusParserFieldCountErr 205 } 206 207 ts := common.Copy().Merge(opentsdb.TagSet{"failgroup": fields[0]}) 208 209 v1, err1 := sqlplusValueConv(fields[1]) 210 v2, err2 := sqlplusValueConv(fields[2]) 211 v3, err3 := sqlplusValueConv(fields[3]) 212 v4, err4 := sqlplusValueConv(fields[4]) 213 v5, err5 := sqlplusValueConv(fields[5]) 214 v6, err6 := sqlplusValueConv(fields[6]) 215 v7, err7 := sqlplusValueConv(fields[7]) 216 v8, err8 := sqlplusValueConv(fields[8]) 217 218 for _, err := range []error{err1, err2, err3, err4, 219 err5, err6, err7, err8} { 220 if err != nil { 221 return err 222 } 223 } 224 225 rate := metadata.RateType(metadata.Counter) 226 227 Add(md, prefix+"asm_disk_iostat.reads", v1, ts, rate, metadata.None, "asm disk total reads") 228 Add(md, prefix+"asm_disk_iostat.writes", v2, ts, rate, metadata.None, "asm disk total writes") 229 Add(md, prefix+"asm_disk_iostat.read_errors", v3, ts, rate, metadata.Error, "asm disk total read errors") 230 Add(md, prefix+"asm_disk_iostat.write_errors", v4, ts, rate, metadata.Error, "asm disk total write errors") 231 Add(md, prefix+"asm_disk_iostat.read_time", v5, ts, rate, metadata.Second, "asm disk total read time in second") 232 Add(md, prefix+"asm_disk_iostat.write_time", v6, ts, rate, metadata.Second, "asm disk total write time in second") 233 Add(md, prefix+"asm_disk_iostat.bytes_read", v7, ts, rate, metadata.Bytes, "asm disk total bytes read") 234 Add(md, prefix+"asm_disk_iostat.bytes_written", v8, ts, rate, metadata.Bytes, "asm disk total bytes written") 235 236 return nil 237 }, 238 }, 239 { 240 "select b.WAIT_CLASS || ',' || a.AVERAGE_WAITER_COUNT || ',' || a.DBTIME_IN_WAIT || ',' || b.TOTAL_WAITS || ',' || b.TIME_WAITED || ',' || b.TOTAL_WAITS_FG || ',' || b.TIME_WAITED_FG from v$waitclassmetric a, v$system_wait_class b where a.WAIT_CLASS_ID = b.WAIT_CLASS_ID and WAIT_CLASS <> 'Idle';\n", 241 func(row string, md *opentsdb.MultiDataPoint, prefix string, common opentsdb.TagSet) error { 242 fields := strings.Split(row, ",") 243 if len(fields) != 7 { 244 return sqlplusParserFieldCountErr 245 } 246 247 ts := common.Copy().Merge(opentsdb.TagSet{"wait_class": fields[0]}) 248 249 v1, err1 := sqlplusValueConv(fields[1]) 250 v2, err2 := sqlplusValueConv(fields[2]) 251 v3, err3 := sqlplusValueConv(fields[3]) 252 v4, err4 := sqlplusValueConv(fields[4]) 253 v5, err5 := sqlplusValueConv(fields[5]) 254 v6, err6 := sqlplusValueConv(fields[6]) 255 256 for _, err := range []error{err1, err2, err3, err4, err5, err6} { 257 if err != nil { 258 return err 259 } 260 } 261 262 Add(md, prefix+"wait_class.avg_waiter_1m", v1, ts, metadata.Gauge, metadata.None, "average waiter count for one minute") 263 Add(md, prefix+"wait_class.avg_dbtime_wait_1m", v2, ts, metadata.Gauge, metadata.Pct, "average database time in wait for one minute") 264 Add(md, prefix+"wait_class.total_waits", v3, ts, metadata.Counter, metadata.None, "total waits") 265 Add(md, prefix+"wait_class.total_time_waited", v4, ts, metadata.Counter, metadata.Second, "total time waited") 266 Add(md, prefix+"wait_class.total_foreground_waits", v5, ts, metadata.Counter, metadata.None, "total foreground waits") 267 Add(md, prefix+"wait_class.total_time_foreground_waited", v6, ts, metadata.Counter, metadata.Second, "total time foreground waited") 268 269 return nil 270 }, 271 }, 272 } 273 274 type sqlplusParser struct { 275 parsedQuery int 276 277 md *opentsdb.MultiDataPoint 278 prefix string 279 common opentsdb.TagSet 280 } 281 282 func (p *sqlplusParser) ParseAndAdd(line string) error { 283 parsed, n := p.parsedQuery, len(sqlplusParsers) 284 285 // query result separator is blank line 286 if line == "" { 287 return nil 288 } 289 290 // handle feed, end of one query 291 if line == "no rows selected" || strings.HasSuffix(line, " rows selected.") || 292 strings.HasSuffix(line, " row selected.") { 293 p.parsedQuery++ 294 return nil 295 } 296 297 // finished all queries 298 if parsed == n { 299 return nil 300 } 301 302 // process actual queries 303 if err := sqlplusParsers[parsed].parse(line, p.md, p.prefix, p.common); err != nil { 304 slog.Errorln("oracle sqlplus parser error:", err) 305 } 306 return nil 307 } 308 309 func sqlplusFormatOutput(w io.Writer) (err error) { 310 _, err = io.WriteString(w, "set linesize 32767;\n") 311 if err != nil { 312 return 313 } 314 315 _, err = io.WriteString(w, "set pagesize 32767;\n") 316 if err != nil { 317 return 318 } 319 320 _, err = io.WriteString(w, "set head off;\n") 321 if err != nil { 322 return 323 } 324 325 _, err = io.WriteString(w, "set feed on;\n") 326 if err != nil { 327 return 328 } 329 330 _, err = io.WriteString(w, "set colsep \",\";\n") 331 if err != nil { 332 return 333 } 334 335 _, err = io.WriteString(w, "set trimspool on;\n") 336 if err != nil { 337 return 338 } 339 340 _, err = io.WriteString(w, "set trimout on;\n") 341 return 342 } 343 344 func sqlplusExit(w io.Writer) (err error) { 345 _, err = io.WriteString(w, "exit;\n") 346 return 347 } 348 349 func sqlplusWrite(pw *io.PipeWriter) { 350 var err error 351 defer func() { 352 pw.CloseWithError(err) 353 }() 354 355 err = sqlplusFormatOutput(pw) 356 if err != nil { 357 return 358 } 359 360 for _, p := range sqlplusParsers { 361 _, err = io.WriteString(pw, p.query) 362 if err != nil { 363 return 364 } 365 } 366 367 err = sqlplusExit(pw) 368 } 369 370 func sqlplusValueConv(value string) (interface{}, error) { 371 if strings.Contains(value, ".") { 372 // opentsdb only accept single precision floating point number 373 return strconv.ParseFloat(value, 32) 374 } 375 return strconv.ParseInt(value, 10, 64) 376 } 377 378 func sqlplusMetricNameConv(name string) string { 379 name = strings.Replace(name, "<", "below_", -1) 380 name = strings.Replace(name, ">=", "above_", -1) 381 name = strings.Replace(name, "(", "", -1) 382 name = strings.Replace(name, ")", "", -1) 383 name = strings.Replace(name, "%", "ratio", -1) 384 385 // ignore this error 386 name, _ = opentsdb.Replace(strings.ToLower(name), "_") 387 return name 388 }