@cryptotaxi247 / netdata-1 / commits / 7f7a50abb

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&...&paramN=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&...&paramN=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