Skip to content

Latest commit

 

History

History
197 lines (150 loc) · 13.7 KB

output_modules.md

File metadata and controls

197 lines (150 loc) · 13.7 KB

Output modules

Output modules form the interface between the messages and any output channel. Their task is to receive the message, format it accordingly and write it to the desired location. In order to be able to work with any number of output modules without changing the core implementation of PIT, a concept of an abstract object type named PIT_MODULE and inheritance is used. Beeing an abstract object means that you can't instantiate it. But it is not final, so it's allowed to create a new object that inherits from PIT_MODULE. By this, two goals are achieved:

  1. PIT is able to recognize new modules automatically as they can be found in view USER_TYPES. They share the same attribute PIT_MODULE as their supertype.

  2. As all output module inherit from PIT_MODULE, they all have the methods defined at PIT_MODULE. This way, all output modules automatically implement all methods required by PIT. But only those methods you require are overwritten by you to make sure your output module works as expected. All other methods fall back to a default behaviour which is an empty stub: They simply do nothing. This is called an interface, a language construct that is not possible in PL/SQL without using objects.

PIT_MODULE implements the following methods:

  • LOG_VALIDATION with MESSAGE_TYPE parameter, used to handle user input validation without which normally are not persisted but only shown on the respective GUI.
  • LOG_EXCEPTION with MESSAGE_TYPE parameter, used to handle fatal, severe, error, warn, info or debug messages if an exception occurred.
  • PANIC with MESSAGE_TYPE parameter, used as a final resort. Intended use is to catch unforeseen, unrecoverable errors such as in the when others branch of an exception handler.
  • LOG_STATE with MSG_PARAMS parameter, used to log state of arbitrary key-value-pairs. Can be used to trace the actual status of any number of variables within a method.
  • NOTIFY, used to signal changes. Normal use would be to report progress of long running actions etc. The implementation may require a websocket server to send the messages to.
  • TWEET with a message type parameter. It uses a generic message with only plain text and is meant to output code debug information quick and dirty. Not meant to be part of the final code.
  • PRINT, used to pass messages without any severity etc. Useful to print output to a GUI or similar.
  • ENTER, used to log entering a method, including parameters etc.
  • LEAVE, used to log leaving a method, including parameters, timing information etc.
  • PURGE_LOG, used to clean the log based on severity and/or date settings
  • CONTEXT_CHANGED, used to signal that the log settings were changed

As all of them are implemented as stubs doing nothing, you're free to overload only those methods you require in your inherited object. Each output module may have any number of parameters and supporting objects like tables, but at the bare minimum it should define a parameter setting the fire threshold of the output module. By defining this threshold, message of a severity less than this threshold will be ignored.

All output modules require a parameterless constructor method. This method should check whether the module is available for use. A mail output module should check whether the mail server is available and working, a file output module may check whether the directory is in place and an APEX module will check whether there is a living APEX environment.

Predefined output modules

PIT ships with some predefined output modules. They cover a range of widely used output channels, such as the console, a trace file or a log table. But they are by no means »special« and you're free to change them without affecting any functionality of PIT. Use them as a blue print for your own output modules.

PIT_CONSOLE

The most basic output module probably is PIT_CONSOLE. It implements almost any method of the abstract supertype with the exception of the purge method which does not seem to make too much sense when writing to a console window.

Only task of this module is to format the messages, call stack objects and message parameter objects to strings and print them to the console.

In a project, I created an alternative solutioin that writes to a package to overcome the length limitations of dbms_output. This is according to Tom Kyte's blog, Option 3. This is just an example on how flexible the idea of PIT is.

PIT_TABLE

This module writes messages and call stack information to log tables. A total of three tables are created for this output module: PIT_TABLE_LOG, PIT_TABLE_CALL_STACK and PIT_TABLE_CALL_PARAMS. Recall that any message contains its own unique ID generated by PIT. These IDs are taken from a sequence that is ordered, meaning that it will create IDs in the correct order even in RAC environments. Therefore this ID is used as the primary key for the log tables. It can also be used to order your log entries in time correctly.

PIT_TABLE only implements the LOG, ENTER, LEAVE and PURGE methods.

PIT_FILE

This module writes messages and call stack information to a trace file. In order to use this output module, you need to create a directory object for it where it may write the trace files to. In essence, the duty of this package is comparable to the PIT_CONSOLE package in that it has to cast the messages etc. to string.

PIT_MAIL

This module sends mail messages containing the messages received. As this output module has to create message texts with salutations etc. it depends on helper package UTL_TEXT see here. The dependency is mild though, so if you don't want to utilize this package it will not take too long to replace it with your own utility.

This module create a table named PIT_MAIL_QUEUE. It implements the LOG method only and differs between fatal and »normal« errors in that it sends a mail directly if a fatal error occurs whereas it stores message of level pit.LEVEL_ERROR at the mail queue. There is a functionality in the accompanying package that allows to send a report with all error messages once a day, controlled by a scheduler job.

PIT_APEX

This module integrates PIT into an APEX environment. Main task of the package is to convert messages to APEX error messages and integrate them into the APEX error or debug stack. Together with this module, a special session adapter called PIT_APEX_ADAPTER is created that replaces the normally used PIT_DEFAULT_ADAPTER. This is because in APEX the session information may not be taken from the database session but the APEX session identified by the session id contained in USERENV. Plus, this adapter checks whether APEX is set to debug mode. If so, the adatper recommends a context switch to a context named CONTEXT_APEX that switches debugging on and chooses PIT_APEX as the only output module.

This module implements the LOG, PRINT, NOTIFY and ENTER/LEAVE methods. Method NOTIFY is experimental and not fully implemented yet, as the plan is to deliver messages to APEX via a WebSocket server.

