-
frontlogger
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
is a useful wrapper if you want to usefrontlogger
for your React application. -
frontlogger-observer-plugin
is a first plugin forfrontlogger
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.
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.
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.
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
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
})
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 theuseMemo
hook)
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>
</>
)
}
In the examples
folder you could find possible usages.
Sink is a place where the logs are going. We use terminology from Serilog since we like it.
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 })
}
}
}
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})
};
We introduced the new options for remote logging sink that allows to collect multiple requests before sending. It is enabled by default.
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.
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 })
}
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.
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