SAS Log Parser

16. Dezember 2024

Im Laufe verschiedener Projekte kam es immer wieder zu der Situation, dass eine Umstellung von Verarbeitungsquellen oder in ETL Logiken auf Veränderungen gegenüber dem Urzustand geprüft werden sollten. Hierzu habe ich einen einfachen Log Parser geschrieben, der Bibliothek, Tabellennamen, Anzahl der Tabellen und die Anzahl der Variablen in den Tabellen extrahiert und als SAS Tabelle zugänglich macht.

Hierzu mache ich mir zunutze, dass die Meldungen zu erstellten Tabellen im SAS-Log in der Regel zwei Varianten haben:

  1. NOTE: Table LIBNAME.TABLENAME created, with NUMBER rows and NUMBER columns.
  2. NOTE: The data set LIBNAME.TABLENAME has NUMBER observations and NUMBER variables.

Zusätzlich werden Errors und Warnings ermittelt und in SAS-Tabellen bereitgestellt. Diese beginngen immer mit der Zeichenfolge „ERROR:“ bzw. „WARNING:“.

1. SAS Log Parser: Macro Parameter

Um die Analyse der SAS-Logs etwas flexibler zu machen habe ich einige Parameter beim Aufruf integriert.

Path

Der Path Parameter übergibt das zu analysierende Verzeichnis in Hochkommata:

Beispiel: PATH='e:\Logs'

Fileextension

Der Fileextension-Paramter gibt die verwendete Dateiendung in Hochkommata an. Über Proc Printo kann diese beim Wegschreiben der Logs gewählt werden. Üblich sind z.B. „.log“ oder „.txt“. Standardmäßig gesetzt ist im Analysemakro die Endung „log“.

Beispiel: FILEEXTENSION="log"

Timestamp

Über diesen Parameter kann ein Timesstamp mitgegeben werden, der beim Auslesen der Dateien aus dem Path-Parameter geprüft wird. Ist der Timestamp im Dateinamen enthalten, werden nur Dateien mit diesem analysiert. Dies ist nützlich wenn z.B. nur ein Tag ausgewertet werden soll. Standardwert ist hier allerding "NO", so dass alle Logs im Verzeichnis analysiert werden.

Beispiele:

TIMESTAMP="NO"

TIMESTAMP="20241118"

2. SAS Log Parser: Code im Detail

2.1 SAS Code Parser Ordnerinhalt ermitteln

Aufbauend auf meinem Code zur Analyse von Ordnerinhalten wird zunächst ausgelesen wie viele und welche Logs im angegebenen Verzeichnis vorhanden sind. Die ermittelten Lognamen und deren Anzahl werden für die Parsing-Schleife in Makrovariablen geschrieben.

