diff --git a/oracledb-config.yml.sample b/oracledb-config.yml.sample index b24cba9..00f9044 100644 --- a/oracledb-config.yml.sample +++ b/oracledb-config.yml.sample @@ -26,13 +26,13 @@ instances: tablespaces: '["tablespace1", "tablespace2"]' # Collect extended metrics. If omitted, defaults to false. extended_metrics: true - # custom_metrics_query: >- - # SELECT - # 'physical_reads' AS "metric_name", - # 'gauge' AS "metric_type", - # SUM(PHYRDS) AS "metric_value", - # INST_ID AS "inst_id" - # FROM gv$filestat - # GROUP BY INST_ID + # A custom metrics query will run the custom query, then save the columns as + # metrics on the OracleCustomSample event type. + custom_metrics_query: >- + SELECT + SUM(PHYRDS) AS "physical_reads", + INST_ID AS "inst_id" + FROM gv$filestat + GROUP BY INST_ID labels: env: production diff --git a/src/metric_definitions.go b/src/metric_definitions.go index e0e42db..31def4c 100644 --- a/src/metric_definitions.go +++ b/src/metric_definitions.go @@ -34,8 +34,9 @@ type newrelicMetric struct { // a channel along with the metadata needed to insert it into the correct // metric set type newrelicMetricSender struct { - metric *newrelicMetric - metadata map[string]string + metric *newrelicMetric + metadata map[string]string + customMetrics []map[string]interface{} } // oracleMetricGroup is a struct that contains all the information needed @@ -157,6 +158,12 @@ func (mg *customMetricGroup) Collect(db *sqlx.DB, wg *sync.WaitGroup, metricChan return } } + defer func() { + err := rows.Close() + if err != nil { + log.Error("Failed to close rows: %s", err) + } + }() rows, err = db.Queryx(mg.Query) if err != nil { @@ -170,6 +177,11 @@ func (mg *customMetricGroup) Collect(db *sqlx.DB, wg *sync.WaitGroup, metricChan } }() + sender := newrelicMetricSender{ + metadata: map[string]string{ + "instanceID": instanceID, + }, + } for rows.Next() { row := make(map[string]interface{}) err := rows.MapScan(row) @@ -178,58 +190,25 @@ func (mg *customMetricGroup) Collect(db *sqlx.DB, wg *sync.WaitGroup, metricChan return } - nameInterface, ok := row["metric_name"] - if !ok { - log.Error("Missing required column 'metric_name' in custom query") - return - } - name, ok := nameInterface.(string) - if !ok { - log.Error("Non-string type %T for custom query 'metric_name' column", nameInterface) - continue - } - - metricTypeInterface, ok := row["metric_type"] - if !ok { - log.Error("Missing required column 'metric_type' in custom query") - return - } - metricTypeString, ok := metricTypeInterface.(string) - if !ok { - log.Error("Non-string type %T for custom query 'metric_type' column", metricTypeInterface) - continue - } - metricType, err := metric.SourceTypeForName(metricTypeString) - if err != nil { - log.Error("Invalid metric type %s: %s", metricTypeString, err) - continue - } - - value, ok := row["metric_value"] - if !ok { - log.Error("Missing required column 'metric_type' in custom query") - return - } - - metadata := make(map[string]string) - metadata["instanceID"] = instanceID - for k, v := range row { - if k == "metric_name" || k == "metric_type" || k == "metric_value" { - continue + convertedMetrics := make(map[string]interface{}) + for key, val := range row { + switch v := val.(type) { + case goracle.Number: + num, err := strconv.ParseFloat(string(v), 64) + if err != nil { + log.Error("Failed to convert %s to a number") + continue + } + convertedMetrics[key] = num + default: + convertedMetrics[key] = val } - - metadata[k] = fmt.Sprintf("%v", v) - } - - newMetric := &newrelicMetric{ - name: name, - metricType: metricType, - value: value, } - metricChan <- newrelicMetricSender{metric: newMetric, metadata: metadata} - + sender.customMetrics = append(sender.customMetrics, convertedMetrics) } + + metricChan <- sender } var oracleLongRunningQueries = oracleMetricGroup{ @@ -1135,7 +1114,7 @@ var oracleTablespaceMetrics = oracleMetricGroup{ if len(tablespaceWhiteList) > 0 { query += ` - WHERE TABLESPACE_NAME IN (` + WHERE a.TABLESPACE_NAME IN (` for i, tablespace := range tablespaceWhiteList { query += fmt.Sprintf(`'%s'`, tablespace) diff --git a/src/metrics.go b/src/metrics.go index 6ae9869..1895b1e 100644 --- a/src/metrics.go +++ b/src/metrics.go @@ -6,7 +6,7 @@ import ( "sync" "github.com/jmoiron/sqlx" - "github.com/newrelic/infra-integrations-sdk/data/metric" + nrmetric "github.com/newrelic/infra-integrations-sdk/data/metric" "github.com/newrelic/infra-integrations-sdk/integration" "github.com/newrelic/infra-integrations-sdk/log" ) @@ -71,8 +71,8 @@ func collectMetrics(db *sqlx.DB, populaterWg *sync.WaitGroup, i *integration.Int func populateMetrics(metricChan <-chan newrelicMetricSender, i *integration.Integration, instanceLookUp map[string]string) { // Create storage maps for tablespace and instance metric sets - tsMetricSets := make(map[string]*metric.Set) - instanceMetricSets := make(map[string]*metric.Set) + tsMetricSets := make(map[string]*nrmetric.Set) + instanceMetricSets := make(map[string]*nrmetric.Set) for { metricSender, ok := <-metricChan @@ -88,6 +88,25 @@ func populateMetrics(metricChan <-chan newrelicMetricSender, i *integration.Inte if err := ms.SetMetric(metric.name, metric.value, metric.metricType); err != nil { log.Error("Failed to set metric %s: %s", metric.name, err) } + } else if metricSender.customMetrics != nil { + instanceID := metricSender.metadata["instanceID"] + instanceName := func() string { + if name, ok := instanceLookUp[instanceID]; ok { + return name + } + + return instanceID + }() + + for _, row := range metricSender.customMetrics { + ms := createCustomMetricSet(instanceName, i) + for key, val := range row { + err := ms.SetMetric(key, val, inferMetricType(val)) + if err != nil { + log.Error("Failed to set metric %s with value %v and type %T: %s", key, val, val, err) + } + } + } } else if instanceID, ok := metricSender.metadata["instanceID"]; ok { instanceName := func() string { if name, ok := instanceLookUp[instanceID]; ok { @@ -101,13 +120,25 @@ func populateMetrics(metricChan <-chan newrelicMetricSender, i *integration.Inte if err := ms.SetMetric(metric.name, metric.value, metric.metricType); err != nil { log.Error("Failed to set metric %s: %s", metric.name, err) } + } } } +func inferMetricType(val interface{}) nrmetric.SourceType { + switch val.(type) { + case string: + return nrmetric.ATTRIBUTE + case float32, float64, int, int32, int64: + return nrmetric.GAUGE + default: + return nrmetric.ATTRIBUTE + } +} + // getOrCreateMetricSet either retrieves a metric set from a map or creates the metric set // and inserts it into the map. -func getOrCreateMetricSet(entityIdentifier string, entityType string, m map[string]*metric.Set, i *integration.Integration) *metric.Set { +func getOrCreateMetricSet(entityIdentifier string, entityType string, m map[string]*nrmetric.Set, i *integration.Integration) *nrmetric.Set { // If the metric set already exists, return it set, ok := m[entityIdentifier] @@ -126,11 +157,11 @@ func getOrCreateMetricSet(entityIdentifier string, entityType string, m map[stri serviceIDAttr, ) - var newSet *metric.Set + var newSet *nrmetric.Set if entityType == "instance" { - newSet = e.NewMetricSet("OracleDatabaseSample", metric.Attr("entityName", "ora-instance:"+entityIdentifier), metric.Attr("displayName", entityIdentifier)) + newSet = e.NewMetricSet("OracleDatabaseSample", nrmetric.Attr("entityName", "ora-instance:"+entityIdentifier), nrmetric.Attr("displayName", entityIdentifier)) } else if entityType == "tablespace" { - newSet = e.NewMetricSet("OracleTablespaceSample", metric.Attr("entityName", "ora-tablespace:"+entityIdentifier), metric.Attr("displayName", entityIdentifier)) + newSet = e.NewMetricSet("OracleTablespaceSample", nrmetric.Attr("entityName", "ora-tablespace:"+entityIdentifier), nrmetric.Attr("displayName", entityIdentifier)) } else { log.Error("Unreachable code") os.Exit(1) @@ -142,6 +173,20 @@ func getOrCreateMetricSet(entityIdentifier string, entityType string, m map[stri return newSet } +func createCustomMetricSet(instanceID string, i *integration.Integration) *nrmetric.Set { + endpointIDAttr := integration.IDAttribute{Key: "endpoint", Value: fmt.Sprintf("%s:%s", args.Hostname, args.Port)} + serviceIDAttr := integration.IDAttribute{Key: "serviceName", Value: args.ServiceName} + e, _ := i.EntityReportedVia( //can't error if both name and namespace are defined + fmt.Sprintf("%s:%s", args.Hostname, args.Port), + instanceID, + "ora-instance", + endpointIDAttr, + serviceIDAttr, + ) + + return e.NewMetricSet("OracleCustomSample", nrmetric.Attr("entityName", "ora-instance:"+instanceID), nrmetric.Attr("displayName", instanceID)) +} + // maxTablespaces is the maximum amount of Tablespaces that can be collect. // If there are more than this number of Tablespaces then collection of // Tablespaces will fail. diff --git a/src/metrics_definitions_test.go b/src/metrics_definitions_test.go index 95a4ae2..4aa7840 100644 --- a/src/metrics_definitions_test.go +++ b/src/metrics_definitions_test.go @@ -316,7 +316,7 @@ func TestOracleTablespaceMetrics_Whitlist(t *testing.T) { t.Error(err) } - mock.ExpectQuery(`.*WHERE TABLESPACE_NAME IN \('testtablespace','othertablespace'\).*`).WillReturnRows( + mock.ExpectQuery(`.*WHERE a.TABLESPACE_NAME IN \('testtablespace','othertablespace'\).*`).WillReturnRows( sqlmock.NewRows([]string{"TABLESPACE_NAME", "USED", "OFFLINE", "SIZE", "USED_PERCENT"}). AddRow("testtablespace", 1234, 0, 4321, 12), )