Automating Oracle RMAN Backup Reports with PL/SQL Using the RMAN Catalog

The reason for this blog post is to provide an alternative way on how to review RMAN backups for Oracle Database fleet. It is obvious that there are many monitoring tools available across markets ranging from open source Xymon and up to Oracle Enterprise Manager Cloud Control or IBM Tivoli Monitoring. And no doubts there are many in-house created custom scripts or tools supporting the need.
Quick sidestep from the main topic—based on my previous experience, Xymon is a very useful tool. Although it is open source, I have used it for local and remote system monitoring, starting from a small set of servers and databases up to enterprise level. Highly recommended.
Case details
But let us return to the main topic—how to review RMAN backups of Oracle Databases compactly.
My use case is:
- 100+ Oracle Database instances running on different versions
- Fleet divided into production/test/development/training databases
- Some number of production databases have DR sites
- Point of interest is production instances and corresponding production DR sites
- Oracle Enterprise Manager is in use but there is a need for a lightweight tool that follow up on RMAN backup runs for production databases and their DRs
- The key point is that Oracle RMAN Catalog is in use and used as the main data source.
Disclaimer
The setup and code provided below can be modified according to your wishes and needs.
Evaluate, use, optimize, modify, finetune code according to configuration, performance or any other specifics of your environment.
Daily reports retrieving successful and unsuccessful RMAN backup runs
UTL_MAIL and SMTP configuration
Before deploying custom code, make sure the UTL_MAIL package is installed and the SMTP server related configuration applied. SMTP configuration can be specific to the environment where the described solution is applied. In the example provided, UTL_MAIL and SMTP configuration reside in the same database where Oracle RMAN Catalog.
SQL> !ls -l utlmail.sql
-rw-r--r-- 1 oracle oinstall 7543 Mar 7 2018 utlmail.sql
SQL> @utlmail.sql
Package created.
Synonym created.
SQL> !ls -l prvtmail.plb
-rw-r--r-- 1 oracle oinstall 6327 Apr 17 2019 prvtmail.plb
SQL> @prvtmail.plb
Package created.
Package body created.
Grant succeeded.
Package body created.
No errors.
SQL> desc UTL_MAIL
PROCEDURE SEND
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
SENDER VARCHAR2 IN
RECIPIENTS VARCHAR2 IN
CC VARCHAR2 IN DEFAULT
BCC VARCHAR2 IN DEFAULT
SUBJECT VARCHAR2 IN DEFAULT
MESSAGE VARCHAR2 IN DEFAULT
MIME_TYPE VARCHAR2 IN DEFAULT
PRIORITY BINARY_INTEGER IN DEFAULT
REPLYTO VARCHAR2 IN DEFAULT
PROCEDURE SEND_ATTACH_RAW
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
SENDER VARCHAR2 IN
RECIPIENTS VARCHAR2 IN
CC VARCHAR2 IN DEFAULT
BCC VARCHAR2 IN DEFAULT
SUBJECT VARCHAR2 IN DEFAULT
MESSAGE VARCHAR2 IN DEFAULT
MIME_TYPE VARCHAR2 IN DEFAULT
PRIORITY BINARY_INTEGER IN DEFAULT
ATTACHMENT RAW IN
ATT_INLINE BOOLEAN IN DEFAULT
ATT_MIME_TYPE VARCHAR2 IN DEFAULT
ATT_FILENAME VARCHAR2 IN DEFAULT
REPLYTO VARCHAR2 IN DEFAULT
PROCEDURE SEND_ATTACH_VARCHAR2
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
SENDER VARCHAR2 IN
RECIPIENTS VARCHAR2 IN
CC VARCHAR2 IN DEFAULT
BCC VARCHAR2 IN DEFAULT
SUBJECT VARCHAR2 IN DEFAULT
MESSAGE VARCHAR2 IN DEFAULT
MIME_TYPE VARCHAR2 IN DEFAULT
PRIORITY BINARY_INTEGER IN DEFAULT
ATTACHMENT VARCHAR2 IN
ATT_INLINE BOOLEAN IN DEFAULT
ATT_MIME_TYPE VARCHAR2 IN DEFAULT
ATT_FILENAME VARCHAR2 IN DEFAULT
REPLYTO VARCHAR2 IN DEFAULT
SQL> show parameter smtp_out_server
NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
smtp_out_server string
SQL> ALTER SYSTEM SET smtp_out_server='127.0.0.1:25' SCOPE=SPFILE;
System altered.
SQL> ALTER SYSTEM SET smtp_out_server='127.0.0.1:25';
System altered.
SQL> show parameter smtp_out_server
NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
smtp_out_server string 127.0.0.1:25
Code to generate daily RMAN backup reports
Code for RMAN backup report generation.
The provided example code resides in the same database with Oracle RMAN Catalog.
Ensure that the owner of the code has read permissions on underlying RMAN Catalog objects like:
RC_RMAN_BACKUP_JOB_DETAILS
RC_BACKUP_SET_DETAILS
RC_BACKUP_SET
The report is suitable for daily DBA operational needs. As per the use case, there are many production databases, and a number of these production databases have DR sites. It is more convenient to read through reports rather than check database by database manually. Below code can be modified according to individual needs or situation.
create
or replace PROCEDURE OWNER.BACKUP_REPORT_DAILY(v_report_type IN VARCHAR2) IS
v_attachment VARCHAR2(32767):= null;
v_repdate VARCHAR2(30):= null;
v_filename VARCHAR2(10):= null;
v_mail_status VARCHAR2(15):= null;
v_message_text VARCHAR2(200):= null;
CURSOR v_report_G IS
select
report_line
FROM
(
select
RPAD(db_name, 8, ' ')|| ' | ' || RPAD(db_unique_name, 15, ' ')|| ' | ' || RPAD(database_role, 7, ' ')|| ' | ' || RPAD(session_recid, 5, ' ')|| ' | ' || session_stamp || ' | ' || time_taken_display || ' | ' || RPAD(status, 24, ' ')|| ' | ' || input_type || ' | ' || start_time || ' | ' || end_time as report_line
from
(
select
J.DB_NAME,
s.db_unique_name,
s.database_role,
j.session_recid,
j.session_stamp,
j.time_taken_display,
j.status,
j.input_type,
to_char(
j.start_time, 'yyyy-mm-dd hh24:mi:ss'
) start_time,
to_char(
j.end_time, 'yyyy-mm-dd hh24:mi:ss'
) end_time
from
RMAN.RC_RMAN_BACKUP_JOB_DETAILS j
left outer join (
select
s.db_key,
s.site_key,
d.session_recid,
d.session_stamp,
sum(
case when d.controlfile_included = 'BACKUP' then d.pieces else 0 end
) CF,
sum(
case when d.controlfile_included = 'NO'
and d.backup_type || d.incremental_level = 'D' then d.pieces else 0 end
) DF,
sum(
case when d.backup_type || d.incremental_level = 'D0' then d.pieces else 0 end
) I0,
sum(
case when d.backup_type || d.incremental_level = 'I1' then d.pieces else 0 end
) I1,
sum(
case when d.backup_type = 'L' then d.pieces else 0 end
) L
from
RMAN.RC_BACKUP_SET_DETAILS d
join RMAN.RC_BACKUP_SET s on s.set_stamp = d.set_stamp
and s.set_count = d.set_count
where
s.input_file_scan_only = 'NO'
group by
d.session_recid,
d.session_stamp,
s.db_key,
s.site_key
) x on x.session_recid = j.session_recid
and x.session_stamp = j.session_stamp,
rman.rc_site s
where
(
j.start_time > TRUNC(SYSDATE)-1
AND j.start_time < TRUNC(SYSDATE)
)
and s.db_key = x.db_key
and s.site_key = x.site_key
order by
j.start_time desc
)
where
status NOT IN (
'FAILED', 'COMPLETED WITH ERRORS',
'RUNNING WITH ERRORS'
)
);
CURSOR v_report_B IS
select
report_line
FROM
(
select
RPAD(db_name, 8, ' ')|| ' | ' || RPAD(db_unique_name, 15, ' ')|| ' | ' || RPAD(database_role, 7, ' ')|| ' | ' || RPAD(session_recid, 5, ' ')|| ' | ' || session_stamp || ' | ' || time_taken_display || ' | ' || RPAD(status, 24, ' ')|| ' | ' || input_type || ' | ' || start_time || ' | ' || end_time as report_line
from
(
select
J.DB_NAME,
s.db_unique_name,
s.database_role,
j.session_recid,
j.session_stamp,
j.time_taken_display,
j.status,
j.input_type,
to_char(
j.start_time, 'yyyy-mm-dd hh24:mi:ss'
) start_time,
to_char(
j.end_time, 'yyyy-mm-dd hh24:mi:ss'
) end_time
from
RMAN.RC_RMAN_BACKUP_JOB_DETAILS j
left outer join (
select
s.db_key,
s.site_key,
d.session_recid,
d.session_stamp,
sum(
case when d.controlfile_included = 'BACKUP' then d.pieces else 0 end
) CF,
sum(
case when d.controlfile_included = 'NO'
and d.backup_type || d.incremental_level = 'D' then d.pieces else 0 end
) DF,
sum(
case when d.backup_type || d.incremental_level = 'D0' then d.pieces else 0 end
) I0,
sum(
case when d.backup_type || d.incremental_level = 'I1' then d.pieces else 0 end
) I1,
sum(
case when d.backup_type = 'L' then d.pieces else 0 end
) L
from
RMAN.RC_BACKUP_SET_DETAILS d
join RMAN.RC_BACKUP_SET s on s.set_stamp = d.set_stamp
and s.set_count = d.set_count
where
s.input_file_scan_only = 'NO'
group by
d.session_recid,
d.session_stamp,
s.db_key,
s.site_key
) x on x.session_recid = j.session_recid
and x.session_stamp = j.session_stamp,
rman.rc_site s
where
(
j.start_time > TRUNC(SYSDATE)-1
AND j.start_time < TRUNC(SYSDATE)
)
and s.db_key = x.db_key
and s.site_key = x.site_key
order by
j.start_time desc
)
where
status IN (
'FAILED', 'COMPLETED WITH ERRORS',
'RUNNING WITH ERRORS'
)
);
BEGIN
select
TO_CHAR(
SYSDATE, 'yyyy-mm-dd hh24:mi:ss'
) INTO v_repdate
from
dual;
select
TO_CHAR(SYSDATE, 'yyyy-mm-dd') INTO v_filename
from
dual;
IF v_report_type = 'G' THEN FOR V_REC IN V_REPORT_G LOOP v_attachment := v_attachment || V_REC.report_line || CHR(10);
END LOOP;
v_mail_status := 'SUCCESSFUL';
v_message_text := 'Review attachment reporting details of ' || v_mail_status || ' backups.';
END IF;
IF v_report_type = 'B' THEN FOR V_REC IN V_REPORT_B LOOP v_attachment := v_attachment || V_REC.report_line || CHR(10);
END LOOP;
v_mail_status := 'FAILED';
v_message_text := 'Review attachment report of ' || v_mail_status || ' backups. If there is no attachment, all backups are SUCCESSFUL.';
END IF;
UTL_MAIL.SEND_ATTACH_VARCHAR2(
sender => 'BACKUP-REPORT@domain.com',
recipients => 'DBA1@domain.com,DBA2@domain.com,USER1@domain.com,DistributionList1@domain.com',
cc => '',
bcc => '',
subject => 'Oracle ' || v_mail_status || ' backup report: ' || v_repdate,
message => v_message_text,
mime_type => 'text/plain; charset=us-ascii',
priority => null,
attachment => TO_CHAR(v_attachment),
att_inline => TRUE,
att_mime_type => 'text/plain; charset=us-ascii',
att_filename => v_filename || '-report.txt',
replyto => ''
);
END BACKUP_REPORT_DAILY;
Report output example
Below you will find an example of report output. The output is delivered as a text file attachment to an email. As per the below scheduling configuration, there will be two separate emails sent. One email will contain the successful backup runs and the second will contain failed runs. If there are no failed runs, email for failed backups arrives empty without attachment.
DB01 | DB01 | PRIMARY | 28039 | 1193695029 | 00:01:04 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 21:57:31 | 2025-02-21 21:58:35
DB02 | DB02_DR | STANDBY | 19929 | 1193694956 | 00:24:48 | COMPLETED WITH WARNINGS | DB FULL | 2025-02-21 21:56:17 | 2025-02-21 22:21:05
DB03 | DB03 | PRIMARY | 17086 | 1193694603 | 00:06:21 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 21:50:28 | 2025-02-21 21:56:49
19C | 19C_DR | STANDBY | 20214 | 1193694072 | 00:12:03 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 21:41:41 | 2025-02-21 21:53:44
PROD | PROD_DR | STANDBY | 15314 | 1193693841 | 00:01:58 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 21:37:48 | 2025-02-21 21:39:46
DATA | DATA | PRIMARY | 22222 | 1193691900 | 00:42:52 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 21:05:34 | 2025-02-21 21:48:26
DEV | DEV_DR | STANDBY | 89343 | 1193691602 | 00:08:22 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 21:01:17 | 2025-02-21 21:09:39
EBS | EBS_DR | STANDBY | 11360 | 1193691610 | 00:36:16 | COMPLETED | DB INCR | 2025-02-21 21:00:32 | 2025-02-21 21:36:48
TEST | TEST | PRIMARY | 72522 | 1193691609 | 00:03:59 | COMPLETED WITH WARNINGS | DB FULL | 2025-02-21 21:00:32 | 2025-02-21 21:04:31
QADB | QADB | PRIMARY | 8773 | 1193691602 | 00:02:21 | COMPLETED | DB INCR | 2025-02-21 21:00:11 | 2025-02-21 21:02:32
SANDBOX | SANDBOX | PRIMARY | 3141 | 1193684692 | 00:00:25 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 19:04:58 | 2025-02-21 19:05:23
DEV21C | DEV21C | PRIMARY | 4575 | 1193684414 | 00:04:29 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 19:00:19 | 2025-02-21 19:04:48
PRODCOPY | PRODCOPY | PRIMARY | 4600 | 1193684349 | 00:00:47 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 18:59:17 | 2025-02-21 19:00:04
11GDB | 11GDB | PRIMARY | 4334 | 1193684204 | 00:02:14 | COMPLETED WITH WARNINGS | DB INCR | 2025-02-21 18:56:50 | 2025-02-21 18:59:04
SANDBOX | SANDBOX | PRIMARY | 13079 | 1193682736 | 00:24:16 | COMPLETED | DB FULL | 2025-02-21 18:32:24 | 2025-02-21 18:56:40
DB04 | DB04 | PRIMARY | 4256 | 1193682708 | 00:00:20 | COMPLETED | DB INCR | 2025-02-21 18:31:53 | 2025-02-21 18:32:13
DB100 | DB100 | PRIMARY | 15201 | 1193681211 | 00:24:39 | COMPLETED | DB FULL | 2025-02-21 18:06:59 | 2025-02-21 18:31:38
Limitations
I received a reasonable question regarding the report output format: is it possible to create HTML output?
The answer is yes—and no.
It solely depends on the amount of data that is reported. Let me explain in detail: the package UTL_MAIL has its limitations; the attachment size cannot exceed 32KB. Therefore, as HTML output considers lots of tagging characters, it can significantly increase the size of an attachment according to my tests. As a result, plain text output is more suitable. Newer versions of databases may have other limitations (my test case is built on version 19c.)
In addition, report output details can be modified as per individual needs. Therefore, in the case that the database list is short, HTML formatting may work well.
Scheduler setup for daily RMAN backup reports
The scheduler setup is documented below. Create a schedule according to your desired setup. Consider specifying attributes like job_name, start_date, repeat_interval, comments as per your own need.
Scheduler job for reporting failed backups for previous day
BEGIN
DBMS_SCHEDULER.CREATE_JOB (
job_name => '"OWNER"."FAILED_RMAN_RUNS"',
job_type => 'PLSQL_BLOCK',
job_action => 'BEGIN OWNER.BACKUP_REPORT_DAILY(''B''); END;',
number_of_arguments => 0,
start_date => TO_TIMESTAMP_TZ('2024-02-07 00:05:00.000000000 -06:00','YYYY-MM-DD HH24:MI:SS.FF TZR'),
repeat_interval => 'freq=daily;byhour=00;byminute=30;bysecond=00',
end_date => NULL,
enabled => FALSE,
auto_drop => FALSE,
comments => 'Oracle database RMAN backup report - FAILED.');
DBMS_SCHEDULER.SET_ATTRIBUTE(
name => '"OWNER"." FAILED_RMAN_RUNS "',
attribute => 'logging_level', value => DBMS_SCHEDULER.LOGGING_OFF);
DBMS_SCHEDULER.enable(
name => '"OWNER"." FAILED_RMAN_RUNS "');
END;
Scheduler job for reporting successful backups for previous day
BEGIN
DBMS_SCHEDULER.CREATE_JOB (
job_name => '"OWNER"."GOOD_RMAN_RUNS"',
job_type => 'PLSQL_BLOCK',
job_action => 'BEGIN OWNER.BACKUP_REPORT_DAILY(''G''); END;',
number_of_arguments => 0,
start_date => TO_TIMESTAMP_TZ('2024-02-07 01:00:00.000000000 -06:00','YYYY-MM-DD HH24:MI:SS.FF TZR'),
repeat_interval => 'freq=daily;byhour=02;byminute=00;bysecond=00',
end_date => NULL,
enabled => FALSE,
auto_drop => FALSE,
comments => 'Oracle database RMAN backup report - SUCCESSFUL.');
DBMS_SCHEDULER.SET_ATTRIBUTE(
name => '"OWNER"."GOOD_RMAN_RUNS"',
attribute => 'logging_level', value => DBMS_SCHEDULER.LOGGING_OFF);
DBMS_SCHEDULER.enable(
name => '"OWNER"."GOOD_RMAN_RUNS"');
END;
Conclusion
I hope the above material was useful and will help DBAs optimize their daily routine work.
The given solution can be used not only for backup reporting, but also be adopted for many different needs and use cases. In my case, it was essential to have a quick solution in place for better visibility over the database backups and to have a lightweight footprint of backup runs history.
Some of my additional concerns were:
- NOT TO spend a lot of time and effort on reorganizing Oracle Enterprise Manager setup and configuration that may result in a long initiative considering how big server fleet is.
- NOT TO introduce new instances of monitoring tools or software with underlying physical resources.
- Utilize existing resources and implement lightweight solutions to be able oversee the backup sector and have necessary input data for KPI reporting.
In the future, I’m planning on sharing some additional materials with a few tips on how to optimize daily operational work in database backup management.
Share this
You May Also Like
These Related Stories

No Comments Yet
Let us know what you think