Felhantering av script i ServiceNow

För långsiktig, och varför inte kortsiktig, stabilitet i ServiceNow bör man ha en bra felhantering i scripten man utvecklar. Här beskriver jag hur man kan skapa ett standardiserat sätt för detta i plattformen för att inte behöva återskapa hjulet hela tiden samt för att alla ska ha samma sätt att arbeta på.

Allt i denna artikel är gjort i Orlando.

Lösningen består av följande:

  • En Script Include

  • Ett UI Script

  • Ett Scheduled Script

Exempel för användning från både klientsida och serversida är beskrivna nedan.

I Script Include och UI Script finns javaklasser/API definierade som används för att på ett standardiserat sätt logga fel som uppstår i script man utvecklat. Denna loggning hamnar i error logg (‘syslog’).

För att man inte ska behöva gå in och titta varje dag om det kommit några nya fel så skapar man ett schemalagt script som kollar om det dykt upp några nya fel. Har det gjort det skapas en incident med information om de fel som uppstått och läggs på rätt CI och rätt grupp.

Lösningen är gjord på det sätt att det inte är någon skillnad på hur man använder API:t på klient eller serversidan.

Format på information i error log

Outputen i loggen kan exempelvis se ut så här:

[ERROR][Error type: RangeError][Script type: Client Script][Script: Change Assigned To][Function: onChange][Other info: n/a][Thrown error message: toPrecision() argument must be between 1 and 100]

Beskrivning i den ordning de loggas:

[<nivå>] 		[ERROR] 		 

INFO, WARNING, ERROR | Notera att man anger manuellt detta värde i koden och det sker ingen validering.

[<feltyp>] 		[RangeError]

Här skrivs felet in som man hämtar från objekt “Error” som kastas av systemet.

[<scripttyp>] 		[Client Script]

Vilken typ av script det är så man lätt ska hitta det i systemet

[<namn>] 		[Change Assigned To]

Namnet på det script, affärsregel, etc. så man lätt ska hitta var felet kastades.

[<funktionsnamn>] 	[onChange]

Namnet på den funktion i vilken felet kastades.

[<övrig info>] 	[xxx]

Övrig information som man vill logga hamnar i det här fältet. Detta används troligtvis om man har fel som är återkommande och man behöver mer information vid debugging.

[<info om fel>] 	[Thrown error message: toPrecision() argument must be between 1 and 100]

Felmeddelande som systemet skickar med i det felobjekt som kastas.

Script Include

Först börjar vi titta på den Script Include som anropas från något script på serversidan eller från lösningens UI Script.

Som du ser i bilden nedan så jobbar vi i globala scopet då detta är något som ska kunna användas i hela plattformen. Den måste också vara anropbar från klienten.

Script Include scLogginUtil

Script Include scLogginUtil

Koden nedan kan du kopiera in i en script include som du döper till “scLoggingUtil”. I övrigt har jag dokumenterat vad som händer i koden i scriptet.

Vill du läsa mer om script includes och GlideAjax kan du läs här:

Script Include

GlideAjax

Script Include scLoggingUtil:

