Publish any SQL query as a custom OCI Monitoring metric, from inside Oracle Autonomous AI Database.
You register a SELECT. A scheduler job inside the database turns its rows into OCI PostMetricData JSON every 60 seconds and posts it, signed, to the OCI telemetry ingestion endpoint. The result is a first-class OCI metric: it charts in Metrics Explorer, alarms through OCI Monitoring, exports through Service Connector Hub, and is pulled by any third-party observability tool that reads the OCI Monitoring API.
No agent. No compute instance. No ORDS. Nothing to operate outside the database.
This guide assumes nothing. It covers creating the OCI user, group, API key and policy from scratch, installing the package, and every command you will ever run against it. If you already have some of this, skip ahead using the contents.
Validated on: Autonomous AI Database — Dedicated, Oracle AI Database 26ai (23.26.2.0.0), us-ashburn-1. Also expected to work on Serverless; the shipped metrics use only DBA_ views.
- 1. What you need before you start
- 2. Concepts you must understand first
- 3. Step 1 — Choose your authentication path
- 4. Step 2 — OCI setup, Option A: resource principal
- 5. Step 3 — OCI setup, Option B: API signing key
- 6. Step 4 — Install the package
- 7. Step 5 — Choose your namespace
- 8. Step 6 — Create the signing credential
- 9. Step 7 — Verify with a test publish
- 10. Step 8 — Start the scheduler
- 11. Step 9 — Confirm the data in OCI
- 12. Registering your own metric
- 13. The built-in metrics
- 14. Grants: the one thing that will trip you up
- 15. Building alarms
- 16. Diagnostics — which function to reach for
- 17. Day-two operations
- 18. Troubleshooting
- 19. Complete API reference
- 20. Limits, cost and cardinality
- 21. Uninstalling
- 22. Appendix — how it works inside
- 23. What changed in v0.3
| A database | Autonomous AI Database (Dedicated or Serverless), 19c or later. 26ai recommended — the grant diagnosis in §14 is far more precise there. |
ADMIN access to it |
For the one-time install. You will run SQL as ADMIN, and once as the new METRIC_EXPORTER user. |
| An OCI account with IAM rights | To create a policy. If you are a tenancy administrator you already have everything; §4 and §5 tell you exactly what to create otherwise. |
| A SQL client | Database Actions (SQL worksheet in the OCI console) is enough for everything except one step. SQLcl or SQL Developer with the database wallet is needed for §8, because you must connect as a specific user. |
| The script | dbms_metric_v0.3.sql. |
| Optional: the OCI CLI | Only for the verification commands in §11 and the alarm commands in §15. Everything can be done in the console instead. |
Note: the database needs outbound network access to https://telemetry-ingestion.<region>.oraclecloud.com. On a standard Autonomous AI Database this works out of the box; DBMS_CLOUD reaches OCI service endpoints without any ACL or gateway configuration on your part.
Five ideas. Get these and the rest of the guide is mechanical.
Every metric is a row in a table holding a SELECT statement. That statement must obey one rule:
It must return a column named
VALUE. That is the number OCI stores. It may return a column namedTS(a date or timestamp). That is the time of the datapoint. Every other column it returns becomes an OCI dimension.
That is the entire contract. There is no label list to declare and no PL/SQL to write.
SELECT COUNT(*) AS value FROM app.orders -- one number, no dimensions
SELECT status, COUNT(*) AS value FROM app.orders -- one number per status,
GROUP BY status -- dimensioned by "status"A metric namespace is the top-level bucket OCI files your metrics under, alongside built-in ones like oci_autonomous_database. You choose yours once, at install, and every metric this database publishes goes into it.
It is one-per-database rather than one-per-metric on purpose: the IAM policy that authorises publishing is scoped to a namespace, so a per-metric namespace would mean editing IAM every time somebody adds a metric.
Rules OCI enforces: starts with a letter, contains only letters, digits and underscores, and cannot begin with oci_ or oracle_ (reserved). The package validates this before it stores anything.
You do not create the namespace anywhere. OCI creates it implicitly on the first successful publish.
A resource group is a label attached to each metric, used to filter when reading. DBMS_METRIC defaults it to db and lets you set it per metric.
Use it wherever you might have reached for a second namespace — sales_app, db_health, batch — because changing it needs no IAM change at all.
Remember this or you will lose an hour: when reading a metric back, you must pass the resource group.
oci monitoring metric-data summarize-metrics-datawithout--resource-groupreturns zero rows and exit code 0 — no error, no warning. Same in the console: set the Resource group field or the metric appears absent.
A dimension is a key/value label on a datapoint. DBMS_METRIC injects two on every metric automatically:
resourceId— this database's OCID. This is what lets you correlate your custom metric with the platform metrics for the same database.dbName— the database name, which is what you will scope alarms by.
Your own columns are added alongside. A stream is one unique combination of metric name plus dimension values. orders_by_status over four statuses is four streams.
Streams matter because OCI caps a single API call at 50 streams, and because each stream is billable ingestion. §20 covers cost.
Every example in this guide uses an explicit block:
BEGIN dbms_metric.some_call('arg'); END;
/rather than the EXEC shorthand. EXEC is a client-side command that SQLcl
and Database Actions expand into BEGIN <rest of line>; END;. If a comment ends
up on the same logical line — which happens routinely when you paste a block —
the comment swallows the END; and you get a baffling PLS-00103: Encountered the symbol "end-of-file". The block form is immune to it.
The database opens the connection itself, and it opens it inward — to OCI Monitoring, in your own region and compartment, under your own IAM. Nothing polls it, nothing is exposed: no endpoint to secure, no listener, no inbound firewall rule. Your telemetry never leaves your tenancy for a third-party endpoint.
Because the push starts inside the database rather than arriving through the components in front of it, it keeps reporting when the application tier, the REST layer, or the network path a scraper would have used is the thing that has failed. Note the boundary, though: the publisher is the database, so a database that is down publishes nothing. Pair a value alarm on the canary with an active liveness check if you need to detect that case — see §15.
OCI will not accept metrics from an anonymous caller. The database must prove who it is. There are two ways, and you pick one now because it changes the next two sections.
| Option A — Resource principal | Option B — API signing key | |
|---|---|---|
| How it works | The database authenticates as itself, using a token OCI manages | A dedicated IAM user's private key, stored in a database credential, signs each request |
| Keys to manage | None | One private key, which you must rotate on your own schedule |
| Setup | Enable in the database, create a dynamic group, write a policy | Create a user, a group, a key pair, upload the public key, write a policy |
| Availability | Autonomous AI Database Serverless; check whether your Dedicated instance has it yet | Everywhere |
| Recommendation | Use this if you can. It removes key handling entirely | Use this if resource principal is not available to you |
Check quickly whether Option A is available to you — run this as ADMIN:
BEGIN
DBMS_CLOUD_ADMIN.ENABLE_RESOURCE_PRINCIPAL();
DBMS_OUTPUT.PUT_LINE('Resource principal enabled — use Option A.');
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Not available here — use Option B. ' || SQLERRM);
END;
/If it succeeds, go to §4. If it errors, go to §5.
You can switch later: it is one call to set_credential, plus the matching policy.
Do these in the OCI console. You need the database OCID, which the package prints for you in §7 — or take it from the database's console page (Oracle Database → Autonomous Database → your database → OCID → Copy).
As ADMIN:
BEGIN DBMS_CLOUD_ADMIN.ENABLE_RESOURCE_PRINCIPAL(); END;
/This creates a credential named OCI$RESOURCE_PRINCIPAL whose token auto-refreshes. It is reversible with DBMS_CLOUD_ADMIN.DISABLE_RESOURCE_PRINCIPAL().
A dynamic group is how IAM refers to a resource (your database) rather than a person.
Console: Identity & Security → Domains → (your domain) → Dynamic groups → Create dynamic group.
- Name:
dg-adb-metrics(any name; you will use it in the policy) - Matching rule:
ALL { resource.id = 'ocid1.autonomousdatabase.oc1.<region>.<your-db-ocid>' }
CLI equivalent:
oci iam dynamic-group create \
--name dg-adb-metrics \
--description "ADB instances allowed to publish custom metrics" \
--matching-rule "ALL {resource.id = 'ocid1.autonomousdatabase.oc1.<region>.<db-ocid>'}"Console: Identity & Security → Policies → Create Policy → Show manual editor.
Create it in the compartment holding the database (or in the tenancy root).
Allow dynamic-group dg-adb-metrics to use metrics
in compartment id ocid1.compartment.oc1..<your-compartment-ocid>
where target.metrics.namespace = '<your-namespace>'
Substitute your compartment OCID and the namespace you intend to choose in §7. use metrics is the aggregate verb that includes publishing; the namespace condition keeps the grant tight.
Write the compartment as
in compartment id <ocid>, not by name. A policy that lives inside the compartment it targets resolves a bare name as a non-existent child compartment. The result is a bare404 NotAuthorizedOrNotFoundthat looks nothing like a policy problem, and it costs people hours.
Publishing and reading are separate permissions. Your operations group needs:
Allow group <ops-group> to read metrics in compartment id ocid1.compartment.oc1..<comp>
Allow group <ops-group> to manage alarms in compartment id ocid1.compartment.oc1..<comp>
Allow group <ops-group> to manage ons-topics in compartment id ocid1.compartment.oc1..<comp>
The last one is for notification topics, which alarms need in order to page anyone.
IAM takes a minute or two to propagate. Now go to §6; you can skip §5 entirely.
Five things to create. Take them in order.
Do not reuse your own account. A service user with exactly one permission is the right posture.
Console: Identity & Security → Domains → (your domain) → Users → Create user.
- Username:
svc-adb-metrics - Email: any valid address (required by the form)
- Untick "create an IAM credential" prompts — this user never logs in.
CLI:
oci iam user create \
--name svc-adb-metrics \
--description "Publishes ADB custom metrics to OCI Monitoring"Note the user OCID it returns.
Policies attach to groups, not users.
Console: Groups → Create group → name it grp-adb-metrics → open it → Add user to group → pick svc-adb-metrics.
CLI:
oci iam group create --name grp-adb-metrics --description "ADB metric publishers"
oci iam group add-user --user-id <user-ocid> --group-id <group-ocid>On your own machine. The private key never leaves it except to go into the database credential.
mkdir -p ~/.oci && cd ~/.oci
openssl genrsa -out adb_metrics_key.pem 2048
chmod 600 adb_metrics_key.pem
openssl rsa -pubout -in adb_metrics_key.pem -out adb_metrics_key_public.pemDo not add a passphrase — DBMS_CLOUD cannot use a passphrase-protected key.
Console: Users → svc-adb-metrics → API keys → Add API key → Paste public key → paste the contents of adb_metrics_key_public.pem → Add.
The console then shows the fingerprint, something like 01:6f:d1:20:fa:90:.... Copy it.
CLI:
oci iam user api-key upload --user-id <user-ocid> --key-file ~/.oci/adb_metrics_key_public.pemVerify it is active later with:
oci iam user api-key list --user-id <user-ocid> --query 'data[*].fingerprint'Console: Identity & Security → Policies → Create Policy → Show manual editor.
Allow group grp-adb-metrics to use metrics
in compartment id ocid1.compartment.oc1..<your-compartment-ocid>
where target.metrics.namespace = '<your-namespace>'
Same warning as §4.3: use in compartment id <ocid>, never the bare name.
And the read side for your team:
Allow group <ops-group> to read metrics in compartment id ocid1.compartment.oc1..<comp>
Allow group <ops-group> to manage alarms in compartment id ocid1.compartment.oc1..<comp>
Allow group <ops-group> to manage ons-topics in compartment id ocid1.compartment.oc1..<comp>
You now have four values you will need in §8. Write them down:
| Value | Where it came from |
|---|---|
| User OCID | §5.1 |
| Tenancy OCID | Console → Profile menu → Tenancy, or oci iam compartment list --all |
| Fingerprint | §5.4 |
| Private key file | §5.3 (~/.oci/adb_metrics_key.pem) |
Open dbms_metric_v0.3.sql. Connect as ADMIN — Database Actions SQL worksheet is fine.
Run PART A and PART B of the script, in order. Together they create:
| Object | What it is |
|---|---|
User METRIC_EXPORTER |
An unprivileged schema that owns everything. Change the password in the script before running. |
| Grants | CREATE SESSION/TABLE/PROCEDURE/JOB, execute on DBMS_CLOUD, DBMS_SCHEDULER, DBMS_LOB, DBMS_SQL, and SELECT on SYS.V_$PDBS — that last one is the only data grant shipped, and §14 explains why. |
ADMIN.METRIC_OBJECT_KIND |
A tiny lookup function that makes the grant diagnosis in §14 exact. |
METRIC_CONFIG |
One row of configuration. |
METRIC_REGISTRY |
Your metrics. Seeded with a canary and four disabled built-ins. |
METRIC_PUSH_LOG |
Every request and every response, kept for forensics. |
DBMS_METRIC |
The package: spec and body. |
Confirm it landed:
SELECT object_name, object_type, status
FROM all_objects
WHERE owner = 'METRIC_EXPORTER'
AND object_type IN ('PACKAGE','PACKAGE BODY','TABLE')
ORDER BY object_type, object_name;You want two VALID rows for DBMS_METRIC and three tables. If the package body is INVALID, see §18.
Note on schema qualification. Everything in this guide calls metric_exporter.dbms_metric.<something>, so you can run it all as ADMIN. If you connect as METRIC_EXPORTER itself, drop the prefix.
This is the decision you make once. Everything this database publishes lands here.
Pick something identifying and stable: adbd_custom_metric, payments_db, myapp_metrics.
ACCEPT ns CHAR PROMPT 'Metric namespace [letters, digits, underscore; not oci_/oracle_]: '
ACCEPT ok CHAR PROMPT 'Use "&ns" for ALL metrics published by this database? (yes/no): '
BEGIN
IF LOWER('&ok') != 'yes' THEN
RAISE_APPLICATION_ERROR(-20001, 'Aborted — re-run and choose a namespace.');
END IF;
metric_exporter.dbms_metric.configure(p_namespace => '&ns');
END;
/ACCEPT is a client-side command. It does nothing in Database Actions or through an MCP client — use the direct call there.
SET SERVEROUTPUT ON
BEGIN
metric_exporter.dbms_metric.configure(p_namespace => 'adbd_custom_metric');
END;
/If you chose Option A in §3, add the credential name:
BEGIN
metric_exporter.dbms_metric.configure(
p_namespace => 'adbd_custom_metric',
p_credential => 'OCI$RESOURCE_PRINCIPAL');
END;
/configure validates the namespace, stores it, then prints the complete IAM setup with every OCID already filled in — your database OCID, compartment OCID, tenancy OCID, both auth options, and the ops read policies. Real output:
============================================================================
DBMS_METRIC v0.3 - IAM setup for namespace "adbd_custom_metric"
============================================================================
Database : MYDB01 Region: us-ashburn-1
DB OCID : ocid1.autonomousdatabase.oc1.iad.anuw...
Compartm.: ocid1.compartment.oc1..aaaa...
Tenancy : ocid1.tenancy.oc1..aaaa...
...
Allow dynamic-group <your-dynamic-group> to use metrics
in compartment id ocid1.compartment.oc1..aaaa...
where target.metrics.namespace = 'adbd_custom_metric'
...
Copy the block matching your chosen option into the OCI console policy editor, fill in your group or dynamic-group name, and save it. You can reprint it any time:
SET SERVEROUTPUT ON
BEGIN metric_exporter.dbms_metric.print_policy; END;
/Skip this entire section if you chose Option A (resource principal). You already have OCI$RESOURCE_PRINCIPAL; just make sure §7 pointed at it, or run:
BEGIN metric_exporter.dbms_metric.set_credential('OCI$RESOURCE_PRINCIPAL'); END;
/For Option B, read on.
DBMS_CLOUD.CREATE_CREDENTIAL always creates the credential in the schema of the user running it. The package runs with definer's rights as METRIC_EXPORTER, so that is the only schema where it can find the credential. A credential created by ADMIN is invisible to it, and you will get ORA-20004: Credential ... does not exist.
This is the one step you cannot do from an ADMIN session.
How to connect as METRIC_EXPORTER: use SQLcl or SQL Developer with the same database wallet you use for ADMIN, but username METRIC_EXPORTER and the password you set in PART A of the script. In Database Actions, an ADMIN can grant the user access to Database Actions first, but SQLcl is quicker.
DBMS_CLOUD wants the key as a single line of PKCS#8 with the header and footer stripped:
openssl pkcs8 -topk8 -nocrypt -in ~/.oci/adb_metrics_key.pem | grep -v -- '-----' | tr -d '\n'Copy the entire output. It is one long line with no spaces.
Connected as METRIC_EXPORTER:
BEGIN
DBMS_CLOUD.CREATE_CREDENTIAL(
credential_name => 'OCI_METRICS_CRED',
user_ocid => 'ocid1.user.oc1..<your-user-ocid>',
tenancy_ocid => 'ocid1.tenancy.oc1..<your-tenancy-ocid>',
private_key => '<the single line from 8.2>',
fingerprint => '01:6f:d1:20:fa:90:...');
END;
/
SELECT credential_name, enabled FROM user_credentials
WHERE credential_name = 'OCI_METRICS_CRED';The name must match what is in metric_config.credential_name — OCI_METRICS_CRED by default.
An expired or rotated key is the single most common reason an exporter like this goes quiet. Make rotation one call. Still as METRIC_EXPORTER:
CREATE OR REPLACE PROCEDURE metric_exporter.reset_metrics_cred(
p_user_ocid IN VARCHAR2,
p_tenancy_ocid IN VARCHAR2,
p_fingerprint IN VARCHAR2,
p_private_key IN CLOB) AUTHID DEFINER AS
BEGIN
BEGIN DBMS_CLOUD.DROP_CREDENTIAL('OCI_METRICS_CRED');
EXCEPTION WHEN OTHERS THEN NULL; END;
DBMS_CLOUD.CREATE_CREDENTIAL(
credential_name => 'OCI_METRICS_CRED',
user_ocid => p_user_ocid,
tenancy_ocid => p_tenancy_ocid,
private_key => p_private_key,
fingerprint => p_fingerprint);
END;
/After any rotation, re-run §9 before walking away.
Before testing the metrics path, check the identity in isolation:
SELECT dbms_metric.check_credential() AS credential FROM dual;This probes an Identity endpoint with the same credential the exporter uses, and it separates the two issues that are otherwise easy to conflate:
| Verdict | Meaning |
|---|---|
OK |
OCI verified your signature. It does not prove you may publish — that is the next step. |
MISSING |
No such credential in this schema. You created it as ADMIN; redo §8 connected as METRIC_EXPORTER. |
FAILED |
OCI rejected the signature, and the message lists the four things that can be wrong. |
RESOURCE PRINCIPAL in use |
Nothing to check; go straight to the publish test. |
The FAILED case is worth understanding, because it is the most common
first-run problem and the most misleading. ORA-20401 means the signature was
rejected — almost always because user_ocid does not own the key with that
fingerprint. That happens constantly when your user lives in a custom
identity domain: the OCID from an older profile, or one copied from a
colleague, is paired with your current key. All three values must come from the
same profile in ~/.oci/config.
A useful detail encoded in check_credential: a 404 counts as success. OCI
returns 401 for a bad signature but 404 NotAuthorizedOrNotFound once it has
verified you and then failed to find or authorise the object — so reaching a 404
proves authentication worked. Users in a custom identity domain always get 404
here, because their records do not live under the legacy /users/ path.
This is the moment of truth. Back as ADMIN (or still as METRIC_EXPORTER — either works):
SELECT metric_exporter.dbms_metric.test_publish() AS verdict FROM dual;It publishes a single canary datapoint (value 42) and returns a verdict in English, not a status code. You will get one of these:
| Verdict starts with | What it means | What to do |
|---|---|---|
OK |
Published. Namespace is live. | Go to §10. |
NOT AUTHORIZED |
The IAM policy is missing, names the wrong compartment, or has not propagated. | Re-run print_policy, compare it to what you actually created, wait two minutes, retry. Check you used in compartment id <ocid>. |
FAILED - status -1 ... Credential ... does not exist |
The credential is missing, or it is in the wrong schema. | Redo §8 connected as METRIC_EXPORTER. |
BAD REQUEST |
Authentication succeeded — a 400 means OCI got past signing — but the payload was rejected. | This is a bug in the package, not your setup. Capture last_payload and report it. |
PARTIAL |
OCI accepted the call but rejected some streams. | SELECT * FROM TABLE(metric_exporter.dbms_metric.failed_metrics(10)); |
NOT CONFIGURED |
You skipped §7. | Run configure. |
A successful test publish is recorded. The scheduler will not start without one — see §10.
BEGIN metric_exporter.dbms_metric.start_push(60); END;
/The argument is the push interval in seconds and it is entirely yours to choose — 300 for five minutes, 900 for fifteen, whatever suits the signal. A slow-moving business metric does not need a 60-second cadence.
It is validated, though, against min_schedule_secs and max_schedule_secs in metric_config (defaults 60 and 3600):
BEGIN dbms_metric.start_push(300); END; -- 5 minutes: fine
/
BEGIN dbms_metric.start_push(1); END; -- ORA-20107
/Why there is a floor. OCI aggregates custom metrics at a one-minute minimum, so every datapoint inside the same minute collapses into a single aggregated value. Pushing every second therefore buys no extra resolution at all — it just multiplies billable ingestion and eats into the per-tenancy PostMetricData transaction budget. The floor stops you paying for resolution that a one-minute aggregation cannot expose.
Why there is a ceiling. Past an hour, alarms evaluated over short windows start seeing gaps between pushes and report "no data", which reads exactly like an exporter that has stopped.
Both are your defaults to change, not hard limits — the error tells you how:
UPDATE metric_config SET min_schedule_secs = 30 WHERE config_id = 1;
COMMIT;
BEGIN dbms_metric.start_push(30); END; -- now accepted
/Fractional intervals are rejected outright (start_push(90.5)), since DBMS_SCHEDULER takes whole seconds.
start_pushrefuses if §9 has never succeeded, raisingORA-20106. This is deliberate. Without the check, the classic outcome is a job quietly logging authorisation errors every 60 seconds for a week while everybody assumes monitoring is working.
Confirm it is running and pushing:
SELECT * FROM TABLE(metric_exporter.dbms_metric.job_status());
SELECT * FROM TABLE(metric_exporter.dbms_metric.recent_pushes(10));You want consecutive rows with HTTP_STATUS = 200 and FAILED_COUNT = 0, roughly one per minute.
Why one minute matters: nearly the entire built-in oci_autonomous_database namespace on Dedicated is emitted every five minutes. A metric you publish yourself is bounded only by OCI's one-second minimum publish frequency and one-minute minimum aggregation. For fault detection, that difference is much of the value.
To stop:
BEGIN metric_exporter.dbms_metric.stop_push; END;
/Observability & Management → Monitoring → Metrics Explorer.
- Compartment: the one holding your database.
- Metric namespace: your namespace. If it is not in the dropdown, no datapoint has landed yet — wait a minute.
- Resource group:
db(or whatever you configured). Leave this blank and you will see nothing. - Metric name:
canary. - Interval: 1m, Statistic: Max.
- Click Update Chart — the editor does not refresh on its own.
You should see a flat line at 42.
# what metrics exist in the namespace?
oci monitoring metric list \
--compartment-id <compartment-ocid> \
--namespace adbd_custom_metric \
--query 'data[*].name'
# are datapoints actually arriving?
oci monitoring metric-data summarize-metrics-data \
--region us-ashburn-1 \
--compartment-id <compartment-ocid> \
--namespace adbd_custom_metric \
--resource-group db \
--query-text "canary[1m].max()" \
--start-time 2026-09-05T00:00:00Z \
--end-time 2026-09-05T01:00:00Zcanary has no dimensions of its own, so it draws one line. A metric like
orders_by_status (dimensioned by status and region) needs one more decision,
and the console's controls are easy to misread.
The Dimension name/value pair is a FILTER, not a split. Setting
status = PENDING narrows the chart to that one stream; it does not draw a line
per status. Three ways to get what you want:
| You want | Do this |
|---|---|
| One line per stream | Delete the dimension filter (the ✕), leave Aggregate metric streams OFF. You get every combination — 3 statuses × 2 regions = 6 lines. |
One line per status |
Advanced/MQL mode: orders_by_status[1m].max().groupBy(status) |
| One specific stream | Add both dimensions as filters: status = PENDING, region = EMEA |
| One combined line | Turn Aggregate metric streams ON |
Set Statistic to Max, not Mean, for counters. The job pushes about once a
minute, so two pushes can land in the same one-minute bucket and Mean averages
them — a value stepping from 10 to 50 renders as a smeared ~30 instead of a clean
jump. Same rule as the alarm table in §15.
Two CLI traps worth knowing now rather than later:
--resource-groupis required on the read. Omit it and you get zero rows with exit code 0.--query-textcarries the MQL, not--query.--queryis the CLI's global JMESPath output filter; it will accept your MQL string and silently do nothing useful with it.
The intended workflow is three commands. Do them in order; each one saves you from a mistake the next would have made expensive.
SELECT item, result FROM TABLE(metric_exporter.dbms_metric.check_sql(
q'[SELECT status, COUNT(*) AS value FROM app.orders GROUP BY status]'));Typical good output:
verdict parses OK in the exporter's own environment (roles disabled)
value column VALUE
timestamp column none - datapoints stamped at push time
dimensions (auto) resourceId, dbName, status
streams produced 4
rows skipped 0
This verdict is authoritative, and that is the point of the command. check_sql parses your SQL inside the package — with definer's rights and roles disabled — which is exactly the environment the scheduler will run it in. SQL that works in your session can still fail there (see §14). If it works here, it works at push time.
If something is wrong you get verdict = FAILED and a specific error, including the exact GRANT statement when a privilege is missing.
SELECT stream_num, json_text FROM TABLE(metric_exporter.dbms_metric.metric_dryrun(
'orders_by_status',
q'[SELECT status, COUNT(*) AS value FROM app.orders GROUP BY status]'));One row per stream, each the literal JSON object that would go to OCI. Nothing is sent and nothing is registered.
{"namespace":"adbd_custom_metric","compartmentId":"ocid1.compartment...",
"resourceGroup":"db","name":"orders_by_status",
"dimensions":{"resourceId":"ocid1.autonomousdatabase...","dbName":"MYDB01","status":"SHIPPED"},
"metadata":{"unit":"count"},
"datapoints":[{"timestamp":"2026-09-05T01:06:19Z","value":42}]}BEGIN
metric_exporter.dbms_metric.add_metric(
p_metric_name => 'orders_by_status',
p_source_sql => q'[SELECT status, COUNT(*) AS value FROM app.orders GROUP BY status]',
p_unit => 'count',
p_description => 'Open orders by status',
p_resource_group => 'sales_app', -- optional; defaults to the configured group
p_enabled => TRUE);
END;
/add_metric re-runs the dry run internally before it persists anything, so a metric that would fail cannot get into the registry. It flows on the next scheduler tick — no recompile, no restart.
Zero rows at registration is fine and accepted: a COUNT of error conditions on a healthy database is legitimately empty today.
If your query knows when each measurement happened, return it as a column named TS:
SELECT region, reading AS value, measured_at AS ts
FROM sensor_readings
WHERE measured_at > SYSTIMESTAMP - INTERVAL '5' MINUTEDATE,TIMESTAMP,TIMESTAMP WITH TIME ZONEandTIMESTAMP WITH LOCAL TIME ZONEare all accepted.SYSTIMESTAMPworks.- Zone-aware values are converted to UTC. A naive
DATEorTIMESTAMPis taken as UTC — convert it yourself if your column stores local time. - OCI only accepts datapoints less than 2 hours in the past and less than 10 minutes in the future. Rows outside that window are dropped and counted in
rows skipped, rather than being sent — one bad timestamp would otherwise cause OCI to reject the whole batch. - Rows sharing the same dimensions collapse into one stream with several datapoints, which is both the correct shape and cheaper.
Metric names must start with a letter and contain only letters, digits, dots, underscores, hyphens and dollar signs. orders.by_status and myapp_success_rate are both fine.
Five metrics ship in the registry. Only the canary is enabled.
| Metric | SQL | Enabled | Needs grant |
|---|---|---|---|
canary |
SELECT 42 AS value FROM dual |
Yes | none |
invalid_objects |
count of DBA_OBJECTS where STATUS='INVALID' |
No | SYS.DBA_OBJECTS |
invalid_objects_by_owner |
same, GROUP BY owner |
No | SYS.DBA_OBJECTS |
unusable_indexes |
count of DBA_INDEXES where STATUS='UNUSABLE' |
No | SYS.DBA_INDEXES |
failed_scheduler_jobs_5m |
failed scheduler runs in the last 5 minutes | No | SYS.DBA_SCHEDULER_JOB_RUN_DETAILS |
Why so few, and why disabled? The oci_autonomous_database namespace already publishes 40+ metrics for this database — CPU, sessions, storage, IOPS, latency, availability — at no cost to you. Re-publishing those here would duplicate data you already have and bill you for custom ingestion. These four sit outside the platform metric set, and each is a single-table COUNT(*) with one predicate, so you can verify any value by running the same SELECT by hand.
Enabling one is the recommended first exercise, because it teaches the grant model on a metric where a mistake costs nothing:
BEGIN metric_exporter.dbms_metric.enable_metric('invalid_objects'); END;
/That will fail with ORA-20142 and hand you the exact GRANT. Read §14, run the grant as ADMIN, and enable again.
Keep the canary enabled. It is your proof the pipeline is alive — if canary stops arriving in OCI, the exporter is down, and that is something you can alarm on.
Your metric's SQL runs inside the package, with definer's rights as METRIC_EXPORTER. In definer's-rights PL/SQL, roles are disabled. Therefore:
Every object your metric reads needs a direct
GRANTtoMETRIC_EXPORTER. A grant through a role will not work, no matter how many roles you pile on.
This is why the package ships with almost no data grants. Granting a handful of views up front would only hide the rule until you registered your first metric against your own tables.
SELECT item, result FROM TABLE(metric_exporter.dbms_metric.check_sql(
q'[SELECT COUNT(*) AS value FROM dba_objects WHERE status='INVALID']'));verdict FAILED
error ORA-20142: metric SQL references SYS.DBA_OBJECTS (VIEW), which
METRIC_EXPORTER cannot read.
Ask the owner or ADMIN to run:
GRANT SELECT ON SYS.DBA_OBJECTS TO METRIC_EXPORTER;
A grant via a ROLE will NOT work - roles are disabled inside this
package. It must be a direct grant. Parsing stops at the first
unreadable object, so fix this one and re-run check_sql.
Original error: ORA-00942: table or view "SYS"."DBA_OBJECTS" does not exist
Run the printed line as ADMIN, then re-run check_sql or enable_metric. That is the whole loop.
- Public synonyms. Write
v$sessionand the grant you need is onSYS.V_$SESSION— a different name. The message gives you the resolved one. ACD_cross-container views on Dedicated are owned byC##CLOUD$SERVICE, which nobody would guess, and they needGRANT ALLrather thanGRANT SELECT. Both are handled.- Typos. If the object does not exist anywhere, you get
ORA-20143telling you to check spelling and owner, instead of a misleading grant suggestion.
If the metric reads your application's tables, the table owner runs:
GRANT SELECT ON app.orders TO METRIC_EXPORTER;Grant only what the metric reads. METRIC_EXPORTER should never accumulate broad privileges.
Parsing stops at the first object it cannot resolve, so a query touching three ungranted tables takes three fix-and-retry cycles. The message says so; it is not giving you an exhaustive list.
A metric nobody alarms on is a chart nobody looks at.
Alarms need somewhere to send. Console: Developer Services → Notifications → Create Topic, then Create Subscription (email, Slack, PagerDuty, or a function).
oci ons topic create --name adb-metric-alarms --compartment-id <comp-ocid>
oci ons subscription create --topic-id <topic-ocid> --compartment-id <comp-ocid> \
--protocol EMAIL --subscription-endpoint you@example.comConfirm the email subscription from your inbox or it will never fire.
Console: Monitoring → Alarm Definitions → Create Alarm.
Switch to advanced mode and type the MQL query. This matters:
A custom metric is invisible in the guided alarm builder until its first datapoint arrives. In advanced/MQL mode you can type the query, save the alarm in a "no data" state, and it arms itself when data lands. Otherwise you are stuck in a chicken-and-egg loop.
CLI:
oci monitoring alarm create \
--compartment-id "$COMP" --metric-compartment-id "$COMP" \
--namespace adbd_custom_metric --resource-group db \
--display-name "Orders backlog too high" \
--query-text 'orders_by_status[1m]{dbName="MYDB01",status="PENDING"}.max() > 500' \
--severity CRITICAL --pending-duration PT5M \
--destinations "[\"$TOPIC\"]" --is-enabled trueFour things in that command decide whether the alarm ever fires:
--resource-group db. Your metrics are published with a resource group; an alarm that omits it matches no stream and stays silent forever.--query-text, not--query. As in §11.{dbName="..."}scopes the alarm to one database. In a compartment holding several, an unscoped alarm pages every team for one database's fault.- Severity and pending duration decide whether people trust it.
PT5Mmeans the condition must persist for five minutes.
The alarm evaluates continuously, but the notification is not instant. With
--pending-duration PT1M the condition must hold for a full minute, and OCI's own
evaluation adds a little more — expect the state change within about two to
three minutes of the data being present.
Check the alarm's state, not your inbox. The authoritative answer is:
oci monitoring alarm-status list-alarms-status \
--compartment-id <compartment-ocid> \
--query 'data[].{name:"display-name",status:status,ts:"timestamp-triggered"}'FIRINGbut no email → the notification subscription is stillPENDING. Confirm it from the address you subscribed:oci ons subscription list --compartment-id <comp> --query 'data[].{endpoint:endpoint,state:"lifecycle-state"}'OKwhen you expect FIRING → the query matches no stream. Check resource group first, then thedbNamevalue, thenmax()vsmean().
When it does fire, the notification names the exact stream that breached:
"totalMetricsFiring": 1,
"dimensions": [{"region":"EMEA","status":"PENDING","dbName":"...","resourceId":"..."}],
"metricValues": [{"orders_by_status[1m]{...}.max()":"50.00"}]That is the practical argument for putting dimensions on a metric — an alarm that
says "EMEA/PENDING is at 50" is actionable; one that says "orders are high" is
not. The dedupeKey in the payload is what stops OCI re-paging you every minute
while the condition persists: you get one message per state transition, and a
matching FIRING_TO_OK when it clears.
This is where alarms quietly succeed or fail.
| Metric shape | Use | Why |
|---|---|---|
| A counter (errors, retries, backlog) | max() or sum() |
Preserves a single bad sample instead of averaging it away |
| A 1/0 fault bit | min() |
The worst sample in the window is the one you care about |
| A ratio from many samples | mean() |
Grades the severity instead of treating one blip as an outage |
| Latency | mean() with a long pending duration |
A mean is easily dragged by one outlier; require persistence rather than a looser threshold |
The behaviour to internalise: the job pushes about once a minute, so two pushes can land in the same one-minute bucket. With mean(), a brief fault that put a 0 next to a healthy 1 reads as 0.5 — which slips under a < 1 threshold and never pages. That is why fault bits use min() and counters use max().
Your most valuable alarm may be the one that tells you the exporter itself died:
canary[5m]{dbName="MYDB01"}.max() < 42
Design for one thing, though: a gap in a series is ambiguous. "No data" and "no traffic" look identical in any metrics system, so an alarm on the canary's value confirms the payload is well formed, but it cannot by itself tell you the scheduler has stopped. Cover that with job_status in your regular operational review, or by watching for the alarm entering its "no data" state.
All are pipelined; select from them with TABLE(...).
| Question | Command |
|---|---|
| Is the whole thing healthy? Start here. | SELECT * FROM TABLE(metric_exporter.dbms_metric.health_summary(30)); |
| Are pushes succeeding? | SELECT * FROM TABLE(metric_exporter.dbms_metric.recent_pushes(15)); |
| Did a specific metric's SQL break? | SELECT * FROM TABLE(metric_exporter.dbms_metric.metric_errors(30)); |
| Did OCI accept the call but reject some data? | SELECT * FROM TABLE(metric_exporter.dbms_metric.failed_metrics(30)); |
| Is the scheduler running? | SELECT * FROM TABLE(metric_exporter.dbms_metric.job_status()); |
| What is registered? | SELECT * FROM TABLE(metric_exporter.dbms_metric.list_metrics()); |
| What exactly did we send last? | SELECT metric_exporter.dbms_metric.last_payload() FROM dual; |
| Which version is installed? | SELECT metric_exporter.dbms_metric.version() FROM dual; |
health_summary in one screen: version, namespace, resource group, credential name, the push interval and its allowed range, when the last test publish succeeded, how many metrics are enabled and disabled, batches accepted in the window, streams OCI rejected, and metric errors.
failed_metrics is the most important function here. OCI can return HTTP 200 while rejecting individual streams, reporting the count in the response body. Without looking, a schema mistake looks like success indefinitely. The package parses failedMetricsCount on every push and surfaces it — but you have to look, or alarm on it.
Change the push interval — recreates the job at the new cadence:
BEGIN metric_exporter.dbms_metric.start_push(300); END; -- every 5 minutes
/Rejected with ORA-20107 outside min_schedule_secs–max_schedule_secs (60–3600 by default). health_summary shows the current interval and the allowed range, so you never have to go looking for it.
Pause and resume
BEGIN metric_exporter.dbms_metric.stop_push; END;
/
BEGIN metric_exporter.dbms_metric.start_push; END;
/Force a push right now
BEGIN metric_exporter.dbms_metric.push_metrics; END;
/Rotate the API key — as METRIC_EXPORTER, using the helper from §8.4, then re-run §9.
Switch to resource principal later
BEGIN DBMS_CLOUD_ADMIN.ENABLE_RESOURCE_PRINCIPAL(); END;
/
BEGIN metric_exporter.dbms_metric.set_credential('OCI$RESOURCE_PRINCIPAL'); END;
/
SELECT metric_exporter.dbms_metric.test_publish() FROM dual;Add the matching dynamic group and policy from §4 first. set_credential clears the recorded verification, so you must pass §9 again before start_push will work.
Enable, disable, remove metrics
BEGIN metric_exporter.dbms_metric.enable_metric('invalid_objects'); END;
/
BEGIN metric_exporter.dbms_metric.disable_metric('invalid_objects'); END;
/
BEGIN metric_exporter.dbms_metric.remove_metric('orders_by_status'); END;
/Built-in metrics can be disabled but not removed (ORA-20121). enable_metric re-validates the SQL rather than just flipping a flag, so a metric whose grant was revoked is caught loudly at enable time rather than quietly at push time.
Reset to the as-installed state — useful before a demo or after experimenting:
SET SERVEROUTPUT ON
BEGIN dbms_metric.reset_to_shipped; END;
/It enables the canary, disables every other built-in, and then reports what it
did not touch: your custom metrics (still registered — use remove_metric) and
every grant added since install, printed as ready-to-run REVOKE statements. It
never deletes anything, so nothing disappears behind your back.
Prune the push log — it keeps full request and response bodies:
-- keep 14 days
BEGIN metric_exporter.dbms_metric.prune_log(14); END;
/Worth scheduling monthly if you run at 60-second cadence.
Change the namespace — rarely a good idea; it orphans the existing series and invalidates your IAM policy:
BEGIN metric_exporter.dbms_metric.set_namespace('new_ns', p_force => TRUE); END;
/It refuses without p_force once the database has published, and reprints the new policy.
Adjust the cardinality guardrail
UPDATE metric_exporter.metric_config SET max_streams_metric = 500 WHERE config_id = 1;
COMMIT;Default is 200 streams per metric. Raise it knowingly — see §20.
| Code | Meaning | Fix |
|---|---|---|
ORA-20101 |
No namespace configured | Run configure (§7) |
ORA-20102/3/4 |
Invalid namespace: bad characters, reserved oci_/oracle_ prefix, or too long |
Choose another name |
ORA-20105 |
Changing namespace after publishing | Re-run with p_force => TRUE if intended |
ORA-20106 |
start_push without a successful test publish |
Run §9 and fix what it reports |
ORA-20107 |
Push interval outside the allowed range, or not a whole number | Pick a value between min_schedule_secs and max_schedule_secs, or move those bounds deliberately — the message shows the exact UPDATE |
ORA-20120 |
Metric not in the registry | Check list_metrics for the exact name |
ORA-20121 |
Cannot remove a built-in metric | Disable it instead |
ORA-20140 |
SQL has no VALUE column |
Alias your number: ... AS value |
ORA-20141 |
SQL will not parse | Read the original error appended to the message |
ORA-20142 |
Object exists but METRIC_EXPORTER cannot read it |
Run the GRANT the message prints — §14 |
ORA-20143 |
Object does not exist, or a column collides with an injected dimension (resourceid, dbname) |
Fix the spelling, or alias the column |
ORA-20144 |
More than 20 dimensions | OCI's limit; return fewer columns |
ORA-20145 |
Over the cardinality guardrail | Reduce dimension cardinality, or raise max_streams_metric (§20) |
ORA-20146 |
Invalid metric name | Letters, digits, ., _, -, $; must start with a letter |
ORA-20150 |
TS column is not a date/timestamp type |
CAST(your_column AS TIMESTAMP) AS ts |
ORA-20401 |
OCI rejected the signature — an identity problem, not a policy one | SELECT dbms_metric.check_credential() FROM dual; and follow its checklist |
ORA-20404 |
OCI verified the signature and did not find the object | Not an error for check_credential — it counts as success |
| Symptom | Cause and fix |
|---|---|
ORA-20004: Credential "METRIC_EXPORTER"."OCI_METRICS_CRED" does not exist |
The credential is missing or was created in the wrong schema. Redo §8 as METRIC_EXPORTER. |
test_publish returns NOT AUTHORIZED |
Policy missing, wrong compartment, or still propagating. Compare against print_policy. Check you wrote in compartment id <ocid>, not the compartment name. |
Push log shows http_status = -1 |
A non-2xx surfaces as an ORA-2040x exception rather than a status code. Read err_text — it names the real cause. |
| HTTP 200 but nothing in Metrics Explorer | Check failed_metrics. Then check you set Resource group in the console and --resource-group on the CLI. Then click Update Chart. |
| A metric stopped, others fine | metric_errors(60). Per-metric isolation means one problem query never blocks the batch — it is logged and skipped. |
| Everything stopped at once | job_status (is the job there?), then recent_pushes (are we getting 401/404?). An expired API key is the usual answer. |
rows skipped is non-zero |
Either a NULL value, or a TS outside the −2h/+10min window. Both are dropped deliberately. |
PLS-00103: Encountered the symbol "end-of-file" on a pasted block |
A comment merged onto an EXEC line and swallowed the END;. Use BEGIN ... END; / — see §2.5 |
version() disagrees with the script you ran |
Somebody patched only the body, or only the spec. health_summary shows both compile times; re-run the whole script section |
Package body is INVALID after install |
SELECT * FROM all_errors WHERE owner='METRIC_EXPORTER' AND name='DBMS_METRIC'; Usually a missing grant from PART A — re-run it. |
| Alarm saves but never fires | Resource-group mismatch, or a dimension filter the metric does not carry. Confirm with summarize-metrics-data ... --query 'data[].dimensions'. |
All calls are metric_exporter.dbms_metric.<name>.
| Call | Purpose |
|---|---|
configure(p_namespace, p_credential, p_resource_group) |
One-time: validate and store the namespace, then print the IAM policy with real OCIDs |
print_policy |
Reprint that policy at any time |
set_namespace(p_namespace, p_force) |
Change the namespace; refuses after publishing unless forced |
set_credential(p_credential) |
Point at a different DBMS_CLOUD credential |
check_credential (function) |
Does OCI accept this credential at all? Isolates a signing problem from a policy problem |
test_publish (function) |
Publish the canary, return a plain-English verdict |
start_push(p_seconds) |
Create the scheduler job; refuses without a successful test_publish |
stop_push |
Drop the scheduler job |
| Call | Purpose |
|---|---|
check_sql(p_source_sql) (pipelined) |
Validate SQL in the exporter's own environment: parse, value column, timestamp column, dimensions, streams, skipped rows, grant diagnosis |
metric_dryrun(p_metric_name, p_source_sql, p_unit, p_resource_group) (pipelined) |
Show the exact JSON that would be sent, without sending or registering |
add_metric(p_metric_name, p_source_sql, p_unit, p_description, p_resource_group, p_enabled) |
Validate then register |
enable_metric(p_metric_name) |
Re-validate, then enable |
disable_metric(p_metric_name) |
Stop publishing it |
remove_metric(p_metric_name) |
Delete it; built-ins refuse |
list_metrics (pipelined) |
Everything registered, with state, description and the effective resource group (db (default) when inherited) |
reset_to_shipped |
Back to as-installed: canary on, other built-ins off. Reports the custom metrics and added grants it deliberately left alone |
| Call | Purpose |
|---|---|
push_metrics |
One full publish cycle; what the scheduler calls |
recent_pushes(p_minutes) (pipelined) |
Batch-level history with status and response |
metric_errors(p_minutes) (pipelined) |
Per-metric SQL errors |
failed_metrics(p_minutes) (pipelined) |
Pushes OCI accepted but partially rejected |
health_summary(p_minutes) (pipelined) |
One-screen overall state |
job_status (pipelined) |
Scheduler job state and next run |
last_payload (function) |
The most recent request body |
version (function) |
Package version |
prune_log(p_keep_days) |
Trim the push log |
metric_exporter.metric_config, one row, config_id = 1:
| Column | Default | Meaning |
|---|---|---|
namespace |
— | Set by configure |
resource_group |
db |
Default for new metrics |
credential_name |
OCI_METRICS_CRED |
DBMS_CLOUD credential used to sign |
compartment_ocid |
NULL | Override; NULL means self-discovered |
schedule_secs |
60 | Current push interval, set by start_push |
min_schedule_secs |
60 | Cadence floor — below this OCI's one-minute aggregation makes the extra pushes pure cost |
max_schedule_secs |
3600 | Cadence ceiling — beyond this, short-window alarms see gaps and report no data |
batch_atomicity |
NON_ATOMIC |
So one bad stream does not sink the batch |
max_streams_call |
50 | OCI's per-call cap; do not raise |
max_streams_metric |
200 | Your cardinality guardrail |
canary_ok_at |
NULL | Set by a successful test_publish; gates start_push |
enabled |
Y |
Master switch for push_metrics |
| Limit | Value | How it is handled |
|---|---|---|
| Streams per API call | 50 | Batched automatically; multiple POSTs per cycle if needed |
| Dimensions per metric | 20 | Two are injected, so 18 of your columns; ORA-20144 at registration |
| Dimension key / value length | 256 / 512 chars | Keys sanitised, values truncated |
| Empty dimension values | Rejected by OCI | The key is dropped for that row |
| Datapoint age | < 2h past, < 10min future | Out-of-window rows skipped and counted |
| Namespace prefix | Not oci_ / oracle_ |
Rejected at configure |
| Minimum publish / aggregation | 1 second / 1 minute | 60s default cadence |
Custom metric ingestion is billable, and the unit is the stream. A metric returning one row is one stream. A metric grouped by a column with 400 distinct values is 400 streams, every minute, forever.
Before adding a dimension, ask what its cardinality will be in production, not on your test database. check_sql tells you the stream count today; the guardrail (max_streams_metric, default 200) stops a single runaway metric from surprising you.
Rules of thumb:
- Dimension by things with bounded cardinality: status, region, node, tier.
- Never dimension by user ID, session ID, order ID, or anything unbounded.
- If you need per-entity detail, that is a logging problem, not a metrics problem.
-- as ADMIN
BEGIN metric_exporter.dbms_metric.stop_push; END;
/
DROP USER metric_exporter CASCADE;
DROP FUNCTION admin.metric_object_kind;
-- optional: revoke the grants you added for your metrics
-- REVOKE SELECT ON SYS.DBA_OBJECTS FROM metric_exporter; (implied by DROP USER)Also remove, on the OCI side: the policy, the dynamic group or the service user and its API key, and any alarms you built.
Published metric data is not deleted. It ages out under OCI Monitoring's retention.
DBMS_SCHEDULER (every 60s)
│
▼
for each enabled registry row:
DBMS_SQL.PARSE ──► DESCRIBE_COLUMNS ──► find VALUE, find TS,
everything else = dimension
fetch rows ──► group by dimension set ──► one stream, N datapoints
│
▼
batch at 50 streams ──► {"metricData":[...],"batchAtomicity":"NON_ATOMIC"}
│
▼
DBMS_CLOUD.SEND_REQUEST (signs the OCI request for you)
│
▼
https://telemetry-ingestion.<region>.oraclecloud.com/20180401/metrics
│
▼
parse failedMetricsCount ──► metric_push_log ──► diagnostics
Four design decisions worth knowing:
Identity is self-discovered. V$PDBS.CLOUD_IDENTITY gives the region, compartment OCID, database OCID and database name as JSON at runtime. Nothing is hardcoded, so the same script installs unchanged in any region or tenancy.
DBMS_CLOUD.SEND_REQUEST is the supported egress path on Autonomous AI Database, and it performs OCI request signing for you. No Signature v1 implementation, no date header, no body hashing — and no network ACL configuration needed to reach OCI service endpoints.
Issues are isolated per metric. A problem in one metric's SQL is logged and that metric is skipped; the rest of the batch still publishes.
Everything is logged. metric_push_log keeps the full request body and the full response for every push. When something looks wrong, the evidence is already there.
Every item here came out of a first-user run by someone who had not written the code.
| Change | Why |
|---|---|
c_version moved from the spec into the body |
It was in the spec, so a body-only patch left the package reporting a stale version — which is exactly what happened between v0.1 and v0.2 |
check_credential() added |
"Signature rejected" and "policy missing" were indistinguishable, so the wrong one got investigated first. 200 and 404 both pass; only 401 is an error, and it now prints a four-point checklist |
test_publish() recognises ORA-20401 |
It used to fall through to "check the credential exists", pointing at the wrong thing entirely |
list_metrics() resolves the resource group |
A bare NULL left you unable to tell which resource group a metric would publish under — the top cause of "my metric is missing" |
health_summary() shows spec and body compile times |
So a partial patch is visible directly instead of being inferred |
reset_to_shipped() added |
Testing leaves metrics enabled and grants behind; there was no way back to a clean state |
Every EXEC replaced with BEGIN ... END; / |
EXEC swallows an adjacent comment line and produces a baffling PLS-00103 |
| §9 split into credential check, then publish check | The two issues are different and were being conflated |
| §11 gained "reading a metric that has dimensions" | The console's dimension control filters rather than splits, and nothing says so |
| §15.3 gained alarm timing and state-checking | "It didn't fire" usually means "I looked in my inbox two minutes early" |
start_push() validates the interval |
The cadence was always configurable, but nothing stopped start_push(1) — which costs money and buys nothing, since OCI aggregates custom metrics at one minute. Bounds live in metric_config so you can move them knowingly |
| Identity-domain guidance throughout §5, §8, §9 | A user in a custom domain is the common modern case, and the OCID/key mismatch it invites was the single biggest time sink in testing |
DBMS_METRIC v0.3. Validated on Autonomous AI Database — Dedicated, Oracle AI Database 26ai (23.26.2.0.0), us-ashburn-1. This is a self-contained solution you deploy and own in your own tenancy, rather than a built-in capability; it is built to sit cleanly alongside whatever we ship natively in this space later.