SPFx – Using PnP Logging

Introduction

Hi Friends, in this post we will see how we can leverage on the logging feature provided by PnP to capture different logs while communicating with SharePoint or for any other features implemented in the component. Let us see how we can make use of logging feature in SPFx web part.

Understanding the Logging concept by PnP

The logging module can be used by installing the npm package named @pnp/logging. This is a very light weight susbscribable and extensive logging framework. There are multiple ways to make use of this feature in our web part, let us see the concept of using Listeners. There are 2 listeners provided by PnP.

ConsoleListener

This is a very simple listener as the name indicates, the log message is written to the browser console. There is no settings for this listener. It writes to corressponding console method based on the log level.

FunctionListener

This listener type will allow us to wrap our own functionality by creating a function that takes a LogEntry as its single argument. This is very useful if we already have a common logging mechanism which we can leverage on it. Using this listener type, the message can be passed to any system or stored in a list or in a file based on our own implementation. The below steps and guidance will use this listener to write the logs in a SharePoint list.

Some of the log levels provided are

  • Verbose
  • Info
  • Warning
  • Error
  • Off

Focus on Code

Let us start by creating a new web part project using yeoman SharePoint generator, before that create a folder where you want to create the web part. Navigate to that folder and run the below command

yo @microsoft/sharepoint

The generator will asks you couple of questions,

  • Enter the webpart name as your solution name, and then select Enter.
  • Select Create a subfolder with solution name for where to place the files.
  • Select Y to allow the solution to be deployed to all sites immediately.
  • Select N on the question if solution contains unique permissions.
  • Enter the webpart name
  • Enter the webpart description
  • Choose the framework as ‘React

Once the project is created, install the required pnp modules from npm using the below command. I had created a webpart with a name PnPLogging.

npm i @pnp/sp @pnp/odata @pnp/logging --save-exact

Open the code in VSCode which is my favorite and flexible code editor for SharePoint Framework. You can directly open the project folder from the file menu or use the below command to open the VSCode from command line.

cd \web part folder\
code .

Lets create a new folder under the components folder named common. Inside the common folder create 2 files as named below

  1. CustomLogger.ts – This file will have all the interfaces and methods for implementing the logging mechanism to write the logs in SharePoint List.
  2. Util.ts – This file will have all the log methods to be implemented in our custom methods to pass certain information to the log entry.

Copy-paste the below code in the CustomLogger.ts file.

import { LogLevel, ILogListener, ILogEntry } from "@pnp/logging";
import { sp } from "@pnp/sp";
import { IWeb, Web } from '@pnp/sp/webs';
import "@pnp/sp/webs";
import "@pnp/sp/site-users";
import "@pnp/sp/lists/web";
import "@pnp/sp/items/list";

export interface ILogData {
    FileName: string;
    MethodName: string;
    StackTrace: string;
}

export class LogData implements ILogData {
    constructor(
        public FileName: string = "",
        public MethodName: string = "",
        public StackTrace: string = ""
    ) { }
}

export interface ILogItem {
    ApplicationName: string;
    CodeFileName: string;
    MethodName: string;
    LoggedOn: Date;
    LoggedById: number;
    ErrorMessage: string;
    StackTrace: string;
}

export class LogItem implements ILogItem {
    constructor(
        public ApplicationName: string = "",
        public CodeFileName: string = "",
        public MethodName: string = "",
        public LoggedOn: Date = new Date(),
        public LoggedById: number = 0,
        public ErrorMessage: string = "",
        public StackTrace: string = ""
    ) { }
}

export default class CustomLogger implements ILogListener {
    private _applicationName: string;
    private _logListName: string;
    private _web: IWeb;
    private _userId: number;
    private _currentuser: string;
    private _writeLogFailed: boolean;

    constructor(applicationName: string, logWebUrl: string, logListName: string, currentUser: string) {
        //Initialize
        try {
            this._writeLogFailed = false;
            this._applicationName = applicationName;
            this._logListName = logListName;
            this._web = Web(logWebUrl);
            this._currentuser = currentUser;
            //this.init(currentUser);
        } catch (err) {
            console.error(`Error initializing CustomLogger - ${err}`);
        }
    }

