Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ ENV GOOS=${GOOS:-linux}
ARG GOARCH
ENV GOARCH=${GOARCH:-amd64}

ARG TAGS
ENV TAGS=${TAGS:-godror}

ARG CGO_ENABLED
ENV CGO_ENABLED=${CGO_ENABLED:-1}

RUN microdnf install wget gzip gcc && \
wget -q https://go.dev/dl/go1.23.10.${GOOS}-${GOARCH}.tar.gz && \
rm -rf /usr/local/go && \
Expand All @@ -22,9 +28,9 @@ RUN go mod download
ARG VERSION
ENV VERSION=${VERSION:-1.0.0}

RUN CGO_ENABLED=1 GOOS=${GOOS} GOARCH=${GOARCH} go build -v -ldflags "-X main.Version=${VERSION} -s -w"
RUN CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} go build --tags=${TAGS} -v -ldflags "-X main.Version=${VERSION} -s -w"

FROM ${BASE_IMAGE:-ghcr.io/oracle/oraclelinux:8-slim} AS exporter
FROM ${BASE_IMAGE:-ghcr.io/oracle/oraclelinux:8-slim} AS exporter-godror
LABEL org.opencontainers.image.authors="Oracle America, Inc."
LABEL org.opencontainers.image.description="Oracle Database Observability Exporter"

Expand Down Expand Up @@ -56,3 +62,18 @@ EXPOSE 9161
USER 1000

ENTRYPOINT ["/oracledb_exporter"]

FROM ${BASE_IMAGE:-ghcr.io/oracle/oraclelinux:8-slim} AS exporter-goora

COPY --from=build /go/src/oracledb_exporter/oracle-db-appdev-monitoring /oracledb_exporter
ADD ./default-metrics.toml /default-metrics.toml

# create the mount point for alert log exports (default location)
RUN mkdir /log && chown 1000:1000 /log
RUN mkdir /wallet && chown 1000:1000 /wallet

EXPOSE 9161

USER 1000

ENTRYPOINT ["/oracledb_exporter"]
23 changes: 13 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ OS_TYPE ?= $(shell uname -s | tr '[:upper:]' '[:lower:]')
ARCH_TYPE ?= $(subst x86_64,amd64,$(patsubst i%86,386,$(ARCH)))
GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH)
TAGS ?= godror
DOCKER_TARGET ?= exporter-godror
CGO_ENABLED ?= 1
VERSION ?= 2.1.0
LDFLAGS := -X main.Version=$(VERSION)
GOFLAGS := -ldflags "$(LDFLAGS) -s -w"
GOFLAGS := -ldflags "$(LDFLAGS) -s -w" --tags $(TAGS)
BUILD_ARGS = --build-arg VERSION=$(VERSION)
OUTDIR = ./dist

Expand Down Expand Up @@ -37,31 +40,31 @@ go-build:

.PHONY: go-build-linux-amd64
go-build-linux-amd64:
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) go-build -j2
CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=amd64 $(MAKE) go-build -j2

.PHONY: go-build-linux-arm64
go-build-linux-arm64:
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 $(MAKE) go-build -j2
CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=arm64 $(MAKE) go-build -j2

.PHONY: go-build-linux-gcc-arm64
go-build-linux-gcc-arm64:
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(MAKE) go-build -j2
CGO_ENABLED=$(CGO_ENABLED) CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(MAKE) go-build -j2

.PHONY: go-build-darwin-amd64
go-build-darwin-amd64:
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 $(MAKE) go-build -j2
CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=amd64 $(MAKE) go-build -j2

.PHONY: go-build-darwin-arm64
go-build-darwin-arm64:
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 $(MAKE) go-build -j2
CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=arm64 $(MAKE) go-build -j2

.PHONY: go-build-windows-amd64
go-build-windows-amd64:
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 $(MAKE) go-build -j2
CGO_ENABLED=$(CGO_ENABLED) GOOS=windows GOARCH=amd64 $(MAKE) go-build -j2

.PHONY: go-build-windows-x86
go-build-windows-x86:
CGO_ENABLED=1 GOOS=windows GOARCH=386 $(MAKE) go-build -j2
CGO_ENABLED=$(CGO_ENABLED) GOOS=windows GOARCH=386 $(MAKE) go-build -j2

dist: go-build-linux-gcc-arm64 go-build-linux-amd64

Expand Down Expand Up @@ -89,10 +92,10 @@ push-images:
@make --no-print-directory push-oraclelinux-image

docker:
docker build --no-cache --progress=plain $(BUILD_ARGS) -t "$(IMAGE_ID)-amd64" --build-arg BASE_IMAGE=$(ORACLE_LINUX_BASE_IMAGE) --build-arg GOARCH=amd64 .
docker build --no-cache --target=$(DOCKER_TARGET) --progress=plain $(BUILD_ARGS) -t "$(IMAGE_ID)-amd64" --build-arg BASE_IMAGE=$(ORACLE_LINUX_BASE_IMAGE) --build-arg GOARCH=amd64 .

