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.

To report this post you need to login first.

26 Comments

You must be Logged on to comment or reply to a post.

    1. Bruno Esperança

      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 🙂

      (0) 
  1. Jaideep Sharma

    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,

    (0) 
    1. Eric Peterson Post author

      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.

      (0) 
      1. Bruno Esperança

        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

        (0) 
  2. Thomas Porcham

    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

    (0) 
    1. Eric Peterson Post author

      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.

      (0) 
  3. Johan von Reedtz

    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

    (1) 
    1. Eric Peterson Post author

      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 write log->popup( ) or log->display( ).

      (0) 
      1. Johan von Reedtz

        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

        (0) 
        1. Eric Peterson Post author

          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.

          (0) 
  4. Custodio de Oliveira

    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

    (0) 
    1. Johan von Reedtz

      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

      (0) 
  5. Jim Tasker

    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.

    selscreen log.jpg

    (0) 
      1. Jim Tasker

        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 dfiesfieldname,

             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 = sycprog.

        * 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 sylangu.

        * 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 sysubrc <> 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 sysubrc = 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 sysubrc = 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 sysubrc = 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 sysubrc <> 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 sysubrc = 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    = sylangu

                       IMPORTING

                         dd04v_wa = ls_dd04v.

                     lv_text = ls_dd04vscrtext_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 = textoff.

                     ELSE.

                       lv_sel_value = textonn.

                     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 textexc 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.

        (0) 
        1. Shai Sinai

          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? 😉

          (0) 
    1. Shai Sinai

      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).

      (0) 
      1. Jim Tasker

        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

        (0) 
  6. Wiktor Nyckowski

    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

    (0) 
    1. Eric Peterson Post author

      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.

      (0) 
      1. Wiktor Nyckowski

        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

        (0) 
  7. Clemens Li

    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

    (0) 
    1. Jim Tasker

      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

      (0) 

Leave a Reply