Logging in SharePoint Framework solutions

Proper logs are invaluable when your solution isn't working as expected. Here is how you can log information in the SharePoint Framework.

You're blind without logging

When building software it's hard to avoid bugs. The more complex the solution, the bigger the odds that one of its parts will malfunction in a way you did not anticipate, rendering your solution useless.

When building SharePoint customizations on the SharePoint Framework, you take dependencies on the SharePoint Framework itself, a JavaScript library that you might be using, the SharePoint API, Office 365 and maybe even other line of business APIs. Any of these parts can fail on its own, not to mention in combination with each other.

When things aren't working as expected, having access to a proper log, showing you what exactly is happening in your code, makes a huge difference between being in the dark and figuring out what is wrong.

Logging in the SharePoint Framework

In the v1.1 release of the SharePoint Framework Microsoft introduced logging infrastructure that you can use to log information in your solutions. When scaffolding SharePoint Framework extensions you can see how logging works in practice:

import { Log } from '@microsoft/sp-core-library';  
// ...

const LOG_SOURCE: string = 'HelloWorldFieldCustomizer';

export default class HelloWorldFieldCustomizer  
  extends BaseFieldCustomizer<IHelloWorldProperties> {

  @override
  public onInit(): Promise<void> {
    // Add your custom initialization to this method.  The framework will wait
    // for the returned promise to resolve before firing any BaseFieldCustomizer events.
    Log.info(LOG_SOURCE, 'Activated HelloWorldFieldCustomizer with properties:');
    Log.info(LOG_SOURCE, JSON.stringify(this.properties, undefined, 2));
    Log.info(LOG_SOURCE, `The following string should be equal: "HelloWorld" and "${strings.Title}"`);
    return Promise.resolve<void>();
  }
  // ...
}

The Log class offers you four methods that you can use to log information. Each of these methods corresponds to the particular log level, eg: verbose, info, warn and error. The first parameter is always the location from which you're logging. By default it's the name of the web part class, but you could extend it to include the name of the method if you need more information. The next part is the message you want to log. Optionally, you can specify the current service scope.

When debugging SharePoint Framework client-side web parts in the local SharePoint workbench, these log messages are logged to the browser console. A logged message looks like:

***HelloWorldFieldCustomizer: Activated HelloWorldFieldCustomizer with properties:

When debugging solutions in SharePoint Online there is however a slight problem. At this moment, there are no log handlers registered with the SharePoint Framework. As a result, all logged information is lost. Luckily, you can easily change this default behavior, by writing your own logging handler and registering it with the log.

Writing custom SharePoint Framework Log Handler

Writing a custom log handler for use with the SharePoint Framework solutions is easy and comes down to implementing a class with four methods. Consider the following log handler implementation that would log information to the browser console:

import ILogHandler from '@microsoft/sp-core-library/lib/log/ILogHandler';  
import { ServiceScope } from '@microsoft/sp-core-library';

export enum LogLevel {  
  Verbose = 1,
  Info,
  Warning,
  Error
}

export default class ConsoleLogHandler implements ILogHandler {  
  constructor(private logLevel: LogLevel) {
  }

  public verbose(source: string, message: string, scope: ServiceScope | undefined): void {
    this.log(source, message, LogLevel.Verbose, scope);
  }

  public info(source: string, message: string, scope: ServiceScope | undefined): void {
    this.log(source, message, LogLevel.Info, scope);
  }

  public warn(source: string, message: string, scope: ServiceScope | undefined): void {
    this.log(source, message, LogLevel.Warning, scope);      
  }

  public error(source: string, error: Error, scope: ServiceScope | undefined): void {
    this.log(source, error.message, LogLevel.Error, scope);
  }

  private log(source: string, message: string, logLevel: LogLevel, scope: ServiceScope | undefined): void {
    if (this.logLevel > logLevel) {
      return;
    }

    const msg: string = `***${source}: ${LogLevel[logLevel].toUpperCase()} ${message}"`;

    switch (logLevel) {
      case LogLevel.Verbose:
        console.log(msg);
        break;
      case LogLevel.Info:
        console.info(msg);
        break;
      case LogLevel.Warning:
        console.warn(msg);
        break;
      case LogLevel.Error:
        console.error(msg);
        break;
    }
  }
}

The custom log handler implements the ILogHandler interface that defines four logging methods: verbose, info, warn and error. Internally, all these methods call the private log method that performs the actual logging to the browser console. When instantiating the log handler, you specify the current log level. Only messages with the specified severity or higher will be logged.

To use this handler, you have to configure it with the Log class by calling the _initialize method:

import { Log } from '@microsoft/sp-core-library';  
import ConsoleLogHandler, { LogLevel } from './ConsoleLogHandler';  
// ...

const LOG_SOURCE: string = 'HelloWorldFieldCustomizer';

export default class HelloWorldFieldCustomizer  
  extends BaseFieldCustomizer<IHelloWorldProperties> {

  @override
  public onInit(): Promise<void> {
    // Add your custom initialization to this method.  The framework will wait
    // for the returned promise to resolve before firing any BaseFieldCustomizer events.
    Log._initialize(new ConsoleLogHandler((window as any).LOG_LEVEL || LogLevel.Error));
    Log.info(LOG_SOURCE, 'Activated HelloWorldFieldCustomizer with properties:');
    Log.info(LOG_SOURCE, JSON.stringify(this.properties, undefined, 2));
    Log.info(LOG_SOURCE, `The following string should be equal: "HelloWorld" and "${strings.Title}"`);
    return Promise.resolve<void>();
  }
  // ...
}

When initializing the log handler, it's good to allow specifying the log level on runtime. That way you can keep logging limited to errors by default, and increase the logging severity if necessary. In the example above, the logging level can be increased on runtime by assigning the desired log level to the global LOG_LEVEL variable when loading the page. You could also choose to have the logging level persisted in a session storage variable so that you don't need to keep setting it every time you reload the page.

Summary

When debugging solutions beyond the development environment, access to detailed logs is invaluable. SharePoint Framework offers you an API to log information in your solutions. By default, these logging messages are not written anywhere, but by creating a custom log handler, you can have them logged for example to the browser console or an external API.

Comments

comments powered by Disqus