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  }