docker-arm:
docker buildx build --platform linux/arm64 --load --no-cache --progress=plain $(BUILD_ARGS) -t "$(IMAGE_ID)-arm64" --build-arg BASE_IMAGE=$(ORACLE_LINUX_BASE_IMAGE) --build-arg GOARCH=arm64 .
docker buildx build --target=$(DOCKER_TARGET) --platform linux/arm64 --load --no-cache --progress=plain $(BUILD_ARGS) -t "$(IMAGE_ID)-arm64" --build-arg BASE_IMAGE=$(ORACLE_LINUX_BASE_IMAGE) --build-arg GOARCH=arm64 .

push-oraclelinux-image:
docker push $(IMAGE_ID)
Expand Down
49 changes: 2 additions & 47 deletions collector/connect_godror.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
// Copyright (c) 2025, Oracle and/or its affiliates.
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.

//go:build !goora
//go:build godror

package collector

import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/godror/godror"
"github.com/godror/godror/dsn"
"log/slog"
"strings"
"time"
)

Expand Down Expand Up @@ -78,53 +76,10 @@ func connect(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) *sql.D
// note that this just configures the connection, it does not actually connect until later
// when we call db.Ping()
db := sql.OpenDB(godror.NewConnector(P))
logger.Debug(fmt.Sprintf("set max idle connections to %d", dbconfig.MaxIdleConns), "database", dbname)
db.SetMaxIdleConns(dbconfig.GetMaxIdleConns())
logger.Debug(fmt.Sprintf("set max open connections to %d", dbconfig.MaxOpenConns), "database", dbname)
db.SetMaxOpenConns(dbconfig.GetMaxOpenConns())
db.SetConnMaxLifetime(0)
logger.Debug(fmt.Sprintf("Successfully configured connection to %s", maskDsn(dbconfig.URL)), "database", dbname)

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if _, err := db.ExecContext(ctx, `
begin
dbms_application_info.set_client_info('oracledb_exporter');
end;`); err != nil {
logger.Info("Could not set CLIENT_INFO.", "database", dbname)
}

var sysdba string
if err := db.QueryRowContext(ctx, "select sys_context('USERENV', 'ISDBA') from dual").Scan(&sysdba); err != nil {
logger.Error("error checking my database role", "error", err, "database", dbname)
}
logger.Info("Connected as SYSDBA? "+sysdba, "database", dbname)

initdb(logger, dbname, dbconfig, db)
return db
}

// ping the database. If the database is disconnected, try to reconnect.
// If the database type is unknown, try to reload it.
func (d *Database) ping(logger *slog.Logger) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := d.Session.PingContext(ctx)
if err != nil {
d.Up = 0
if isInvalidCredentialsError(err) {
d.invalidate()
return err
}
// If database is closed, try to reconnect
if strings.Contains(err.Error(), "sql: database is closed") {
d.Session = connect(logger, d.Name, d.Config)
}
return err
}
d.Up = 1
return nil
}

func isInvalidCredentialsError(err error) bool {
err = errors.Unwrap(err)
if err == nil {
Expand Down
83 changes: 83 additions & 0 deletions collector/connect_goora.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2025, Oracle and/or its affiliates.
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.

//go:build goora

package collector

import (
"database/sql"
"errors"
"fmt"
_ "github.com/sijms/go-ora/v2"
"github.com/sijms/go-ora/v2/network"
"log/slog"
)

func connect(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) *sql.DB {
logger.Debug("Launching connection to "+maskDsn(dbconfig.URL), "database", dbname)

password := dbconfig.GetPassword()
username := dbconfig.GetUsername()
dbconfig.ExternalAuth = password == ""

logger.Debug(fmt.Sprintf("external authentication set to %t", dbconfig.ExternalAuth), "database", dbname)

msg := "Using Username/Password Authentication."
if dbconfig.ExternalAuth {
msg = "Database Password not specified; will attempt to use external authentication (ignoring user input)."
dbconfig.Username = ""
}
logger.Info(msg, "database", dbname)

// Build connection string for go-ora
var dsn string
if dbconfig.ExternalAuth {
// go-ora doesn't directly support external authentication
// So we rely on OS authentication (set Oracle wallet/env)
dsn = fmt.Sprintf("oracle://@%s", dbconfig.URL)
} else if username != "" {
dsn = fmt.Sprintf("oracle://%s:%s@%s", username, password, dbconfig.URL)
} else {
dsn = fmt.Sprintf("oracle://%s", dbconfig.URL)
}

// open connection (lazy until first use)
db, err := sql.Open("oracle", dsn)
if err != nil {
logger.Error("Failed to create DB handle", "error", err, "database", dbname)
return nil
}

// Configure connection pool (sql.DB handles pooling)
setConnectionPool(logger, dbname, dbconfig, db)
initdb(logger, dbname, dbconfig, db)
return db
}

func setConnectionPool(logger *slog.Logger, dbname string, dbconfig DatabaseConfig, db *sql.DB) {
if dbconfig.GetPoolMaxConnections() > 0 {
logger.Debug(fmt.Sprintf("set pool max connections to %d", dbconfig.PoolMaxConnections), "database", dbname)
db.SetMaxOpenConns(dbconfig.GetPoolMaxConnections())
} else {
db.SetMaxOpenConns(dbconfig.GetMaxOpenConns())
}
if dbconfig.GetPoolMinConnections() > 0 {
logger.Debug(fmt.Sprintf("set pool min connections to %d", dbconfig.PoolMinConnections), "database", dbname)
db.SetMaxIdleConns(dbconfig.GetPoolMinConnections())
} else {
db.SetMaxIdleConns(dbconfig.GetMaxIdleConns())
}
}

func isInvalidCredentialsError(err error) bool {
if err == nil {
return false
}
var oraErr *network.OracleError
ok := errors.As(err, &oraErr)
if !ok {
return false
}
return oraErr.ErrCode == ora01017code || oraErr.ErrCode == ora28000code
}
48 changes: 48 additions & 0 deletions collector/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package collector
import (
"context"
"database/sql"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"log/slog"
"strings"
"time"
)

Expand Down Expand Up @@ -91,10 +93,56 @@ func (d *Database) WarmupConnectionPool(logger *slog.Logger) {
}
}

// ping the database. If the database is disconnected, try to reconnect.
// If the database type is unknown, try to reload it.
func (d *Database) ping(logger *slog.Logger) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := d.Session.PingContext(ctx)
if err != nil {
d.Up = 0
if isInvalidCredentialsError(err) {
d.invalidate()
return err
}
// If database is closed, try to reconnect
if strings.Contains(err.Error(), "sql: database is closed") {
d.Session = connect(logger, d.Name, d.Config)
}
return err
}
d.Up = 1
return nil
}