/** @class      scLogUtil       Utility class for handling logging */
var scLoggingUtil = Class.create();
scLoggingUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    /** Handles logging requests from the client*/
    logClientError: function() {
        try {
            
            //We need the data from the client to be put in the same ojbect structure as if the data was coming from a server side script and also mapped to some of the data coming with a thrown error (e.g. .name and .message). Do note that since a thrown error is an object we use object here instead of array.
            var parameters = {};
            parameters.name = this.getParameter('sysparm_error_name');
            parameters.level = this.getParameter('sysparm_level');
            parameters.typeOfScript = this.getParameter('sysparm_type_of_script');
            parameters.nameOfScript = this.getParameter('sysparm_name_of_script');
            parameters.nameOfFunction = this.getParameter('sysparm_name_of_function');
            parameters.otherInfo = this.getParameter('sysparm_other_info');
            parameters.message = this.getParameter('sysparm_message');

            this.logError(parameters);

            return true;

        } catch (err) { //In the unlikely event that something goes wrong in logClitenError(), we do a print to the error log directly from here

            gs.logError("[ERROR][" + err.name + "][Script include][scLoggingUtil][logClientError][" + err.message + "]");
            return false;

        }
    },

    /**
     *Print the error to the error log
     *@param        err     An object containing a defined set of information about the error. 
     *                      err.level: INFO, WARNING, ERROR. 
     *                      err.name: Type of error caught by a thrown error or defined manually. Preferable the first on if possible. 
     *                      err.typeOfScript: Client Script, Script Include, Business Rule, etc. 
     *                      err.nameOfScript: The actually name of the entitey where the script retains. 
     *                      err.nameOfFunction: A script can contain multiple functions. This defines the function where the error was thrown. 
     *                      err.otherInfo: In case there is a need, mainly for debuggin purpose, to post additional information to the error log, this information can be added to otherInfo.
     *                      err.message: The message thrown by the error. 
     */
    logError: function(err) {

        //The actually writing to the error log
        gs.logError("[" + err.level + "]" + 
                    "[Error type: " + err.name + "]" + 
                    "[Script type: " + err.typeOfScript + "]" + 
                    "[Script: " + err.nameOfScript + "]" + 
                    "[Function: " + err.nameOfFunction + "]" +
                    "[Other info: " + err.otherInfo + "]" + 
                    "[Thrown error message: " + err.message + "]");

    },

    type: 'scLoggingUtil'

});

UI Script

För att kunna använda denna funktion från klienten måste vi också ha ett UI Script som anropar ovan Script Include. Klienten anropar funktion logClientError() (om man anropar från script på serversida anropar man logError() direkt). Detta är inbakat i API:t och inget vi egentligen behöver tänka på när vi använder API:t.

UI Script ska vara globalt då det ska kunna nås från hela plattformen. I detta fall har jag bara brytt mig om Desktop som UI Type.

UI Script scLoggingUtil

UI Script scLoggingUtil

Koden nedan kan du kopiera in i ett UI Script som du döper till “scLoggingUtil”. I övrigt har jag dokumenterat vad som händer i koden i scriptet.

Vill du läsa mer om UI Script kan du gör det här:

UI Script

/** @class      scLogUtil       Utility class for handling logging */
var scLoggingUtil = Class.create();

scLoggingUtil.prototype = {

    initialize: function(){

    },

    /**Prepares the error data and sends it to the server
    *@param             level           INFO, WARNING, ERROR
    *@param             err             Thrown error object
    *@param             typeOfScript    The type of script where the error was thrown
    *@param             nameOfScript    The name of the script where the error was thrown
    *@param             nameOfFunction  Name of the function where the error was thrown 
    *@param             otherInfo       If additional information is needed, mainly for debuggin purpose, that information can be added here. 
    *
    */
    logError: function(err, level, typeOfScript, nameOfScript, nameOfFunction, otherInfo) {
        try{
            var ga = new GlideAjax('scLoggingUtil');        
            ga.addParam('sysparm_name', 'logClientError');
            ga.addParam('sysparm_message', err.message);
            ga.addParam('sysparm_error_name', err.name);
            ga.addParam('sysparm_type_of_script', typeOfScript);
            ga.addParam('sysparm_name_of_script', nameOfScript);
            ga.addParam('sysparm_name_of_function', nameOfFunction);
            ga.addParam('sysparm_other_info', otherInfo);

            ga.addParam('sysparm_level', level);
            ga.getXML(responseHandler);

        }catch(err){//In the unlikely event that something goes wrong in above function, we do a print to the concole  directly from here

            jslog("[ERROR]" + 
                  "[Error type: " + err.name + "]" + 
                  "[Script type: UI Script]" + 
                  "[Script: scLoggingUtil]" + 
                  "[Function: logError->catch]" +
                  "[Thrown error message: " + err.message + "]");

        }
        /**This callback function handles the response from the server. If everyhting works fine we do nothing. If something goes wrong on the server side (and answer is false) we print an error message to the console. 
        */
        function responseHandler(response) {
            var answer = response.responseXML.documentElement.getAttribute("answer");

            //Do note that the result is returned as a string, not a bool
            if(answer == "false"){
                jslog("[ERROR]" + 
                      "[Error type: Problem logging error in error log]" +
                      "[Script type: UI Script]" +
                      "[Script: scLogingUtil]" +
                      "[Function: logError->responseHandler]");
            }
        }
    },

    type: 'scLoggingUtil '


};

