Monitors

Modern.js 是一个全栈框架,它可以同时支持客户端和服务端的开发。当 Modern.js 在服务端渲染页面时,框架会在运行时插入更多日志与指标,帮助开发者在线上运维时定位问题。

但服务端代码运行在 Node.js 环境中,开发者无法直接通过浏览器控制台来排查问题,而不同的项目可能使用不同的日志库,或是将数据上报到不同的平台。框架无法覆盖所有的场景,因此需要有统一的方式,允许开发者自行管理所有的内置日志与指标。

Monitors 是 Modern.js 提供的帮助开发者监控应用程序的运行情况的模块。它包含注册 Monitor 和分发监控事件两部分能力。当开发者中调用 Monitors 的某个 API 时,框架会对应的监控事件分发到所有注册的 Monitor 中。

Note

在 Modern.js 中内置了默认的 Monitor,开发者也可以注册自定义的 Monitor。

Monitors 定义

Monitors 模块的类型定义如下,其中 push 方法用于注册 Monitor,其他方法用于分发监控事件。

export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
export interface LogEvent {
  type: 'log';
  payload: {
    level: LogLevel;
    message: string;
    args?: any[];
  };
}

export interface TimingEvent {
  type: 'timing';
  payload: {
    name: string;
    dur: number;
    desc?: string;
    args?: any[];
  };
}

export interface CounterEvent {
  type: 'counter';
  payload: {
    name: string;
    args?: any[];
  };
}

export type MonitorEvent = LogEvent | TimingEvent | CounterEvent;

export type CoreMonitor = (event: MonitorEvent) => void;

export interface Monitors {
  // 注册 Monitor
  push(monitor: CoreMonitor): void;

  // 日志事件
  error(message: string, ...args: any[]): void;
  warn(message: string, ...args: any[]): void;
  debug(message: string, ...args: any[]): void;
  info(message: string, ...args: any[]): void;
  trace(message: string, ...args: any[]): void;

  // 指标事件
  timing(name: string, dur: number, ...args: any[]): void;
  counter(name: string, ...args: any[]): void;
}

内置 Monitor

Modern.js 内置了默认的 Monitor,不同的事件会触发内置 Monitor 不同的行为。详细内容查看:

注册 Monitors

开发者可以通过 Monitors 的 push API 注册自己的 Monitor,但只能在服务端中间件服务端插件中注册,在 Data Loader、组件、init 函数中无法注册。

server/modern.server.ts
import {
  defineServerConfig,
  type MiddlewareHandler,
} from '@modern-js/server-runtime';
import type { MonitorEvent } from '@modern-js/types';

const injectMonitorMiddleware: MiddlewareHandler = async (c, next) => {
  const monitors = c.get('monitors');
  const myMonitor = (event: MonitorEvent) => {
    if (event.type === 'log') {
      // 处理日志事件
      console.log(`[${event.payload.level}] ${event.payload.message}`);
    } else if (event.type === 'timing') {
      // 处理性能指标事件
      console.log(`Timing: ${event.payload.name} = ${event.payload.dur}ms`);
    } else if (event.type === 'counter') {
      // 处理计数事件
      console.log(`Counter: ${event.payload.name}`);
    }
  };
  monitors.push(myMonitor);
  await next();
};

export default defineServerConfig({
  middlewares: [
    {
      name: 'inject-monitor',
      handler: injectMonitorMiddleware,
    },
  ],
});

在服务端插件中注册 Monitor:

server/plugins/my-monitor-plugin.ts
import type { ServerPlugin } from '@modern-js/server-runtime';
import type { MonitorEvent } from '@modern-js/types';

const myMonitorPlugin = (): ServerPlugin => ({
  name: '@my-org/my-monitor-plugin',
  setup(api) {
    api.onPrepare(() => {
      const { middlewares } = api.getServerContext();

      // 定义 monitor,确保只创建一次
      const myMonitor = (event: MonitorEvent) => {
        if (event.type === 'log') {
          // 处理日志事件
          console.log(`[${event.payload.level}] ${event.payload.message}`);
        } else if (event.type === 'timing') {
          // 处理性能指标事件
          console.log(`Timing: ${event.payload.name} = ${event.payload.dur}ms`);
        } else if (event.type === 'counter') {
          // 处理计数事件
          console.log(`Counter: ${event.payload.name}`);
        }
      };

      // 使用标志确保 monitor 只注册一次
      let monitorRegistered = false;

      middlewares.push({
        name: 'inject-monitor',
        handler: async (c, next) => {
          const monitors = c.get('monitors');
          // 只在第一次请求时注册 monitor
          if (!monitorRegistered) {
            monitors.push(myMonitor);
            monitorRegistered = true;
          }
          await next();
        },
      });
    });
  },
});

export default myMonitorPlugin;

然后在 server/modern.server.ts 中配置插件:

server/modern.server.ts
import { defineServerConfig } from '@modern-js/server-runtime';
import myMonitorPlugin from './plugins/my-monitor-plugin';

export default defineServerConfig({
  plugins: [myMonitorPlugin()],
});

调用 Monitors

Modern.js 允许开发者在 Data Loader、组件中调用 Monitors。

Tip

只有在 Node.js 环境才可以调用 Monitors,在浏览器环境调用无任何效果。

在 Data Loader 中,开发者可以这样使用:

routes/page.data.ts
import type { LoaderFunctionArgs } from '@modern-js/runtime/router';
import { getMonitors } from '@modern-js/runtime';
const loader = async ({ context }: LoaderFunctionArgs) => {
    const monitors = getMonitors();
    const start = Date.now();
    try {
        await fetch(...);
        monitors.timing('loader_fetch_timing', Date.now() - start);
    } catch(e) {
        monitors.error(e);
    }
}

在组件中调用 Monitors,需要判断当前运行环境是否为 Node.js:

routes/page.tsx
import { use } from 'react';
import { RuntimeContext, getMonitors } from '@modern-js/runtime';
const Page = () => {
  const context = use(RuntimeContext);
  if (process.env.MODERN_TARGET === 'node') {
    const monitors = getMonitors();
    monitors.info('Page rendered');
  }
  return <div>Hello World</div>;
};
export default Page;

在中间件中,我们也可以调用 Monitors,但方式与在运行时代码中不同,需要通过 context 来获取:

server/modern.server.ts
import {
  defineServerConfig,
  type MiddlewareHandler,
} from '@modern-js/server-runtime';

export const handler: MiddlewareHandler = async (c, next) => {
  const monitors = c.get('monitors');
  const start = Date.now();

  await next();

  const end = Date.now();
  // 上报耗时
  monitors.timing('request_timing', end - start);
};

export default defineServerConfig({
  middlewares: [
    {
      name: 'request-timing',
      handler,
    },
  ],
});