What Would a Developer-Friendly Application Log Look Like?
9/30/14 UPDATE: I’ve begun work on a tool like I describe here. It’s detailed in this post: Show SCN: The better logger I promised
Do you ever have this problem? You’re in the middle of writing a fantastic ABAP program. It hums, it purrs, it looks reeeeally good. The code is so clean uncle Bob would be proud. You use the best design patterns and your application is modularized well. Then you realize you need to log a few lines of text to view later in SLG1. Your heart sinks. Your robust, clean application is about to become polluted by countless 10-line calls to FM ‘BAL_LOG_CREATE’ and its fellow hoodlums, all because you wanted to save the output of a BAPI you called. First world problems, AMIRITE? Anyone?
The obvious solution is to put an object-oriented wrapper around these functions. This solution is so obvious, in fact, that every department in SAP has written their own verison of it, and my company has added two more. This is consistent with SAP’s approach to developer tools: Quantity Guaranteed.
So why do we need another OO wrapper? Well, because I believe in the power of collaboration, for one. None of the other logs have been designed by the developer community. Another reason is that logs have come a long way since 1999, when SAP released their function modules for the application log, and developers are used to a more concise syntax. For instance, if I want to write to the console in javascript, it’s:
console.log(‘The System is Down’);
but in SAP, I have to declare 2 variables, then spend a page of code calling function modules BAL_LOG_CREATE, BAL_LOG_MSG_ADD, and BAL_DB_SAVE. Part of the reason is that SAP has multiple logs, while a web browser only logs messages in one console. So when you log anything in SAP, it must go to a specific application log object and sub-object. Android (java) also writes to multiple logs. Its logs are called with two arguments, tag and message, like:
Log.e(“SystemMsgs”, “The system is Down”);
If logging in ABAP was going to be just as awesome (and I know it can be), what would it look like? Please post your ideas and discuss! Once enough good ideas are gathered, I’d like to start building something on github. Anyone is welcome to contribute code or just ideas and insights. Here’s an example what I think would make a good application log API:
log = zcl_app_log=>get( object = ‘ZFICO’ subobject = ‘INTERFACES’ ).
TRY.
zcl_accounting=>do_some_important_upload( ).
log->add( ‘The interface has finished’ ).
CATCH zcx_upload_failed INTO err.
log->add( err ).
ENDTRY.
Hi Eric,
nice written. I like the idea and why don't you start with some lines of coding to it. I'm pretty sure Bruno Esperança would love to see it in his GitHub-project.
~Florian
You mean our github project 😀
By the way, I owe you a reply, sorry for the delay!! Been quite busy these days.
Cheers!
Bruno
PS: You're right, this would look great in the "UTILS" folder of Project Object 🙂
Hello Eric,
Really nice blog. I am currently going through the same pain of calling BAL* function modules in one of my applications to display different messages. A library like this to create,display and save application log would be of great help to developer community.
Regards,
Jaideep,
Definitely. The tough part will be making it look good to program for display and/or saving. On the one hand, I'd like it to save automatically so I don't have to call
log->save( )
a bunch of times. But I'd also like to have a way to not save automatically since there are plenty of cases where you only want to display (often just a popup).Beginning with create, add, display, and save would be a good start.
Hi Eric!
I'm thinking about that problem myself, regarding the classes for other objects like sales orders and stuff like that.
Maybe an optional parameter when you get the instance for the log, like "automatic_save_enabled" or something like that. Which would be similar to the "commit" parameter in some function modules.
What do you think?
Best,
Bruno
You and I aren't the only people who've been thinking about that problem. The first section of this guide gives a nice summary of the concepts of Active Record and Object-Relational Mapping, two concepts central to "objectifying" enterprise data.
Active Record Basics — Ruby on Rails Guides
Hi Eric,
I know of 2 custom OO wrappers for the logging function modules in our company (I suppose there are some more). I created one myself.
Here are some of the requirements we had. We wanted to be able to log plain text (literal strings in the code), exceptions and messages (structure SCX_T100KEY).
we also wanted to be able to log errors, warnings and infos like
log->error(...). log->warning(...). log->info(...).
Some logging frameworks also have a logging level. According to the logging level only errors or errors and warnings are logged. Would that be a requirement?
One thing I wanted to be able was to raise an exception and log the error in a concise manner. I came up with this coding:
RAISE EXCEPTION TYPE /zatsp/cx_hrpt_cust_not_found
EXPORTING
textid = log->error( i_no = '006' i_at1 = i_auswertungsweg ).
Cheers,
Thomas
Thanks for the input, Thomas!
I really like the different methods for different importance levels. I also think it would be nice to have the log methods accept TYPE ANY as their input and introspect the right way to add their inputs to the log. That way I can call log->e( dobj ) and pass in an exception, a string or charlike, or a table of BAPIRET2.
Your example is pretty cool. That's helpful to have the error method returns the error textid.
As for the logging level and the framework, I'm not sure. My main goal is to have the programming reflect what you see in SLG1. It should be intuitive for someone who's familiar with how SAP organizes its logs.
Hi all
This is already existing in SAP std for ERP at least since long! No need to invent this again.
Please read this blog by my friend Björn-Henrik Zink http://scn.sap.com/community/abap/application-development/objects/blog/2010/01/07/abap-objects-custom-sap-erp-hcm-class-library--example-3--exceptions
I have successfully used this approach for a very long time now and the interface IF_RECA_MESSAGE_LIST combined with the above described exception handling class ZCX_MESSAGES solves all these problem in a beautiful way making programming simple and reliable.
Also have a look at http://scn.sap.com/people/uwe.schieferstein/blog/2008/12/11/a-christmas-collection-of-useful-classes by http://scn.sap.com/people/uwe.schieferstein
Regards, Johan
Thanks Johan. That is a lot of helpful information. It seems like Björn-Henrik Zink's blog is for the slightly different purpose of collecting exceptions and processing them together. I'm proposing an interface to the application log.
CL_RECA_MESSAGE_LIST looks very powerful and I was not aware of that class. I do have two problems with it, though. It requires you to instantiate the object and then call method
init
, which defeats the point of a constructor method (or a factory, which is probably more appropriate in this case). It also has no methods for displaying the log. I'd like to be able to writelog->popup( )
orlog->display( )
.Hi Eric
The mentioned interface can of course be used without to an exception class. But introducing the exception class as well gives you the benefit of both the BAL log functionality and error handling. If you only need the log then it is still very convinent using the exception class interface. Just write to the log and then display it at your convinience. I prefer to write it like this:
Constructor snippet:
method constructor.
data:
lv_log_object type balobj_d,
lv_log_subobject type balsubobj.
" Init:
lv_log_object = iv_log_object.
lv_log_subobject = iv_log_subobject.
" Init message handler:
if lv_log_object is not initial.
" Use SBAL Log Object which will give us a possibility to save this log
" and later evalutate it via transaction SLG1: zif_message_handling~mo_message_list = cf_reca_message_list=>create( id_object = lv_log_object
id_subobject = lv_log_subobject ).
else.
" Only create log in memory which is sufficent when error messages are
" only to be displayed for end user online:
zif_message_handling~mo_message_list =
cf_reca_message_list=>create( ).
endif.
endmethod.
Writing to the created log snippet (several ways to do it but I prefer this one for Where-Used functionality):
data:
lv_dummy type char01.
" No business partner was selected.
message e016(bnk) into lv_dummy. " Gives Where-Used search which is convinient
" Add SYST message:
zif_message_handling~mo_message_list->add_symsg( ).
" If you also would like to raise the error and not only collecting messages:
raise exception type zcx_messages
exporting
messages = zif_message_handling~mo_message_list.
To get the BAPIRET2-table structure and save the log this typical coding can be used:
data:
lo_cx_messages type ref to /vfs/cx_messages,
lo_message_list type ref to if_reca_message_list,
lt_return type bapiret2_tt,
lv_text type syucomm,
lv_dummy type char1. "#EC *
try.
" Do something that raises some exception...
catch zcx_messages into lo_cx_messages.
" Get error messages:
lo_message_list ?= lo_cx_messages->zif_message_handling~get_message_list( ).
if lo_message_list is bound.
lo_message_list->get_list_as_bapiret( importing et_list = lt_return[] ).
" Save SBAL Message Log in db -> Log can later be read via transaction SLG1:
lo_message_list->store( if_in_update_task = abap_false ). cf_reca_storable=>commit( ).
endtry.
If the log is to be displyed in a popup the following code is useful in SAPGUI:
call function 'C14ALD_BAPIRET2_SHOW'
tables
i_bapiret2_tab = lt_return[].
This is not part of the log functionality really but just displays the BAPIRET2-table in an old type ALV dislay. I use it rarely but comes handy once in a while.
In package RE_CA_BC there are some useful FM that can be used as well:
Snippet code:
data:
lv_handle type balloghndl.
lv_handle = lo_message_list->get_handle( ).
" Get the log reference:
lo_msglist = cf_reca_message_list=>find_by_handle( lv_handle ).
if lo_msglist is bound.
" Display Log in an ALV popup:
call function 'RECA_GUI_MSGLIST_POPUP'
exporting
io_msglist = lo_msglist.
endif.
The package RE_CA is certainly a real treasure (strange to me but SAP std doesn't make use of it or in a very limited way - maybe more going forward?) to explore and simply brilliant for logging and exception handling. Using this approach there is IMO never a need to create specific exception classes! This is a HUGE time saver and enforces also reuse and easy handling of logging and, as mentioned, exception handling.
If in WDA FPM, WDA, BSP etc it still works, of course, but no popup is for free - you have to program that. I never did so far as no need for this yet. SLG1 transaction is good enough normally.
I encapsulate any needed support functionaly in either a MODEL-class or a UTILITY-class designed for the task at hand as to never reapeat any code but structure it in a decent way.
Eric - what do you think of all this?
I hope it can help. Both Björn-Henrik and myself use it all the time so we for sure know this works like a charm.
BTW: Example coding is from an SAP ERP EhP5 system and slightly modified to only display the basic and important concepts.
Regards and good luck, Johan
Johan,
Thanks for the example. CL_RECA_MESSAGE_LIST is better than the BAL* function modules, but it's nothing like how logging is done in modern programming. I just found another example of how to do logging in Ruby, a popular scripting language.
Class: Logger (Ruby 2.1.2)
Is logging in Ruby like CL_RECA_MESSAGE_LIST or more like my above examples of Android and javascript? Logging in ABAP is painfully different than other frameworks.
Another thing is that when developing application logic, I don't want to learn a bunch of methods for logging different types of messages like add_bapiret2 or add_symsg. I want to just call methods add, info, warn, error, or fatal, and pass a bapiret2 structure, an error object, a literal string, or a table of messages, and let the logger object figure out what I'm passing it. If I pass nothing perhaps it will add the system message.
The good thing is SAP logging doesn't have to be this difficult, and I plan to make it happen.
Hi Eric,
This is a nice blog post, I implemented a ZCL_LOG myself a few times.
Johan von Reedtz, I have to say that even knowing CL_RECA_MESSAGE_LIST for a while I kind of reinvented the wheel too. What I actually did was I created a subclass for CL_RECA_MESSAGE_LIST, and added a few methods/attributes I found relevant/useful, i.e. counter by type of message, a proper constructor so I didn't need top call INIT manually and so on. In other implementations, I created the class from scratch.
We also implemented, among other things, sending an email with the log attached to specific user(s) when problem class is high or other criteria (for some object/sub object, etc).
Cheers,
Custodio
I have also done a few more things like sending a Workflow instance with reference to the log.
If Eric decides to improve and simplify I'm all ears!
BR, Johan
I've got v0.0.1 up and running! I've put a link to my post about it at the top of this blog.
I've built a couple of OO wrappers like this myself. I really like BAL log for a lot of proceses. One feature I use a lot to make it very readable for users is the Detail Level feature you can do with messages. With Detail Level you can design a hierarchy for the messages like:
- Messages from record validation
- Bad value in record 1
- Bad value in record 5
- 10 records processed, 2 with errors
- Messages from record processing
- Invoice creation failed for record 3
- 8 records processed, 7 success 1 error
...that sort of thing. My method for adding messages is using based on T100 messages and I would code it like:
MESSAGE E001(XXX) WITH record_nbr INTO lv_dummy. " 'Messages from record validation'
GO_LOG->LOG_MESSAGE( IV_DETAIL_LEVEL = 1 ).
MESSAGE E001(XXX) WITH record_nbr INTO lv_dummy. " 'Bad value in record &1'
GO_LOG->LOG_MESSAGE( IV_DETAIL_LEVEL = 2 ).
My LOG_MESSAGE method has optional parameters for message id, number etc; that all default to SY-MSGID, SY-MSGNO etc. So you use the MESSAGE ... INTO statement to set up the SY-MSGxx variables - which gives the benefits of where-used list on the message and gives a simple method call to get it added to the log.
I've also done a method that you can call from a report program with a selection screen that can add an entire node of messages to list everything the user entered in the selection screen. This method reads the source code for the selection screen definition and reading the title text of selection-sreen blocks, parameters, select-options.
I would love to get my hands on that!
Here you go.... it was written prior to the "MESSAGE text" style so you should replace the use of generic (& & & & type) message 899(FI) with just a text type message. And you'll need to support detail level as an import parameter in the method to log the messages; also to support in the display that the detail level is used to organize the tree when you display the log. I use the BAL display profile from function module BAL_DSP_PROFILE_DETLEVEL_GET.
It's best for the calling program to have its selection-screen definition in a separate include so that the method is not trying to parse the entire report program; but it should work regardless.
Here's the code for my LOG_SELECTION_SCREEN_VALUES method - sorry for the length....
Parameters (both importing)
- IV_SELSCREEN_INCLUDE_NAME type PROGRAMM
- optional IT_EXCLUDE type (standard table of RSSCR_NAME)
METHOD log_selection_screen_values .
* Inserts messages into the log that give the values for all selection-screen
* parameter and select-options.
TYPES:
BEGIN OF ty_level_flag,
level TYPE i,
any_items TYPE boole_d,
END OF ty_level_flag.
DATA:
lv_message TYPE char80, "#EC NEEDED
lv_repid TYPE syrepid,
lv_tabname TYPE ddobjname,
lv_fieldname TYPE dfies-fieldname,
lt_level_flag TYPE STANDARD TABLE OF ty_level_flag,
ls_dd04v TYPE dd04v,
lv_date TYPE datum,
lv_pos TYPE i,
lv_sel_value TYPE text80,
lv_text TYPE string,
lv_level TYPE i,
lv_item_level TYPE i,
lv_textid(8) TYPE c,
lt_dfies TYPE STANDARD TABLE OF dfies,
lt_textpool TYPE SORTED TABLE OF textpool
WITH UNIQUE KEY id key,
lt_words TYPE STANDARD TABLE OF char20,
lt_rsscr TYPE STANDARD TABLE OF rsscr,
lt_source TYPE STANDARD TABLE OF string,
lt_sel_crit TYPE STANDARD TABLE OF rsparams.
FIELD-SYMBOLS:
<typed_value> TYPE any,
<level_flag> TYPE ty_level_flag,
<dfies> TYPE dfies,
<textpool> TYPE textpool,
<source> TYPE string,
<rsscr> TYPE rsscr,
<word> TYPE char20,
<sel_crit> TYPE rsparams.
* Show the selection-screen values in the application log.
* This routine reads the report's source code for the selection-screen
* definition to be able to add nodes in the message log for each block
* on the selection screen and gives the values for each
* parameter/select-options within the block.
lv_level = 1.
MESSAGE i004 INTO lv_message. " 'Selection Screen Values'
me->log_message( ).
lv_repid = sy-cprog.
* Get values of all parameters/select-options:
CALL FUNCTION 'RS_REFRESH_FROM_SELECTOPTIONS'
EXPORTING
curr_report = lv_repid
TABLES
selection_table = lt_sel_crit.
* Get definition data about each parameters/select-option
LOAD REPORT lv_repid PART 'SSCR' INTO lt_rsscr. "#EC NOTEXT
* Get source code of selection-screen definition:
READ REPORT iv_selscreen_include_name INTO lt_source. "#EC NOTEXT
* Get text symbols and selection texts from textpool:
READ TEXTPOOL lv_repid INTO lt_textpool LANGUAGE sy-langu.
* Analyze selection-screen source code to find blocks,
* parameters/select-options and add them into application log:
LOOP AT lt_source ASSIGNING <source>.
CHECK NOT <source> IS INITIAL.
CLEAR: lv_textid, lv_text.
**********************************************************************
* Treat each selection-screen block as a hierarchy node:
**********************************************************************
IF <source> CS 'BEGIN OF BLOCK' OR "#EC NOTEXT
<source> CS 'BEGIN OF SCREEN'.
* Find the "TITLE TEXT-xxx" part of line for the text:
FIND 'TEXT-' "#EC NOTEXT
IN <source>
IGNORING CASE
MATCH OFFSET lv_pos.
* Ignore if no title text:
IF sy-subrc <> 0.
CONTINUE.
ENDIF.
ADD 5 TO lv_pos. " Get to the xxx part of TEXT-xxx
lv_textid = <source>+lv_pos(3).
TRANSLATE lv_textid TO UPPER CASE.
READ TABLE lt_textpool ASSIGNING <textpool>
WITH KEY id = 'I' key = lv_textid
BINARY SEARCH.
IF sy-subrc = 0.
lv_text = <textpool>-entry.
ELSE.
lv_text = lv_textid.
ENDIF.
* Mark parent level as having entries:
IF lv_level > 1.
READ TABLE lt_level_flag ASSIGNING <level_flag> WITH KEY level = lv_level.
<level_flag>-any_items = abap_true.
ENDIF.
* Initial flag for this level:
ADD 1 TO lv_level.
READ TABLE lt_level_flag ASSIGNING <level_flag> WITH KEY level = lv_level.
IF sy-subrc = 0.
CLEAR <level_flag>-any_items.
ELSE.
APPEND INITIAL LINE TO lt_level_flag ASSIGNING <level_flag>.
<level_flag>-level = lv_level.
ENDIF.
* Add as a node in hierarchy on application log:
MESSAGE i899(fi) WITH lv_text INTO lv_message.
me->log_message( EXPORTING iv_detail_level = lv_level ).
ENDIF.
* End of a block or subscreen?
IF <source> CS 'END OF BLOCK' OR "#EC NOTEXT
<source> CS 'END OF SCREEN'.
CHECK <source> NS 'BLOCK TABBED'.
* If a block has no filled parameters/select-options, add a "No selections entered"
* node under it:
READ TABLE lt_level_flag ASSIGNING <level_flag> WITH KEY level = lv_level.
* Added this to avoid short dump
IF <level_flag> IS NOT ASSIGNED.
CONTINUE.
ENDIF.
IF <level_flag>-any_items IS INITIAL.
lv_item_level = lv_level + 1.
MESSAGE i002 INTO lv_message. " 'No selections entered'
me->log_message( EXPORTING iv_detail_level = lv_item_level ).
ENDIF.
SUBTRACT 1 FROM lv_level.
CONTINUE.
ENDIF.
* Check to see if the first non-blank word in the statement is a parameter or
* select-option name:
CLEAR lt_words.
SPLIT <source> AT space INTO TABLE lt_words.
LOOP AT lt_words ASSIGNING <word>.
* Is the first non-blank word in the line found as a parameter or select-option in
* the SSCR section of the report?
CHECK NOT <word> IS INITIAL.
TRANSLATE <word> TO UPPER CASE. "#EC TRANSLANG
READ TABLE lt_sel_crit ASSIGNING <sel_crit> WITH KEY selname = <word>. "#EC *
* Yes - its a parameter or select-option - does it have any values assigned?
IF sy-subrc = 0.
IF <source> NS 'AS CHECKBOX'.
IF ( <sel_crit>-kind = 'P' AND <sel_crit>-low IS INITIAL ) OR "#EC NOTEXT
( <sel_crit>-kind = 'S' AND <sel_crit>-sign IS INITIAL ). "#EC NOTEXT
EXIT. " exit loop at lt_words
ENDIF.
ENDIF.
ELSE.
EXIT. " exit loop at lt_words since we only check first non-blank word
ENDIF.
* Check if this parameter is excluded from the output:
READ TABLE it_exclude TRANSPORTING NO FIELDS
with key table_line = <word>.
CHECK sy-subrc <> 0.
* Get info about the parameter definition:
READ TABLE lt_rsscr ASSIGNING <rsscr> WITH KEY name = <word>. "#EC *
* Get the selection text:
READ TABLE lt_textpool ASSIGNING <textpool>
WITH KEY id = 'S' key = <word>
BINARY SEARCH. "#EC *
IF sy-subrc = 0.
lv_text = <textpool>-entry.
* Is the text drawn from dictionary?
IF lv_text(1) = 'D'. "#EC NOTEXT
* DBFIELD may be a DTEL name or tabname-fieldname:
SPLIT <rsscr>-dbfield AT '-' INTO lv_tabname lv_fieldname.
IF lv_fieldname IS INITIAL.
CALL FUNCTION 'DDIF_DTEL_GET'
EXPORTING
name = lv_tabname
langu = sy-langu
IMPORTING
dd04v_wa = ls_dd04v.
lv_text = ls_dd04v-scrtext_m.
ELSE.
CALL FUNCTION 'DDIF_FIELDINFO_GET'
EXPORTING
tabname = lv_tabname
fieldname = lv_fieldname
TABLES
dfies_tab = lt_dfies.
READ TABLE lt_dfies ASSIGNING <dfies> INDEX 1.
lv_text = <dfies>-scrtext_m.
ENDIF.
ENDIF.
ELSE.
* If no text available use the parameter name:
lv_text = <word>.
ENDIF.
SHIFT lv_text LEFT DELETING LEADING space.
CONCATENATE lv_text ':' INTO lv_text.
lv_item_level = lv_level + 1.
LOOP AT lt_sel_crit ASSIGNING <sel_crit>
WHERE selname = <word>. "#EC *
* Format the value of a date parameter without DDIC reference:
IF <rsscr>-type = 'D' AND <rsscr>-dbfield IS INITIAL. "#EC NOTEXT
MOVE <sel_crit>-low TO lv_date.
WRITE lv_date TO <sel_crit>-low.
MOVE <sel_crit>-high TO lv_date.
WRITE lv_date TO <sel_crit>-high.
ENDIF.
* If parameter:
IF <sel_crit>-kind = 'P'. "#EC NOTEXT
IF <source> CS 'AS CHECKBOX'.
IF <sel_crit>-low IS INITIAL.
lv_sel_value = text-off.
ELSE.
lv_sel_value = text-onn.
ENDIF.
ELSE.
* Use typed variable based on ddic type so we can use WRITE to format values:
IF <rsscr>-dbfield IS NOT INITIAL.
IF <rsscr>-length > 45. " The data in lt_sel_crit is always 45 long, ASSIGN fails if db type > 45
lv_sel_value = <sel_crit>-low.
ELSE.
TRY.
ASSIGN <sel_crit>-low TO <typed_value> CASTING TYPE (<rsscr>-dbfield).
WRITE <typed_value> TO lv_sel_value.
CATCH cx_root.
lv_sel_value = <sel_crit>-low.
ENDTRY.
ENDIF.
ELSE.
lv_sel_value = <sel_crit>-low.
ENDIF.
ENDIF.
ELSE.
* Format value etc of a select-option:
CASE <sel_crit>-option.
WHEN 'EQ'. "#EC NOTEXT
IF <rsscr>-dbfield IS NOT INITIAL.
IF <rsscr>-length > 45. " The data in lt_sel_crit is always 45 long, ASSIGN fails if db type > 45
lv_sel_value = <sel_crit>-low.
ELSE.
TRY.
ASSIGN <sel_crit>-low TO <typed_value> CASTING TYPE (<rsscr>-dbfield).
WRITE <typed_value> TO lv_sel_value.
CATCH cx_root.
lv_sel_value = <sel_crit>-low.
ENDTRY.
ENDIF.
ELSE.
lv_sel_value = <sel_crit>-low.
ENDIF.
WHEN 'BT'. "#EC NOTEXT
CONCATENATE <sel_crit>-low <sel_crit>-high INTO lv_sel_value SEPARATED BY ' - '.
WHEN OTHERS.
CONCATENATE <sel_crit>-option <sel_crit>-low <sel_crit>-high INTO lv_sel_value SEPARATED BY space.
ENDCASE.
IF <sel_crit>-sign = 'E'. "#EC NOTEXT
CONCATENATE text-exc lv_sel_value INTO lv_sel_value SEPARATED BY space. " add 'excluding'
ENDIF.
ENDIF.
* Add parameter/select-option text+value into log below current block text:
MESSAGE i899(fi) WITH lv_text lv_sel_value INTO lv_message.
me->log_message( EXPORTING iv_detail_level = lv_item_level ).
READ TABLE lt_level_flag ASSIGNING <level_flag> WITH KEY level = lv_level.
* This added to prevent a short dump
IF <level_flag> IS NOT ASSIGNED.
CONTINUE.
ENDIF.
<level_flag>-any_items = abap_true.
ENDLOOP.
* Only process first word in the line:
EXIT.
ENDLOOP.
ENDLOOP.
ENDMETHOD.
Interesting code.
In my opinion, you should even post it as a separate blog.
I've done something similar in the past, although it was that sophisticated as I didn't parsed the code myself 🙂 .
For the matter of fact, you may retrieve most of the field attributes, except the assignment to blocks, via standard (not released) FM RS_SELECTIONS_DESCRIPTION.
Few tips:
1. You may use FM RS_TEXTPOOL_READ to read also texts from dictionary (instead of manual handling).
2. I think that no special handling is required for DATS (Type=D), only for packed numbers.
3. What about radio buttons? 😉
Detailed view is indeed a sophisticated option for displaying log.
However, its' main drawback is (as far as I know) it cannot be combined with any other hierarchy display (i.e. First build standard hierarchy and only then build sub hierarchy according message detail level).
Most of the times, I prefer to build the hierarchy by context data (e.g. Invoice number, in your example).
Hi Shai,
It's true that it's hard to get the detailed view in some of the standard methods like SLG1. For the detailed view I have a little report program that lets the user specify some critieria about which logs they want to view. If more than one log is found, I bring up an ALV list for them to choose just one. Then I show that one using the detailed view.
Jim
Hi Eric,
While your approach definitely addresses the logging problem in ABAP OO to some extent, let me notice that logging belongs to a different functional domain than (any) application. In fact, we have a problem of a cross-cutting concern at our hands here and the most obvious solution is a logging aspect (AOP approach). In such a way you can not only log where you want, what you want and how you want but also retain full flexibility to inject or remove logging altogether at any time in the future, fully independently from the application itself.
If this sounds to you like a story from Java or .NET, let me prepare a Vesna Framework AOP demo showing exactly that in ABAP OO (somewhere in next days).
Kind regards
Wiktor Nyckowski
Wiktor,
You're perfectly right about logging being a cross-cutting concern, but introducing AOP to the ABAP ecosystem is a slight increase of scope, and all I really want is a log object instead of a handful of function modules.
Hi Eric,
Thanks;) When you have a chance, please have a look at the blog post (as promised) devoted specifically to this topic:
Venturous Logging Solution for ABAP OO
The demo app attached to it actually works (and presents a very nice AOP use case). And, btw, your OO BAL wrapper could by all means be used with it. I did not do that in order not to increase the complexity of the sample case but in fact, it would prefectly fit this scenario.
Kind regards
Wiktor Nyckowski
Hi Eric,
yes, there are so many Application Log wrappers around.
Some years ago I started to use always a MESSAGE defined in T100 with addition ... INTO <string variable>. The next statement is always log->add_sy_msg( ) where the method takes the message details from the SYST variables. According to message type, a fixed severity level is assigned.
Pure text messages are not supported.
This is a KISS/YAGNI approach to Clean Code development - as 100 % opposed to the spaghetti-approach by Jim Tasker.
Using this approach, you can see the full message in debugger and from the log you have forward navigation to the message, from there the where-used-list will take you to the source position.
SAP provides a (far too) powerful application log framework - use parts of it. SAP also provides the powerful T100 message framework, use it.
Regards
Clemens
Hi Clemens,
Actually my approach to logging most things is exactly as you say: a MESSAGE.... INTO using T100 messages, then calling a method that logs the message from SY-MSGxx variables. It is only the specialized function I wrote to log everything entered on the selection screen that has what I could agree is a spaghetti-coding kind of approach.
Jim