diff --git a/Dockerfile b/Dockerfile index 259063c..69e1e56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 && \ @@ -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" @@ -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"] diff --git a/Makefile b/Makefile index 3c04dd8..b64f7d4 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 @@ -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) diff --git a/collector/connect_godror.go b/collector/connect_godror.go index dc8e502..2b469be 100644 --- a/collector/connect_godror.go +++ b/collector/connect_godror.go @@ -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" ) @@ -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 { diff --git a/collector/connect_goora.go b/collector/connect_goora.go new file mode 100644 index 0000000..3def68a --- /dev/null +++ b/collector/connect_goora.go @@ -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 +} diff --git a/collector/database.go b/collector/database.go index 0478ec1..c87a056 100644 --- a/collector/database.go +++ b/collector/database.go @@ -6,8 +6,10 @@ package collector import ( "context" "database/sql" + "fmt" "github.com/prometheus/client_golang/prometheus" "log/slog" + "strings" "time" ) @@ -91,6 +93,28 @@ 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 } @@ -98,3 +122,27 @@ func (d *Database) IsValid() bool { 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) +} diff --git a/go.mod b/go.mod index 58984c2..e161b97 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b46fff5..8ef2603 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/site/docs/advanced/go-ora.md b/site/docs/advanced/go-ora.md new file mode 100644 index 0000000..c309a92 --- /dev/null +++ b/site/docs/advanced/go-ora.md @@ -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 +``` diff --git a/site/docs/releases/changelog.md b/site/docs/releases/changelog.md index 4735bfa..7940f46 100644 --- a/site/docs/releases/changelog.md +++ b/site/docs/releases/changelog.md @@ -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.