DATA TEST;
    PATH=&PATH;
    SUFFIX=SCAN(PATH,-1,'\');
    CALL SYMPUTX('SUFFIX',SUFFIX);
RUN;

PROC FORMAT;
    INVALUE MONZUZAHL "Januar"=1 "Februar"=2 "März"=3 "April"=4 "Mai"=5 "Juni"=6
        "Juli"=7 "August"=8 "September"=9 "Oktober"=10 "November"=11 "Dezember"=
        12 other=.;
RUN;

data DIRCONTENTS;
    keep Verzeichnis Dateiname Endung PfadundName LetzteAendDat AENDDAT
        ErstellDat ERSTDAT;
    length Verzeichnis $40 fref $8 Dateiname $80 Endung $30 PfadundName $200
        LetzteAendDat $40 ErstellDat $40 ;
    rc=filename(fref, &PATH);

    if rc=0 then do;
        did=dopen(fref);
        rc=filename(fref);
    end;
    else do;
        length msg $200.;
        msg=sysmsg();
        put msg=;
        did=.;
    end;

    if did <= 0 then putlog 'ERR' 'OR: Unable to open directory.';
    dnum=dnum(did);

    do i=1 to dnum;
        Verzeichnis=&PATH;
        Dateiname=dread(did, i);
        Endung=LOWCASE(SUBSTR(Dateiname,Find(Dateiname,".")+1));

        /* Ausgabe falls Eintrag eine Datei ist. Falls Eintrag ein Unterverzeichnis, nicht ausgeben.*/
        fid=mopen(did, Dateiname);

        /* Pfad und Dateiname */
        opt=FOPTNAME(fid,1);
        PfadundName=FINFO(fid,upcase(opt));

        /* Letzte Änderung */
        opt=FOPTNAME(fid,5);
        LetzteAendDat=STRIP(FINFO(fid,upcase(opt)));
        AENDDAT=MDY
            (INPUT(SCAN(LetzteAendDat,2),MONZUZAHL.),Substr(LetzteAendDat,1,2),SCAN(LetzteAendDat,3));

        /* Erstelldatum */
        opt=FOPTNAME(fid,6);
        ErstellDat=FINFO(fid,upcase(opt));
        ERSTDAT=MDY
            (INPUT(SCAN(ErstellDat,2),MONZUZAHL.),Substr(ErstellDat,1,2),SCAN(ErstellDat,3));

        if fid > 0 AND UPCASE(Endung)=UPCASE(&FILEEXTENSION) then output;
    end;

    rc=dclose(did);
    Format AENDDAT ERSTDAT DEUDFDD10.;
    LABEL AENDDAT="Datum der letzten Änderung" ERSTDAT="Erstelldatum" ;
run;

Die Anzahl der zu parsenden Logs wird in die Makrovariable ANZ_LOGS geschrieben.

Die Namen der Logs in n-Makrovariablen mit dem Namensschema:

LOG_n

Sollte ein Timestamp mitgeben worden sein, kommt er beim Schreiben der Makrovariablen zum Einsatz und berücksichtigt nur Dateien, deren Namen den Timestamp beinhaltet.

/*Laden der Dateinamen in Makrovariablen */ 
%LET ANZ_LOGS =0; 
DATA _NULL_; 
SET DIRCONTENTS; 
CALL SYMPUTX ('LOG_'||LEFT(_N_),PfadundName); 
CALL SYMPUTX ('ANZ_LOGS',_N_); 
    %IF &TIMESTAMP NE "NO" %THEN 
    %DO; 
    WHERE FIND(Dateiname,&TIMESTAMP)>0; 
    %END; 
RUN; 

2.2 SAS Log Parser: Code Analyse-Schleife

%IF &ANZ_LOGS > 0 %THEN 
                %DO; 
                        %DO i=1 %TO &ANZ_LOGS; 
                                %PUT NOTE: &&LOG_&i wird verarbeitet!; 
                                FILENAME LOGTEXT "&&LOG_&i"; 
                                
                                /******************************************************/ 
                                /* Log Parsing Rows and Columns                       */ 
                                /******************************************************/ 

Nachdem ein Filename auf die gerade in der Verarbeitung befindliche Log-Datei gelegt wurde,

wird diese über einen Data Step eingelesen und nur die für diesen Log Parser relevanten Log-Zeilen nach der Logik:

  1. Falls die Zeile nicht mit „NOTE: „ oder den Text „there were“ beinhaltet wird diese ausgeschlossen
  2. Falls die Zeile nicht mit „NOTE: „ oder den Text „dropped“ beinhaltet wird diese ausgeschlossen
  3. Übernommen werden nur Zeilen die die Texte „table“ oder „the data set“ beinhalten

beibehalten.

DATA LOG_TMP; 
LENGTH TEXT $150; 
INFILE LOGTEXT DSD; 
INPUT TEXT $1-134; 
IF FIND(Text,"NOTE:")=0 OR  FIND(UPCASE(Text),"THERE WERE")>0 THEN 
DELETE; 
IF FIND(Text,"NOTE:")=0 OR  FIND(UPCASE(Text),"DROPPED")>0 THEN 
DELETE; 
IF FIND(UPCASE(Text),"TABLE")>0 OR FIND(UPCASE(Text),"THE DATA SET")>0; 
RUN; 

Log Parser Syntax

NOTE: Table LIBNAME.TABLENAME created, with NUMBER rows and NUMBER columns

Hier werden die Logs nach dem genannten Muster durchsucht. Ermittelt werden der volle Tabellenname, Libname, Tabellenname, die Anzahl der Zeilen und die Anzahl der Variablen.

DATA LOG_&i; 
SET LOG_TMP; 
LENGTH LOGNAME $100 FULLTABNAME $40 LIBNAME $8 TABNAME $32 ROWS 8 COLUMNS 8; 
LOGNAME = "&&LOG_&i"; 
/* Dateiennamen ermittlen*/ 
/* Syntax mit TABLE x.y CREATED WITH XXX ROWS AND XXX COLUMNS*/ 
IF FIND(UPCASE(Text),"TABLE")>0 THEN 
    DO; 
    FULLTABNAME = SUBSTR( 
                     TEXT, 
                     FIND(UPCASE(Text),"TABLE")+6, 
                     FIND(UPCASE(Text),"CREATED") - (FIND(UPCASE(Text),"TABLE")+6) ); 
    LIBNAME=SUBSTR(FULLTABNAME,1,FIND(FULLTABNAME,".")-1); 
    TABNAME=SUBSTR(FULLTABNAME,FIND(FULLTABNAME,".")+1); 
    ROWS = INPUT( 
             SUBSTR( 
             TEXT, 
             FIND(UPCASE(Text),"WITH")+5 /* +5 character count of WITH + 1 blank*/, 
             FIND(UPCASE(Text),"ROWS")-1 /* ROWS - 1 blank*/ - 
             (FIND(UPCASE(Text),"WITH")+5) /* +5 character count of WITH + 1 blank*/ 
             ), 
             BEST32.); 
    COLUMNS = INPUT( 
                SUBSTR( 
                TEXT, 
                FIND(UPCASE(Text),"AND")+4 /* +4 character count of AND + 1 blank*/, 
                FIND(UPCASE(Text),"COLUMNS")-1 /* COLUMNS - 1 blank*/ - 
                (FIND(UPCASE(Text),"AND")+4) /* +4 character count of AND + 1 blank*/), 
                BEST32.); 
    END; 

Log Parser Syntax

NOTE: The data set LIBNAME.TABLENAME has NUMBER observations and NUMBER variables.

Zusätzlich zu dem erst genannten Muster gibt es auch noch das hier analysierte. Ermittelt werden ebenfalls der volle Tabellenname, Libname, Tabellenname, die Anzahl der Zeilen und die Anzahl der Variablen.

/* Syntax mit THE DATA SET x.y HAS XXX OBSERVATIONS AND XXX VARIABLES*/ 
ELSE IF FIND(UPCASE(Text),"THE DATA SET")>0 THEN 
    DO; 
    FULLTABNAME = SUBSTR( 
                         TEXT, 
                         FIND(UPCASE(Text),"SET")+4, /*3 Characters for SET + 1 Blank*/ 
                         FIND(UPCASE(Text),"HAS")- (FIND(UPCASE(Text),"SET")+4)); 
                         LIBNAME=SUBSTR(FULLTABNAME,1,FIND(FULLTABNAME,".")-1); 
                         TABNAME=SUBSTR(FULLTABNAME,FIND(FULLTABNAME,".")+1); 
    ROWS = INPUT( 
                 SUBSTR( 
                        TEXT, 
                        FIND(UPCASE(Text),"HAS")+4 /* +4 character count of HAS + 1 blank*/, 
                        FIND(UPCASE(Text),"OBSERVATIONS")-1 /* OBSVERBVATIONS - 1 blank*/ - 
                        (FIND(UPCASE(Text),"HAS")+4 /* +4 character count of AND + 1 blank*/) 
                        ), 
           BEST32.); 
COLUMNS = INPUT( 
                SUBSTR( 
                       TEXT, 
                       FIND(UPCASE(Text),"AND")+4, 
                       FIND(UPCASE(Text),"VARIABLES")-1 /* OBSVERBVATIONS - 1 blank*/ 
                       - (FIND(UPCASE(Text),"AND")+4) /* +4 character count of AND + 1 blank*/) 
                 ,BEST32.); 
    END; 
FORMAT _NUMERIC_ COMMAX20.; 
RUN; 

SAS Log Parser Errors und Warnings

Zur Ermittlung von Errors und Warnings werden die Logs nach den Zeichenfolgen „ERROR:“ bzw. „WARNING:“ durchsucht. Gefundene Zeilen werden temporär gespeichert.

DATA ERROR_TMP; 
LENGTH TEXT $134; 
INFILE LOGTEXT DSD delimiter='|'; 
INPUT TEXT $; 
IF FIND(UPCASE(Text),"ERROR:")>0;
RUN; 

DATA WARNING_TMP; 
LENGTH TEXT $134; 
INFILE LOGTEXT DSD delimiter='|'; 
INPUT TEXT $; 
IF FIND(UPCASE(Text),"WARNING:")>0;
RUN;
 

Die temporären Tabellen für Fehler (ERROR_TMP) und Warnungen (WARNING_TMP) werden gesichert und es werden Übersichtstabellen für alle Fehler und Warnungen im ersten Schleifendurchgang erzeugt.

/* Finde Fehler und Warnungen */ 
DATA ERR_LOG_&i; 
SET ERROR_TMP; 
LENGTH LOGNAME $100 MESSAGE $500; 
MESSAGE=Text; 
RUN; 

DATA WARN_LOG_&i; 
SET WARNING_TMP; 
LENGTH LOGNAME $100 MESSAGE $500; 
MESSAGE=Text; 
RUN; 

   %IF &i=1 %THEN 
       %DO; 
       PROC SQL; 
       CREATE TABLE LOGS_GESAMT_&SUFFIX LIKE LOG_&i; 
       CREATE TABLE ERRORS_GESAMT_&SUFFIX LIKE ERR_LOG_&i; 
       CREATE TABLE WARNINGS_GESAMT_&SUFFIX LIKE WARN_LOG_&i; 
       QUIT; 
       %END; 

SAS Log Parser Ergebnistabellen

Am Ende der jeweiligen Log-Analyseschleife werden die gefundenen Informationen an die entsprechenden Übersichtstabellen angehängt und stehen zur Analyse oder Weiterverarbeitung zur Verfügung.

PROC APPEND BASE=LOGS_GESAMT_&SUFFIX DATA=LOG_&i; 
RUN; 

PROC APPEND BASE=ERRORS_GESAMT_&SUFFIX DATA=ERR_LOG_&i; 
RUN; 

PROC APPEND BASE=WARNINGS_GESAMT_&SUFFIX DATA=WARN_LOG_&i; 
RUN; 

/******************************************************/ 
/* Log Parsing Error Messages                         */ 
/******************************************************/ 

        %END; /* Ende der Log Parsing Schleife*/ 
    %END; /* Es gibt Logs zu parsen */ 
    %ELSE %PUT ERROR: No Logs with this Timestamp in the Folder; 
    %PUT NOTE: ENDE DER VERARBEITUNG; 
%MEND LOG_ANALYSIS; 

3. SAS Log Parser: kompletter Code

%MACRO LOG_ANALYSIS (PATH='e:\Logs' , FILEEXTENSION="log" , TIMESTAMP="NO");

    DATA TEST;
        PATH=&PATH;
        SUFFIX=SCAN(PATH,-1,'\');
        CALL SYMPUTX('SUFFIX',SUFFIX);
    RUN;

    PROC FORMAT;
        INVALUE MONZUZAHL "Januar"=1 "Februar"=2 "März"=3 "April"=4 "Mai"=5
            "Juni"=6 "Juli"=7 "August"=8 "September"=9 "Oktober"=10 "November"=
            11 "Dezember"=12 other=.;
    RUN;

    data DIRCONTENTS;
        keep Verzeichnis Dateiname Endung PfadundName LetzteAendDat AENDDAT
            ErstellDat ERSTDAT;
        length Verzeichnis $40 fref $8 Dateiname $80 Endung $30 PfadundName $200
            LetzteAendDat $40 ErstellDat $40 ;
        rc=filename(fref, &PATH);

        if rc=0 then do;
            did=dopen(fref);
            rc=filename(fref);
        end;
        else do;
            length msg $200.;
            msg=sysmsg();
            put msg=;
            did=.;
        end;

        if did <= 0 then putlog 'ERR' 'OR: Unable to open directory.';
        dnum=dnum(did);

        do i=1 to dnum;
            Verzeichnis=&PATH;
            Dateiname=dread(did, i);
            Endung=LOWCASE(SUBSTR(Dateiname,Find(Dateiname,".")+1));

            /* Ausgabe falls Eintrag eine Datei ist. Falls eintrag ein Unterverzeichnis, nicht ausgeben.*/
            fid=mopen(did, Dateiname);

            /* Pfad und Dateiname */
            opt=FOPTNAME(fid,1);
            PfadundName=FINFO(fid,upcase(opt));

            /* Letzte Änderung */
            opt=FOPTNAME(fid,5);
            LetzteAendDat=STRIP(FINFO(fid,upcase(opt)));
            AENDDAT=MDY
                (INPUT(SCAN(LetzteAendDat,2),MONZUZAHL.),Substr(LetzteAendDat,1,2),SCAN(LetzteAendDat,3));

            /* Erstelldatum */
            opt=FOPTNAME(fid,6);
            ErstellDat=FINFO(fid,upcase(opt));
            ERSTDAT=MDY
                (INPUT(SCAN(ErstellDat,2),MONZUZAHL.),Substr(ErstellDat,1,2),SCAN(ErstellDat,3));

            if fid > 0 AND UPCASE(Endung)=UPCASE(&FILEEXTENSION) then output;
        end;

        rc=dclose(did);
        Format AENDDAT ERSTDAT DEUDFDD10.;
        LABEL AENDDAT="Datum der letzten Änderung" ERSTDAT="Erstelldatum" ;
    run;

    /*Laden der Dateinamen in Makrovariablen */
    %LET ANZ_LOGS=0;

    DATA TEST2;
        SET DIRCONTENTS;
        CALL SYMPUTX ('LOG_'||LEFT(_N_),PfadundName);
        CALL SYMPUTX ('ANZ_LOGS',_N_);

        %IF &TIMESTAMP NE "NO" %THEN %DO;
            WHERE FIND(Dateiname,&TIMESTAMP)>0;
        %END;
    RUN;

    %IF &ANZ_LOGS > 0 %THEN %DO;
        %DO i=1 %TO &ANZ_LOGS;
            %PUT NOTE: &&LOG_&i wird verarbeitet!;
            FILENAME LOGTEXT "&&LOG_&i";

            /******************************************************/
            /* Log Parsing Rows and Columns                       */

            /******************************************************/
            DATA LOG_TMP;
                LENGTH TEXT $134;
                INFILE LOGTEXT DSD delimiter='|';

                INPUT TEXT $;

                IF FIND(Text,"NOTE:")=0 OR FIND(UPCASE(Text),"THERE WERE")>0
                    THEN DELETE;

                IF FIND(Text,"NOTE:")=0 OR FIND(UPCASE(Text),"DROPPED")>0 THEN
                    DELETE;

                IF FIND(UPCASE(Text),"TABLE")>0 OR
                    FIND(UPCASE(Text),"THE DATA SET")>0 ;
            RUN;

            DATA ERROR_TMP;
                LENGTH TEXT $134;
                INFILE LOGTEXT DSD delimiter='|';

                INPUT TEXT $;

                IF FIND(UPCASE(Text),"ERROR:")>0 ;
            RUN;

            DATA WARNING_TMP;
                LENGTH TEXT $134;
                INFILE LOGTEXT DSD delimiter='|';

                INPUT TEXT $;

                IF FIND(UPCASE(Text),"WARNING:")>0 ;
            RUN;

            DATA LOG_&i;
                SET LOG_TMP;
                LENGTH LOGNAME $100 FULLTABNAME $41 LIBNAME $8 TABNAME $32 ROWS
                    8 COLUMNS 8;
                LOGNAME="&&LOG_&i";

                /* Dateiennamen ermittlen*/
                /* Syntax mit TABLE x.y CREATED WITH XXX ROWS AND XXX COLUMNS*/
                IF FIND(UPCASE(Text),"TABLE")>0 THEN DO;
                    FULLTABNAME=SUBSTR( TEXT, FIND(UPCASE(Text),"TABLE")+6,
                        FIND(UPCASE(Text),"CREATED") -
                        (FIND(UPCASE(Text),"TABLE")+6) );
                    LIBNAME=SUBSTR(FULLTABNAME,1,FIND(FULLTABNAME,".")-1);
                    TABNAME=SUBSTR(FULLTABNAME,FIND(FULLTABNAME,".")+1);
                    ROWS=INPUT( SUBSTR( TEXT, FIND(UPCASE(Text),"WITH")+5
                        /* +5 character count of WITH + 1 blank*/,
                        FIND(UPCASE(Text),"ROWS")-1 /* ROWS - 1 blank*/ -
                        (FIND(UPCASE(Text),"WITH")+5)
                        /* +5 character count of WITH + 1 blank*/ ), BEST32.);
                    COLUMNS=INPUT( SUBSTR( TEXT, FIND(UPCASE(Text),"AND")+4
                        /* +4 character count of AND + 1 blank*/,
                        FIND(UPCASE(Text),"COLUMNS")-1 /* COLUMNS - 1 blank*/ -
                        (FIND(UPCASE(Text),"AND")+4)
                        /* +4 character count of AND + 1 blank*/), BEST32.);
                END;

                /* Syntax mit THE DATA SET x.y HAS XXX OBSERVATIONS AND XXX VARIABLES*/
                ELSE IF FIND(UPCASE(Text),"THE DATA SET")>0 THEN DO;
                    FULLTABNAME=SUBSTR( TEXT, FIND(UPCASE(Text),"SET")+4,
                        /*3 Characters for SET + 1 Blank*/
                        FIND(UPCASE(Text),"HAS")- (FIND(UPCASE(Text),"SET")+4));
                    LIBNAME=SUBSTR(FULLTABNAME,1,FIND(FULLTABNAME,".")-1);
                    TABNAME=SUBSTR(FULLTABNAME,FIND(FULLTABNAME,".")+1);
                    ROWS=INPUT( SUBSTR( TEXT, FIND(UPCASE(Text),"HAS")+4
                        /* +4 character count of HAS + 1 blank*/,
                        FIND(UPCASE(Text),"OBSERVATIONS")-1
                        /* OBSVERBVATIONS - 1 blank*/ -
                        (FIND(UPCASE(Text),"HAS")+4
                        /* +4 character count of AND + 1 blank*/) ), BEST32.);
                    COLUMNS=INPUT( SUBSTR( TEXT, FIND(UPCASE(Text),"AND")+4,
                        FIND(UPCASE(Text),"VARIABLES")-1
                        /* OBSVERBVATIONS - 1 blank*/ -
                        (FIND(UPCASE(Text),"AND")+4)
                        /* +4 character count of AND + 1 blank*/) ,BEST32.);
                END;

                FORMAT _NUMERIC_ COMMAX20.;
            RUN;

            /* Finde Fehler und Warnungen */
            DATA ERR_LOG_&i;
                SET ERROR_TMP;
                LENGTH LOGNAME $100 MESSAGE $500;
                MESSAGE=Text;
            RUN;

            DATA WARN_LOG_&i;
                SET WARNING_TMP;
                LENGTH LOGNAME $100 MESSAGE $500;
                MESSAGE=Text;
            RUN;
            ;

            %IF &i=1 %THEN %DO;

                PROC SQL;
                    CREATE TABLE LOGS_GESAMT_&SUFFIX LIKE LOG_&i;
                    CREATE TABLE ERRORS_GESAMT_&SUFFIX LIKE ERR_LOG_&i;
                    CREATE TABLE WARNINGS_GESAMT_&SUFFIX LIKE WARN_LOG_&i;
                QUIT;

            %END;

            PROC APPEND BASE=LOGS_GESAMT_&SUFFIX DATA=LOG_&i;
            RUN;

            PROC APPEND BASE=ERRORS_GESAMT_&SUFFIX DATA=ERR_LOG_&i;
            RUN;

            PROC APPEND BASE=WARNINGS_GESAMT_&SUFFIX DATA=WARN_LOG_&i;
            RUN;

            /******************************************************/
            /* Log Parsing Error Messages                         */
            /******************************************************/
        %END; /* Ende der Log Parsing Schleife*/
    %END; /* Es gibt Logs zu parsen */
    %ELSE %PUT ERROR: No Logs with this Timestamp in the Folder;

    %PUT NOTE: ENDE DER VERARBEITUNG;
%MEND LOG_ANALYSIS;

3.1 SAS Log Parser Beispielaufrufe

%LOG_ANALYSIS (PATH='e:\Logs', TIMESTAMP="20241018");

Hier ist der Makroparameter  FILEEXTENSION="log" bereits implizit gesetzt.

%LOG_ANALYSIS (PATH='e:\Logs\Lauf1', FILEEXTENSION="txt"  ,TIMESTAMP="20241029");
cross-circleCookie Consent Banner von Real Cookie Banner