add go.d/oracle (#18654)
Ilya Mashchenko committed
Oct 3, 2024 at 15:14 UTC
7f7a50abb6280ac86428b7f02fc4ce17947bf1d9
25 files changed
+1633
-3
src/go/go.mod
+1
@@ -42,6 +42,7 @@ require (
42
github.com/prometheus/common v0.59.1
43
github.com/prometheus/prometheus v2.5.0+incompatible
44
github.com/redis/go-redis/v9 v9.6.1
45
+ github.com/sijms/go-ora/v2 v2.8.21
46
github.com/stretchr/testify v1.9.0
47
github.com/tidwall/gjson v1.17.3
48
github.com/valyala/fastjson v1.6.4
src/go/go.sum
+2
@@ -355,6 +355,8 @@ github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9Nz
355
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
356
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
357
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
358
+github.com/sijms/go-ora/v2 v2.8.21 h1:zhvaKaJzgEn+jq7TJF8YlY4H0P5RHxGc99y9QNoeU+s=
359
+github.com/sijms/go-ora/v2 v2.8.21/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU=
360
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
361
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
362
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
src/go/plugin/go.d/README.md
+1
@@ -117,6 +117,7 @@ see the appropriate collector readme.
117
| [openvpn](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/openvpn) | OpenVPN |
118
| [openvpn_status_log](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/openvpn_status_log) | OpenVPN |
119
| [pgbouncer](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/pgbouncer) | PgBouncer |
120
+| [oracledb](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/oracledb) | Oracle DB |
121
| [phpdaemon](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/phpdaemon) | phpDaemon |
122
| [phpfpm](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/phpfpm) | PHP-FPM |
123
| [pihole](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/pihole) | Pi-hole |
src/go/plugin/go.d/agent/discovery/sd/pipeline/pipeline_test.go
+3
-3
@@ -35,13 +35,13 @@ func Test_defaultConfigs(t *testing.T) {
35
require.NoError(t, err, "abs path")
36
37
bs, err := os.ReadFile(file)
38
- require.NoError(t, err, "read config file")
38
+ require.NoErrorf(t, err, "read config file '%s'", file)
39
40
var cfg Config
41
- require.NoError(t, yaml.Unmarshal(bs, &cfg), "unmarshal")
41
+ require.NoErrorf(t, yaml.Unmarshal(bs, &cfg), "unmarshal '%s'", e.Name())
42
43
_, err = New(cfg)
44
- require.NoError(t, err, "create pipeline")
44
+ require.NoErrorf(t, err, "create pipeline '%s'", e.Name())
45
}
46
}
47
src/go/plugin/go.d/config/go.d/oracledb.conf
new
+9
@@ -0,0 +1,9 @@
1
+## All available configuration options, their descriptions and default values:
2
+## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/oracledb#readme
3
+
4
+#jobs:
5
+# - name: local
6
+# dsn: oracle://username:password@127.0.0.1:1521/XE
7
+#
8
+# - name: local
9
+# dsn: 'oracle://username:password@127.0.0.1:1521/XE?ssl=true&ssl verify=false' # TLS connection
src/go/plugin/go.d/config/go.d/sd/docker.conf
+11
@@ -60,6 +60,8 @@ classify:
60
expr: '{{ match "sp" .Image "nginx nginx:*" }}'
61
- tags: "nginxunit"
62
expr: '{{ match "sp" .Image "nginx/unit nginx/unit:*" }}'
63
+ - tags: "oracledb"
64
+ expr: '{{ and (eq .PrivatePort "1521" "2484") (match "sp" .Image "oracle/database oracle/database:*") }}'
65
- tags: "pgbouncer"
66
expr: '{{ or (eq .PrivatePort "6432") (match "sp" .Image "*/pgbouncer */pgbouncer:*") }}'
67
- tags: "pika"
@@ -207,6 +209,15 @@ compose:
209
- module: nginxunit
210
name: docker_{{.Name}}
211
url: http://{{.Address}}
212
+ - selector: "oracledb"
213
+ template: |
214
+ module: oracledb
215
+ name: docker_{{.Name}}
216
+ {{ if eq .PrivatePort "1521" -}}
217
+ dsn: 'oracle://username:password@{{.Address}}/XE'
218
+ {{ else -}}
219
+ dsn: 'oracle://username:password@{{.Address}}/XE?ssl=true&ssl verify=false'
220
+ {{ end -}}
221
- selector: "pgbouncer"
222
template: |
223
module: pgbouncer
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+11
@@ -100,6 +100,8 @@ classify:
100
expr: '{{ eq .Comm "slapd" }}'
101
- tags: "openvpn"
102
expr: '{{ and (eq .Port "7505") (eq .Comm "openvpn") }}'
103
+ - tags: "oracledb"
104
+ expr: '{{ and (eq .Port "1521" "2484") (eq .Comm "tnslsnr") }}'
105
- tags: "pgbouncer"
106
expr: '{{ or (eq .Port "6432") (eq .Comm "pgbouncer") }}'
107
- tags: "pihole"
@@ -417,6 +419,15 @@ compose:
419
module: openvpn
420
name: local
421
address: {{.Address}}
422
+ - selector: "oracledb"
423
+ template: |
424
+ module: oracledb
425
+ name: local
426
+ {{ if eq .Port "1521" -}}
427
+ dsn: 'oracle://username:password@{{.Address}}/XE'
428
+ {{ else -}}
429
+ dsn: 'oracle://username:password@{{.Address}}/XE?ssl=true&ssl verify=false'
430
+ {{ end -}}
431
- selector: "pgbouncer"
432
template: |
433
module: pgbouncer
src/go/plugin/go.d/modules/init.go
+1
@@ -71,6 +71,7 @@ import (
71
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/openldap"
72
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/openvpn"
73
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/openvpn_status_log"
74
+ _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/oracledb"
75
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/pgbouncer"
76
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/phpdaemon"
77
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/phpfpm"
src/go/plugin/go.d/modules/oracledb/charts.go
new
+363
@@ -0,0 +1,363 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package oracledb
4
+
5
+import (
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
10
+)
11
+
12
+const (
13
+ prioSessionsCount = module.Priority + iota
14
+ prioActiveSessionsCount
15
+ prioSessionsUtilization
16
+
17
+ prioCurrentLogons
18
+ prioLogonsRate
19
+
20
+ prioTablespaceUtilization
21
+ prioTablespaceUsage
22
+
23
+ prioDatabaseWaitTimeRatio
24
+ prioSqlServiceResponseTime
25
+ prioWaitClassWaitTime
26
+ prioEnqueueTimeouts
27
+
28
+ prioActivities
29
+
30
+ prioDiskIO
31
+ prioDiskIOPS
32
+
33
+ prioDiskSorts
34
+
35
+ prioTableScans
36
+
37
+ prioCacheHitRatio
38
+ prioGlobalCacheBlocks
39
+)
40
+
41
+var globalCharts = module.Charts{
42
+ sessionsCountChart.Copy(),
43
+ averageActiveSessionsCountChart.Copy(),
44
+ sessionsUtilizationChart.Copy(),
45
+
46
+ currentLogonsChart.Copy(),
47
+ logonsRateChart.Copy(),
48
+
49
+ databaseWaitTimeRatioChart.Copy(),
50
+ sqlServiceResponseTimeChart.Copy(),
51
+ enqueueTimeoutsChart.Copy(),
52
+
53
+ activityChart.Copy(),
54
+
55
+ diskIOChart.Copy(),
56
+ diskIOPSChart.Copy(),
57
+
58
+ sortsChart.Copy(),
59
+
60
+ tableScansChart.Copy(),
61
+
62
+ cacheHitRatioChart.Copy(),
63
+ globalCacheBlocksChart.Copy(),
64
+}
65
+
66
+var (
67
+ sessionsCountChart = module.Chart{
68
+ ID: "sessions",
69
+ Title: "Sessions",
70
+ Units: "sessions",
71
+ Fam: "sessions",
72
+ Ctx: "oracledb.sessions",
73
+ Type: module.Line,
74
+ Priority: prioSessionsCount,
75
+ Dims: module.Dims{
76
+ {ID: "Session Count", Name: "sessions", Div: precision},
77
+ },
78
+ }
79
+ averageActiveSessionsCountChart = module.Chart{
80
+ ID: "average_active_sessions",
81
+ Title: "Average Active Sessions",
82
+ Units: "sessions",
83
+ Fam: "sessions",
84
+ Ctx: "oracledb.average_active_sessions",
85
+ Type: module.Line,
86
+ Priority: prioActiveSessionsCount,
87
+ Dims: module.Dims{
88
+ {ID: "Average Active Sessions", Name: "active", Div: precision},
89
+ },
90
+ }
91
+ sessionsUtilizationChart = module.Chart{
92
+ ID: "sessions_utilization",
93
+ Title: "Sessions Limit %",
94
+ Units: "percent",
95
+ Fam: "sessions",
96
+ Ctx: "oracledb.sessions_utilization",
97
+ Type: module.Area,
98
+ Priority: prioSessionsUtilization,
99
+ Dims: module.Dims{
100
+ {ID: "Session Limit %", Name: "session_limit", Div: precision},
101
+ },
102
+ }
103
+
104
+ currentLogonsChart = module.Chart{
105
+ ID: "current_logons",
106
+ Title: "Current Logons",
107
+ Units: "logons",
108
+ Fam: "logons",
109
+ Ctx: "oracledb.current_logons",
110
+ Type: module.Line,
111
+ Priority: prioCurrentLogons,
112
+ Dims: module.Dims{
113
+ {ID: "logons current", Name: "logons"},
114
+ },
115
+ }
116
+ logonsRateChart = module.Chart{
117
+ ID: "logons_rate",
118
+ Title: "Logons",
119
+ Units: "logons/s",
120
+ Fam: "logons",
121
+ Ctx: "oracledb.logons",
122
+ Type: module.Line,
123
+ Priority: prioLogonsRate,
124
+ Dims: module.Dims{
125
+ {ID: "logons cumulative", Name: "logons", Algo: module.Incremental},
126
+ },
127
+ }
128
+
129
+ databaseWaitTimeRatioChart = module.Chart{
130
+ ID: "database_wait_time_ratio",
131
+ Title: "Database Wait Time Ratio",
132
+ Units: "percent",
133
+ Fam: "performance",
134
+ Ctx: "oracledb.database_wait_time_ratio",
135
+ Type: module.Area,
136
+ Priority: prioDatabaseWaitTimeRatio,
137
+ Dims: module.Dims{
138
+ {ID: "Database Wait Time Ratio", Name: "db_wait_time", Div: precision},
139
+ },
140
+ }
141
+ sqlServiceResponseTimeChart = module.Chart{
142
+ ID: "sql_service_response_time",
143
+ Title: "SQL Service Response Time",
144
+ Units: "seconds",
145
+ Fam: "performance",
146
+ Ctx: "oracledb.sql_service_response_time",
147
+ Type: module.Line,
148
+ Priority: prioSqlServiceResponseTime,
149
+ Dims: module.Dims{
150
+ {ID: "SQL Service Response Time", Name: "sql_resp_time", Div: precision * 100},
151
+ },
152
+ }
153
+ enqueueTimeoutsChart = module.Chart{
154
+ ID: "enqueue_timeouts",
155
+ Title: "Enqueue Timeouts",
156
+ Units: "timeouts/s",
157
+ Fam: "performance",
158
+ Ctx: "oracledb.enqueue_timeouts",
159
+ Type: module.Line,
160
+ Priority: prioEnqueueTimeouts,
161
+ Dims: module.Dims{
162
+ {ID: "enqueue timeouts", Name: "enqueue", Algo: module.Incremental},
163
+ },
164
+ }
165
+
166
+ diskIOChart = module.Chart{
167
+ ID: "disk_io",
168
+ Title: "Disk IO",
169
+ Units: "bytes/s",
170
+ Fam: "disk",
171
+ Ctx: "oracledb.disk_io",
172
+ Type: module.Area,
173
+ Priority: prioDiskIO,
174
+ Dims: module.Dims{
175
+ {ID: "physical read bytes", Name: "read", Algo: module.Incremental},
176
+ {ID: "physical write bytes", Name: "written", Mul: -1, Algo: module.Incremental},
177
+ },
178
+ }
179
+
180
+ diskIOPSChart = module.Chart{
181
+ ID: "disk_physical_iops",
182
+ Title: "Disk IOPS",
183
+ Units: "operations/s",
184
+ Fam: "disk",
185
+ Ctx: "oracledb.disk_iops",
186
+ Type: module.Line,
187
+ Priority: prioDiskIOPS,
188
+ Dims: module.Dims{
189
+ {ID: "physical reads", Name: "read", Algo: module.Incremental},
190
+ {ID: "physical writes", Name: "write", Mul: -1, Algo: module.Incremental},
191
+ },
192
+ }
193
+
194
+ sortsChart = module.Chart{
195
+ ID: "sorts",
196
+ Title: "Sorts",
197
+ Units: "sorts/s",
198
+ Fam: "sorts",
199
+ Ctx: "oracledb.sorts",
200
+ Type: module.Line,
201
+ Priority: prioDiskSorts,
202
+ Dims: module.Dims{
203
+ {ID: "sorts (memory)", Name: "memory", Algo: module.Incremental},
204
+ {ID: "sorts (disk)", Name: "disk", Algo: module.Incremental},
205
+ },
206
+ }
207
+
208
+ tableScansChart = module.Chart{
209
+ ID: "table_scans",
210
+ Title: "Table Scans",
211
+ Units: "scans/s",
212
+ Fam: "table scans",
213
+ Ctx: "oracledb.table_scans",
214
+ Type: module.Line,
215
+ Priority: prioTableScans,
216
+ Dims: module.Dims{
217
+ {ID: "table scans (short tables)", Name: "short_table", Algo: module.Incremental},
218
+ {ID: "table scans (long tables)", Name: "long_table", Algo: module.Incremental},
219
+ },
220
+ }
221
+
222
+ cacheHitRatioChart = module.Chart{
223
+ ID: "cache_hit_ratio",
224
+ Title: "Cache Hit Ratio",
225
+ Units: "percent",
226
+ Fam: "cache",
227
+ Ctx: "oracledb.cache_hit_ratio",
228
+ Type: module.Line,
229
+ Priority: prioCacheHitRatio,
230
+ Dims: module.Dims{
231
+ {ID: "Buffer Cache Hit Ratio", Name: "buffer", Div: precision},
232
+ {ID: "Cursor Cache Hit Ratio", Name: "cursor", Div: precision},
233
+ {ID: "Library Cache Hit Ratio", Name: "library", Div: precision},
234
+ {ID: "Row Cache Hit Ratio", Name: "row", Div: precision},
235
+ },
236
+ }
237
+ globalCacheBlocksChart = module.Chart{
238
+ ID: "global_cache_blocks",
239
+ Title: "Global Cache Blocks",
240
+ Units: "blocks/s",
241
+ Fam: "cache",
242
+ Ctx: "oracledb.global_cache_blocks",
243
+ Type: module.Line,
244
+ Priority: prioGlobalCacheBlocks,
245
+ Dims: module.Dims{
246
+ {ID: "Global Cache Blocks Corrupted", Name: "corrupted", Algo: module.Incremental, Div: precision},
247
+ {ID: "Global Cache Blocks Lost", Name: "lost", Algo: module.Incremental, Div: precision},
248
+ },
249
+ }
250
+)
251
+
252
+var (
253
+ activityChart = module.Chart{
254
+ ID: "activity",
255
+ Title: "Activities",
256
+ Units: "events/s",
257
+ Fam: "activity",
258
+ Ctx: "oracledb.activity",
259
+ Type: module.Line,
260
+ Priority: prioActivities,
261
+ Dims: module.Dims{
262
+ {ID: "parse count (total)", Name: "parse", Algo: module.Incremental},
263
+ {ID: "execute count", Name: "execute", Algo: module.Incremental},
264
+ {ID: "user commits", Name: "user_commits", Algo: module.Incremental},
265
+ {ID: "user rollbacks", Name: "user_rollbacks", Algo: module.Incremental},
266
+ },
267
+ }
268
+)
269
+
270
+var waitClassChartsTmpl = module.Charts{
271
+ waitClassWaitTimeChartTmpl.Copy(),
272
+}
273
+
274
+var (
275
+ waitClassWaitTimeChartTmpl = module.Chart{
276
+ ID: "wait_class_%s_wait_time",
277
+ Title: "Wait Class Wait Time",
278
+ Units: "milliseconds",
279
+ Fam: "performance",
280
+ Ctx: "oracledb.wait_class_wait_time",
281
+ Type: module.Line,
282
+ Priority: prioWaitClassWaitTime,
283
+ Dims: module.Dims{
284
+ {ID: "wait_class_%s_wait_time", Name: "wait_time", Div: precision},
285
+ },
286
+ }
287
+)
288
+
289
+var tablespaceChartsTmpl = module.Charts{
290
+ tablespaceUtilizationChartTmpl.Copy(),
291
+ tablespaceUsageChartTmpl.Copy(),
292
+}
293
+
294
+var (
295
+ tablespaceUtilizationChartTmpl = module.Chart{
296
+ ID: "tablespace_%s_utilization",
297
+ Title: "Tablespace Utilization",
298
+ Units: "percent",
299
+ Fam: "tablespace",
300
+ Ctx: "oracledb.tablespace_utilization",
301
+ Type: module.Area,
302
+ Priority: prioTablespaceUtilization,
303
+ Dims: module.Dims{
304
+ {ID: "tablespace_%s_utilization", Name: "utilization", Div: precision},
305
+ },
306
+ }
307
+ tablespaceUsageChartTmpl = module.Chart{
308
+ ID: "tablespace_%s_usage",
309
+ Title: "Tablespace Usage",
310
+ Units: "bytes",
311
+ Fam: "tablespace",
312
+ Ctx: "oracledb.tablespace_usage",
313
+ Type: module.Stacked,
314
+ Priority: prioTablespaceUsage,
315
+ Dims: module.Dims{
316
+ {ID: "tablespace_%s_avail_bytes", Name: "avail"},
317
+ {ID: "tablespace_%s_used_bytes", Name: "used"},
318
+ },
319
+ }
320
+)
321
+
322
+func (o *OracleDB) addTablespaceCharts(tablespace string) {
323
+ charts := tablespaceChartsTmpl.Copy()
324
+
325
+ for _, chart := range *charts {
326
+ chart.ID = cleanChartId(fmt.Sprintf(chart.ID, tablespace))
327
+ chart.Labels = []module.Label{
328
+ {Key: "tablespace", Value: tablespace},
329
+ }
330
+ for _, dim := range chart.Dims {
331
+ dim.ID = fmt.Sprintf(dim.ID, tablespace)
332
+ }
333
+ }
334
+
335
+ if err := o.Charts().Add(*charts...); err != nil {
336
+ o.Warningf("failed to add tablespace '%s' charts: %v", tablespace, err)
337
+ }
338
+}
339
+
340
+func (o *OracleDB) removeTablespaceChart(tablespace string) {}
341
+
342
+func (o *OracleDB) addWaitClassCharts(waitClass string) {
343
+ charts := waitClassChartsTmpl.Copy()
344
+
345
+ for _, chart := range *charts {
346
+ chart.ID = cleanChartId(fmt.Sprintf(chart.ID, waitClass))
347
+ chart.Labels = []module.Label{
348
+ {Key: "wait_class", Value: waitClass},
349
+ }
350
+ for _, dim := range chart.Dims {
351
+ dim.ID = fmt.Sprintf(dim.ID, waitClass)
352
+ }
353
+ }
354
+
355
+ if err := o.Charts().Add(*charts...); err != nil {
356
+ o.Warningf("failed to add wait class '%s' charts: %v", waitClass, err)
357
+ }
358
+}
359
+
360
+func cleanChartId(id string) string {
361
+ r := strings.NewReplacer(" ", "_", ".", "_")
362
+ return r.Replace(id)
363
+}
src/go/plugin/go.d/modules/oracledb/collect.go
new
+107
@@ -0,0 +1,107 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package oracledb
4
+
5
+import (
6
+ "context"
7
+ "database/sql"
8
+ "fmt"
9
+ "time"
10
+)
11
+
12
+const precision = 1000
13
+
14
+func (o *OracleDB) collect() (map[string]int64, error) {
15
+ if o.db == nil {
16
+ if err := o.openConnection(); err != nil {
17
+ return nil, fmt.Errorf("failed to open connection: %v", err)
18
+ }
19
+ }
20
+
21
+ mx := make(map[string]int64)
22
+
23
+ // TODO: https://www.oracle.com/technical-resources/articles/schumacher-analysis.html
24
+
25
+ if err := o.collectSysMetrics(mx); err != nil {
26
+ return nil, fmt.Errorf("failed to collect system metrics: %v", err)
27
+ }
28
+ if err := o.collectSysStat(mx); err != nil {
29
+ return nil, fmt.Errorf("failed to collect activities: %v", err)
30
+ }
31
+ if err := o.collectWaitClass(mx); err != nil {
32
+ return nil, fmt.Errorf("failed to collect wait time: %v", err)
33
+ }
34
+ if err := o.collectTablespace(mx); err != nil {
35
+ return nil, fmt.Errorf("failed to collect tablespace: %v", err)
36
+ }
37
+
38
+ return mx, nil
39
+}
40
+
41
+func (o *OracleDB) doQuery(query string, assign func(column, value string, lineEnd bool) error) error {
42
+ ctx, cancel := context.WithTimeout(context.Background(), o.Timeout.Duration())
43
+ defer cancel()
44
+
45
+ rows, err := o.db.QueryContext(ctx, query)
46
+ if err != nil {
47
+ return err
48
+ }
49
+ defer func() { _ = rows.Close() }()
50
+
51
+ columns, err := rows.Columns()
52
+ if err != nil {
53
+ return err
54
+ }
55
+
56
+ vs := makeValues(len(columns))
57
+
58
+ for rows.Next() {
59
+ if err := rows.Scan(vs...); err != nil {
60
+ return err
61
+ }
62
+ for i, l := 0, len(vs); i < l; i++ {
63
+ if err := assign(columns[i], valueToString(vs[i]), i == l-1); err != nil {
64
+ return err
65
+ }
66
+ }
67
+ }
68
+
69
+ return rows.Err()
70
+}
71
+
72
+func (o *OracleDB) openConnection() error {
73
+ db, err := sql.Open("oracle", o.DSN)
74
+ if err != nil {
75
+ return fmt.Errorf("error on sql open: %v", err)
76
+ }
77
+
78
+ db.SetConnMaxLifetime(10 * time.Minute)
79
+
80
+ ctx, cancel := context.WithTimeout(context.Background(), o.Timeout.Duration())
81
+ defer cancel()
82
+
83
+ if err := db.PingContext(ctx); err != nil {
84
+ _ = db.Close()
85
+ return fmt.Errorf("error on pinging: %v", err)
86
+ }
87
+
88
+ o.db = db
89
+
90
+ return nil
91
+}
92
+
93
+func makeValues(size int) []any {
94
+ vs := make([]any, size)
95
+ for i := range vs {
96
+ vs[i] = &sql.NullString{}
97
+ }
98
+ return vs
99
+}
100
+
101
+func valueToString(value any) string {
102
+ v, ok := value.(*sql.NullString)
103
+ if !ok || !v.Valid {
104
+ return ""
105
+ }
106
+ return v.String
107
+}
src/go/plugin/go.d/modules/oracledb/collect_sysmetric.go
new
+58
@@ -0,0 +1,58 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package oracledb
4
+
5
+import (
6
+ "fmt"
7
+ "strconv"
8
+)
9
+
10
+const querySysMetrics = `
11
+SELECT
12
+ METRIC_NAME,
13
+ VALUE
14
+FROM
15
+ v$sysmetric
16
+WHERE
17
+ METRIC_NAME IN (
18
+ 'Session Count',
19
+ 'Session Limit %',
20
+ 'Average Active Sessions',
21
+ 'Buffer Cache Hit Ratio',
22
+ 'Cursor Cache Hit Ratio',
23
+ 'Library Cache Hit Ratio',
24
+ 'Row Cache Hit Ratio',
25
+ 'Global Cache Blocks Corrupted',
26
+ 'Global Cache Blocks Lost',
27
+ 'Database Wait Time Ratio',
28
+ 'SQL Service Response Time'
29
+ )
30
+ AND
31
+ intsize_csec
32
+ = (SELECT max(intsize_csec) FROM sys.v_$sysmetric)
33
+`
34
+
35
+func (o *OracleDB) collectSysMetrics(mx map[string]int64) error {
36
+ q := querySysMetrics
37
+ o.Debugf("executing query: %s", q)
38
+
39
+ var name, val string
40
+
41
+ return o.doQuery(q, func(column, value string, lineEnd bool) error {
42
+ switch column {
43
+ case "METRIC_NAME":
44
+ name = value
45
+ case "VALUE":
46
+ val = value
47
+ }
48
+ if lineEnd {
49
+ v, err := strconv.ParseFloat(val, 64)
50
+ if err != nil {
51
+ return fmt.Errorf("could not parse metric '%s' value '%s': %w", name, val, err)
52
+ }
53
+ mx[name] = int64(v * precision)
54
+
55
+ }
56
+ return nil
57
+ })
58
+}
src/go/plugin/go.d/modules/oracledb/collect_sysstat.go
new
+58
@@ -0,0 +1,58 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package oracledb
4
+
5
+import (
6
+ "fmt"
7
+ "strconv"
8
+)
9
+
10
+const querySysStat = `
11
+SELECT
12
+ name,
13
+ value
14
+FROM
15
+ v$sysstat
16
+WHERE
17
+ name IN (
18
+ 'enqueue timeouts',
19
+ 'table scans (long tables)',
20
+ 'table scans (short tables)',
21
+ 'sorts (disk)',
22
+ 'sorts (memory)',
23
+ 'physical write bytes',
24
+ 'physical read bytes',
25
+ 'physical writes',
26
+ 'physical reads',
27
+ 'logons cumulative',
28
+ 'logons current',
29
+ 'parse count (total)',
30
+ 'execute count',
31
+ 'user commits',
32
+ 'user rollbacks'
33
+ )
34
+`
35
+
36
+func (o *OracleDB) collectSysStat(mx map[string]int64) error {
37
+ q := querySysStat
38
+ o.Debugf("executing query: %s", q)
39
+
40
+ var name, val string
41
+
42
+ return o.doQuery(q, func(column, value string, lineEnd bool) error {
43
+ switch column {
44
+ case "NAME":
45
+ name = value
46
+ case "VALUE":
47
+ val = value
48
+ }
49
+ if lineEnd {
50
+ v, err := strconv.ParseInt(val, 10, 64)
51
+ if err != nil {
52
+ return fmt.Errorf("could not parse activity '%s' value '%s': %w", name, val, err)
53
+ }
54
+ mx[name] = v
55
+ }
56
+ return nil
57
+ })
58
+}
src/go/plugin/go.d/modules/oracledb/collect_tablespace.go
new
+106
@@ -0,0 +1,106 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package oracledb
4
+
5
+import (
6
+ "fmt"
7
+ "strconv"
8
+)
9
+
10
+const queryTablespace = `
11
+SELECT
12
+ f.tablespace_name,
13
+ f.autoextensible,
14
+ SUM(f.bytes) AS allocated_bytes,
15
+ SUM(f.maxbytes) AS max_bytes,
16
+ (SUM(f.bytes) - COALESCE(SUM(fs.free_bytes), 0)) AS used_bytes
17
+FROM
18
+ dba_data_files f
19
+LEFT JOIN
20
+ (
21
+ SELECT
22
+ tablespace_name,
23
+ SUM(bytes) AS free_bytes
24
+ FROM
25
+ dba_free_space
26
+ GROUP BY
27
+ tablespace_name
28
+ ) fs
29
+ ON f.tablespace_name = fs.tablespace_name
30
+GROUP BY
31
+ f.tablespace_name, f.autoextensible
32
+`
33
+
34
+func (o *OracleDB) collectTablespace(mx map[string]int64) error {
35
+ q := queryTablespace
36
+ o.Debugf("executing query: %s", q)
37
+
38
+ var ts struct {
39
+ name string
40
+ autoExtent bool
41
+ allocBytes float64
42
+ maxBytes float64
43
+ usedBytes float64
44
+ }
45
+
46
+ seen := make(map[string]bool)
47
+
48
+ err := o.doQuery(q, func(column, value string, lineEnd bool) error {
49
+ var err error
50
+
51
+ switch column {
52
+ case "TABLESPACE_NAME":
53
+ ts.name = value
54
+ case "AUTOEXTENSIBLE":
55
+ ts.autoExtent = value == "YES"
56
+ case "ALLOCATED_BYTES":
57
+ ts.allocBytes, err = strconv.ParseFloat(value, 64)
58
+ case "MAX_BYTES":
59
+ ts.maxBytes, err = strconv.ParseFloat(value, 64)
60
+ case "USED_BYTES":
61
+ ts.usedBytes, err = strconv.ParseFloat(value, 64)
62
+ }
63
+ if err != nil {
64
+ return fmt.Errorf("could not parse column '%s' value '%s': %w", column, value, err)
65
+ }
66
+
67
+ if lineEnd {
68
+ seen[ts.name] = true
69
+
70
+ limit := ts.allocBytes
71
+ if ts.autoExtent {
72
+ limit = ts.maxBytes
73
+ }
74
+
75
+ px := fmt.Sprintf("tablespace_%s_", ts.name)
76
+
77
+ mx[px+"max_size_bytes"] = int64(limit)
78
+ mx[px+"used_bytes"] = int64(ts.usedBytes)
79
+ mx[px+"avail_bytes"] = int64(limit - ts.usedBytes)
80
+ mx[px+"utilization"] = 0
81
+ if limit > 0 {
82
+ mx[px+"utilization"] = int64(ts.usedBytes / limit * 100 * precision)
83
+ }
84
+ }
85
+
86
+ return nil
87
+ })
88
+ if err != nil {
89
+ return err
90
+ }
91
+
92
+ for name := range seen {
93
+ if !o.seenTablespaces[name] {
94
+ o.seenTablespaces[name] = true
95
+ o.addTablespaceCharts(name)
96
+ }
97
+ }
98
+ for name := range o.seenTablespaces {
99
+ if !seen[name] {
100
+ delete(o.seenTablespaces, name)
101
+ o.removeTablespaceChart(name)
102
+ }
103
+ }
104
+
105
+ return nil
106
+}
src/go/plugin/go.d/modules/oracledb/collect_wait_class.go
new
+60
@@ -0,0 +1,60 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package oracledb
4
+
5
+import (
6
+ "fmt"
7
+ "strconv"
8
+)
9
+
10
+const queryWaitClass = `
11
+SELECT
12
+ n.wait_class AS wait_class,
13
+ round(m.time_waited / m.intsize_csec, 3) AS wait_time
14
+FROM
15
+ v$waitclassmetric m,
16
+ v$system_wait_class n
17
+WHERE
18
+ m.wait_class_id = n.wait_class_id
19
+ AND n.wait_class != 'Idle'
20
+`
21
+
22
+func (o *OracleDB) collectWaitClass(mx map[string]int64) error {
23
+ q := queryWaitClass
24
+ o.Debugf("executing query: %s", q)
25
+
26
+ seen := make(map[string]bool)
27
+ var wclass, wtime string
28
+
29
+ err := o.doQuery(q, func(column, value string, lineEnd bool) error {
30
+ switch column {
31
+ case "WAIT_CLASS":
32
+ wclass = value
33
+ case "WAIT_TIME":
34
+ wtime = value
35
+ }
36
+ if lineEnd {
37
+ seen[wclass] = true
38
+
39
+ v, err := strconv.ParseFloat(wtime, 64)
40
+ if err != nil {
41
+ return fmt.Errorf("could not parse class '%s' value '%s': %w", wclass, wtime, err)
42
+ }
43
+ mx["wait_class_"+wclass+"_wait_time"] = int64(v * precision)
44
+ }
45
+
46
+ return nil
47
+ })
48
+ if err != nil {
49
+ return err
50
+ }
51
+
52
+ for name := range seen {
53
+ if !o.seenWaitClasses[name] {
54
+ o.seenWaitClasses[name] = true
55
+ o.addWaitClassCharts(name)
56
+ }
57
+ }
58
+
59
+ return nil
60
+}
src/go/plugin/go.d/modules/oracledb/config_schema.json
new
+48
@@ -0,0 +1,48 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "OracleDB collector configuration.",
5
+ "type": "object",
6
+ "properties": {
7
+ "update_every": {
8
+ "title": "Update every",
9
+ "description": "Data collection interval, measured in seconds.",
10
+ "type": "integer",
11
+ "minimum": 1,
12
+ "default": 1
13
+ },
14
+ "dsn": {
15
+ "title": "DSN",
16
+ "description": "Oracle server Data Source Name specifying the connection details.",
17
+ "type": "string",
18
+ "default": ""
19
+ },
20
+ "timeout": {
21
+ "title": "Timeout",
22
+ "description": "Query timeout, in seconds.",
23
+ "type": "number",
24
+ "minimum": 0.5,
25
+ "default": 1
26
+ }
27
+ },
28
+ "required": [
29
+ "dsn"
30
+ ],
31
+ "additionalProperties": false,
32
+ "patternProperties": {
33
+ "^name$": {}
34
+ }
35
+ },
36
+ "uiSchema": {
37
+ "uiOptions": {
38
+ "fullPage": true
39
+ },
40
+ "dsn": {
41
+ "ui:help": "Format is `oracle://username:password@host:port/service`.",
42
+ "ui:placeholder": "oracle://username:password@host:port/service?param1=value1&...¶mN=valueN"
43
+ },
44
+ "timeout": {
45
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
46
+ }
47
+ }
48
+}
src/go/plugin/go.d/modules/oracledb/init.go
new
+41
@@ -0,0 +1,41 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package oracledb
4
+
5
+import (
6
+ "errors"
7
+ "net/url"
8
+ "strings"
9
+
10
+ goora "github.com/sijms/go-ora/v2"
11
+)
12
+
13
+func (o *OracleDB) validateDSN() (string, error) {
14
+ if o.DSN == "" {
15
+ return "", errors.New("dsn required but not set")
16
+ }
17
+ if _, err := goora.ParseConfig(o.DSN); err != nil {
18
+ return "", err
19
+ }
20
+
21
+ u, err := url.Parse(o.DSN)
22
+ if err != nil {
23
+ return "", err
24
+ }
25
+
26
+ if u.User == nil {
27
+ return u.String(), nil
28
+ }
29
+
30
+ var user, pass string
31
+ if user = u.User.Username(); user != "" {
32
+ user = strings.Repeat("x", len(user))
33
+ }
34
+ if pass, _ = u.User.Password(); pass != "" {
35
+ pass = strings.Repeat("x", len(pass))
36
+ }
37
+
38
+ u.User = url.UserPassword(user, pass)
39
+
40
+ return u.String(), nil
41
+}
src/go/plugin/go.d/modules/oracledb/metadata.yaml
new
+271
@@ -0,0 +1,271 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-oracledb
5
+ plugin_name: go.d.plugin
6
+ module_name: oracledb
7
+ monitored_instance:
8
+ name: Oracle DB
9
+ link: https://www.oracle.com/database/
10
+ categories:
11
+ - data-collection.database-servers
12
+ icon_filename: oracle.svg
13
+ related_resources:
14
+ integrations:
15
+ list: []
16
+ info_provided_to_referring_integrations:
17
+ description: ""
18
+ keywords:
19
+ - database
20
+ - oracle
21
+ - sql
22
+ most_popular: false
23
+ overview:
24
+ multi_instance: true
25
+ data_collection:
26
+ metrics_description: |
27
+ This collector monitors the health and performance of Oracle DB servers and collects general statistics, replication and user metrics.
28
+ method_description: |
29
+ It establishes a connection to the Oracle DB instance via a TCP or UNIX socket and extracts metrics from the following database tables:
30
+
31
+ - `v$sysmetric`
32
+ - `v$sysstat`
33
+ - `v$waitclassmetric`
34
+ - `v$system_wait_class`
35
+ - `dba_data_files`
36
+ - `dba_free_space`
37
+ default_behavior:
38
+ auto_detection:
39
+ description: |
40
+ The collector can automatically detect Oracle DB instances running on:
41
+
42
+ - Localhost, listening on port 1521
43
+ - Within Docker containers
44
+
45
+ > **Note**: Oracle DB requires a username and password. While Netdata can automatically discover Oracle DB instances and create data collection jobs, these jobs will fail unless you provide the correct credentials.
46
+ limits:
47
+ description: ""
48
+ performance_impact:
49
+ description: ""
50
+ additional_permissions:
51
+ description: ""
52
+ supported_platforms:
53
+ include: []
54
+ exclude: []
55
+ setup:
56
+ prerequisites:
57
+ list:
58
+ - title: Create a read only user for netdata
59
+ description: |
60
+ Follow the official instructions for your oracle RDBMS to create a read-only user for netdata. The operation may follow this approach
61
+
62
+ Connect to your Oracle database with an administrative user and execute:
63
+
64
+ ```bash
65
+ CREATE USER netdata IDENTIFIED BY <PASSWORD>;
66
+
67
+ GRANT CONNECT TO netdata;
68
+ GRANT SELECT_CATALOG_ROLE TO netdata;
69
+ ```
70
+ configuration:
71
+ file:
72
+ name: go.d/oracle.conf
73
+ options:
74
+ description: |
75
+ The following options can be defined globally: update_every, autodetection_retry.
76
+ folding:
77
+ title: Config options
78
+ enabled: true
79
+ list:
80
+ - name: update_every
81
+ description: Data collection frequency.
82
+ default_value: 1
83
+ required: false
84
+ - name: autodetection_retry
85
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
86
+ default_value: 0
87
+ required: false
88
+ - name: dsn
89
+ description: Oracle server DSN (Data Source Name). Format is `oracle://username:password@host:port/service?param1=value1&...¶mN=valueN`.
90
+ default_value: ""
91
+ required: true
92
+ - name: timeout
93
+ description: Query timeout in seconds.
94
+ default_value: 1
95
+ required: false
96
+ examples:
97
+ folding:
98
+ title: Config
99
+ enabled: true
100
+ list:
101
+ - name: TCP socket
102
+ description: An example configuration.
103
+ config: |
104
+ jobs:
105
+ - name: local
106
+ dsn: oracle://netdata:secret@127.0.0.1:1521/XE
107
+ - name: TLS connection (TCPS)
108
+ description: An example configuration for TLS connection.
109
+ config: |
110
+ jobs:
111
+ - name: local
112
+ dsn: 'oracle://netdata:secret@127.0.0.1:1521/XE?ssl=true&ssl verify=true'
113
+ - name: Multi-instance
114
+ description: |
115
+ > **Note**: When you define multiple jobs, their names must be unique.
116
+
117
+ Local and remote instances.
118
+ config: |
119
+ jobs:
120
+ - name: local
121
+ dsn: oracle://netdata:secret@127.0.0.1:1521/XE
122
+
123
+ - name: remote
124
+ dsn: oracle://netdata:secret@203.0.113.0:1521/XE
125
+ troubleshooting:
126
+ problems:
127
+ list: []
128
+ alerts: []
129
+ metrics:
130
+ folding:
131
+ title: Metrics
132
+ enabled: false
133
+ description: ""
134
+ availability: []
135
+ scopes:
136
+ - name: global
137
+ description: These metrics refer to the entire monitored application.
138
+ labels: []
139
+ metrics:
140
+ - name: oracledb.sessions
141
+ description: Sessions
142
+ unit: sessions
143
+ chart_type: line
144
+ dimensions:
145
+ - name: session
146
+ - name: oracledb.average_active_sessions
147
+ description: Average Active Sessions
148
+ unit: sessions
149
+ chart_type: line
150
+ dimensions:
151
+ - name: active
152
+ - name: oracledb.sessions_utilization
153
+ description: Sessions Limit %
154
+ unit: percent
155
+ chart_type: area
156
+ dimensions:
157
+ - name: session_limit
158
+ - name: oracledb.current_logons
159
+ description: Current Logons
160
+ unit: logons
161
+ chart_type: line
162
+ dimensions:
163
+ - name: logons
164
+ - name: oracledb.logons
165
+ description: Logons
166
+ unit: logons/s
167
+ chart_type: line
168
+ dimensions:
169
+ - name: logons
170
+ - name: oracledb.database_wait_time_ratio
171
+ description: Database Wait Time Ratio
172
+ unit: percent
173
+ chart_type: area
174
+ dimensions:
175
+ - name: db_wait_time
176
+ - name: oracledb.sql_service_response_time
177
+ description: SQL Service Response Time
178
+ unit: seconds
179
+ chart_type: line
180
+ dimensions:
181
+ - name: sql_resp_time
182
+ - name: oracledb.enqueue_timeouts
183
+ description: Enqueue Timeouts
184
+ unit: timeouts/s
185
+ chart_type: line
186
+ dimensions:
187
+ - name: enqueue
188
+ - name: oracledb.disk_io
189
+ description: Disk IO
190
+ unit: bytes/s
191
+ chart_type: area
192
+ dimensions:
193
+ - name: read
194
+ - name: written
195
+ - name: oracledb.disk_iops
196
+ description: Disk IOPS
197
+ unit: operations/s
198
+ chart_type: line
199
+ dimensions:
200
+ - name: read
201
+ - name: write
202
+ - name: oracledb.sorts
203
+ description: Sorts
204
+ unit: sorts/s
205
+ chart_type: line
206
+ dimensions:
207
+ - name: memory
208
+ - name: disk
209
+ - name: oracledb.table_scans
210
+ description: Table Scans
211
+ unit: scans/s
212
+ chart_type: line
213
+ dimensions:
214
+ - name: short_table
215
+ - name: long_table
216
+ - name: oracledb.cache_hit_ratio
217
+ description: Cache Hit Ratio
218
+ unit: percent
219
+ chart_type: line
220
+ dimensions:
221
+ - name: buffer
222
+ - name: cursor
223
+ - name: library
224
+ - name: row
225
+ - name: oracledb.global_cache_blocks
226
+ description: Global Cache Blocks
227
+ unit: blocks/s
228
+ chart_type: line
229
+ dimensions:
230
+ - name: corrupted
231
+ - name: lost
232
+ - name: oracledb.activity
233
+ description: Activities
234
+ unit: events/s
235
+ chart_type: line
236
+ dimensions:
237
+ - name: parse
238
+ - name: execute
239
+ - name: user_commits
240
+ - name: user_rollbacks
241
+ - name: tablespace
242
+ description: These metrics refer to the Tablespace.
243
+ labels:
244
+ - name: tablespace
245
+ description: Tablespace name.
246
+ metrics:
247
+ - name: oracledb.tablespace_utilization
248
+ description: Tablespace Utilization
249
+ unit: percent
250
+ chart_type: area
251
+ dimensions:
252
+ - name: utilization
253
+ - name: oracledb.tablespace_usage
254
+ description: Tablespace Usage
255
+ unit: bytes
256
+ chart_type: stacked
257
+ dimensions:
258
+ - name: avail
259
+ - name: used
260
+ - name: wait class
261
+ description: These metrics refer to the [Wait Class](https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/classes-of-wait-events.html).
262
+ labels:
263
+ - name: wait_class
264
+ description: '[Wait Class name](https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/classes-of-wait-events.html).'
265
+ metrics:
266
+ - name: oracledb.wait_class_wait_time
267
+ description: Wait Class Wait Time
268
+ unit: milliseconds
269
+ chart_type: line
270
+ dimensions:
271
+ - name: wait_time
src/go/plugin/go.d/modules/oracledb/oracledb.go
new
+109
@@ -0,0 +1,109 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package oracledb
4
+
5
+import (
6
+ "database/sql"
7
+ _ "embed"
8
+ "errors"
9
+ "fmt"
10
+ "time"
11
+
12
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
13
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/confopt"
14
+)
15
+
16
+//go:embed "config_schema.json"
17
+var configSchema string
18
+
19
+func init() {
20
+ module.Register("oracledb", module.Creator{
21
+ JobConfigSchema: configSchema,
22
+ Create: func() module.Module { return New() },
23
+ Config: func() any { return &Config{} },
24
+ })
25
+}
26
+
27
+func New() *OracleDB {
28
+ return &OracleDB{
29
+ Config: Config{
30
+ Timeout: confopt.Duration(time.Second * 2),
31
+ charts: globalCharts.Copy(),
32
+ seenTablespaces: make(map[string]bool),
33
+ seenWaitClasses: make(map[string]bool),
34
+ },
35
+ }
36
+}
37
+
38
+type Config struct {
39
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
40
+ DSN string `json:"dsn" yaml:"dsn"`
41
+ Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"`
42
+
43
+ charts *module.Charts
44
+
45
+ publicDSN string // with hidden username/password
46
+
47
+ seenTablespaces map[string]bool
48
+ seenWaitClasses map[string]bool
49
+}
50
+
51
+type OracleDB struct {
52
+ module.Base
53
+ Config `yaml:",inline" json:""`
54
+
55
+ db *sql.DB
56
+}
57
+
58
+func (o *OracleDB) Configuration() any {
59
+ return o.Config
60
+}
61
+
62
+func (o *OracleDB) Init() error {
63
+ dsn, err := o.validateDSN()
64
+ if err != nil {
65
+ return fmt.Errorf("invalid oracle DSN: %w", err)
66
+ }
67
+
68
+ o.publicDSN = dsn
69
+
70
+ return nil
71
+}
72
+
73
+func (o *OracleDB) Check() error {
74
+ mx, err := o.collect()
75
+ if err != nil {
76
+ return fmt.Errorf("failed to collect metrics [%s]: %w", o.publicDSN, err)
77
+ }
78
+ if len(mx) == 0 {
79
+ return errors.New("no metrics collected")
80
+ }
81
+
82
+ return nil
83
+}
84
+
85
+func (o *OracleDB) Charts() *module.Charts {
86
+ return o.charts
87
+}
88
+
89
+func (o *OracleDB) Collect() map[string]int64 {
90
+ mx, err := o.collect()
91
+ if err != nil {
92
+ o.Error(fmt.Sprintf("failed to collect metrics [%s]: %s", o.publicDSN, err))
93
+ }
94
+
95
+ if len(mx) == 0 {
96
+ return nil
97
+ }
98
+
99
+ return mx
100
+}
101
+
102
+func (o *OracleDB) Cleanup() {
103
+ if o.db != nil {
104
+ if err := o.db.Close(); err != nil {
105
+ o.Errorf("cleanup: error on closing connection [%s]: %v", o.publicDSN, err)
106
+ }
107
+ o.db = nil
108
+ }
109
+}
src/go/plugin/go.d/modules/oracledb/oracledb_test.go
new
+317
@@ -0,0 +1,317 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package oracledb
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "database/sql/driver"
9
+ "errors"
10
+ "fmt"
11
+ "os"
12
+ "strings"
13
+ "testing"
14
+
15
+ "github.com/DATA-DOG/go-sqlmock"
16
+ "github.com/stretchr/testify/assert"
17
+ "github.com/stretchr/testify/require"
18
+
19
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
20
+)
21
+
22
+var (
23
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
24
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
25
+
26
+ dataVer2130XESysMetric, _ = os.ReadFile("testdata/v21.3.0-xe/sysmetric.txt")
27
+ dataVer2130XESysStat, _ = os.ReadFile("testdata/v21.3.0-xe/sysstat.txt")
28
+ dataVer2130XETablespace, _ = os.ReadFile("testdata/v21.3.0-xe/tablespace.txt")
29
+ dataVer2130XEWaitClass, _ = os.ReadFile("testdata/v21.3.0-xe/wait_class.txt")
30
+)
31
+
32
+func Test_testDataIsValid(t *testing.T) {
33
+ for name, data := range map[string][]byte{
34
+ "dataConfigJSON": dataConfigJSON,
35
+ "dataConfigYAML": dataConfigYAML,
36
+ "dataVer2130XESysMetric": dataVer2130XESysMetric,
37
+ "dataVer2130XESysStat": dataVer2130XESysStat,
38
+ "dataVer2130XETablespace": dataVer2130XETablespace,
39
+ "dataVer2130XEWaitClass": dataVer2130XEWaitClass,
40
+ } {
41
+ require.NotNil(t, data, name)
42
+ if !strings.HasPrefix(name, "dataConfig") {
43
+ _, err := prepareMockRows(data)
44
+ require.NoError(t, err, fmt.Sprintf("prepare mock rows: %s", name))
45
+ }
46
+ }
47
+}
48
+
49
+func TestOracleDB_ConfigurationSerialize(t *testing.T) {
50
+ module.TestConfigurationSerialize(t, &OracleDB{}, dataConfigJSON, dataConfigYAML)
51
+}
52
+
53
+func TestOracleDB_Charts(t *testing.T) {
54
+ assert.NotNil(t, New().Charts())
55
+}
56
+
57
+func TestOracleDB_Init(t *testing.T) {
58
+ tests := map[string]struct {
59
+ config Config
60
+ wantFail bool
61
+ }{
62
+ "empty DSN": {
63
+ config: Config{DSN: ""},
64
+ wantFail: true,
65
+ },
66
+ }
67
+
68
+ for name, test := range tests {
69
+ t.Run(name, func(t *testing.T) {
70
+ ora := New()
71
+ ora.Config = test.config
72
+
73
+ if test.wantFail {
74
+ assert.Error(t, ora.Init())
75
+ } else {
76
+ assert.NoError(t, ora.Init())
77
+ }
78
+ })
79
+ }
80
+}
81
+
82
+func TestOracleDB_Cleanup(t *testing.T) {
83
+ tests := map[string]func(t *testing.T) (ora *OracleDB, cleanup func()){
84
+ "db connection not initialized": func(t *testing.T) (ora *OracleDB, cleanup func()) {
85
+ return New(), func() {}
86
+ },
87
+ "db connection initialized": func(t *testing.T) (ora *OracleDB, cleanup func()) {
88
+ db, mock, err := sqlmock.New()
89
+ require.NoError(t, err)
90
+
91
+ mock.ExpectClose()
92
+ ora = New()
93
+ ora.db = db
94
+ cleanup = func() { _ = db.Close() }
95
+
96
+ return ora, cleanup
97
+ },
98
+ }
99
+
100
+ for name, prepare := range tests {
101
+ t.Run(name, func(t *testing.T) {
102
+ ora, cleanup := prepare(t)
103
+ defer cleanup()
104
+
105
+ assert.NotPanics(t, ora.Cleanup)
106
+ assert.Nil(t, ora.db)
107
+ })
108
+ }
109
+
110
+}
111
+
112
+func TestOracleDB_Check(t *testing.T) {
113
+ tests := map[string]struct {
114
+ prepareMock func(t *testing.T, m sqlmock.Sqlmock)
115
+ wantFail bool
116
+ }{
117
+ "success on all queries": {
118
+ wantFail: false,
119
+ prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
120
+ mockExpect(t, m, querySysMetrics, dataVer2130XESysMetric)
121
+ mockExpect(t, m, querySysStat, dataVer2130XESysStat)
122
+ mockExpect(t, m, queryWaitClass, dataVer2130XEWaitClass)
123
+ mockExpect(t, m, queryTablespace, dataVer2130XETablespace)
124
+ },
125
+ },
126
+ "fail if any query fails": {
127
+ wantFail: true,
128
+ prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
129
+ mockExpectErr(m, querySysMetrics)
130
+ },
131
+ },
132
+ }
133
+
134
+ for name, test := range tests {
135
+ t.Run(name, func(t *testing.T) {
136
+ db, mock, err := sqlmock.New(
137
+ sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual),
138
+ )
139
+ require.NoError(t, err)
140
+ ora := New()
141
+ ora.DSN = "oracle://user:pass@127.0.0.1:32001/XE"
142
+ ora.db = db
143
+ defer func() { _ = db.Close() }()
144
+
145
+ require.NoError(t, ora.Init())
146
+
147
+ test.prepareMock(t, mock)
148
+
149
+ if test.wantFail {
150
+ assert.Error(t, ora.Check())
151
+ } else {
152
+ assert.NoError(t, ora.Check())
153
+ }
154
+ assert.NoError(t, mock.ExpectationsWereMet())
155
+ })
156
+ }
157
+}
158
+
159
+func TestOracleDB_Collect(t *testing.T) {
160
+ tests := map[string]struct {
161
+ prepareMock func(t *testing.T, m sqlmock.Sqlmock)
162
+ wantCharts int
163
+ wantMetrics map[string]int64
164
+ }{
165
+ "success on all queries": {
166
+ prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
167
+ mockExpect(t, m, querySysMetrics, dataVer2130XESysMetric)
168
+ mockExpect(t, m, querySysStat, dataVer2130XESysStat)
169
+ mockExpect(t, m, queryWaitClass, dataVer2130XEWaitClass)
170
+ mockExpect(t, m, queryTablespace, dataVer2130XETablespace)
171
+ },
172
+ wantCharts: len(globalCharts) + len(tablespaceChartsTmpl)*4 + len(waitClassChartsTmpl)*10,
173
+ wantMetrics: map[string]int64{
174
+ "Average Active Sessions": 93,
175
+ "Buffer Cache Hit Ratio": 100000,
176
+ "Cursor Cache Hit Ratio": 377385,
177
+ "Database Wait Time Ratio": 0,
178
+ "Global Cache Blocks Corrupted": 0,
179
+ "Global Cache Blocks Lost": 0,
180
+ "Library Cache Hit Ratio": 98779,
181
+ "Row Cache Hit Ratio": 99640,
182
+ "SQL Service Response Time": 247,
183
+ "Session Count": 142000,
184
+ "Session Limit %": 7274,
185
+ "enqueue timeouts": 229,
186
+ "execute count": 4066130,
187
+ "logons cumulative": 8717,
188
+ "logons current": 93,
189
+ "parse count (total)": 1251128,
190
+ "physical read bytes": 538132480,
191
+ "physical reads": 65690,
192
+ "physical write bytes": 785661952,
193
+ "physical writes": 95906,
194
+ "sorts (disk)": 0,
195
+ "sorts (memory)": 220071,
196
+ "table scans (long tables)": 998,
197
+ "table scans (short tables)": 798515,
198
+ "tablespace_SYSAUX_avail_bytes": 215023616,
199
+ "tablespace_SYSAUX_max_size_bytes": 912261120,
200
+ "tablespace_SYSAUX_used_bytes": 697237504,
201
+ "tablespace_SYSAUX_utilization": 76429,
202
+ "tablespace_SYSTEM_avail_bytes": 5898240,
203
+ "tablespace_SYSTEM_max_size_bytes": 1415577600,
204
+ "tablespace_SYSTEM_used_bytes": 1409679360,
205
+ "tablespace_SYSTEM_utilization": 99583,
206
+ "tablespace_UNDOTBS1_avail_bytes": 114032640,
207
+ "tablespace_UNDOTBS1_max_size_bytes": 125829120,
208
+ "tablespace_UNDOTBS1_used_bytes": 11796480,
209
+ "tablespace_UNDOTBS1_utilization": 9375,
210
+ "tablespace_USERS_avail_bytes": 2424832,
211
+ "tablespace_USERS_max_size_bytes": 5242880,
212
+ "tablespace_USERS_used_bytes": 2818048,
213
+ "tablespace_USERS_utilization": 53750,
214
+ "user commits": 16056,
215
+ "user rollbacks": 2,
216
+ "wait_class_Administrative_wait_time": 0,
217
+ "wait_class_Application_wait_time": 0,
218
+ "wait_class_Commit_wait_time": 0,
219
+ "wait_class_Concurrency_wait_time": 0,
220
+ "wait_class_Configuration_wait_time": 0,
221
+ "wait_class_Network_wait_time": 0,
222
+ "wait_class_Other_wait_time": 0,
223
+ "wait_class_Scheduler_wait_time": 0,
224
+ "wait_class_System I/O_wait_time": 4,
225
+ "wait_class_User I/O_wait_time": 0,
226
+ },
227
+ },
228
+ "fail if any query fails": {
229
+ prepareMock: func(t *testing.T, m sqlmock.Sqlmock) {
230
+ mockExpectErr(m, querySysMetrics)
231
+ },
232
+ },
233
+ }
234
+
235
+ for name, test := range tests {
236
+ t.Run(name, func(t *testing.T) {
237
+ db, mock, err := sqlmock.New(
238
+ sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual),
239
+ )
240
+ require.NoError(t, err)
241
+ ora := New()
242
+ ora.DSN = "oracle://user:pass@127.0.0.1:32001/XE"
243
+ ora.db = db
244
+ defer func() { _ = db.Close() }()
245
+
246
+ require.NoError(t, ora.Init())
247
+
248
+ test.prepareMock(t, mock)
249
+
250
+ mx := ora.Collect()
251
+
252
+ require.Equal(t, test.wantMetrics, mx)
253
+ if len(test.wantMetrics) > 0 {
254
+ assert.Equal(t, test.wantCharts, len(*ora.Charts()), "wantCharts")
255
+ module.TestMetricsHasAllChartsDims(t, ora.Charts(), mx)
256
+ }
257
+
258
+ assert.NoError(t, mock.ExpectationsWereMet())
259
+ })
260
+ }
261
+}
262
+
263
+func mockExpect(t *testing.T, mock sqlmock.Sqlmock, query string, rows []byte) {
264
+ mockRows, err := prepareMockRows(rows)
265
+ require.NoError(t, err)
266
+ mock.ExpectQuery(query).WillReturnRows(mockRows).RowsWillBeClosed()
267
+}
268
+
269
+func mockExpectErr(mock sqlmock.Sqlmock, query string) {
270
+ mock.ExpectQuery(query).WillReturnError(fmt.Errorf("mock error (%s)", query))
271
+}
272
+
273
+func prepareMockRows(data []byte) (*sqlmock.Rows, error) {
274
+ if len(data) == 0 {
275
+ return sqlmock.NewRows(nil), nil
276
+ }
277
+
278
+ r := bytes.NewReader(data)
279
+ sc := bufio.NewScanner(r)
280
+
281
+ var numColumns int
282
+ var rows *sqlmock.Rows
283
+
284
+ for sc.Scan() {
285
+ line := strings.TrimSpace(sc.Text())
286
+ if line == "" || strings.HasPrefix(line, "-") {
287
+ continue
288
+ }
289
+
290
+ parts := strings.Split(line, "|")
291
+ for i, v := range parts {
292
+ parts[i] = strings.TrimSpace(v)
293
+ }
294
+
295
+ if rows == nil {
296
+ numColumns = len(parts)
297
+ rows = sqlmock.NewRows(parts)
298
+ continue
299
+ }
300
+
301
+ if len(parts) != numColumns {
302
+ return nil, fmt.Errorf("prepareMockRows(): columns != values (%d/%d)", numColumns, len(parts))
303
+ }
304
+
305
+ values := make([]driver.Value, len(parts))
306
+ for i, v := range parts {
307
+ values[i] = v
308
+ }
309
+ rows.AddRow(values...)
310
+ }
311
+
312
+ if rows == nil {
313
+ return nil, errors.New("prepareMockRows(): nil rows result")
314
+ }
315
+
316
+ return rows, sc.Err()
317
+}
src/go/plugin/go.d/modules/oracledb/testdata/config.json
new
+5
@@ -0,0 +1,5 @@
1
+{
2
+ "update_every": 123,
3
+ "dsn": "ok",
4
+ "timeout": 123.123
5
+}
src/go/plugin/go.d/modules/oracledb/testdata/config.yaml
new
+3
@@ -0,0 +1,3 @@
1
+update_every: 123
2
+dsn: "ok"
3
+timeout: 123.123
src/go/plugin/go.d/modules/oracledb/testdata/v21.3.0-xe/sysmetric.txt
new
+13
@@ -0,0 +1,13 @@
1
+METRIC_NAME | VALUE
2
+---------------------------------------------------------------- ----------
3
+Buffer Cache Hit Ratio | 100
4
+Cursor Cache Hit Ratio | 377.385159
5
+Global Cache Blocks Corrupted | 0
6
+Global Cache Blocks Lost | 0
7
+SQL Service Response Time | .24775561
8
+Database Wait Time Ratio | 0
9
+Row Cache Hit Ratio | 99.6407723
10
+Library Cache Hit Ratio | 98.7795576
11
+Session Limit % | 7.27459016
12
+Session Count | 142
13
+Average Active Sessions | .093455298
src/go/plugin/go.d/modules/oracledb/testdata/v21.3.0-xe/sysstat.txt
new
+17
@@ -0,0 +1,17 @@
1
+NAME | VALUE
2
+---------------------------------------------------------------- ----------
3
+logons cumulative | 8717
4
+logons current | 93
5
+user commits | 16056
6
+user rollbacks | 2
7
+enqueue timeouts | 229
8
+physical reads | 65690
9
+physical read bytes | 538132480
10
+physical writes | 95906
11
+physical write bytes | 785661952
12
+table scans (short tables) | 798515
13
+table scans (long tables) | 998
14
+parse count (total) | 1251128
15
+execute count | 4066130
16
+sorts (memory) | 220071
17
+sorts (disk) | 0
src/go/plugin/go.d/modules/oracledb/testdata/v21.3.0-xe/tablespace.txt
new
+6
@@ -0,0 +1,6 @@
1
+TABLESPACE_NAME | AUT | ALLOCATED_BYTES | MAX_BYTES | USED_BYTES
2
+------------------------------ --- --------------- ---------- ----------
3
+SYSTEM | YES | 1415577600 | 3.4360E+10 | 1409679360
4
+SYSAUX | YES | 912261120 | 3.4360E+10 | 697237504
5
+UNDOTBS1 | YES | 125829120 | 3.4360E+10 | 11796480
6
+USERS | YES | 5242880 | 3.4360E+10 | 2818048
src/go/plugin/go.d/modules/oracledb/testdata/v21.3.0-xe/wait_class.txt
new
+12
@@ -0,0 +1,12 @@
1
+WAIT_CLASS | WAIT_TIME
2
+---------------------------------------------------------------- ----------
3
+Other | 0
4
+Application | 0
5
+Configuration | 0
6
+Administrative | 0
7
+Concurrency | 0
8
+Commit | 0
9
+Network | 0
10
+User I/O | 0
11
+System I/O | .004
12
+Scheduler | 0