LEHS - Log- and Exception-Handling System for ORACLE®
Audience
This Usage Guide is for Developers who decided to enhance their Applications with LEHS.
The're just 3 Prerequisites that must be done before using LEHS by the Application:
- LEHS must be installed on the Database by a DBA (see Install LEHS).
- The Log-Application, which is intended to be used by the Application, must be configured by a LEHS Admin
(see Configure LEHS)
- the Schema, where the Application runs (which will use LEHS), must be granted with read/write Permissions
to this Log-Application (see Configure LEHS)
Write Logs
The're existing 2 Ways of writing Logs to the Log-Application:
- free definable Log-Level
PKG_LEHS.log ('<Log Application>','<Log-Level>','<Log Message>');
- programmable Log-Level
PKG_LEHS.fatal ('<Log Application>','<Log Message>');
PKG_LEHS.error ('<Log Application>','<Log Message>');
PKG_LEHS.warn ('<Log Application>','<Log Message>');
PKG_LEHS.info ('<Log Application>','<Log Message>');
PKG_LEHS.debug ('<Log Application>','<Log Message>');
PKG_LEHS.trace ('<Log Application>','<Log Message>');
How to interprete the Line Number
LEHS works within stored Procedures (Procedures, Functions and Packages) as well as within Triggers and anonymous PL/SQL Blocks. Because LEHS uses the Function DBMS_UTILITY.format_call_stack
and DBMS_UTILITY.format_error_backtrace
as well as DBMS_UTILITY.format_error_stack
to identify the affected Line-Number of the log-raising Code you have to interpret this Line-Number correctly.
- Procedures, Functions and Packages
The Line will be interpreted as shown in USER_SOURCE
. Because the SQL-Developer shows the first line create or replace
on a separate line it causes to mistakes with the interpretation of the Line.
- Triggers
The Line will be interpreted where the Block of the Trigger begins (either with DECLARE
or with BEGIN
).
The Text CREATE OR REPLACE TRIGGER ...
will be ignored by DBMS_UTILITY.format_call_stack
,
DBMS_UTILITY.format_error_backtrace
or DBMS_UTILITY.format_error_stack
.
LEHS works with user named Exceptions. What the Hell are "user named Exceptions"?
Ok, you already know user defined Exceptions and user defined Error Messages:
DECLARE
e_not_null EXCEPTION; -- user defined Exception
PRAGMA EXCEPTION_INIT (e_not_null, -1401);
BEGIN
INSERT INTO some_table (
not_null_column
) VALUES (
NULL
);
EXCEPTION
WHEN e_not_null THEN
-- user defined Error Message
RAISE_APPLICATION_ERROR (-20000, 'Some userdefined Exception', FALSE);
END;
For the user defined Error Messages you can take a long range of Exception Codes (from -20999 to -20000). 1000 Error Codes! But on most Applications I have seen only the Code -20000 has been taken for any Exception that has been raised by this Application. That's not good for any Error hunting Session.
On the other Hand if you have a really big Application you might come into affliction with only 1000 Error Codes (that what I had troubles with). Imagine you define an Exception for just even every kind of Error your Application can raise. Also an Exception for every mandatory Parameter like this code:
CREATE OR REPLACE PROCEDURE test_it (
pi_param1 IN VARCHAR2
,pi_param2 IN DATE
) IS
BEGIN
IF pi_param1 IS NULL THEN
RAISE_APPLICATION_ERROR (-20000, 'Mandatory Parameter pi_param1 IS NULL');
END IF;
IF pi_param2 IS NULL THEN
RAISE_APPLICATION_ERROR (-20000, 'Mandatory Parameter pi_param2 IS NULL');
END IF;
...
END test_it;
And now imagine you have to write your Exception Messages in several Languages (*mega grin*).
Now I tell you what user named Exceptions are. This is a recommendation by Stephen Feuerstein. I really apologize you to book one of his great Courses about PL/SQL Programming. He's just a Genius on this area.
Just define a Name for every Exception you want to raise and join it with any free Exception Code when you raise it. This is the short Version. Just see some Example:
CREATE OR REPLACE PROCEDURE test_it (
pi_param1 IN VARCHAR2
,pi_param2 IN DATE
) IS
c_param1_is_null CONSTANT VARCHAR2(30) := 'PARAM1_IS_NULL';
c_param2_is_null CONSTANT VARCHAR2(30) := 'PARAM2_IS_NULL';
PROCEDURE raise_user_exception (
pi_error_name IN VARCHAR2
) IS
BEGIN
IF pi_error_name IN (c_param1_is_null, c_param2_is_null) THEN
RAISE_APPLICATION_ERROR (-20000, 'Mandatory Parameter pi_param1 IS NULL');
END IF;
END raise_user_exception;
BEGIN
IF pi_param1 IS NULL THEN
raise_user_exception (c_param1_is_null);
END IF;
IF pi_param2 IS NULL THEN
raise_user_exception (c_param2_is_null);
END IF;
END test_it;
Ok, you would say "no Advantage for me as Developer. Far from it I have to code more lines". Yeah, but imagine, if you set this raising Procedure outside of your Program (make it global) then you can take this code for dozens of Procedures just to check the mandatory Parameters.
So, this Procedure already exists within LEHS.
The next Point is the flexibility of such Exception Messages. If you can see above the Exception Message itself is for both always the same. Wouldn't it be fine if the Exception Message is for all these Checks (mandatory Parameter) is always the same except of the Name of the checked Parameter itself? Let's do it:
CREATE OR REPLACE PROCEDURE test_it (
pi_param1 IN VARCHAR2
,pi_param2 IN DATE
) IS
c_param1_is_null CONSTANT VARCHAR2(30) := 'PARAM1_IS_NULL';
c_param2_is_null CONSTANT VARCHAR2(30) := 'PARAM2_IS_NULL';
PROCEDURE raise_mandatory_check (
pi_error_name IN VARCHAR2
,pi_parameter IN VARCHAR2
) IS
v_message VARCHAR2;
BEGIN
v_message := 'Mandatory Parameter ' || pi_parameter || ' IS NULL';
IF pi_error_name IN (c_param1_is_null, c_param2_is_null) THEN
RAISE_APPLICATION_ERROR (-20000, v_message);
END IF;
END raise_user_exception;
BEGIN
IF pi_param1 IS NULL THEN
raise_mandatory_check (c_param1_is_null, 'pi_param1');
END IF;
IF pi_param2 IS NULL THEN
raise_mandatory_check (c_param2_is_null, 'pi_param2');
END IF;
END test_it;
We have defined a flexible user named Exception Message.
So, if we join these all Examples together the Code for LEHS looks like this:
CREATE OR REPLACE PROCEDURE test_it (
pi_param1 IN VARCHAR2
,pi_param2 IN DATE
) IS
c_log_application CONSTANT PKG_LEHS.pst_application_name := 'LOG_APPLICATION';
c_parameter_missing CONSTANT PKG_LEHS.pst_error_name := 'MISSING MANDATORY PARAMETER';
BEGIN
-- Initialize the Error Message
PKG_LEHS.add_message (
pi_application => c_log_application
,pi_error_name => c_parameter_missing
,pi_error_code => -20000
,pi_message => 'Mandatory Parameter %1 is NULL'
-- %1 will be replaced by the first Entry of the Error-Arguments List
);
PKG_LEHS.add_message (
pi_application => c_log_application
,pi_error_name => c_parameter_missing
,pi_error_code => -20000
,pi_message => 'Zwingender Parameter %1 ist NULL'
,pi_nls_language => 'GERMAN'
);
PKG_LEHS.debug (c_log_application, 'Check for mandatory Parameters');
-- Do the Checks (Assertions)
PKG_LEHS.assert (
pi_assertion => pi_param1 IS NOT NULL
,pi_application => c_log_application
,pi_error_name => c_parameter_missing
,pi_error_arguments => PKG_LEHS.pt_error_arguments('pi_param1')
);
PKG_LEHS.assert (
pi_assertion => pi_param2 IS NOT NULL
,pi_application => c_log_application
,pi_error_name => c_parameter_missing
,pi_error_arguments => PKG_LEHS.pt_error_arguments('pi_param2')
);
END test_it;
Ok, you might say "That's too much code! What's the advantage for the Developer?". Let' have a look on a short advantage List:
- The Initialization of the Error-Message can be done by a separate Script.
- You can define the Exception Message in several Languages.
The Default-Language is English (NLS-Language: AMERICAN
).
In accordance to the Parameter NLS_LANG
of the Client it would show the
Message with the correct Language.
- The raised Exception has been logged automatically. If you want to supress this Behaviour you can set the Parameter
PKG_LEHS.assert => pi_log
to FALSE
- You can define up to 9 Error-Arguments which can be replaced during Runtime (by using the varying Array
PKG_LEHS.pt_error_arguments
).
So you can reuse Exception Messages by setting variable Parts within the Exception Message (Mandatory Parameter %1 is missing
)
Assertions
Assertions can help the Developer to keep the Logic simple within his/her Application.
The first Argument is a BOOLEAN
-Condition. If this this Condition fails the Assertion will raise the Exception the Developer has specified. In fact PKG_LEHS.assert
is only a Combination of a IF
-Statement with the Procedure PKG_LEHS.handle_exception
or PKG_LEHS.handle_exception_and_log
.
By Default a raised Exception will also be logged with the Log-Level defined for the Log-Application. If you want to supress this Behaviour you can set the Parameter pi_log
to FALSE
. Then only the Exception will be raised without any logging. Except you define a Log-Topic, then the failed Assertion will always be logged (because it makes no sense to give a Log-Topic without any logging).
Handle non user defined Exceptions
So, now you know how to raise a user defined named Exception. But what happens if Oracle raises an Exception by itself (i.E. you make a SELECT INTO
and forget to handle the Exception NO_DATA_FOUND
). For this Oracle gives you a very powerful enhancement to catch all Exceptions:
DECLARE
c_log_application CONSTANT PKG_LEHS.pst_application_name := 'LOG_APPLICATION';
v_dummy BINARY_INTEGER;
BEGIN
-- the following SELECT will cause the Exception NO_DATA_FOUND
SELECT 1
INTO v_dummy
FROM dual
WHERE x = 'Y';
EXCEPTION
WHEN OTHERS THEN
-- Log the Exception
PKG_LEHS.log_exception (c_log_application);
RAISE;
END;
As you can see above LEHS has a simple Solution to log the raised Exception. Simply call PKG_LEHS.log_exception
and the raised Exception will be logged to the Line where the Exception has been raised. In the Example above the Line-Number is 7. This works also for Exceptions that are raised within Procedures, Functions and Triggers.
Any user defined named Exception, which will also be caught by WHEN OTHERS THEN
, will not be logged because a Developer can decide at the time where the Exception will be raised if he/she also wants to log or not. Except you set the Parameter pi_log_app_exception
to TRUE
. This is useful for some Situations where you want to handle a user defined named Exception by PKG_LEHS.log_excepiton
(see Topic Exception Messages).
Note: You have to re-raise the caught Exception by yourself. Otherwise the Exception will not be propagated to the calling PL/SQL-Block.
The general Recommendation is, that you end every Procedure/Function with this general Exception Handling Code:
CREATE OR REPLACE PROCEDURE prc_test IS
c_log_application CONSTANT PKG_LEHS.pst_application_name := 'LOG_APPLICATION';
BEGIN
-- do some stuff
NULL;
EXCEPTION
WHEN OTHERS THEN
PKG_LEHS.log_exception (c_log_application);
RAISE;
END prc_test;
When you put this Part of Code in nearby every Procedure/Function you automatically build your own Error-Stack with the Logging Routines.
Because LEHS is a Log-System for the whole Database many Applications can use LEHS at the same time. The Log-Entries for the Table would be ordered by a Log-ID (look at View LEHS_LOGS.log_id
, a Sequence for every Log-Entry). Ordering by this Log-ID shows you the Log-Entries in the correct Order.
But this Log-ID can't help if 2 Programs running at the same time to the same Application. Both Programs will fill the Log-Table but you have no chance to see, which Log-Entry has been created by which Program. For this Situation a Log-Transaction will help. Every Procedure creates a new Log-Transaction and the Log-Entries will be logged to this Log-Transaction ID.
DECLARE
c_log_application PKG_LEHS.pst_application_name := 'LOG_APPLICATION';
-- Define a Variable to get the Log-Transaction ID
v_log_transaction BINARY_INTEGER;
BEGIN
-- Start a new Log-Transaction
v_log_transaction := PKG_LEHS.start_log_transaction (c_log_application);
-- do some stuff
-- close the Log-Transaction
PKG_LEHS.end_log_transaction (c_log_application);
EXCEPTION
WHEN OTHERS THEN
PKG_LEHS.log_exception (c_log_application);
PKG_LEHS.end_log_transaction (c_log_application);
RAISE;
END;
A Log Transaction will be started by PKG_LEHS.start_log_transaction
. The returned Transaction ID will automatically be stored within the Log-Context (Memory where all the needed Variables are stored) and will be logged automatically. This Transaction ID stays at the Log-Context until another call of PKG_LEHS.start_log_transaction
has been done or the Log-Transaction has been closed by PKG_LEHS.end_log_transaction
. Also, if you call another internal Procedure this Transaction ID will be used.
Join to an open Log Transaction
By calling PKG_LEHS.start_log_transaction
the Transaction to the returned ID is in State OPEN
until PKG_LEHS.end_log_transaction
has been called. When you call another Procedure which also starts a new Log-Transaction you must join to the old Log-Transaction after returning to the calling Procedure.
CREATE OR REPLACE PACKAGE BODY pkg_test IS
pc_log_application CONSTANT PKG_LEHS.pst_application_name := 'LOG_APPLICATION';
PROCEDURE inner_proc IS
v_log_transaction BINARY_INTEGER;
BEGIN
v_log_transaction := PKG_LEHS.start_log_transaction (pc_log_application);
-- do some stuff
-- at the end
PKG_LEHS.end_log_transaction (pc_log_application);
EXCEPTION
WHEN OTHERS THEN
PKG_LEHS.log_exception (pc_log_application);
PKG_LEHS.end_log_transaction (pc_log_application);
RAISE;
END inner_proc;
PROCEDURE public_proc IS
v_log_transaction BINARY_INTEGER;
BEGIN
-- first start the Log Transaction
v_log_transaction := PKG_LEHS.start_log_transaction (pc_log_application);
-- now call the inner procedure
BEGIN
inner_proc;
PKG_LEHS.join_log_transaction (pc_log_application, v_log_transaction);
EXCEPTION
WHEN OTHERS THEN
PKG_LEHS.join_log_transaction (pc_log_application, v_log_transaction);
RAISE;
END;
-- now end the Log Transaction
PKG_LEHS.end_log_transaction;
EXCEPTION
WHEN OTHERS THEN
PKG_LEHS.log_exception (pc_log_application);
PKG_LEHS.end_log_transaction (pc_log_application);
RAISE;
END public_proc;
END pkg_test;
Maybe a Developer will now say "What the Hell? Why should I join to a Transaction ID? Why can't I give the Transaction ID by a Parameter to the Log-Call?". The Answer: Simplicity and Performance. It is less complex to focus on the Development than handle complex logging. A Log Transaction is optional, you can use it but it is not obligatory. And: most of the Programs will run within one Log Transaction (also the Sub-Programs). So it is better to save the Transaction ID within the Log Context than giving it all the Time to the Subprograms. Imagine: You always have to give the Log-Transaction as by Parameters to your Sub-Programs. What a mess.
Keep it simple was one of the main focus during the Development of LEHS. LEHS should support the Developer and not embarrass him/her. By the Way: this Conception makes the real Application Code more readable (and maintainable) than giving the Transaction ID by Parameter (or even by setting a Package Variable or something else).
So you see, some coding is needed to use a Log-Transaction. Uh, now we can different the Runtimes of our Programs (to the same Log-Application). But there is another feature you can use by the Log-Transactions.
CREATE OR REPLACE PROCEDURE prc_test (
pi_param1 IN VARCHAR2
,pi_param2 IN DATE DEFAULT NULL
,pi_param3 IN NUMBER DEFAULT 3.1415927
) IS
c_procname CONSTANT VARCHAR2(30) := 'PRC_TEST';
c_log_application CONSTANT PKG_LEHS.pst_application_name := 'LOG_APPLICATION';
v_log_transaction BINARY_INTEGER;
BEGIN
v_log_transaction := PKG_LEHS.start_log_transaction (c_log_application);
-- set some Keywords to the Log-Transaction
PKG_LEHS.add_transaction_key (c_log_application, v_log_transaction, 'PROCNAME', c_procname);
PKG_LEHS.add_transaction_key (c_log_application, v_log_transaction, 'START_TIME', SYSDATE);
PKG_LEHS.add_transaction_key (c_log_application, v_log_transaction, 'PI_PARAM1', pi_param1);
PKG_LEHS.add_transaction_key (c_log_application, v_log_transaction, 'PI_PARAM2', pi_param2);
PKG_LEHS.add_transaction_key (c_log_application, v_log_transaction, 'PI_PARAM3', pi_param3);
-- do the Rest of the Stuff
PKG_LEHS.end_log_transaction (c_log_application);
EXCEPTION
WHEN OTHERS THEN
PKG_LEHS.log_exception (c_log_application);
PKG_LEHS.end_log_transaction (c_log_application);
RAISE;
END prc_test;
You can register Keywords to your Log-Transaction. This feature is very powerful because it gives you the Possibility to search for the throughput of your Application. If your Application has been started by calling execute PRC_TEST ('first', TO_DATE('20.09.2009','DD.MM.RRRR'), 5.25);
you can now use the following:
SELECT transaction_id
FROM lehs_transactions TRX
WHERE TRX.application = 'LOG_APPLICATION'
AND TRX.key = 'PROCNAME' AND TRX.value = 'PRC_TEST'
AND EXISTS (SELECT 1
FROM lehs_transactions SUBTRX
WHERE SUBTRX.transaction_id = TRX.transaction_id
AND SUBTRX.key = 'PI_PARAM1' AND SUBTRX.value = 'first'
)
AND EXISTS (SELECT 1
FROM lehs_transactions SUBTRX
WHERE SUBTRX.transaction_id = TRX.transaction_id
AND SUBTRX.key = 'PI_PARAM2'
AND SUBTRX.datatype = 'DATE'
AND TO_DATE(SUBTRX.value) = TO_DATE('20.09.2009','DD.MM.RRRR')
)
AND EXISTS (SELECT 1
FROM lehs_transactions SUBTRX
WHERE SUBTRX.transaction_id = TRX.transaction_id
AND SUBTRX.key = 'PI_PARAM3'
AND SUBTRX.datatype = 'NUMERIC'
AND TO_NUMBER(SUBTRX.value) >= 3
);
The Example above shows you all Log Transactions to the Log-Application LOG_APPLICATION
with the following Characteristics:
- A Key named
PROCNAME
with Value PRC_TEST
- A Key named
PI_PARAM1
with the Value first
- A Key named
PI_PARAM2
with the Value 20.9.2009
(given as a Date)
- A Key named
PI_PARAM3
with a Value greater or equal to 3
(interpreted as a Number)
In the End this Query shows us the Transaction ID of the throughput of our Program.
A Transaction Key can have one of the following Datatypes:
VARCHAR2/CHAR
DATE
NUMBER
TIMESTAMP (up to 9 factional Digits)
On the View LEHS_TRANSACTIONS
all Transaction Keys with either the Datatype DATE
, NUMBER
or TIMESTAMP
will be transformed to the Character Interpretation by simply converting with TO_CHAR
without any formatting Pattern. Therefor it will use the Default of the Session so that you can reconvert this Value simply by calling either TO_DATE
, TO_NUMBER
or TO_TIMESTAMP
(also every Function without a formatting Description so you also use the Default of the Session).
To avoid falling into a Conversion Error you can also query the Parameter Datatype before converting the Value (i.E. AND SUBTRX.datatype = 'NUMERIC'
).
We already defined an Exception Message at the Topic Log Exceptions. Now we go deeper.
Exception Messages, which will be administered by LEHS can have the following Features:
- dynamic Exception Messages with up to 9 variable Parts (Error Parameter from %1 to %9)
- i18n Exception Messages (dependet by the Clients
NLS_LANG
)
- Can be defined either by the Application itself (Developer driven) or by an external Script (Admin driven)
- No Limitation of Exceptions to the Application (except the Error Codes itself from -20999 to -20000)
- Group dozens of Exception Messages to an Exception Message Group (with the same Error-Code)
- The existing Error-Stack can be saved or created newly (dependent by the Exception Message)
- No Length Limitation for the Exception Message (uses
CLOB
)
Looks like a Marketing Table, isn't it? Ok, look at all these Features within one simple Example:
DECLARE
c_application CONSTANT PKG_LEHS.pst_application_name := 'LOG_APPLICATION';
BEGIN
-- the following Error Definition would be delivered by a Script
-- so this is called the Admin driven Installation of an Exception Message
PKG_LEHS.add_message (
pi_application => c_application
,pi_error_name => 'UNANTICIPATED ERROR'
,pi_error_code => -20000
,pi_message => 'Unanticipated Error %1 has been raised. Reason: %2'
,pi_show_error_stack => TRUE
,pi_nls_language => 'AMERICAN'
);
PKG_LEHS.add_message (
pi_application => c_application
,pi_error_name => 'UNANTICIPATED ERROR'
,pi_error_code => -20000
,pi_message => 'Unerwarteter Fehler %1 wurde ausgelöst. Begründung: %2'
,pi_show_error_stack => TRUE
,pi_nls_language => 'GERMAN'
);
END;
CREATE OR REPLACE PACKAGE pkg_constant IS
pc_log_application CONSTANT PKG_LEHS.pst_application_name := 'LOG_APPLICATION';
pc_unanticipated_error CONSTANT PKG_LEHS.pst_error_name := 'UNANTICIPATED ERROR';
pc_missing_mandatory_parameter CONSTANT PKG_LEHS.pst_error_name := 'MANDATORY PARAMETER MISSING';
pc_errgrp_entity1 CONSTANT PKG_LEHS.pst_error_name := 'ERROR GROUP ENTITY 1'
pc_errgrp_entity2 CONSTANT PKG_LEHS.pst_error_name := 'ERROR GROUP ENTITY 2';
pe_errgrp EXCEPTION;
PRAGMA EXCEPTION_INIT (pe_errgrp, -20002);
END pkg_constant;
CREATE OR REPLACE PACKGE BODY pkg_constant IS
BEGIN
-- Initialization of the Exception Messages
-- the following Exception Messages will be defined by the Application itself
-- this is called Developer driven Installation.
PKG_LEHS.add_message (
pi_application => pc_log_application
,pi_error_name => pc_unanticipated_error
,pi_error_code => -20000
,pi_message => 'Unanticipated Error %1 has been raised. Reason: %2'
,pi_show_error_stack => TRUE
,pi_nls_language => 'AMERICAN'
);
PKG_LEHS.add_message (
pi_application => pc_log_application
,pi_error_name => pc_unanticipated_error
,pi_error_code => -20000
,pi_message => 'Unerwarteter Fehler %1 wurde ausgelöst. Begründung: %2'
,pi_show_error_stack => TRUE
,pi_nls_language => 'GERMAN'
);
PKG_LEHS.add_message (
pi_application => pc_log_application
,pi_error_name => pc_missing_mandatory_parameter
,pi_error_code => -20001
,pi_message => 'Mandatory Parameter %1 is missing or NULL'
,pi_show_error_stack => FALSE
,pi_nls_language => 'AMERICAN'
);
PKG_LEHS.add_message (
pi_application => pc_log_application
,pi_error_name => pc_missing_mandatory_parameter
,pi_error_code => -20001
,pi_message => 'Zwingender Parameter %1 wurde nicht angegeben oder ist NULL'
,pi_show_error_stack => FALSE
,pi_nls_language => 'GERMAN'
);
PKG_LEHS.add_message (
pi_application => pc_log_application
,pi_error_name => pc_errgrp_entity1
,pi_error_code => -20002
,pi_message => 'Error 1 of an Error Group: %1 (only English)'
,pi_show_error_stack => FALSE
,pi_nls_language => 'AMERICAN'
);
PKG_LEHS.add_message (
pi_application => pc_log_application
,pi_error_name => pc_errgrp_entity2
,pi_error_code => -20002
,pi_message => 'Error 2 of an Error Group: %1 (only English)'
,pi_show_error_stack => FALSE
,pi_nls_language => 'AMERICAN'
);
END pkg_constant;
CREATE OR REPLACE PROCEDURE use_lehs (
pi_entity1 IN BOOLEAN
) IS
BEGIN
IF NVL(pi_entity1, TRUE) THEN
PKG_LEHS.handle_exception (
pi_application => PKG_CONSTANT.pc_log_application
,pi_error_name => PKG_CONSTANT.pc_errgrp_entity1
,pi_error_arguments => PKG_LEHS.pt_error_arguments ('Argument')
);
ELSE
PKG_LEHS.handle_exception (
pi_application => PKG_CONSTANT.pc_log_application
,pi_error_name => PKG_CONSTANT.pc_errgrp_entity2
,pi_error_arguments => PKG_LEHS.pt_error_arguments ('Argument')
END IF;
END use_lehs;
CREATE OR REPLACE PROCEDURE use_lehs2 (
pi_entity1 IN BOOLEAN
) IS
BEGIN
use_lehs (pi_entity1);
EXCEPTION
WHEN PKG_CONSTANT.pe_errgrp THEN
IF PKG_LEHS.is_named_exception (PKG_CONSTANT.pc_log_application, PKG_CONSTANT.pc_errgrp_entity1, SQLCODE) THEN
-- When pc_errgrp_entity1 has been raised then only warn with a Log-Message but don't re-raise this Exception
PKG_LEHS.warn (PKG_CONSTANT.pc_log_application, SQLERRM(SQLCODE));
ELSE
PKG_LEHS.log_exception (PKG_CONSTANT.pc_log_application, TRUE);
RAISE;
END IF;
WHEN OTHERS THEN
PKG_LEHS.log_exception (PKG_CONSTANT.pc_log_application);
RAISE;
END use_lehs2;
Done. Much code, isn't it? But not much ado nothing. This is a Recommendation by myself how to make an Application code:
Define a global Package for any Exception Names and the Log-Application as Constants itself!
Also define any PL/SQL named Exceptions you like to handle within your Application!
Then you can use these Constants from nearby any Procedure within the same Schema. The Package Body of this global Package can be used to initialize the Exception Messages when the Package is loaded into the Memory. With this Method you can redefine your Exception Messages every time you like.
Log Topics are, as also the Log Transactions, another way to separate your Log-Information in Pieces. Otherwise than the Log-Transaction (which cuts into the Runtime Throughputs) are the Log-Topics an organizational Way to cut the Log-Information in Pieces.
The best Way to describe Log-Topics is to describe, how a normal Book works. If you plan to write a Book you start with the first Chapter. This Chapter can have several Subchapters (and also these can Subchapters). Then you write the next Chapter. Also with several Subchapters. Finally you might have a Structure like these:
1. Install LEHS
1.1 Download LEHS
1.2 Create a User for LEHS
1.3 Install LEHS
1.4 Do some public Stuff
2. Configure LEHS
2.1 Who am I?
2.2 Concepts
2.3 Log Application
2.4 Log Appender
2.4.1 Appender Parameter
2.4.2 Log Format
2.5 Log Levels
2.6 Programmable Log Levels
2.7 Log Privileges
2.8 Get the Metadata Information
Does this looks similar for you? Yes, this is called a Hierarchy. With Topics you can sort your Log-Entries by a Hierarchy. But this Chapter-List is just more than a normal Hierarchy. It's a Hierarchy with a strong self-managed Order within the same Hierarchy-Level. So we have a linked List within a Hierarchy-Level.
Defining a Topic is very easy. A Topic has always identified by the Name, which is case sensitive. A Top-Topic is a topic with no Parent-Topic. The Top of the Tree!
Within the same Hierarchy you can define the Position of the Topic either by the Position itself or by a Predecessor Topic.
- define by Position
PKG_LEHS.add_topic (
pi_application => 'LOG_APPLICATION'
,pi_topic => 'Top-Topic 3'
,pi_topic_parent => NULL
,pi_position => 3
)
- define by Predecessor
PKG_LEHS.add_topic (
pi_application => 'LOG_APPLICATION'
,pi_topic => 'Top-Topic 3'
,pi_topic_parent => NULL
,pi_topic_predecessor => 'Top-Topic 2'
)
"And why is this so powerful?" Let' have a look on another Example:
CREATE OR REPLACE PROCEDURE prc_test IS
c_log_application CONSTANT PKG_LEHS.pst_application_name := 'LOG_APPLICATION';
-- Define the Log-Topics for the technical Log
c_tl CONSTANT PKG_LEHS.pst_log_topic := 'technical Log';
c_tl_main CONSTANT PKG_LEHS.pst_log_topic := 'Main Routine';
c_tl_sub1 CONSTANT PKG_LEHS.pst_log_topic := 'Sub Routine 1';
c_tl_sub2 CONSTANT PKG_LEHS.pst_log_topic := 'Sub Routine 2';
-- Define the Log-Topics for the Business Log
c_bl CONSTANT PKG_LEHS.pst_log_topic := 'Business Log';
PROCEDURE sub1 IS
BEGIN
PKG_LEHS.debug (c_log_application, c_tl_sub1, 'Entry from Subroutine1');
EXCEPTION
WHEN OTHERS THEN
PKG_LEHS.log_exception (c_log_application, c_tl_sub1);
END sub1;
PROCEDURE sub2 IS
BEGIN
PKG_LEHS.debug (c_log_application, c_tl_sub1, 'Entry from Subroutine2');
EXCEPTION
WHEN OTHERS THEN
PKG_LEHS.log_exception (c_log_application, c_tl_sub2);
END sub2;
BEGIN
-- define the Log Hierarchy
PKG_LEHS.add_topic (c_log_application, c_bl, NULL, TO_CHAR(NULL));
PKG_LEHS.add_topic (c_log_application, c_tl, NULL, c_bl);
PKG_LEHS.add_topic (c_log_application, c_tl_main, c_tl, TO_CHAR(NULL));
PKG_LEHS.add_topic (c_log_application, c_tl_sub1, c_tl_main, TO_CHAR(NULL));
PKG_LEHS.add_topic (c_log_application, c_tl_sub2, c_tl_main, c_tl_sub1);
PKG_LEHS.info (c_log_application, c_tl_main, 'Start the Application');
PKG_LEHS.info (c_log_application, c_bl, 'Calculation has been started');
PKG_LEHS.debug (c_log_application, c_tl_main, 'Call sub1');
sub1;
PKG_LEHS.debug (c_log_application, c_tl_main, 'Call sub2');
sub2;
PKG_LEHS.info (c_log_application, c_tl_main, 'End the Application');
PKG_LEHS.info (c_log_application, c_bl, 'Calculation has been ended successfully');
EXCEPTION
WHEN OTHERS THEN
PKG_LEHS.log_exception (c_log_application, c_tl_main);
PKG_LEHS.error (c_log_application, c_bl, 'An Error has been occured. See technical Log for further Information');
RAISE;
END prc_test;
You defined two main Topics: a technical Log (with some Sub-Topics) and a Business Log. The Business Log is for your Clients. The technical Log is for the Developers when something has gone wrong. Your Clients would only know when the Application has been started and if the Application has been ended successfully or with an Error. But your Clients would not know which Error has been occured. So your Clients will query the Logs with the following Statement:
SELECT *
FROM lehs_logs
WHERE log_topic = 'Business Log'
ORDER BY log_id DESC
When something has gone wrong it's important that the Analyst (or the Developer) has some good Structure within the Logs to find the Error quickly. So you will go through your technical Log (with all the Sub-Topics) to get the technical Log-Information:
SELECT LOG.*
FROM lehs_topics TOP
RIGHT OUTER JOIN lehs_logs LOG
ON LOG.application = TOP.application
AND LOG.log_topic = TOP.topic_name
CONNECT BY PRIOR TOP.topic_name = TOP.parent_topic_name
AND TOP.application = 'LOG_APPLICATION'
START WITH TOP.topic_name = 'technical Log'
AND TOP.application = 'LOG_APPLICATION'
ORDER BY LOG.log_id DESC
This is the Reason why the Log Topics are really powerful. You have a Solution for both, technical Log and Business Log with one Tool and the Code is maintainable. When something has gone wrong you can get the needed Log with one SELECT
-Statement! And this can be packed into a View. I think that this has really big Power!
Note: If you ask me for the Reason of the horizontal Order (predecessing) then I will tell you: It's just for fun. Maybe one Day there's neccessary and then you can use it. Until then: Have fun with it. Because LEHS is open Source you can get some Parts as a Design Pattern. I hope so ;-)
If the LEHS Admin has enabled the Table-Appender, the written Logs (and Exceptions) will be available by the View
SELECT * FROM lehs_logs WHERE application = '<Log Application>' ORDER BY log_id DESC;
You can query Log Transactions with its Keywords by this SELECT-Statement:
SELECT * FROM lehs_transactions WHERE application = '<Log Application>';
The defined Topics will be available by the View
SELECT * FROM lehs_topics WHERE application = '<Log Application>';
The defined Log-Levels will be available by the View
SELECT * FROM lehs_levels WHERE application = '<Log Application>';
If you want to show the whole Topic-Tree (in the correct Order of the Sub-Topics) then you can get this Query:
SELECT *
FROM (SELECT level AS top_level
,application
,topic_name
,parent_topic_name
,topic_name_predecessor
FROM lehs_topics
CONNECT BY PRIOR topic_name = topic_name_predecessor
AND application = 'LEHS'
START WITH topic_name_predecessor IS NULL
AND application = 'LEHS')
CONNECT BY PRIOR topic_name = parent_topic_name
START WITH parent_topic_name IS NULL
ORDER SIBLINGS BY top_level
,topic_name
It is a legal Way to wrap the Procedures of LEHS so that your Developers doesn't know about the real Log-Engine in the Background. In fact, there existing several Reasons that you write a Wrapper:
- Hide the Log-Engine. By hiding the Log-Engine you can change the Log-Engine in the Background without changing the API for your Developers (i.E. use Log4PLSQL instead of LEHS)
- Simplify the Log-Calls for your Developer because they don't need to know about the Log-Application.
- You can provide specified Calls for recurring Calls (i.E. check Mandatory Parameters)
- You can implement a fine granular access control
- You can manage the Log-Levels separatly to a fine granular level (i.E. Package 1 logs with ERROR, Package 2 with DEBUG)
- You already have a Package, where you can store up your Exception Names (look above to Secion Exception Messages)
As you can see above ther're existing several good Reasons for a Wrapper Package. Normally the Package PKG_LEHS
gets the Position of the Caller from the Stack. The next Caller after PKG_LEHS
will be used to write the Log to the Appender. For a Wrapper this is not the truth. So, if you use your own Wrapper for LEHS then you have to call the Methods of the Package PKG_WRAPPED_LEHS
. All Methods of this Package will just ignore the next Level after PKG_LEHS
and get the Caller of the Wrapper. So your Wrapper-Class will just be ignored and the real Caller will be get.
A simple Example:
CREATE OR REPLACE PACKAGE LEHS_EXAMPLE.pkg_my_lehs_wrapper IS
pc_own_log_level VARCHAR2(30) := 'MY OWN LOG LEVEL';
PROCEDURE log (
pi_log_level IN VARCHAR2
,pi_message IN CLOB
);
PROCEDURE start_transaction (
pi_keyword1 IN VARCHAR2
,pi_keyword2 IN DATE
,pi_keyword3 IN NUMBER
);
PROCEDURE end_transaction;
PROCEDURE fatal (pi_message IN CLOB);
PROCEDURE error (pi_message IN CLOB);
PROCEDURE warn (pi_message IN CLOB);
PROCEDURE info (pi_message IN CLOB);
PROCEDURE debug (pi_message IN CLOB);
PROCEDURE trace (pi_message IN CLOB);
PROCEDURE mandatory_parameter_missing (pi_parameter_name IN VARCHAR2);
PROCEDURE another_user_defined_exception (
pi_text_parameter IN VARCHAR2
,pi_numeric_parameter IN NUMBER
,pi_date_parameter IN DATE
);
END LEHS_EXAMPLE.pkg_my_lehs_wrapper;
/
CREATE OR REPLACE PACKAGE BODY LEHS_EXAMPLE.pkg_my_lehs_wrapper IS
pc_app CONSTANT PKG_LEHS.pst_application_name := 'MY LOG APP';
pc_mandatory_parameter_missing CONSTANT PKG_LEHS.pst_error_name := 'MANDATORY PARAMETER MISSING';
pc_another_user_def_exception CONSTANT PKG_LEHS.pst_error_name := 'ANOTHER USER DEFINED EXCEPTION';
pv_transaction PKG_LEHS.pst_log_transaction := NULL;
PROCEDURE log (
pi_log_level IN VARCHAR2
,pi_message IN CLOB
) IS
BEGIN
PKG_WRAPPED_LEHS.log (
pi_application => pc_app
,pi_log_level => pi_log_level
,pi_message => pi_message
);
END log;
PROCEDURE start_transaction (
pi_keyword1 IN VARCHAR2
,pi_keyword2 IN DATE
,pi_keyword3 IN NUMBER
) IS
c_keyword1 CONSTANT VARCHAR2(30) := 'KEYWORD1';
c_keyword2 CONSTANT VARCHAR2(30) := 'KEYWORD2';
c_keyword3 CONSTANT VARCHAR2(30) := 'KEYWORD3';
BEGIN
IF pv_transaction IS NOT NULL THEN
RETURN;
END IF;
pv_transaction := PKG_LEHS.start_transaction (pc_app);
PKG_LEHS.add_transaction_keyword (pc_app, pv_transaction, c_keyword1, pi_keyword1);
PKG_LEHS.add_transaction_keyword (pc_app, pv_transaction, c_keyword2, pi_keyword2);
PKG_LEHS.add_transaction_keyword (pc_app, pv_transaction, c_keyword3, pi_keyword3);
END start_transaction;
PROCEDURE end_transaction IS
BEGIN
IF pv_transaction IS NULL THEN
RETURN;
END IF;
PKG_LEHS.end_log_transaction (pc_app);
END end_transaction;
PROCEDURE fatal (pi_message IN CLOB) IS
BEGIN
PKG_WRAPPED_LEHS.fatal (pc_app, pi_message);
END fatal;
PROCEDURE error (pi_message IN CLOB) IS
BEGIN
PKG_WRAPPED_LEHS.error (pc_app, pi_message);
END error;
PROCEDURE warn (pi_message IN CLOB) IS
BEGIN
PKG_WRAPPED_LEHS.warn (pc_app, pi_message);
END warn;
PROCEDURE info (pi_message IN CLOB) IS
BEGIN
PKG_WRAPPED_LEHS.info (pc_app, pi_message);
END info;
PROCEDURE debug (pi_message IN CLOB) IS
BEGIN
PKG_WRAPPED_LEHS.debug (pc_app, pi_message);
END debug;
PROCEDURE trace (pi_message IN CLOB) IS
BEGIN
PKG_WRAPPED_LEHS.trace (pc_app, pi_message);
END trace;
PROCEDURE mandatory_parameter_missing (pi_parameter_name IN VARCHAR2) IS
BEGIN
PKG_WRAPPED_LEHS.handle_exception_and_log (
pi_application => pc_app
,pi_error_name => pc_mandatory_parameter_missing
,pi_error_arguments => PKG_LEHS.pt_error_arguments (UPPER(pi_parameter_name))
);
END mandatory_parameter_missing;
PROCEDURE another_user_defined_exception (
pi_text_parameter IN VARCHAR2
,pi_numeric_parameter IN NUMBER
,pi_date_parameter IN DATE
) IS
BEGIN
PKG_WRAPPED_LEHS.handle_exception_and_log (
pi_application => pc_app
,pi_error_name => pc_another_user_def_exception
,pi_error_arguments => PKG_LEHS.pt_error_arguments (
pi_text_parameter
,TO_CHAR(pi_numeric_parameter)
,TO_CHAR(pi_date_parameter)
);
);
END another_user_defined_exception;
BEGIN
PKG_LEHS.add_message (
pi_application => pc_app
,pi_error_name => pc_mandatory_parameter_missing
,pi_error_code => -20000
,pi_message => 'Mandatory Parameter %1 is missing or NULL'
);
PKG_LEHS.add_message (
pi_application => pc_app
,pi_error_name => pc_another_user_def_exception
,pi_error_code => -20999
,pi_message => 'Another Exception with Text Parameter %1, numeric Parameter %2 and a Date Parameter %3'
);
END LEHS_EXAMPLE.pkg_my_lehs_wrapper;
/
As you can see in the Example (and of course in the API) not all Methods of the Package PKG_LEHS
would be overwritten in the Package PKG_WRAPPED_LEHS
.
The Reason is, that only these Methods are required to be overwritten where the Caller of PKG_LEHS
have to be ignored so that the Caller of this Wrapper-Package would be recognized as the real Caller.
One of the best Reasons by using a Wrapper-Package is, that the real Log-Application can be hidden. So the Developers can use your Package but they don't know anything about the Log-Engine in the Background (that could either be LEHS or Log4PLSQL or another else).