Setting APEX to debug does emit any ENTER/LEAVE call stack entries. This is bound to LEVEL5 and upward. So to see those entries, set the debug level accordingly.

The constructor will check whether APEX is usable, i.e. whether there is a living APEX session context. If not, the module will be installed, but not available.

Writing your own output module

Writing a new output module is easy. There are two steps to take: Create a new object under PIT_MODULE and implement overwrites of the methods you need within that new module.

Create an object under PIT_MODULE

In order to create your own output module, you start by examining PIT_MODULE. Make yourself familiar with the methods available. Here is a short overview:

  • context_changed This method is called if a context switch was detected. It is rarely used, but I implement it in a test output module to document that changing the context does call those methods.
  • log This method is the core logging methods for all errors, warnings, debug messages and so on. If called, it passes the actual instance of MESSAGE_TYPE to allow you to work with the message.
  • log This overloaded method accepts a parameter of type MSG_PARAMS and is used to log state information. MSG_PARAMS is a list of MSG_PARAM instances, each containing a name/value pair.
  • print This method is called if a message is passed to the UI.
  • notify This method is used to pass status messages and notifications. For web application output modules, it was planned to make use of WebSocketrs to pass small information chunks to the browser. The PIT_APEX module does not implement this feature yet though.
  • enter and leave These methods trace entering and leaving a method. They pass an instance of CALL_STACK_TYPE with further information on the method.
  • purge Use this to purge to log. Two parameters, a date up to which all log entries are to be purged and a severity filter allow for fine grained purging.

To create your own output module, you only overwrite the methods you need. Examine this example to understand how this is done:

create or replace type pit_file under pit_module(
  overriding member procedure log_exception(
    self in out nocopy pit_file,
    p_message in message_type),
  overriding member procedure purge_log(
    self in out nocopy pit_file,
    p_purge_date in date,
    p_severity_greater_equal in integer default null),
  overriding member procedure enter(
    self in out nocopy pit_file,
    p_call_stack in call_stack_type),
  overriding member procedure leave(
    self in out nocopy pit_file,
    p_call_stack in call_stack_type),
  constructor function pit_file(
    self in out nocopy pit_file)
    return self as result)
  final instantiable;
/

Simply replace the module name with your name and make sure that the constructor method returns the correct object type.

Implementing your own functionality

My approach to implement functionality for output modules is to write wrapper methods that simply call a package with the respective functionality. I do this because the programming features of object bodies are not as strong as PL/SQL package features. No private methods or attributes are allowed and other restrictions apply. Therefore, delegating the implementation to a separate package does make sense.

So this is the implementation of the PIT_FILE output module:

create or replace type body pit_file
as
  overriding member procedure log_exception(
    self in out nocopy pit_file,
    p_message in message_type)
  as
  begin
    if p_message.severity <= fire_threshold then
      pit_file_pkg.log(self, p_message);
    end if;
  end log_exception;

  overriding member procedure purge_log(
    self in out nocopy pit_file,
    p_purge_date in date,
    p_severity_greater_equal in integer default null)
  as
  begin
    pit_file_pkg.purge;
  end purge_log;

  overriding member procedure enter(
    self in out nocopy pit_file,
    p_call_stack in call_stack_type)
  as
  begin
    pit_file_pkg.enter(self, p_call_stack);
  end enter;

  overriding member procedure leave(
    self in out nocopy pit_file,
    p_call_stack in call_stack_type)
  as
  begin
    pit_file_pkg.leave(self, p_call_stack);
  end leave;

  constructor function pit_file (
    self in out nocopy pit_file)
    return self as result
  as
  begin
    pit_file_pkg.initialize_module(self);
    return;
  end pit_file;
end;
/

Implementing package PIT_FILE is not special by any means. It's a simple PL/SQL package working with the messages. There's only one thing to obey: PIT takes care of the transaction context, as it spans an autonomous transaction for each call. So you don't have to and shouldn't include any transaction control within this output module. Write to a table if you like and PIT will take care of the commit for you.

I delegated the constructor functionality to PIT_FILE_PKG as well. It may be interesting to see the implementation of this method:

  procedure initialize_module(
    self in out pit_file)
  as
  begin
    g_dir := param.get_string(C_OUT_DIRECTORY, C_PARAM_GROUP);
    g_filename := param.get_string(C_FILE_NAME, C_PARAM_GROUP);
    -- Test
    open_file(C_WRITE_APPEND);
    self.fire_threshold := param.get_integer(C_FIRE_THRESHOLD, C_PARAM_GROUP);
    self.status := msg.PIT_MODULE_INSTANTIATED;
  exception
    when others then
      -- Do NOT throw any exceptions during initalization phase!
      self.status := msg.PIT_FAIL_MODULE_INIT;
      self.stack := pit_util.get_error_stack;
  end initialize_module;

As the bare minimum, I define a fire threshold and set my status to msg.PIT_MODULE_INSTANTIATED. Here, I also try to open a file at the directory defined at a parameter for that output module. If this fails, I catch this error and mark the instance as UNUSABLE, using message msg.PIT_FAIL_MODULE_INIT. Please keep in mind that it is not possible to pass any arguments to the constructor method. It is required that there is a constructor without parameters in order to use it from PIT.

Parameters

Any output module should have at least one parameter that controls a fire threshold for that module. Create this parameter by calling admin_param.merge_parameter method. But you're free to add any number of parameters to your output module. There is no restriction of what you do with the message. Write an incident in your ticket system, send mails, do whatever you like. The only thing to keep in mind is performance. But if a severe error occurs, performance is probable your least problem.

If you want to learn about how output modules are created, simply review the ones delivered with PIT. You will quickly get the idea and understand that there's nothing special about it.