    private async init(currentUser: string): Promise<number> {
        //Implement an asyncronous call to ensure the user is part of the web where the ApplicationLog list is and get their user id.
        try {
            let userResult = await this._web.ensureUser(`i:0#.f|membership|${currentUser}`);
            return userResult.data.Id;
            //console.log(this._userId);
        } catch (err) {
            console.error(`Error initializing CustomLogger (init) - ${err}`);
        }
    }

    public async log(entry: ILogEntry): Promise<void> {
        try {
            //If the entry is an error then log it to my Application Log table.  All other logging is handled by the console listener
            if (entry.level == LogLevel.Error) {
                if (!this._writeLogFailed) {
                    this._userId = await this.init(this._currentuser);
                    let stackArray = null;
                    if (entry.data.StackTrace && entry.data.StackTrace.length > 0)
                        stackArray = JSON.stringify(entry.data.StackTrace.split('\n').map((line) => { return line.trim(); }));
                    let newLogItem: LogItem = new LogItem(this._applicationName,
                        entry.data.FileName,
                        entry.data.MethodName,
                        new Date(),
                        this._userId,
                        entry.message,
                        stackArray);
                    sp.web.lists.getByTitle(this._logListName).items.add(newLogItem);
                }
            }
        } catch (err) {
            //Assume writing to SharePoint list failed and stop continuous writing
            this._writeLogFailed = true;
            console.error(`Error logging error to SharePoint list ${this._logListName} - ${err}`);
        }
        return;
    }
}

The main artefacts in the above code is

  1. ILogData – This is an interface which is defined to tell what kind of informations are gonna be logged to the list.
  2. ILogItem – This is an interface which is defined to map the columns in the SharePoint Error log list to write the logs to the list.
  3. CustomLogger – The actual class which implements the ILogListener interface
  4. init – This method is implemented to get the current user information while writing the logs to the list.
  5. log – This is the actual method where we will be implementing the actual functionality of parsing the log and write the detailed log info in the SharePoint List.

Copy-paste the below code in the Util.ts file

import { LogLevel, ILogEntry, Logger } from '@pnp/logging';
import { ILogData } from './CustomLogger';

export function writeErrorLog(filename: string, methodname: string, stack: string, loglevel: LogLevel, err: any) {
    let data: ILogData = { FileName: filename, MethodName: methodname, StackTrace: stack };
    let logEntry: ILogEntry = { message: `${err.message}`, level: loglevel, data: data };
    Logger.log(logEntry);
}

This class has only one method named writeErrorLog which we will call this on the catch block of our function. This method inturn communicate with the FunctionListener and write the log to the SharePoint List.

The logger with the custom methods has been implemented and util function to call the method has also been implemented, now let us see how to subscribe to the listener.

Navigate to the <webpart>.ts file and do the following

Import the following modules and class in-addition to the existing imports

import CustomLogger from './components/common/CustomLogger';
import { sp } from "@pnp/sp";
import { Logger, LogLevel, FunctionListener, ILogEntry } from "@pnp/logging";

Under the class, copy-paste the below code along with the boilerplate code.

public onInit(): Promise<void> {        
        return super.onInit().then(_ => {
            sp.setup(this.context);
            this.customLogging();
        });
    }

    private customLogging(): void {
        try {
            let listener = new FunctionListener((entry: ILogEntry) => {
                try {
                    switch (entry.level) {
                        case LogLevel.Verbose:
                            //console.info(entry.message);
                            break;
                        case LogLevel.Info:
                            //console.log(entry.message);
                            break;
                        case LogLevel.Warning:
                            //console.warn(entry.message);
                            break;
                        case LogLevel.Error:
                            let advanceLogging = new CustomLogger("Sample Logging", this.context.pageContext.site.absoluteUrl, "Error Log", this.context.pageContext.user.loginName);
                            Logger.subscribe(advanceLogging);
                            break;
                    }
                } catch (err) {
                    console.error(`Error executing customLogging FunctionListener - ${err}`);
                }
            });
            Logger.subscribe(listener);
        } catch (err) {
            console.error(`Error initializing customLogging - ${err}`);
        }
        return;
    }

In the above code, we are setting up the pnp by providing the context and we are also subscribing our custom logger to the Function Listener.

Navigate to the <webpart>.tsx and copy-paste the below code

import * as React from 'react';
import styles from './PnPLogging.module.scss';
import { IPnPLoggingProps } from './IPnPLoggingProps';
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';

