CREATE OR REPLACE PACKAGE BODY xxbbr AS
/******************************************************************************************************
* *
* Name : xxbbr.plb *
* Version : 1.1 *
* *
* Module Description *
* ================== *
* This package provides calls to standarise code logging. *
* *
* Incorporated in this, is an approach for handling unexpected errors in production systems by *
* acting like an airline Black Box Recorder where the complete detail is stored in a "black box" in *
* case an unexpected event occurs. *
* *
* Essentially, the Black Box Recorder is a respository for low level debug and log messages which *
* can then be printed out in the event of an unexpected error. Most of the code is here just to *
* force a consistant way to interact with the Black Box Recorder and to minimise the amount of code *
* which needs to be added to the actual program code (i.e. the code being monitored). *
* *
* The key enhancement is to have an output mechanism which provides all of the log detail in the *
* circumstances where the process ends in error. Usually this would require the process to be rerun *
* with the debug level changed but in a production environment this often isn't possible. Therefore,*
* this logging approach stores everything temporarily in a plsql table so that if a exception is *
* raised by the calling program, then a call can be placed in the exception handler to produce all *
* the detail if required. *
* *
* To get the optimum benefit from the Black Box Recorder feature, it is necessary to record every *
* parameter passed into each function / procedure and every return value. It is also suggested not *
* to comment out low level debug messages which are often created during the development cycle. *
* Instead, these messages should be assigned a high log_level value so that the messages only *
* appear in the log file when the reporting level is set to high or when an expected event occurs. *
* *
* The log_level parameter is used to record the log level of a particular message record which in *
* turn determines whether that message will be displayed in the log file depending on the level of *
* detail requested by the user querying the log file. If a log_level hasn't been set for a *
* particular message record, then the log_level is inherited from the parent. *
* *
* The hierarchy_num parameter is used to map out the code flow (e.g. one function calls another *
* function which calls another function). *
* *
* *
* *
* Change History *
* ============== *
* *
* *
* *
* Name Date Description *
* -------------------- --------- ----------------------------------------------------------------- *
* Fergal Grist 04-MAR-08 Module Created. *
* Fergal Grist 02-JUL-08 Added the current_log_level function to avoid ORA-01403 errors *
* under certain circumstances (e.g. when the users of this package *
* miss out an end_point call which can result in the code hierarchy *
* getting out of sequence). *
* Fergal Grist 04-JUL-08 Renamed the code_level variable to hierarchy_num because it was *
* easy to confuse it with log_level. Renamed the *
* reference_point_rectype/tab to hierarchy_rectype/tab. *
* Fergal Grist 06-AUG-08 Put a SUBSTR around the l_indent_str variable to prevent a *
* 'character string buffer too small' error. *
* Fergal Grist 15-AUG-08 Added the end_point_exception function. *
* Fergal Grist 11-JAN-09 Changed the names of some of the functions, added in functionality*
* to only store g_history_size records. *
* Fergal Grist 11-JAN-09 Added the reset_log procedure. *
* Fergal Grist 11-JAN-09 Changed the name of the 'start_point' procedure to 'begin_point' *
* (which is a bit more intuitive). *
* Fergal Grist 11-JAN-09 Added the module field name and changed the size of the message *
* field to 4000 (in line with FND Debug log). *
* Changed exception handling in the end_point_exception function. *
* *
* *
* *
* *
*****************************************************************************************************/
/*
|| The log collection below is the main data structure associated with this package.
|| It holds every log message, which code unit it relates to, where it comes in the processing
|| hierarchy, etc.
||
|| Only a certain number of records are maintained. This is to avoid a situation where so much detail
|| is collected that a memory error is generated. Once the size of the collection gets to the number
|| of records indicated by g_black_box_max_storage, the structure cycles back to position 1 and cycles
|| back to the maximum position; repeating this cycle as required.
*/
TYPE log_rectype IS RECORD (module VARCHAR2(4000),
-- this is for compatibility with the Oracle Apps FND Debug Log approach
reference_point VARCHAR2(255),
-- this is the current proc/function name or a some user defined ref
hierarchy_num NUMBER,
-- this is used for indenting the output to make it more readable
log_level NUMBER,
-- this allows a selective extraction from the collection if required
type VARCHAR2(10), -- BEGIN, END, PARAM, RETVAL, NOTE, ERROR
message VARCHAR2(4000),
timestamp DATE);
TYPE log_tabtype IS TABLE OF log_rectype INDEX BY BINARY_INTEGER;
g_log_tab log_tabtype; -- this holds each of the log lines
g_current_log_index NUMBER := 0;
g_black_box_max_storage NUMBER := 500;
g_black_box_lapped BOOLEAN := FALSE; -- to record whether the max record count has been exceeded
g_current_module VARCHAR2(4000) := 'xxbbr'; -- for compatibility with the FND Debug Log
/*
|| The hierarchy collection below holds a record of the procedure/function names and their default
|| log_levels. This allows us to deduce a log level for a log_msg within a procedure if no log level
|| has been specified i.e. a message within a procedure takes the log level associated with the start
|| of the procedure unless overrided.
*/
TYPE hierarchy_rectype IS RECORD (reference_point VARCHAR2(255), log_level NUMBER);
TYPE hierarchy_tabtype IS TABLE OF hierarchy_rectype INDEX BY BINARY_INTEGER;
g_hierarchy_tab hierarchy_tabtype;
g_current_hierarchy_num NUMBER := 0; -- this records the current level in the code hierarchy
/*
|| The following are wrapper functions around the g_hierarchy_tab plsql table.
||
|| They return the relevant values (e.g. current log level), if the record has been initialized.
|| Otherwise, a default value is returned.
||
|| This has been set up as a function to avoid having to check whether it exists before it is
|| referenced
|| ---------------------------------------------------------------------------------------------------
*/
FUNCTION current_log_level RETURN NUMBER IS
BEGIN
RETURN NVL(g_hierarchy_tab(g_current_hierarchy_num).log_level, 0);
EXCEPTION
WHEN OTHERS THEN RETURN 0;
END current_log_level;
FUNCTION current_reference_point RETURN VARCHAR2 IS
BEGIN
RETURN g_hierarchy_tab(g_current_hierarchy_num).reference_point;
EXCEPTION
WHEN OTHERS THEN RETURN 'NULL';
END current_reference_point;
/*
|| The following dumps a line of text to the log location.
|| -------------------------------------------------------
*/
PROCEDURE write_log_line(p_text VARCHAR2 := NULL) IS
BEGIN
-- Use the following code if running in an Oracle Applications (eBusiness Suite) environment
IF fnd_global.conc_request_id = -1 -- i.e. it's not via the concurrent manager
THEN dbms_output.put_line(p_text);
ELSE fnd_file.put_line(fnd_file.log, p_text);
END IF;
----
-- The following call links in with Oracle Applications standard debug logging.
-- In order to keep the hierarchy indenting, the call is made within the
-- write_log_line function which also means that only the messages deemed
-- relevant by the BBR logging level are passed onto the OraApps debug handling.
----
fnd_log.string(log_level => 1, module => SUBSTR(g_current_module,1,255), message => p_text);
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'bbr.write_log_line: ' || SQLERRM);
END write_log_line;
------------------------------------------------------------------------------------------
-- Overloaded version: --
-- The following takes the timestamp and the indent and formats the message accordingly --
------------------------------------------------------------------------------------------
PROCEDURE write_log_line(p_timestamp DATE,
p_message_type VARCHAR2,
p_hierarchy_num NUMBER,
p_text VARCHAR2) IS
l_indent NUMBER;
BEGIN
-- Determine by how much the line should be indented
IF p_message_type IN ('BEGIN', 'END')
THEN l_indent := p_hierarchy_num*2;
ELSE l_indent := p_hierarchy_num*2 + 2;
END IF;
-- Pass the formatted line onto the base write_log_line function to be sent to the output log file
write_log_line(TO_CHAR(p_timestamp, 'HH24:MI:SS') || SUBSTR(NVL(LPAD(' ', l_indent, ' '), ' '),1,250) || p_text);
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'bbr.write_log_line: ' || SQLERRM);
END write_log_line;
/*
|| The following procedure does 2 things:
||
|| 1) It records the log message in the Black Box Recorder.
|| 2) If the log level is less than the global debug level, it sends the message to the log output.
|| ------------------------------------------------------------------------------------------------
*/
PROCEDURE log_msg(p_msg VARCHAR2, p_level NUMBER := NULL, p_type VARCHAR2 := 'NOTE') IS
BEGIN
-- Get the sequence number for the next Black Box record.
-- Reset it if it is greater than the g_black_box_max_storage value.
IF g_current_log_index >= g_black_box_max_storage
THEN g_black_box_lapped := TRUE;
g_current_log_index := 1;
ELSE g_current_log_index := g_current_log_index + 1;
END IF;
g_log_tab(g_current_log_index).module := g_current_module;
g_log_tab(g_current_log_index).reference_point := current_reference_point;
g_log_tab(g_current_log_index).hierarchy_num := g_current_hierarchy_num;
g_log_tab(g_current_log_index).type := p_type;
g_log_tab(g_current_log_index).message := SUBSTR(p_msg,1,4000);
g_log_tab(g_current_log_index).timestamp := SYSDATE;
-- If the level parameter is null, then use the level from the current parent
IF p_level IS NULL
THEN g_log_tab(g_current_log_index).log_level := current_log_level;
ELSE g_log_tab(g_current_log_index).log_level := p_level;
END IF;
-- Finally, if the log_level is less than the global display level, then output the message
IF g_log_tab(g_current_log_index).log_level <= g_log_display_level
THEN write_log_line(p_timestamp => g_log_tab(g_current_log_index).timestamp,
p_message_type => g_log_tab(g_current_log_index).type,
p_hierarchy_num => g_log_tab(g_current_log_index).hierarchy_num,
p_text => g_log_tab(g_current_log_index).message);
END IF;
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'bbr.log_msg: ' || SQLERRM);
END log_msg;
/*
|| The following displays the contents of the Black Box Recorder.
|| --------------------------------------------------------------
*/
PROCEDURE display_log IS
i NUMBER;
l_indent NUMBER;
BEGIN
write_log_line; -- just a blank line
write_log_line('DUMPING OUT THE CONTENTS OF THE BLACK BOX RECORDER');
-- Depending on whether all of the slots in the log have been used up and
-- it's cycled back on itself or the number of records in the log are still
-- less than the max number (as indicated by g_black_box_max_storage), 2
-- different approaches are required to list the contents.
--
--
IF g_black_box_lapped
THEN -- In order to get the sequence of the records correct (i.e. earliest first),
-- list the records in the following order:
--
-- 1) From the record after the current record to the end
-- 2) From 1 to the current record
--
-- i.e. as the current record is the most record, this needs to be displayed last.
FOR i IN g_current_log_index + 1 .. g_black_box_max_storage
LOOP
write_log_line(p_timestamp => g_log_tab(i).timestamp,
p_message_type => g_log_tab(i).type,
p_hierarchy_num => g_log_tab(i).hierarchy_num,
p_text => g_log_tab(i).message);
END LOOP;
FOR i IN 1 .. g_current_log_index
LOOP
write_log_line(p_timestamp => g_log_tab(i).timestamp,
p_message_type => g_log_tab(i).type,
p_hierarchy_num => g_log_tab(i).hierarchy_num,
p_text => g_log_tab(i).message);
END LOOP;
ELSE FOR i IN 1 .. g_current_log_index
LOOP
write_log_line(p_timestamp => g_log_tab(i).timestamp,
p_message_type => g_log_tab(i).type,
p_hierarchy_num => g_log_tab(i).hierarchy_num,
p_text => g_log_tab(i).message);
END LOOP;
END IF;
write_log_line; -- just output a blank line
reset_log;
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'xxbbr.display_log: ' || SQLERRM);
END display_log;
/*
|| The following displays the contents of the Black Box Recorder.
|| --------------------------------------------------------------
*/
PROCEDURE reset_log IS
BEGIN
g_log_tab.DELETE;
g_hierarchy_tab.DELETE;
g_current_hierarchy_num := 0;
g_current_log_index := 0;
g_current_module := 'xxbbr';
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'xxbbr.reset_log: ' || SQLERRM);
END reset_log;
/*
|| The following records the start of a procedure, function
|| or just a component within a code block such as a loop.
|| --------------------------------------------------------
*/
PROCEDURE begin_point(p_reference VARCHAR2, p_level NUMBER := NULL) IS
l_level NUMBER;
BEGIN
-- If this is the top level start, we need to default in a value for the level parameter
-- if one hasn't already been entered. This is because the parent level gets inherited by
-- by the child processes if they're also null.
IF p_level IS NULL
THEN IF g_current_hierarchy_num = 0
THEN l_level := 0;
ELSE l_level := current_log_level;
END IF;
ELSE l_level := p_level;
END IF;
-- Increment the code level, record the new reference point/level and write details to the log table.
g_current_hierarchy_num := g_current_hierarchy_num + 1;
g_current_module := SUBSTR(g_current_module || '.' || p_reference,1,4000);
g_hierarchy_tab(g_current_hierarchy_num).reference_point := SUBSTR(p_reference,1,255);
g_hierarchy_tab(g_current_hierarchy_num).log_level := l_level;
log_msg(p_msg => 'Entering <' || p_reference || '>', p_level => l_level, p_type => 'BEGIN');
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'xxbbr.begin_point: ' || SQLERRM);
END begin_point;
/*
|| The following records the end of a procedure, function
|| or just a component within a code block such as a loop.
|| -------------------------------------------------------
*/
PROCEDURE end_point(p_reference VARCHAR2) IS
l_level NUMBER;
BEGIN
-- As an integrity check, make sure that the end_point reference matches the current reference point.
IF p_reference <> current_reference_point
THEN log_msg('WARNING: Exit point descrepancy - expecting <' || current_reference_point || '>' || ' instead of <' || p_reference || '>');
END IF;
log_msg(p_msg => 'Exiting <' || p_reference || '>', p_type => 'END');
IF g_current_hierarchy_num > 0 -- As we're at an 'end' point, we need to decrement the hierarchy_num
THEN g_current_hierarchy_num := g_current_hierarchy_num - 1;
END IF;
-- Update the g_current_module name to remove the reference from the end of it
g_current_module := SUBSTR(g_current_module, 1, INSTR(g_current_module, '.', -1)-1);
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'xxbbr.end_point: ' || SQLERRM);
END end_point;
/*
|| The following raises an error (but logs the error message first).
|| -----------------------------------------------------------------
*/
PROCEDURE raise_error(p_errmsg VARCHAR2) IS
BEGIN
log_msg('ERROR: ' || p_errmsg);
raise_application_error (-20001, p_errmsg);
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'xxbbr.raise_error: ' || SQLERRM);
END raise_error;
/*
|| The following groups together a couple of the function calls which should be called
|| by the exception handler. As it was easy to forget one, these have been group into
|| a single function for consistancy.
|| -----------------------------------------------------------------------------------
*/
PROCEDURE end_point_exception(p_reference VARCHAR2) IS
l_sqlerrm VARCHAR2(10000);
BEGIN
-- Tidy up the SQLERRM contents to avoid a lot of repetition and spurious data
l_sqlerrm := SUBSTR(REPLACE(REPLACE(REPLACE(SQLERRM, 'ORA-20000: '), 'ORA-20001: '), 'xxbbr.raise_error: '),1,10000);
log_msg(l_sqlerrm);
-- Although this was raised in the exception handler, we still need to highlight that the function is being exited
end_point(p_reference);
-- If we're at the top level and encounter an unhandle exception,
-- then we want to dump out the complete detail from the Black Box
IF g_current_hierarchy_num = 0
THEN display_log;
END IF;
-- Pass the exception up the chain
IF sqlerrm = 'ORA-01403: no data found'
THEN RAISE NO_DATA_FOUND;
ELSE raise_application_error(-20001, l_sqlerrm);
END IF;
END end_point_exception;
/*
|| The following displays the return value(s) from the functions or procedures.
|| ----------------------------------------------------------------------------
*/
PROCEDURE show_retval(p_return_value VARCHAR2) IS
BEGIN
log_msg('Returning: ' || p_return_value);
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'xxbbr.show_retval: ' || SQLERRM);
END show_retval;
------------------------
-- overloaded version --
------------------------
PROCEDURE show_retval(p_return_value BOOLEAN) IS
BEGIN
IF p_return_value = TRUE
THEN log_msg('Returning: TRUE');
ELSE log_msg('Returning: FALSE');
END IF;
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'xxbbr.show_retval: ' || SQLERRM);
END show_retval;
------------------------
-- overloaded version --
------------------------
PROCEDURE show_retval(p_return_name VARCHAR2, p_return_value VARCHAR2) IS
BEGIN
log_msg('Returning value (' || p_return_name || '): ' || p_return_value);
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'xxbbr.show_retval: ' || SQLERRM);
END show_retval;
/*
|| The following displays the parameters to a function or procedure.
|| -----------------------------------------------------------------
*/
PROCEDURE show_parameter(p_param_name VARCHAR2, p_param_value VARCHAR2) IS
BEGIN
log_msg('Parameter ' || p_param_name || ': ' || p_param_value);
EXCEPTION
WHEN OTHERS THEN raise_application_error (-20000, 'xxbbr.show_parameter: ' || SQLERRM);
END show_parameter;
END xxbbr;