If you've ever used DSPJOBLOG OUTPUT(*PRINT) to record someone's job log, you know how much fun it can be processing spool files in RPG. Most people either save the spool file or override the output to a database file and then process it as necessary. That's where the Control Job Log Output (QMHCTLJL) API enters the picture.
The QMHCTLJL API redirects a job's job log to a user-specified database file. Sadly, it doesn't have an option to write immediately to the file; instead, it waits for the user to sign off or the batch job to end and then performs the normal job log generation.
What I mean by normal job log generation is that if the user signs off with *NOLIST, then no job log is produced and nothing is recorded by QMHCTLJL, but if the user signs off with *LIST, then the job log is redirected by this API. In other words, the API only redirects where the output is sent, not whether it is generated nor when its generated.
But this is good enough for our purposes, so I'm going to explain the API in a little more detail and then give you a working program that you can call to redirect "this" job's job log to a database file of your choosing.
First, you call this API at any point in the job's life cycle to perform a redirection of where the job log is written. It can be a database file (two database files actually) or *PRINT. Why *PRINT? In case you redirect a job log to a database file and then want to redirect it back to the standard SPOOL file output mechanism. So, you call QMHCTLJL once (or more) and wait for the job to end to see the results. But again, if the job is an interactive job, and the end user types in SIGNOFF *NOLIST (which is the default in most shops), you'll get nothing!
The QMHCTLJL API has six required parameters, which are
The first five parameters are input only, and the error structure (parameter six) Is, of course, input/output.
The first parameter is a data structure that contains two file and member names, along with a replace-data option.
The two files and members are referred to as the primary output file and the secondary output file.
The primary output file is required and receives the basic message-related information, much of which you'd retrieve using the RCVMSG CL command -- things like severity, time sent, message ID, msgkey, type, sender data, message file, message data, and so on. In fact, you could reproduce the actually message itself with just this information. The file's record length is (as of V5.1) 18,263 bytes wide. And as far as I can tell, there is no published record format for this file. So using the external descriptions or generating it and using my RTVSQLSRC CL command are the only known ways to build a file description.
The secondary output file is optional; you simply leave this entry blank and nothing is produced. If specified, the secondary file receives the first- and second-level message text, the message type (*RQS, *COMP, *INFO, etc.), and some miscellaneous information. There is a subtle way to tie these two files together, and that requires joining the files using the message key. The msgkey is the only consistent, unique value stored in both files.
Just for fun, I created a simple SQL join statement to bring the important data from both files into a single view of the information. That SQL statement follows:
SELECT P.QMHMID, P.QMHTYP, P.QMHSEV, P.QMHMDT, S.QMHLIN
FROM rpgcoder/joblog1 P, rpgcoder/joblog2 S
WHERE P.QMHMRK=S.QMHMKS
The second parameter is the output file format ID. This parameter should always contain the literal 'CTLJ0100'. These format IDs exist so that if IBM enhances an API later on, rather than create a new API, IBM can simply add a new format ID and be done with it.
The third and fourth parameters are the message filter array and array element count. This is an array of filter identifiers. For example, if you only want completion messages and informational messages recorded, you would pass two message filter elements -- one with *COMP and one with *INFO specified. Then pass two for the element count (fourth parameter). You also get to specify a message ID and message severity level for these filters.
The fifth parameter is a qualified message queue that receives messages if/when the API fails while attempting to produce the job log. Normally I leave this parameter blank but, of course, your situation may call for it.
The final parameter is that traditional API exception/error data structure.
This API can be called at any time during a job to change where its job log is written. You can't run this API "for another job" but only for "this" job. That is, the job in which the API is called is alternated.
To simplify things, I've written a Save Job Log (SAVJOBLOG) Command. This command allows you to use CL to run the QMHCTLJL API much more easily. The command lets you control everything except the error message queue parameter, which I tend not to use anyway. But adding it should be a simple process.
The SAVJOBLOG command that I wrote has six parameters, but they do not correspond to the six parameters of the API, rather they correspond to entering the information to call the API.
OUTFILE - The OUTFILE parameter is a qualified database file that the API refers to as the primary file. This file receives the detail message information from the job log. If this file does not exist, it is created.
A special value of OUTFILE(*PRINT) may be specified for this parameter. This routes the job log to the normal spool file system. The other OUTFILE parameters are ignored when OUTFILE(*PRINT) is specified.
OUTMBR - The OUTMBR parameter is the data member of the OUTFILE that receives the job log information. If the member does not exist, it is added to the OUTFILE.
OUTFILE2 - The OUTFILE2 parameter is the optional database file that the API refers to as the secondary file. This file receives the summary message information from the job log. If the file does not exist, it is created. A value of *NONE for this parameter indicates that job log information is not written to the secondary file.
OUTMBR2 - The OUTMBR2 parameter is the data member of the OUTFILE2 that receives the job log information. If the member does not exist, it is added to the OUTFILE2.
REPLACE - The REPLACE parameter is used to control overwriting of data in the primary and secondary files. A value of REPLACE(*YES) replaces any existing data.
MSGFILTER - The MSGFILTER parameter allows you to control what is written to the primary and secondary files. You can specify up to 20 combinations of message type, message severity, and message ID (generic or full). If 20 isn't enough, just change the Command definition from MAX(20) to a larger value, and change the MAXFILTERS named constant to the same value in the RPG IV source code.
Here's an example of using SAVJOBLOG on the Command Entry display:
==> SAVJOBLOG OUTFILE(QGPL/MYJOBLOG1) OUTFILE2(QGPL/MYJOBLOG2) +
MSGFILTER((*ESCAPE) (*EXCP))
In this example, two files are used to log the job log information, but only escape and exception messages are written to the files. Note that the writing only occurs after the interactive job ends (i.e., after signoff) and only if LOG(*LIST) is specified. Otherwise, you get nothing.
If you've never worked with a user-written command before, enter the CMD source code into a source file named QCMDSRC and set the SEU source type to CMD. To compile, just use PDM option 14; the default CRTCMD parameters are fine.
| RPG TnT: 101 Dynamic Tips 'n Techniques with RPG IV by Bob Cozzi is available now. | |
[2] |
Cozzi wrote down every cool technique he's found over the years, updated Them, and consolidated them into this compact book that is your new desktop companion. The latest book from author Bob Cozzi is available today! This 300-page book contains 101 example RPG IV Tips and Techniques for everyday programming -- from date calculations, to regular express searches, to using APIs. These aren't excerpts, but full working source code examples printed right there. Order your copy today. [3] |
The following is the Command Definition Source code for the SAVJOBLOG CL command. Following it is the RPG IV source code that is known as the Command Processing Program. Specify it on the PGM parameter of the CRTCMD command.
SAVJOBLOG: CMD PROMPT('Save Joblog at Job-End')
/* Command processing program is: SAVJOBLOG */
PARM KWD(OUTFILE) TYPE(QUAL) SNGVAL((*PRINT)) +
MIN(1) PROMPT('Primary/Msg details outfile')
PARM KWD(OUTMBR) TYPE(*NAME) DFT(*FIRST) +
SPCVAL((*FIRST)) EXPR(*YES) +
PROMPT('Primary member')
PARM KWD(OUTFILE2) TYPE(QUAL) SNGVAL((*NONE)) +
MIN(1) PROMPT('Secondary/Msg summary outfile')
PARM KWD(OUTMBR2) TYPE(*NAME) DFT(*FIRST) +
SPCVAL((*FIRST)) EXPR(*YES) +
PROMPT('Secondary member')
PARM KWD(REPLACE) TYPE(*LGL) RSTD(*YES) +
DFT(*REPLACE) SPCVAL((*YES '1') (*NO '0') +
(*REPLACE '1') (*ADD '0')) +
PROMPT('Replace member data')
QUAL: QUAL TYPE(*NAME) MIN(1) EXPR(*YES)
QUAL TYPE(*NAME) DFT(*CURLIB) SPCVAL((*CURLIB) +
(*LIBL)) EXPR(*YES) PROMPT('Library')
PARM KWD(MSGFILTER) TYPE(FILTERS) MIN(0) MAX(20) +
PROMPT('Message filters')
FILTERS: ELEM TYPE(*CHAR) LEN(10) RSTD(*YES) DFT(*ANY) +
SPCVAL((*ANY) (*CMD) (*COMP) (*COPY) +
(*DIAG) (*ESCAPE) (*ESC *ESCAPE) (*EXCP) +
(*EXCEPT *EXCP) (*INFO) (*INQ) (*NOTIFY) +
(*RQS) (*RPY)) EXPR(*YES) PROMPT('Message +
Type filter')
ELEM TYPE(*INT4) DFT(*NONE) RANGE(00 99) +
SPCVAL((*NONE 00)) EXPR(*YES) +
PROMPT('Message severity filter')
ELEM TYPE(*NAME) LEN(7) DFT(*ANY) SPCVAL((*ANY) +
(*IMMED)) EXPR(*YES) PROMPT('Message ID +
filter')
H OPTION(*NODEBUGIO:*SRCSTMT)
H DFTACTGRP(*NO) ACTGRP(*NEW)
H COPYRIGHT('Copyright 2008 by Bob Cozzi')
********************************************************
** All rights reserved.
** Permission to use without warranty
** of any kind is granted.
** Offered "as is" and "use at your own risk".
*******************************************************
/include qsysinc/qrpglesrc,qusec
D SAVJOBLOG PR extPGM('SAVJOBLOG')
D primary Const LikeDS(QUAL_T)
D primaryMbr 10A Const
D secondary Const LikeDS(QUAL_T)
D secondaryMbr 10A Const
D bReplace 1N Const
D inFilters LikeDS(inFilters_T)
D SAVJOBLOG PI
D primary Const LikeDS(QUAL_T)
D primaryMbr 10A Const
D secondary Const LikeDS(QUAL_T)
D secondaryMbr 10A Const
D bReplace 1N Const
D inFilters LikeDS(inFilters_T)
*******************************************************************
** Data Structure Templates
*******************************************************************
D maxFilters C Const(20)
D i S 10I 0
D msgFilters_T DS Qualified Inz
D reserved1 4A Inz(*ALLX'00')
D msgsev 10I 0
D msgtype 10A
D msgid 7A
D reserved2 3A Inz(*ALLX'00')
D inFilters_T DS Qualified
D count 5I 0
D offset 5I 0 Dim(maxFilters)
D filler 1024A
D filter_T DS Qualified
D elemCount 5I 0
D msgtype 10A
D msgSev 10I 0
D msgID 7A
D filter DS LikeDS(filter_T)
D QUAL_T DS Qualified
D object 10A
D library 10A
D obj 10A overlay(object)
D name 10A overlay(object)
D file 10A overlay(object)
D lib 10A overlay(library)
D QUAL3_T DS Qualified
D object 10A
D library 10A
D member 10A
D obj 10A overlay(object)
D name 10A overlay(object)
D file 10A overlay(object)
D lib 10A overlay(library)
D mbr 10A overlay(member)
D mbrName 10A overlay(member)
D outFile_Names_T...
D DS Qualified
D length 10I 0 Inz(%size(outFile_Names_T))
D primaryFile LikeDS(Qual3_T)
D pFile LikeDS(Qual3_T)
D overlay(primaryFile)
D secondaryFile LikeDS(Qual3_T)
D sFile LikeDS(Qual3_T)
D overlay(secondaryFile)
D replace 1N
D QMHCTLJL PR extPGM('QMHCTLJL')
D outFiles Const LikeDS(outFile_Names_T)
D outFmt 8A Const
D msgFilters Const LikeDS(msgFilters_T) Dim(50)
D msgFilterCount...
D 10I 0 Const
D msgq Const LikeDS(QUAL_T)
D apiErrorMsg LikeDS(QUSEC) OPTIONS(*VARSIZE)
D apiFmt C Const('CTLJ0100')
D outfiles DS LikeDS(outfile_Names_T) Inz(*LIKEDS)
D msgFilter DS LikeDS(msgFilters_T) Inz(*LIKEDS)
D Dim(maxFilters)
D msgq DS LikeDS(Qual_T)
D apiErrorDS DS Qualified
D api LikeDS(QUSEC)
D workarea 240A
C EVAL *INLR = *ON
/free
msgq.obj = '*SYSOPR';
apiErrorDS.api.qusbprv = %size(apierrorDS);
apiErrorDS.api.qusbavl = %size(apierrorDS);
msgFilter = *ALLX'00';
if (%parms >= 6);
for i = 1 to inFilters.count;
filter = %subst(inFilters:inFilters.Offset(i)+1:%size(filter));
msgFilter(i).msgtype = filter.msgtype;
msgFilter(i).msgid = filter.msgid;
msgFilter(i).msgsev = filter.msgsev;
endfor;
endif;
outfiles.pfile.file = primary.file;
outfiles.pfile.lib = primary.library;
if (%parms()>= 2);
outfiles.pfile.mbr = primaryMbr;
else;
outfiles.pfile.mbr = '*FIRST';
endif;
if (%PARMS() >=3 and seconday.file <> '*NONE');
outfiles.sfile.file = seconday.file;
outfiles.sfile.lib = seconday.library;
if (%PARMS()>=4);
outfiles.sfile.mbr = seconday.Mbr;
else;
outfiles.sfile.mbr = '*FIRST';
endif;
endif;
outfiles.length = %len(outFiles);
outfiles.replace= *ON;
qmhctljl(outfiles : apiFMT : msgFilter : 0 : msgq : apiErrorDS);
return;
/end-free
Bob Cozzi, a black belt in Judo, started practicing Judo in 1974 and began to learn programming in 1975. Today, Bob runs RPG World [4], the most popular third-party, commercial RPG IV Training Event in the world. The next RPG World Conference [5] is open to all, and is coming up on May 4, 2008. If you miss RPG World, join Bob at RPG World Academy at select cities across the U.S. coming Summer 2008.
Links:
[1] http://systeminetwork.com/author/bob-cozzi
[2] http://www.amazon.com/dp/1583041214?tag=rw0c-20&camp=14573&creative=327641&linkCode=as1&creativeASIN=1583041214&adid=0FDDKRAMASMW6SZB3462&
[3] http://www.amazon.com/dp/1583041214?tag=rw0c-20&camp=14573&creative=327641&linkCode=as1&creativeASIN=1583041214&adid=0FDDKRAMASMW6SZB3462&
[4] http://www.rpgworld.com
[5] http://www.rpgworld.com