@dodobrands/frontlogger
TypeScript icon, indicating that this package has built-in type declarations

6.6.0 • Public • Published

Frontlogger

Table of Contents

Packages

  • frontlogger npm allows robust logging with the emphasis on extensibility, strict type-checking and multiple sinks allowing you to follow best practices of centralizing logging. It has zero dependencies and relies on the tools your browser can support.

  • react-logger npm is a useful wrapper if you want to use frontlogger for your React application.

  • frontlogger-observer-plugin npm is a first plugin for frontlogger that implements Reporting API support to automatically log browser deprecations and security issues.

We have plans for the further ecosystem, such as Web Vitals collection and OpenTelemetry support.

Motivation

JS ecosystem has wonderful logging libraries like Winston. Unfortunately, it seemed not enough for us in the following things:

  • Centralized logging standards. Initially we had more expertize on back-ends, and when we needed things like scopes or enrichments, we simply could not find the library that does it in the way to want to that will enforce best practices - this is especially important when you follow the micro-frontends way.
  • Robust TypeScript support. We at Dodo ❤️ Typescript, so we wanted it to be with us every step of the way.
  • Dependency-awareness. We have 0 dependencies and weight 10 times less than winston, and that might not be the limit.
  • Plug-in ecosystem. We do not want to create the most multi-tooled logger in the world. Our logger has simply enough to ease the required goal: safely deliver detailed logs to developers. You might need something else: analytics data, integration with your ecosystem, other 3rd party, etc. We would love the community around the logger to grow, but we're always be vigilant about scope or the core packages and focus to pluginize everything such as sinks.

Why not Sentry

Logging is essential for your apps. While the market has big guys like LogRocket or Sentry, which are awesome and robust tools, sometimes it could cost too much than you can afford. Or you already have a place where all the logs should flow with configured tools to access and visualize it and you're not ready to simply add one more. Than you can ramp up your own server as oTel Exporter or configure your own existing solutions.

This client allows you to safely connect to it while encapsulating and preserving the standards of doing that for all frontends in your organization.

Frontlogger support NodeJS as well, which can be suitable for your desktop apps like Electron. You can use it in server NodeJS while using the console sink which would be stdout, but this is not the main goal - other loggers as Pino or Bole are way more focused on that.

Installation

yarn add @dodobrands/frontlogger
yarn add @dodobrands/react-logger

npm install @dodobrands/frontlogger
npm install @dodobrands/react-logger

pnpm add @dodobrands/frontlogger
pnpm add @dodobrands/react-logger

Usage

Base client

This is an example your could use to ramp up the logger.

import { consoleSink, Logger, LogLevel, RemoteSink, Sink, useSink } from '@dodobrands/frontlogger'
import { getLoggerUrl } from '../settings'

const remoteSinks: Sink[] = process.env.NODE_ENV === 'development'
	? []
	: [new RemoteSink({
		url: getLoggerUrl(),
		defaultLogLevel: LogLevel.Info
	})]

export const logger = new Logger({
	applicationName: 'your-frontend-app',
	sinks: [useSink(consoleSink), ...remoteSinks],
	environmentName: global.window.location.hostname
})

React logger

import React, { useMemo } from 'react'
import { LoggerContextProvider, LogLevel } from '@dodobrands/react-logger'

const WeatherApp = () => {
    const applicationParams = useMemo(() => ({
        applicationName: 'Weather app',
        /**
         * You can get this flag from your environment
         *
         * If this value is true, logger will send logs to the server only
         * If this value is false, logger will send logs to the browser console only
         *
         * true as a default
         */
        isProduction: env.production,
        // required param for sending logs
        remoteUrl: 'https://logger.dev/',
        // true as a default. Disable if you need to immediately send all your logs in a short period of time
        batchEnabled: false,
        // window.location.host as a default
        environmentName: "weather-app.ru",
        debounceTimeout: 3,
        messagesCountForForceSending: 10,
        // LogLevel.Info as a default
        productionLogLevel: LogLevel.Info,
        // LogLevel.Debug as a default
        developmentLogLevel: LogLevel.Debug,
    }), []);
    
    return (
        // Also, your could use customized logger instance as a LoggerContextProvider prop without specifying applicationParams
        <LoggerContextProvider applicationParams={applicationParams}>
            <h1>Weather app</h1>
        </LoggerContextProvider>
    )
}

NOTICE: applicationParams must be a single instance. So, if root component renders several times, you should memoize it (e.g. use the useMemo hook)

Usages outside of components

You can use logger outside of components (redux-thunk or redux-saga) Create logger instance in your application from the root package or use createLoggerInstanceRef method.

For example:

import { createLoggerInstanceRef } from '@dodobrands/react-logger';

export const logger = createLoggerInstanceRef({
	applicationName: `your-app`,
	isProduction: import.meta.env.PROD,
	remoteUrl: window.initParams?.frontendLoggerUrl ?? `https://your-other-url.com`,
});

Next, put logger instance in provider

import { LoggerContextProvider } from '@dodobrands/react-logger'
import { logger } from 'shared/lib/logger';

const WeatherApp = () => (
    <LoggerContextProvider loggerInstanceRef={logger}>
        <h1>Weather app</h1>
    </LoggerContextProvider>
)

Then, use useLogger hook in target component.
Pass param in useLogger. It will be area attribute in the report.

import React, { useState } from 'react'
import { useLogger } from '@dodobrands/react-logger'