func (d *Database) IsValid() bool {
return d.Valid
}

func (d *Database) invalidate() {
d.Valid = false
}

func initdb(logger *slog.Logger, dbname string, dbconfig DatabaseConfig, db *sql.DB) {
logger.Debug(fmt.Sprintf("set max idle connections to %d", dbconfig.MaxIdleConns), "database", dbname)
db.SetMaxIdleConns(dbconfig.GetMaxIdleConns())
logger.Debug(fmt.Sprintf("set max open connections to %d", dbconfig.MaxOpenConns), "database", dbname)
db.SetMaxOpenConns(dbconfig.GetMaxOpenConns())
db.SetConnMaxLifetime(0)
logger.Debug(fmt.Sprintf("Successfully configured connection to %s", maskDsn(dbconfig.URL)), "database", dbname)

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if _, err := db.ExecContext(ctx, `
begin
dbms_application_info.set_client_info('oracledb_exporter');
end;`); err != nil {
logger.Info("Could not set CLIENT_INFO.", "database", dbname)
}

var sysdba string
if err := db.QueryRowContext(ctx, "select sys_context('USERENV', 'ISDBA') from dual").Scan(&sysdba); err != nil {
logger.Error("error checking my database role", "error", err, "database", dbname)
}
logger.Info("Connected as SYSDBA? "+sysdba, "database", dbname)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/sijms/go-ora/v2 v2.9.0 // indirect
github.com/sony/gobreaker v0.5.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sijms/go-ora/v2 v2.9.0 h1:+iQbUeTeCOFMb5BsOMgUhV8KWyrv9yjKpcK4x7+MFrg=
github.com/sijms/go-ora/v2 v2.9.0/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU=
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
28 changes: 28 additions & 0 deletions site/docs/advanced/go-ora.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: go-ora Driver
sidebar_position: 5
---

# Using the go-ora database driver

The Oracle Database Metrics Exporter experimentally supports compiling with the [go-ora database driver](https://github.com/sijms/go-ora). By default, the exporter compiles using the `godror` database driver, which uses CGO execution to invoke Oracle Instant Client. the go-ora driver presents an option for users who want to use a "thin" database client without the Oracle Instant Client and CGO.

### Configuring go-ora

Because go-ora does not use Oracle Instant Client, it is recommended to provide all connection string options in the `database.url` property:

```yaml
databases:
go_ora_db:
username: myuser
password: ******
url: my_tnsname?wallet=/path/to/wallet&ssl=1
```

### Build with go-ora

To build using `go-ora` instead of `godror`, set `TAGS=goora CGO_ENABLED=0`:

```bash
make go-build TAGS=goora CGO_ENABLED=0
```
1 change: 1 addition & 0 deletions site/docs/releases/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ List of upcoming and historic changes to the exporter.

Our current priorities to support metrics for advanced database features and use cases, like Exadata, GoldenGate, and views included in the Oracle Diagnostics Pack.

- Added experimental support for the [go-ora](https://github.com/sijms/go-ora)
- Move `oracledb_dbtype` metric to the default metrics. You may now disable or override this metric like any other database metric.
- Document required database permissions for the exporter.
- Fix an issue where some metrics would not be cached when using a per-metric scrape interval with a global scrape interval.
Expand Down