The most common problem explanation we hear from users is that there should be a program error that triggers when users do everything right. While we never get rid of mistakes we do, we have to admit this special class of elusive errors that magically disappear as soon as we can trace every critical step in the program flow. The key to solution is logging. Sure, you cannot log everything, but only the most critical points. What I have to do sometimes is to add a temporary logging capability to investigate problem popping up in production system. So, let’s see what we can do to give us more chances to smack users with a log and say “Your fault!” 🙂
There are numerous simple ways to show and store messages from your program. What’s important is that they have to be stored for some time to allow analysis after they happen. For example, if it’s a report or a program that is run in background most of the time, you have obvious choices of printing messages into the spool with normal WRITEs or using MESSAGE to have it stored in the job log. Both things can be found then in SM37.
If you want logging capabilities for an online report or a dialog program, you have to use other techniques. It is the same also for user exits that are called from SAP standard transactions or things that run in background by definition, like interfaces (e.g. APO CIF).
The simplest trick I am using in development to check program logic that cannot be easily debugged is my own table, a local object, with a long text field to store my messages and probably couple of other fields like date/time to distinguish the records. The table content is written by simple MODIFY. I usually have a program that zaps the content of my logging table, to clean up the mess.
When you are going to use this kind of logging in a “real” system, some things have to be taken care of:
- Uniqueness of records / keys, especially in case when the messages can be written by different program instances running at the same time
- More intelligent way of log reorganization (schedule).
When in development system, I can simply use the current time as the table key because I know that “I’m alone”. The official way of generating unique keys in SAP, however, is using number ranges. Briefly, you have number ranges for, say, production orders, and number ranges for materials etc. For each number range object, SAP can generate you the “next number” when you call a special function. This functionality is managed centrally and numbers will be unique even if you call the function on different application servers. The disadvantage of number ranges is that they have to be customized and monitored for “exhaustion”.
To save us the effort of creating a new number range object, customizing its range and monitoring, we can use another technique of generating unique numbers (or strings) that can then be used as part of table key: Global Unique Identifiers (GUIDs). Normally this is a long string, generated to be “almost” unique for a given system. The chance of having duplicate GUIDs is so small that I have never seen any checks whether to ID exists already. SAP uses GUIDs everywhere in APO, providing tables that map readable names / codes and internal identifiers – GUIDs. Those are normally 22 or 32 characters long. You can have a GUID generated for you by calling the function GUID_CREATE. When you add date/time as key fields into your logging table, you get a pretty simple logging system without any customizing. An additional reorganization program can be run periodically to have the entries older than, say, one month, deleted. Alternatively, if some errors have to be analysed and fixed (like problems with missing master data etc), you can provide table maintenance dialog (SE11/SE54) so that responsible user can delete checked log records after analysing them.
And finally let’s have a look at the real logging, as blessed by SAP. SAP Application Log, transaction SLG1, is the most common way to store messages. Log entries are created and stored using a set of functions. Logs are grouped by log objects and subobjects. For example, you have log group for object PPORDER (production order) and you can further classify logs with production order subobjects HEADER and OPERATION. Those are selections that you use on the initial screen of SLG1, along with others like: free text name / ID, date/time, user name and transaction code.
When creating a log entry, you can tell the system how long should it be stored before it’s deleted automatically. Unlike in the above case with your own log table, you don’t need a reorganization program. I normally request the log messages to be stored for 30 days, it can be less if you feel like your system will not be happy to carry the weight.
Enough with discussion, let’s see the code now:
* this include contains log system constants and has to be used * in your program (or top include if it's a function group) include sbal_constants. * the form writes a single log entry to application log, * building log entry identifier from parameters pf_par1 and pf_par2, * and writing the content of other parameters into the message form write_log using pf_par1 pf_par2 pf_str1 type char50 pf_str2 type char50 pf_str3 type char50 pf_str4 type char50. data: ls_log type bal_s_log, lt_handle type bal_t_logh, lf_handle type balloghndl, ls_msg type bal_s_msg. * we use production order / operation log objects ls_log-object = 'PPORDER'. ls_log-subobject = 'OPERATION'. concatenate 'Some_Name_' pf_par1 '_' pf_par2 into ls_log-extnumber. ls_log-aluser = sy-uname. ls_log-alprog = sy-repid. ls_log-altcode = 'YOUR_TCODE'. ls_log-aldate_del = sy-datum + 30. "keep for one month ls_log-del_before = 'X'. * create a log call function 'BAL_LOG_CREATE' exporting i_s_log = ls_log importing e_log_handle = lf_handle exceptions others = 1. * define data of message for Application Log * Use generic message template with & & & & ls_msg-msgty = 'S'. ls_msg-msgid = '01'. ls_msg-msgno = '319'. ls_msg-msgv1 = pf_str1. ls_msg-msgv2 = pf_str2. ls_msg-msgv3 = pf_str3. ls_msg-msgv4 = pf_str4. ls_msg-probclass = probclass_none. * add this message to log * this function can be called several times to have one log entry * store several different messages call function 'BAL_LOG_MSG_ADD' exporting i_log_handle = lf_handle i_s_msg = ls_msg exceptions others = 1. * save the log append lf_handle to lt_handle. call function 'BAL_DB_SAVE' exporting i_save_all = 'X' i_t_log_handle = lt_handle exceptions others = 1. endform.
Note that here we are using a generic message (01/319). This way you are free to write anything you want but then you don’t get “long text” of the message when clicking it in the SLG1. Alternatively, we could use application specific message class, assigning it and message variables to appropriate fields of the structure bal_s_msg.
@ilya
Good job, explanation. Thanks.
Comment by Tuncay Karaca — March 7, 2008 @ 9:30 pm
I would like to comment your description:
If you would like to show your saved LOG, you have to do the following:
(0. do not forget – you can not refer with lt_handle, this is just an internal table in the global area of the function group, so if you leave your program, it has “no meaning” )
1. Search your log with the function modul ‘BAL_DB_SEARCH’. Use the external key to find!
2. Load the log in the memory with CALL FUNCTION ‘BAL_DB_LOAD’ ! From now, you can use the lt_handle !
3. display the log ‘BAL_DSP_LOG_DISPLAY’ !
see my sample code :
REPORT zngtest1_bal_log_display .
INCLUDE sbal_constants.
DATA:
ls_log TYPE bal_s_log,
lt_handle TYPE bal_t_logh,
ls_handle TYPE balloghndl,
ls_msg TYPE bal_s_msg.
DATA ls_log_filter TYPE bal_s_lfil.
DATA:
lt_extnumber TYPE bal_r_extn,
ls_extnumber TYPE bal_s_extn,
lt_log_header TYPE balhdr_t,
ls_log_header TYPE balhdr.
ls_extnumber-sign = ‘I’ .
ls_extnumber-option = ‘EQ’ .
* you have to fill here the external key !!!!
ls_extnumber-low = ….
APPEND ls_extnumber TO lt_extnumber.
ls_log_filter-extnumber[] = lt_extnumber[].
CALL FUNCTION ‘BAL_DB_SEARCH’
EXPORTING
i_s_log_filter = ls_log_filter
IMPORTING
e_t_log_header = lt_log_header
EXCEPTIONS
log_not_found = 1
no_filter_criteria = 2
OTHERS = 3.
IF sy-subrc 0.
MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
ENDIF.
CALL FUNCTION ‘BAL_DB_LOAD’
EXPORTING
i_t_log_header = lt_log_header
EXCEPTIONS
OTHERS = 1.
IF sy-subrc 0.
MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
ENDIF.
READ TABLE lt_log_header INDEX 1 INTO ls_log_header .
ls_handle = ls_log_header-log_handle .
APPEND ls_handle TO lt_handle.
CALL FUNCTION ‘BAL_DSP_LOG_DISPLAY’
EXPORTING
i_t_log_handle = lt_handle
EXCEPTIONS
profile_inconsistent = 1
internal_error = 2
no_data_available = 3
no_authority = 4
OTHERS = 5.
IF sy-subrc 0.
MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
ENDIF.
Comment by gnadzon — September 15, 2009 @ 5:22 pm