const ContactsList = () => {
    const secondName = 'DodoPizzavich'
    const { logInfo, logError } = useLogger('Contacts list')
    
    const [contacts, setContacts] = useState([])
    
    async function handleGetContacts() {
        try {
            // example.
            const result = await getContacts(secondName)
            
            setContacts(result.data)
            /**
             * logInfo.
             *
             * @param readable message.
             * @param additionalInfo.
             */
            logInfo('get contacts successfully', secondName)
        } catch (error) {
            /**
             * logError.
             *
             * @param readable message.
             * @param error/custom-error object.
             * @param additionalInfo.
             */
            logError('get contacts error', error, secondName)
        }
    }
    
    return (
        <>
            <button onClick={handleGetContacts}>Get contacts</button>
            <ol>
                {contacts.map(contact => (
                    <li>{contact}</li>
                ))}
            </ol>
        </>
    )
}

Examples

In the examples folder you could find possible usages.

Internal details

Sinks

Sink is a place where the logs are going. We use terminology from Serilog since we like it.

Use object sinks
import { ConsoleSink } from "@dodobrands/frontlogger";

const settings: LogSettings = {
  applicationName: "some-service",
  sinks: [
    new ConsoleSink({
      defaultLogLevel: LogLevel.Warn,
    }),
  ],
}

In order to create your own sink, you need to implement SinkObject, like this:

import { LogLevel,  Message, Sink, SinkSettings } from "@dodobrands/frontlogger";

export class ConsoleLogSink implements SinkObject {
  constructor(public settings: SinkSettings) {}

  log(message: Message, level: LogLevel): void {
    if (level >= this.settings.defaultLogLevel) {
      console.log({ message, level })
    }
  }
}
Use hook-like sinks

You can use useSink syntax like this:

import { consoleSink, useSink } from "@dodobrands/frontlogger";

const settings: LogSettings = {
  applicationName: "some-service",
  sinks: [
    useSink(consoleSink, {
      defaultLogLevel: LogLevel.Warn,
    }),
  ],
};

In order to create your consoleSink, you need to implement FSink, like this:

import { FNonStructuredSink, FStructuredSink, LogLevel } from "@dodobrands/frontlogger";

export const consoleSink: FStructuredSink = () => ({
  [LogLevel.All]: (message) => console.debug(message),
  [LogLevel.Trace]: (message) => console.trace(message),
  [LogLevel.Debug]: (message) => console.debug(message),
  [LogLevel.Info]: (message) => console.info(message),
  [LogLevel.Warn]: (message) => console.warn(message),
  [LogLevel.Error]: (message) => console.error(message),
  [LogLevel.Fatal]: (message) => console.error(message),
  [LogLevel.Off]: (_) => {},
});

// or like this
export const consoleSink2: FNonStructuredSink = (message: Message, level: LogLevel) => {
  console.log(`${level}: ${message.content})
};

Remote data transfer

We introduced the new options for remote logging sink that allows to collect multiple requests before sending. It is enabled by default.

remote-schema

const remoteSink = new RemoteSink({
				url,
				batchOptions: {
					enabled: true,
    // if you set it to `false`, log will be sent on each invocation and other params are ignored.
					debounceMs: 2000, 
    // a sliding window interval in milliseconds before triggering the new sending.
    // After the window is closed, logs for the window interval are sent.
    // If the logging call happens before the window is closed, sliding window expands for more debounceMs

					messagesCountForForceSending: 10,
    // A mechanism to compensate to ever-expanding interval for frequent logging. If log messages in the queue becomes equal to `messagesCountForForceSending`, we send the logs anyway.
				},
				defaultLogLevel: LogLevel.Info,
			}),

By default we use navigator.sendBeacon(). If not available, we fallback to plain XHR.

Scopes

You can add scope when your create the logger. Like this:

import { version } from './package.json'

const logger = new Logger({
    applicationName: "some-service",
    sinks: [useSink(consoleSink)]
}, { appVersion: version }) // <- scope

You can also add scoped loggers for specific places, like this:

const serviceLogger = logger.createScope({ area: 'acceptOrderService' })

function acceptOrder(orderId: string) {
    const acceptOrderLogger = serviceLogger.createScope({ orderId })
    const { amount } = receivePayment()
    acceptOrderLogger.info('Payment received', { amount })
}

Plugins

You can add your own middleware plugins to our app, like this:

import { Logger, useSink, consoleSink } from '@dodobrands/frontlogger'

const logger = new Logger({
	applicationName: 'your-frontend-app',
	sinks: [useSink(consoleSink), ...remoteSinks],
	environmentName: global.window.location.hostname,
	plugins: [new MyPlugin({ some: 'property' })]
})

You can implement your own plugins, like this:

export const MyPlugin: FrontloggerPlugin<MyConfig> = {
		config,
		onStart: getLogger => {
			getLogger().info('testPlugin started`)
		},
		onBeforeLog: () => console.debug(`onBeforeLog`),
		onAfterLog: () => console.debug(`onAfterLog`),
	})
};

The MyConfig type extends DefaultPluginConfig, which contains default values for scope and id.

Local development

We use yarn with yarn workspaces and PnP. Each package can be debugged independently. The versions of both packages are synchronized for convenience.

So, yarn build would allow to run the examples using yarn start.

Commands:

  • yarn build to build everything
  • yarn test to run tests
  • yarn eslint to run linter

Readme

Keywords

none

Package Sidebar

Install

npm i @dodobrands/frontlogger

Weekly Downloads

21

Version

6.6.0

License

MIT

Unpacked Size

145 kB

Total Files

11

Last publish

Collaborators

  • dodopizza