Användning från klientsidan

För att anropa scLoggingUtil från ett, exempelvis, Client Script gör man så här. “err” är det kastade felobjektet: :

var logUtil = new scLoggingUtil();      
logUtil.logError(err, "ERROR", "Client Script","Change Assigned To", "onChange", "n/a");

Du kan också anropar funktionen direkt:

scLoggingUtil.prototype.logError(err, "ERROR", "Client Script","Change Assigned To", "onChange", "n/a");


Se beskrivningen av funktion logError i UI Script scLoggingError ovan.

Om du har skapat både UI Script och Script Include enligt ovan kan du testa funktionen genom följande exempel:

1. Skapa ett Client Script för incident.

2. Sätt Type = onChange

3. Sätt Field name till Assigned to

4. Se till att den är Global

5. Kopiera in följande script och spara:

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
    if (isLoading || newValue === '') {
        return;
    }

    try{
        //As an example, we trigger an error on purpose. toPrecision() cannot be 500. 
        var num = 1;
        num.toPrecision(500); 
        
    }catch(err){
        //Let's catch the error and log it
        var logUtil = new scLoggingUtil();
        logUtil.logError(err, "ERROR", "Client Script","Change Assigned To", "onChange", "n/a");

    }
}

6. Gå till en existerande incident (om den redan är öppen ladda om så att scripten laddas).

7. Ändra Assigend To. Du behöver inte spara.

8. Gå till Error Log (“Errors” i menyn).

9. Sortera så senaste “Created” är högst upp. Nu bör du se något som detta i loggen:

[ERROR][Error type: RangeError][Script type: Client Script][Script: Change Assigned To][Function: onChange][Other info: n/a][Thrown error message: toPrecision() argument must be between 1 and 100]

Användning från serversidan

För att anropa Script Include scLoggingUtil från, exempelvis, en Business Rule gör man så här. “err” är det kastade felobjektet:

var logUtil = new scLoggingUtil();
logUtil.logError(err);

Du kan också anropa funktionen direkt:

scLoggingUtil.prototype.logError(err);

Se beskrivningen av funktion logError i Script Include scLoggingError ovan.

Om du har skapat Script Include enligt ovan kan du testa funktionen genom följande exempel:

1. Skapa en Business Rule för incident. D.v.s. sätt Table = Incident

2. Välj “When” till “Before”

3. Markera endast “Update”

4. Markera “Advanced”

5. Kopiera in följande script:

(function executeRule(current, previous /*null when async*/ ) {

    try {
        //As an example, we trigger an error on purpose. toPrecision() cannot be 500. 
        var num = 1;
        num.toPrecision(500);

    } catch (err) {

        //Prepare the data to be logged
        err.typeOfScript = "Business Rule";
        err.nameOfScript = "convertNum";
        err.level = "ERROR";
        err.nameOfFunction = "executeRule";
        err.otherInfo = "n/a";

        //Access script include scLoggingUtil and log the error
        var logUtil = new scLoggingUtil();
        logUtil.logError(err);


    }

})(current, previous);

6. Öppna eller gå till incidenten du ska testa med.

7. Ändra något och spara. Ändra inte Assigned To om du inte vill trigga ytterligare fel (om du har gjort klientövningen ovan).

8. Gå till Error Log (“Errors” i menyn).

9. Sortera så senaste “Created” är högst upp. Nu bör du se något som detta:

[ERROR][Error type: RangeError][Script type: Business Rule][Script: convertNum][Function: executeRule][Other info: n/a][Thrown error message: Precision 500 out of range.]

Samla ihop felen till en incident m.h.a. Scheduled Script

Det är inte till så stor nytta att ha ett gemensamt sätt att jobba med felhantering om inte felen upptäcks och kan hanteras. Därför är det bra med ett automatiskt jobb som kollar om det dykt upp några nya fel senaste dygnet och skapar en incident med information om felen. Notera att vi skapar max en incident per dygn men alla fel listas i den incidenten. Tänk om något fel uppstår flera hundra gången under ett dygn, jag tycker inte vi behöver ha flera hundra incidenter för det felet då. Antal gånger som felet uppstått kommer ändå stå i incidenten.

1 Skapa ett Scheduled Script