import { LogLevel } from '@pnp/logging';
import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists/web";
import "@pnp/sp/items/list";
import * as util from './common/Util';

export default class PnPLogging extends React.Component<IPnPLoggingProps, {}> {

    constructor(props: IPnPLoggingProps) {
        super(props);
    }

    public _generateError = () => {
        sp.web.lists.getByTitle('Branches').items.select('Title').getAll().then(items => {
            console.log(items);
        }).catch((err: Error) => {
            util.writeErrorLog("PnPLogging.tsx", '_generateError', err.stack, LogLevel.Error, err);
        });
    }

    public _generateAsyncError = async () => {
        try {
            await sp.web.lists.getByTitle('Branches').items.select('Title').getAll();
        } catch (err) {
            util.writeErrorLog("PnPLogging.tsx", '_generateAsyncError', err.stack, LogLevel.Error, err);
        }
    }

    public render(): React.ReactElement<IPnPLoggingProps> {
        return (
            <div className={styles.pnPLogging}>
                <div className={styles.container}>
                    <div className={styles.row}>
                        <div className={styles.column}>
                            <DefaultButton onClick={this._generateError} text="Generate Error" />
                            <DefaultButton onClick={this._generateAsyncError} text="Generate Async Error" />
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

The above code is very simple, I have created 2 buttons to call the method which will get the items from the list that doesn’t exist. The first method will use the pnp method with then and catch implementation, the second method will use the same pnp method with async and await.

Error Log list

Before running the above solution or deploying the package, create a list named ‘Error Log‘ with the following fields. Make sure to maintain the same field name.

  • ApplicationName – Name of the application from which the logs fired
  • CodeFileName – Name of the file from where the exception occurred.
  • MethodName – Name of the method from where the exception occurred.
  • ErrorMessage – Friendly message of the exception.
  • StackTrace – Complete details of the exception message.
  • LoggedBy – Current logged in user
  • LoggedOn – Current date and time when the exception occurred.

Preview

Source Code

The source code along with other samples can be found in the below github link.

SPFx-Demos

Happy Coding…

Advertisement

15 thoughts on “SPFx – Using PnP Logging

      • Thanks for the response Sudharshan. you are logging the errors when user click on Generate Error button. the same thing is working in my solution also but what I want is I want to call the _generateError at page load.
        You are calling _generateError on button click. I am calling it on page load as below but at page load it is not logging the error to Error log list you can check with your example too.

        public componentDidMount() {
        _generateError();
        }

        Liked by 1 person

      • Hi Dinesh, I have tested the solution by generating the error using the ComponentDidMount method and it works as expected. The solution has been updated in github repo, please have a look at it and let me know if you have any clarifications.

        Liked by 1 person

    • Hi Dinesh, if you compare the old and the new code, the only difference is made is added the ComponentDidMount method to call the generateError method. Can you please send me a screenshot of the issue and also can you please pull a fresh copy from github and try it. Let me know the outcome.

      Liked by 1 person

      • Hi Sudharsan, yes i downloaded the latest version and it’s working fine as expected. But I have developed another webpart in that I am putting everything as is and it it not working there on page load but working on button click properly can we connect on teams or any desk so that i can show you the exact problem. my contact no is 8275942761. can you please share yours so that we can connect

        Liked by 1 person

  1. Hi Sudharsan, let me thank you for the great article, it was exactly what I was looking for. But there is one thing that I have notice. When you clicked on any button in order to raise the error then it is handled properly and as a result there is an entry in Error Log list created. But once you click it for the second time 5 new list entries are added, if you click it for the 3rd time then 9 new entries are added etc.
    I tried to figure out what causes that but I failed completely, maybe you can help me and others with that … thank you

    Like

    • Seems that I have managed to find the bug. The above reported behaviour is caused by the following code that lives in .ts

      const listener = new FunctionListener((entry: ILogEntry) => {
      switch (entry.level) {
      …..
      case LogLevel.Error:
      let advanceLogging = new CustomLogger(“Sample Logging”, this.context.pageContext.site.absoluteUrl, “Error Log”, this.context.pageContext.user.loginName);
      Logger.subscribe(advanceLogging);
      break;

      Logger.subscribe() must be moved outside the listener definition

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s