2. Markera “Conditional”.

3. Kopiera in följande kod till “Condition”. Detta för att vi inte behöver köra en del kod om det inte finns några nya fel:

//Only run this if there are any new errors in the error log to collect
answer = checkNewErrors();

function checkNewErrors() {
    try {
        var gr = new GlideRecord('syslog');
        //The query is the same as in the run script. In this request, were are checking to se if there are any errors from yesterday. Depending on when you run the scheduled job you may need to change the request query. Related error messages start with '[ERROR]' in this case. 
        gr.addEncodedQuery("sys_created_onONYesterday@javascript:gs.beginningOfYesterday()@javascript:gs.endOfYesterday()^level=2^messageSTARTSWITH[ERROR]");
        gr.query();
        return gr.hasNext();
    } catch (err) {
        var logUtil = new scLoggingUtil();
        logUtil.logError(err, "ERROR", "Scheduled job", "Create incident on error", "Condition", "n/a");

    }
}

4. Kopiera in följande kod till “Run this script”:

collectErrors();

//If there are any new errors, create an incident with information about the errors
function collectErrors() {
    try {

        //In this request, were are getting errors from yesterday. Depending on when you run the scheduled job you may need to change the request query. Related error messages start with '[ERROR]' in this case. 
        var logGa = new GlideAggregate('syslog');
        logGa.addEncodedQuery("sys_created_onONYesterday@javascript:gs.beginningOfYesterday()@javascript:gs.endOfYesterday()^level=2^messageSTARTSWITH[ERROR]");
        //We aggregate and group the messages so we don't get several rows for the same type of error.
        logGa.addAggregate('count');
        logGa.orderByAggregate('count');
        logGa.groupBy('message');
        logGa.query();
        var errors = "";
        //Loop through the result and create the text for the description field containing the number of occurences of the message and the message itself.
        while (logGa.next()) {
            var messageCount = logGa.getAggregate('count');
            errors += messageCount + "::" + logGa.message + "\n";
        }


        //Create incident

        //We want to assign the incident to the support group of ServiceNow. Hence, we need to get the CI which the support group is related to. You can use the sys_id of the support group directly but if you change supportgroup for the CI ServiceNow the incorrect support group will be assigned to the incident. 
        var grCI = new GlideRecord('cmdb_ci');
        var groupAssigned = grCI.get('869e6e47939f31003b4bb095e57ffbea');

        //Create the new incident. In this case we defined Short Description, Description, CI and Assignment Group. You may want to change/add data for the new incident. E.g. if default priority is not the one you want you may define the priority as well. 
        gr = new GlideRecord('incident');
        gr.initialize();
        gr.short_description = "Errors collected from the error log";
        gr.description = errors;
        gr.cmdb_ci = "869e6e47939f31003b4bb095e57ffbea";
        if (groupAssigned) {
            gr.assignment_group = grCI.support_group;
        }

        gr.insert();

    } catch (err) {
        var logUtil = new scLoggingUtil();
        logUtil.logError(err, "ERROR", "Scheduled job", "Create incident on error", "Run this script", "n/a");
    }
}

4. Du måste ändra de sys_id som är definierade i koden så de matchar den instans du jobbar med. I raden med grCI.get är det sys_id för det CI som gäller för ServiceNow (eller dom du vill använda något annat CI). Vi behöver en Support Group som är kopplad till det CI som används annars blir det problem när incidenten ska tilldelas en grupp.

Du kan så klart definiera vilka parametrar som ska sättas till vad enligt eget önskemål.

5. Spara

6. För att test behöver vi inte definiera något schema. Klicka på “Execute Now”.

7. Gå till incidenterna. Sortera så senaste incidenten kommer högst upp.

8. När du öppnar den ska den se ut liknande denna. Antal förekomster av listat fel står längs till vänster om själva felet:

incident.png

Sen är det bara att reda ut vad som blivit fel och fixa till det.

För att starta schemaläggningen definierar du schemat i Scheduled Script. Ovan lösning letar efter fel “Yesterday”. Rimligt är att köra jobbet mellan midnatt och start av arbetsdag så man har eventuella fel i en incident när man kommer till jobbet, kanske 01:00:

Schemaläggning av Scheduled Script

Schemaläggning av Scheduled Script