feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
33
frontend/packages/arch/logger/src/console-disable.ts
Normal file
33
frontend/packages/arch/logger/src/console-disable.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// import { runtimeEnv } from '@coze-arch/bot-env/runtime';
|
||||
const DEBUG_TAG = 'open_debug';
|
||||
const OPEN_CONSOLE_MARK = new RegExp(`(?:\\?|\\&)${DEBUG_TAG}=true`);
|
||||
|
||||
export const shouldCloseConsole = () => {
|
||||
// 如果URL带有调试开启标记,则允许console打开
|
||||
const { search } = window.location;
|
||||
let isOpenDebug = !!sessionStorage.getItem(DEBUG_TAG);
|
||||
if (!isOpenDebug) {
|
||||
isOpenDebug = OPEN_CONSOLE_MARK.test(search);
|
||||
isOpenDebug && sessionStorage.setItem(DEBUG_TAG, 'true');
|
||||
}
|
||||
// 除了正式正常环境都允许console打开
|
||||
const isProduction = !!(IS_RELEASE_VERSION );
|
||||
console.log('IS_RELEASE_VERSION', IS_RELEASE_VERSION, isProduction);
|
||||
return !isOpenDebug && isProduction;
|
||||
};
|
||||
131
frontend/packages/arch/logger/src/error-boundary/index.tsx
Normal file
131
frontend/packages/arch/logger/src/error-boundary/index.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorBoundary as ReactErrorBoundary,
|
||||
useErrorBoundary,
|
||||
type ErrorBoundaryProps as ReactErrorBoundaryProps,
|
||||
type ErrorBoundaryPropsWithRender,
|
||||
type FallbackProps,
|
||||
} from 'react-error-boundary';
|
||||
import { type ErrorInfo, type ComponentType } from 'react';
|
||||
import React, { useCallback, version } from 'react';
|
||||
|
||||
import { ApiError } from '../slardar/utils';
|
||||
import { useLogger, type Logger } from '../logger';
|
||||
|
||||
// 拷贝自 react-error-boundary@3.1.4版本源码
|
||||
function useErrorHandler(givenError?: unknown): (error: unknown) => void {
|
||||
const [error, setError] = React.useState<unknown>(null);
|
||||
if (givenError !== null && givenError !== undefined) {
|
||||
throw givenError;
|
||||
}
|
||||
if (error !== null && error !== undefined) {
|
||||
throw error;
|
||||
}
|
||||
return setError;
|
||||
}
|
||||
|
||||
export type FallbackRender = ErrorBoundaryPropsWithRender['fallbackRender'];
|
||||
|
||||
export { useErrorBoundary, useErrorHandler, type FallbackProps };
|
||||
|
||||
export type ErrorBoundaryProps = ReactErrorBoundaryProps & {
|
||||
/**
|
||||
* @description componentDidCatch 触发该回调函数,参数透传自 componentDidCatch 的两个参数
|
||||
* @param error 具体错误
|
||||
* @param info
|
||||
* @returns
|
||||
*/
|
||||
onError?: (error: Error, info: ErrorInfo) => void;
|
||||
/**
|
||||
* @description 可在该回调函数中重置组件的一些 state,以避免一些错误的再次发生
|
||||
* @param details reset 后
|
||||
* @returns
|
||||
*/
|
||||
onReset?: (
|
||||
details:
|
||||
| { reason: 'imperative-api'; args: [] }
|
||||
| { reason: 'keys'; prev: [] | undefined; next: [] | undefined },
|
||||
) => void;
|
||||
resetKeys?: [];
|
||||
/**
|
||||
* logger 实例。默认从 LoggerContext 中读取
|
||||
*/
|
||||
// logger?: Logger;
|
||||
logger?: Logger;
|
||||
/**
|
||||
* 发生错误展示的兜底组件
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
FallbackComponent: ComponentType<FallbackProps>;
|
||||
/**
|
||||
* errorBoundaryName,用于在发生错误时上报
|
||||
* 事件:react_error_collection / react_error_by_api_collection
|
||||
*/
|
||||
errorBoundaryName: string;
|
||||
};
|
||||
|
||||
export const ErrorBoundary: React.FC<ErrorBoundaryProps> = ({
|
||||
onError: propsOnError,
|
||||
errorBoundaryName = 'unknown',
|
||||
children,
|
||||
logger: loggerInProps,
|
||||
...restProps
|
||||
}) => {
|
||||
const loggerInContext = useLogger({ allowNull: true });
|
||||
const logger = loggerInProps || loggerInContext;
|
||||
|
||||
if (!logger) {
|
||||
console.warn(
|
||||
`ErrorBoundary: not found logger instance in either props or context. errorBoundaryName: ${errorBoundaryName}`,
|
||||
);
|
||||
}
|
||||
|
||||
const onError = useCallback((error: Error, info: ErrorInfo) => {
|
||||
const { componentStack } = info;
|
||||
|
||||
const meta = {
|
||||
reportJsError: true, // 标记为 JS Error,上报走 slardar.captureException
|
||||
errorBoundaryName,
|
||||
reactInfo: {
|
||||
componentStack,
|
||||
version,
|
||||
},
|
||||
};
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
logger?.persist.error({
|
||||
eventName: 'react_error_by_api_collection',
|
||||
error,
|
||||
meta,
|
||||
});
|
||||
} else {
|
||||
logger?.persist.error({
|
||||
eventName: 'react_error_collection',
|
||||
error,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
propsOnError?.(error, info);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ReactErrorBoundary {...restProps} onError={onError}>
|
||||
{children}
|
||||
</ReactErrorBoundary>
|
||||
);
|
||||
};
|
||||
17
frontend/packages/arch/logger/src/global.d.ts
vendored
Normal file
17
frontend/packages/arch/logger/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
43
frontend/packages/arch/logger/src/index.ts
Normal file
43
frontend/packages/arch/logger/src/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { reporter, Reporter } from './reporter';
|
||||
|
||||
// reporter 需要上报到 slardar 的方法导出
|
||||
export type {
|
||||
LoggerCommonProperties,
|
||||
CustomEvent,
|
||||
CustomErrorLog,
|
||||
CustomLog,
|
||||
ErrorEvent,
|
||||
} from './reporter';
|
||||
// console 控制台打印
|
||||
export { logger, LoggerContext, Logger } from './logger';
|
||||
|
||||
// ErrorBoundary 相关方法
|
||||
export {
|
||||
ErrorBoundary,
|
||||
useErrorBoundary,
|
||||
useErrorHandler,
|
||||
type ErrorBoundaryProps,
|
||||
type FallbackProps,
|
||||
} from './error-boundary';
|
||||
|
||||
export { SlardarReportClient, type SlardarInstance } from './slardar';
|
||||
|
||||
export { LogLevel } from './types';
|
||||
|
||||
export { getSlardarInstance, setUserInfoContext } from './slardar/runtime';
|
||||
93
frontend/packages/arch/logger/src/logger/console-client.ts
Normal file
93
frontend/packages/arch/logger/src/logger/console-client.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import {
|
||||
type LogOptions,
|
||||
type LogLevel,
|
||||
type LoggerReportClient,
|
||||
type CommonLogOptions,
|
||||
LogAction,
|
||||
} from '../types';
|
||||
|
||||
export function getColorByLogLevel(type?: LogLevel) {
|
||||
if (type === 'success') {
|
||||
return '#00CC00';
|
||||
} else if (type === 'warning') {
|
||||
return '#CC9900';
|
||||
} else if (type === 'error') {
|
||||
return '#CC3333';
|
||||
} else if (type === 'fatal') {
|
||||
return '#FF0000';
|
||||
} else {
|
||||
return '#0099CC';
|
||||
}
|
||||
}
|
||||
|
||||
function doConsole(
|
||||
{ namespace, scope, level, message, eventName, ...rest }: LogOptions,
|
||||
...restArgs: unknown[]
|
||||
) {
|
||||
const logs: unknown[] = [
|
||||
`%c Logger %c ${namespace ? namespace : level}${
|
||||
scope ? ` %c ${scope}` : ''
|
||||
} %c`,
|
||||
'background:#444444; padding: 1px; border-radius: 3px 0 0 3px; color: #fff',
|
||||
`background:${getColorByLogLevel(level)}; padding: 1px; border-radius: ${
|
||||
scope ? '0' : '0 3px 3px 0'
|
||||
}; color: #fff`,
|
||||
scope
|
||||
? 'background:#777777; padding: 1px; border-radius: 0 3px 3px 0; color: #fff; margin-left: -1px;'
|
||||
: 'background:transparent',
|
||||
];
|
||||
|
||||
if (scope) {
|
||||
logs.push('background:transparent');
|
||||
}
|
||||
|
||||
logs.push(eventName || message);
|
||||
const payload = rest.error ? rest : rest.meta;
|
||||
if (!isEmpty(payload)) {
|
||||
logs.push(payload);
|
||||
}
|
||||
logs.push(...restArgs);
|
||||
|
||||
console.groupCollapsed(...logs);
|
||||
}
|
||||
|
||||
export class ConsoleLogClient implements LoggerReportClient {
|
||||
send({ meta, message, eventName, action, ...rest }: CommonLogOptions) {
|
||||
const resolvedMsg = message
|
||||
? message
|
||||
: eventName
|
||||
? `Event: ${eventName}`
|
||||
: undefined;
|
||||
if (!action?.includes(LogAction.CONSOLE) || !resolvedMsg) {
|
||||
return;
|
||||
}
|
||||
const payload = { ...rest, message: resolvedMsg };
|
||||
if (meta) {
|
||||
doConsole(payload, meta);
|
||||
} else {
|
||||
doConsole(payload);
|
||||
}
|
||||
console.trace();
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
export const consoleLogClient = new ConsoleLogClient();
|
||||
32
frontend/packages/arch/logger/src/logger/context.ts
Normal file
32
frontend/packages/arch/logger/src/logger/context.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { type Logger } from './core';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const LoggerContext = createContext<Logger | null>(null);
|
||||
|
||||
export function useLogger(options?: { allowNull?: boolean }) {
|
||||
const { allowNull = false } = options || {};
|
||||
const logger = useContext(LoggerContext);
|
||||
if (allowNull !== true && !logger) {
|
||||
throw new Error('expect logger in LoggerContext but not found');
|
||||
}
|
||||
|
||||
return logger;
|
||||
}
|
||||
189
frontend/packages/arch/logger/src/logger/core.ts
Normal file
189
frontend/packages/arch/logger/src/logger/core.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
import {
|
||||
type CommonLogOptions,
|
||||
LogAction,
|
||||
type LoggerReportClient,
|
||||
type BaseLoggerOptions,
|
||||
type LogOptionsResolver,
|
||||
LogLevel,
|
||||
} from '../types';
|
||||
import { LogOptionsHelper } from './log-options-helper';
|
||||
import { consoleLogClient } from './console-client';
|
||||
|
||||
const defaultLogOptions = {
|
||||
level: LogLevel.INFO,
|
||||
action: [LogAction.CONSOLE],
|
||||
};
|
||||
|
||||
function unwrapOptions(payload: string | CommonLogOptions): CommonLogOptions {
|
||||
if (typeof payload === 'string') {
|
||||
return {
|
||||
message: payload,
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function resolveClients(
|
||||
clients: LoggerReportClient[],
|
||||
disableConsole?: boolean,
|
||||
) {
|
||||
const result = clients.includes(consoleLogClient)
|
||||
? clients
|
||||
: [consoleLogClient, ...clients];
|
||||
|
||||
if (disableConsole) {
|
||||
return result.filter(item => item !== consoleLogClient);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export class BaseLogger<T extends CommonLogOptions = CommonLogOptions> {
|
||||
ctx: LogOptionsHelper<CommonLogOptions>;
|
||||
|
||||
logOptionsResolvers: LogOptionsResolver[] = [];
|
||||
|
||||
disableConsole: boolean;
|
||||
|
||||
private clients: LoggerReportClient[];
|
||||
|
||||
constructor({
|
||||
ctx = {},
|
||||
clients = [],
|
||||
beforeSend = [],
|
||||
disableConsole,
|
||||
}: BaseLoggerOptions) {
|
||||
this.ctx = new LogOptionsHelper(ctx);
|
||||
this.clients = clients;
|
||||
this.logOptionsResolvers = beforeSend;
|
||||
this.disableConsole = disableConsole || false;
|
||||
}
|
||||
|
||||
addClient(client: LoggerReportClient) {
|
||||
this.clients.push(client);
|
||||
}
|
||||
|
||||
resolveCloneParams({
|
||||
ctx,
|
||||
clients = [],
|
||||
beforeSend = [],
|
||||
disableConsole,
|
||||
}: BaseLoggerOptions) {
|
||||
return {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
ctx: LogOptionsHelper.merge(this.ctx.get(), ctx),
|
||||
clients: [...this.clients, ...clients],
|
||||
beforeSend: [...this.logOptionsResolvers, ...beforeSend],
|
||||
disableConsole: isNil(disableConsole)
|
||||
? this.disableConsole
|
||||
: disableConsole,
|
||||
};
|
||||
}
|
||||
|
||||
createLoggerWith<P extends CommonLogOptions = CommonLogOptions>(
|
||||
options: BaseLoggerOptions,
|
||||
) {
|
||||
return new BaseLogger<P>(this.resolveCloneParams(options));
|
||||
}
|
||||
|
||||
log(options: CommonLogOptions) {
|
||||
const payload = LogOptionsHelper.merge(
|
||||
defaultLogOptions,
|
||||
this.ctx.get(),
|
||||
options,
|
||||
);
|
||||
const resolvedPayload =
|
||||
this.logOptionsResolvers.length > 0
|
||||
? this.logOptionsResolvers.reduce(
|
||||
(acc, cur) => (cur ? cur(acc) : acc),
|
||||
{ ...payload },
|
||||
)
|
||||
: payload;
|
||||
|
||||
const resolvedClients = this.disableConsole
|
||||
? this.clients.filter(item => item !== consoleLogClient)
|
||||
: this.clients;
|
||||
|
||||
resolvedClients.forEach(client => {
|
||||
client.send(resolvedPayload);
|
||||
});
|
||||
}
|
||||
|
||||
fatal(payload: T & { error: Error }) {
|
||||
this.log({
|
||||
...payload,
|
||||
level: LogLevel.FATAL,
|
||||
});
|
||||
}
|
||||
|
||||
error(payload: T & { error: Error }) {
|
||||
this.log({
|
||||
...payload,
|
||||
level: LogLevel.ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
warning(payload: string | T) {
|
||||
this.log({
|
||||
...unwrapOptions(payload),
|
||||
level: LogLevel.WARNING,
|
||||
});
|
||||
}
|
||||
|
||||
info(payload: string | T) {
|
||||
this.log({
|
||||
...unwrapOptions(payload),
|
||||
level: LogLevel.INFO,
|
||||
});
|
||||
}
|
||||
|
||||
success(payload: string | T) {
|
||||
this.log({
|
||||
...unwrapOptions(payload),
|
||||
level: LogLevel.SUCCESS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Logger extends BaseLogger {
|
||||
constructor({ clients = [], ...rest }: BaseLoggerOptions = {}) {
|
||||
super({
|
||||
...rest,
|
||||
clients: resolveClients(clients, rest.disableConsole),
|
||||
});
|
||||
}
|
||||
|
||||
addClient(client: LoggerReportClient) {
|
||||
super.addClient(client);
|
||||
this.persist.addClient(client);
|
||||
}
|
||||
|
||||
createLoggerWith(options: BaseLoggerOptions) {
|
||||
return new Logger(this.resolveCloneParams(options));
|
||||
}
|
||||
|
||||
persist = super.createLoggerWith<CommonLogOptions>({
|
||||
ctx: {
|
||||
action: [LogAction.CONSOLE, LogAction.PERSIST],
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
19
frontend/packages/arch/logger/src/logger/index.ts
Normal file
19
frontend/packages/arch/logger/src/logger/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { logger } from './logger';
|
||||
export { Logger } from './core';
|
||||
export { LoggerContext, useLogger } from './context';
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import { type CommonLogOptions } from '../types';
|
||||
function mergeLogOption<T extends CommonLogOptions, P extends CommonLogOptions>(
|
||||
source1: T,
|
||||
source2: P,
|
||||
) {
|
||||
const { action: action1 = [], meta: meta1, ...rest1 } = source1;
|
||||
const { action: action2 = [], meta: meta2, ...rest2 } = source2;
|
||||
|
||||
const meta = {
|
||||
...meta1,
|
||||
...meta2,
|
||||
};
|
||||
|
||||
const res: CommonLogOptions = {
|
||||
...rest1,
|
||||
...rest2,
|
||||
action: [...action1, ...action2],
|
||||
...(isEmpty(meta) ? {} : { meta }),
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
export class LogOptionsHelper<T extends CommonLogOptions = CommonLogOptions> {
|
||||
static merge<T extends CommonLogOptions>(...list: CommonLogOptions[]) {
|
||||
return list.filter(Boolean).reduce((r, c) => mergeLogOption(r, c), {}) as T;
|
||||
}
|
||||
|
||||
options: T;
|
||||
|
||||
constructor(options: T) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
updateMeta(
|
||||
updateCb: (
|
||||
prevMeta?: Record<string, unknown>,
|
||||
) => Record<string, unknown> | undefined,
|
||||
) {
|
||||
this.options.meta = updateCb(this.options.meta);
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.options;
|
||||
}
|
||||
}
|
||||
116
frontend/packages/arch/logger/src/logger/logger.ts
Normal file
116
frontend/packages/arch/logger/src/logger/logger.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type CommonLogOptions,
|
||||
type BaseLoggerOptions,
|
||||
type LoggerReportClient,
|
||||
} from '../types';
|
||||
import { SlardarReportClient, type SlardarInstance } from '../slardar';
|
||||
import { shouldCloseConsole } from '../console-disable';
|
||||
import { Logger as RawLogger, type BaseLogger } from './core';
|
||||
export type SetupKey = 'no-console';
|
||||
export type SetupConfig = Record<SetupKey, unknown>;
|
||||
|
||||
export class Logger extends RawLogger {
|
||||
private registeredInstance: Logger[] = [];
|
||||
private slardarInstance: SlardarInstance | null = null;
|
||||
static setupConfig: SetupConfig | null = null;
|
||||
|
||||
private setDisableConsole() {
|
||||
if (!Logger.setupConfig?.['no-console']) {
|
||||
return;
|
||||
}
|
||||
const disableConsole = shouldCloseConsole();
|
||||
this.disableConsole = disableConsole;
|
||||
if (this.persist) {
|
||||
this.persist.disableConsole = disableConsole;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated logger方法仅作控制台打印用,无需手动添加slardar client,如需日志上报请使用`import { reporter } from '@coze-arch/logger',具体规范:
|
||||
*/
|
||||
addClient(client: LoggerReportClient): void {
|
||||
super.addClient(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 该方法已废弃,请统一使用`import { reporter } from '@coze-arch/logger'替换,具体规范:
|
||||
*/
|
||||
persist: BaseLogger<CommonLogOptions> = this.persist;
|
||||
|
||||
/**
|
||||
* @deprecated logger方法仅作控制台打印用,无需手动添加slardar client,如需日志上报请使用`import { reporter } from '@coze-arch/logger',具体规范:
|
||||
*/
|
||||
init(slardarInstance: SlardarInstance) {
|
||||
const client = new SlardarReportClient(slardarInstance);
|
||||
this.persist?.addClient(client);
|
||||
this.slardarInstance = client.slardarInstance;
|
||||
this.registeredInstance.forEach(instance => {
|
||||
instance.init(client.slardarInstance);
|
||||
});
|
||||
this.registeredInstance = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup some attributes of config of logger at any time
|
||||
* @param setupConfig the config object needed to setup
|
||||
*/
|
||||
setup(config: SetupConfig) {
|
||||
Logger.setupConfig = config;
|
||||
}
|
||||
|
||||
createLoggerWith(options: BaseLoggerOptions): Logger {
|
||||
const logger = new Logger(this.resolveCloneParams(options));
|
||||
if (this.slardarInstance) {
|
||||
logger.init(this.slardarInstance);
|
||||
} else {
|
||||
this.registeredInstance.push(logger);
|
||||
}
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
info(payload: string | CommonLogOptions): void {
|
||||
this.setDisableConsole();
|
||||
super.info(payload);
|
||||
}
|
||||
|
||||
success(payload: string | CommonLogOptions): void {
|
||||
this.setDisableConsole();
|
||||
super.success(payload);
|
||||
}
|
||||
|
||||
warning(payload: string | CommonLogOptions): void {
|
||||
this.setDisableConsole();
|
||||
super.warning(payload);
|
||||
}
|
||||
|
||||
error(payload: CommonLogOptions & { error: Error }): void {
|
||||
this.setDisableConsole();
|
||||
super.error(payload);
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger({
|
||||
clients: [],
|
||||
ctx: {
|
||||
meta: {},
|
||||
},
|
||||
});
|
||||
|
||||
export { logger };
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type TracePointName = 'success' | 'fail' | string;
|
||||
|
||||
export interface TraceDuration {
|
||||
points: TracePointName[];
|
||||
interval: {
|
||||
[key: TracePointName]: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function genDurationTracer() {
|
||||
const duration: TraceDuration = {
|
||||
points: [],
|
||||
interval: {},
|
||||
};
|
||||
|
||||
const tracer = (pointName: TracePointName) => {
|
||||
if (!pointName) {
|
||||
return duration;
|
||||
}
|
||||
if (duration.points.indexOf(pointName) === -1) {
|
||||
duration.points.push(pointName);
|
||||
}
|
||||
performance.mark(pointName);
|
||||
if (duration.points.length > 1) {
|
||||
const curIdx = duration.points.length - 1;
|
||||
const measure = performance.measure(
|
||||
'measure',
|
||||
duration.points[curIdx - 1],
|
||||
duration.points[curIdx],
|
||||
);
|
||||
duration.interval[pointName] = measure?.duration ?? 0;
|
||||
}
|
||||
|
||||
return duration;
|
||||
};
|
||||
|
||||
return {
|
||||
tracer,
|
||||
};
|
||||
}
|
||||
320
frontend/packages/arch/logger/src/reporter/index.ts
Normal file
320
frontend/packages/arch/logger/src/reporter/index.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CommonLogOptions, LogAction, LogLevel } from '../types';
|
||||
import { SlardarReportClient, type SlardarInstance } from '../slardar';
|
||||
import { Logger } from '../logger';
|
||||
import { genDurationTracer, type TracePointName } from './duration-tracer';
|
||||
|
||||
export interface LoggerCommonProperties {
|
||||
namespace?: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export interface SlardarMeta {
|
||||
meta?: Record<string, unknown>; // Combination of `categories` and `metrics`, check more:
|
||||
}
|
||||
|
||||
export interface CustomLog extends SlardarMeta, LoggerCommonProperties {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type CustomErrorLog = CustomLog & { error: Error };
|
||||
|
||||
export interface CustomEvent<EventEnum extends string>
|
||||
extends SlardarMeta,
|
||||
LoggerCommonProperties {
|
||||
eventName: EventEnum;
|
||||
}
|
||||
|
||||
export interface ErrorEvent<EventEnum extends string>
|
||||
extends CustomEvent<EventEnum> {
|
||||
error: Error;
|
||||
level?: 'error' | 'fatal';
|
||||
}
|
||||
|
||||
export interface TraceEvent<EventEnum extends string>
|
||||
extends LoggerCommonProperties {
|
||||
eventName: EventEnum;
|
||||
}
|
||||
|
||||
export interface TraceOptions extends SlardarMeta {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
type ReporterConfig = LoggerCommonProperties & SlardarMeta;
|
||||
|
||||
type LogType = 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
export class Reporter {
|
||||
private initialized = false;
|
||||
private logger: Logger;
|
||||
private pendingQueue: CommonLogOptions[] = [];
|
||||
private pendingInstance: Reporter[] = [];
|
||||
public slardarInstance: SlardarInstance | null = null;
|
||||
|
||||
private log(type: LogType, payload: CommonLogOptions) {
|
||||
if (!this.check(payload)) {
|
||||
return;
|
||||
}
|
||||
this.logger.disableConsole = true;
|
||||
this.logger[type](payload as CommonLogOptions & { error: Error });
|
||||
this.logger.persist.disableConsole = true;
|
||||
this.logger.persist[type](payload as CommonLogOptions & { error: Error });
|
||||
}
|
||||
|
||||
constructor(config?: ReporterConfig) {
|
||||
this.logger = new Logger({
|
||||
clients: [],
|
||||
ctx: {
|
||||
...config,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个带有preset的reporter,一般可以配置专属的`namespace`和`scope`
|
||||
* @param preset
|
||||
* @returns
|
||||
*/
|
||||
createReporterWithPreset(preset: ReporterConfig) {
|
||||
const r = new Reporter(preset);
|
||||
if (this.initialized) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
r.init(this.slardarInstance!);
|
||||
} else {
|
||||
this.pendingInstance.push(r);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化reporter
|
||||
* @param slardarInstance 需要上报的slardar实例
|
||||
* @returns
|
||||
*/
|
||||
init(slardarInstance: SlardarInstance) {
|
||||
if (!slardarInstance) {
|
||||
console.warn('You should use reporter with a valid slardar instance');
|
||||
return;
|
||||
}
|
||||
const slardarReportClient = new SlardarReportClient(slardarInstance);
|
||||
this.slardarInstance = slardarReportClient.slardarInstance;
|
||||
this.logger.persist.addClient(slardarReportClient);
|
||||
this.initialized = true;
|
||||
|
||||
// Execute all pending items which are collected before initialization
|
||||
this.pendingQueue.forEach(item => {
|
||||
const levelFuncName: Omit<LogLevel, LogLevel.ERROR> =
|
||||
item.level || LogLevel.INFO;
|
||||
this.log(levelFuncName.toString() as LogType, item);
|
||||
});
|
||||
this.pendingQueue = [];
|
||||
|
||||
// Run init for all pending reporter instances
|
||||
this.pendingInstance.forEach(instance => {
|
||||
instance.init(slardarInstance);
|
||||
});
|
||||
this.pendingInstance = [];
|
||||
}
|
||||
|
||||
getLogger() {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
/// Custom Log
|
||||
/**
|
||||
* 上报一个info日志
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
info(log: CustomLog) {
|
||||
this.log('info', log);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报一个success日志
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
success(log: CustomLog) {
|
||||
const info = this.formatCustomLog(log, LogLevel.SUCCESS);
|
||||
this.log('success', info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报一个warning日志
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
warning(log: CustomLog) {
|
||||
const info = this.formatCustomLog(log, LogLevel.WARNING);
|
||||
this.log('warning', info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报一个error日志
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
error(log: CustomErrorLog) {
|
||||
const info = this.formatCustomLog(
|
||||
log,
|
||||
LogLevel.ERROR,
|
||||
) as CommonLogOptions & { error: Error };
|
||||
this.log('error', info);
|
||||
}
|
||||
|
||||
/// Custom Event
|
||||
/**
|
||||
* 上报一个自定义event事件
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
event<EventEnum extends string>(event: CustomEvent<EventEnum>) {
|
||||
const e = this.formatCustomEvent(event);
|
||||
this.log('info', e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报一个错误event事件(LogLevel = 'error')
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
errorEvent<EventEnum extends string>(event: ErrorEvent<EventEnum>) {
|
||||
const e = this.formatErrorEvent(event) as CommonLogOptions & {
|
||||
error: Error;
|
||||
};
|
||||
this.log('error', e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报一个成功event事件(LogLevel = 'success')
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
successEvent<EventEnum extends string>(event: CustomEvent<EventEnum>) {
|
||||
const e = this.formatCustomEvent(event) as CommonLogOptions;
|
||||
this.log('success', e);
|
||||
}
|
||||
|
||||
/// Trace Event
|
||||
/**
|
||||
* 性能追踪,可以记录一个流程中多个步骤间隔的耗时:
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
tracer<EventEnum extends string>({ eventName }: TraceEvent<EventEnum>) {
|
||||
const { tracer: durationTracer } = genDurationTracer();
|
||||
|
||||
const trace = (pointName: TracePointName, options: TraceOptions = {}) => {
|
||||
const { meta, error } = options;
|
||||
const e = this.formatCustomEvent({
|
||||
eventName,
|
||||
meta: {
|
||||
...meta,
|
||||
error,
|
||||
duration: durationTracer(pointName),
|
||||
},
|
||||
});
|
||||
if (!this.check(e)) {
|
||||
return;
|
||||
}
|
||||
this.log('info', e);
|
||||
};
|
||||
|
||||
return {
|
||||
trace,
|
||||
};
|
||||
}
|
||||
|
||||
private check(info: CommonLogOptions) {
|
||||
if (!this.initialized) {
|
||||
// Initialization has not been called, collect the item into queue and consume it when called.
|
||||
this.pendingQueue.push(info);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private formatCustomLog(
|
||||
log: CustomLog | CustomErrorLog,
|
||||
level: LogLevel,
|
||||
): CommonLogOptions {
|
||||
const {
|
||||
namespace: ctxNamespace,
|
||||
scope: ctxScope,
|
||||
meta: ctxMeta = {},
|
||||
} = this.logger.ctx?.options ?? {};
|
||||
const { namespace, scope, meta = {}, message } = log;
|
||||
return {
|
||||
action: [LogAction.CONSOLE, LogAction.PERSIST],
|
||||
namespace: namespace || ctxNamespace,
|
||||
scope: scope || ctxScope,
|
||||
level,
|
||||
error: (log as CustomErrorLog).error,
|
||||
|
||||
message,
|
||||
meta: {
|
||||
...ctxMeta,
|
||||
...meta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private formatCustomEvent<EventEnum extends string>(
|
||||
event: CustomEvent<EventEnum>,
|
||||
): CommonLogOptions {
|
||||
const {
|
||||
namespace: ctxNamespace,
|
||||
scope: ctxScope,
|
||||
meta: ctxMeta = {},
|
||||
} = this.logger.ctx?.options ?? {};
|
||||
const { eventName, namespace, scope, meta = {} } = event;
|
||||
return {
|
||||
action: [LogAction.CONSOLE, LogAction.PERSIST],
|
||||
namespace: namespace || ctxNamespace,
|
||||
scope: scope || ctxScope,
|
||||
eventName,
|
||||
|
||||
meta: {
|
||||
...ctxMeta,
|
||||
...meta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private formatErrorEvent<EventEnum extends string>(
|
||||
event: ErrorEvent<EventEnum>,
|
||||
): CommonLogOptions {
|
||||
const e = this.formatCustomEvent(event);
|
||||
return {
|
||||
...e,
|
||||
meta: {
|
||||
...e.meta,
|
||||
// !NOTE: Slardar不支持`a.b`的字段的正则搜索(会报错),需要把`error.message`和`error.name`铺平放到第一层
|
||||
errorMessage: event.error.message,
|
||||
errorName: event.error.name,
|
||||
level: event.level ?? 'error',
|
||||
},
|
||||
error: event.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const reporter = new Reporter();
|
||||
|
||||
export { reporter };
|
||||
163
frontend/packages/arch/logger/src/slardar/index.ts
Normal file
163
frontend/packages/arch/logger/src/slardar/index.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isNumber, isString, mapValues, omitBy, isNil } from 'lodash-es';
|
||||
import type { SlardarInstance } from '@coze-studio/slardar-interface';
|
||||
|
||||
import {
|
||||
type CommonLogOptions,
|
||||
type LoggerReportClient,
|
||||
LogAction,
|
||||
LogLevel,
|
||||
} from '../types';
|
||||
import {
|
||||
toFlatPropertyMap,
|
||||
safeJson,
|
||||
getErrorRecord,
|
||||
getLogLevel,
|
||||
getSlardarLevel,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
* 将 meta 根据类型转换成
|
||||
* - 指标 metrics:可以被度量的值,也就是数值
|
||||
* - 维度 categories:分类,维度,用来做筛选,分组
|
||||
*
|
||||
* @param meta
|
||||
* @returns
|
||||
*/
|
||||
function metaToMetricsCategories(meta?: Record<string, unknown>) {
|
||||
const metrics: Record<string, number> = {};
|
||||
const categories: Record<string, string> = {};
|
||||
for (const k in meta) {
|
||||
const val = meta[k];
|
||||
if (isNumber(val)) {
|
||||
metrics[k] = val;
|
||||
} else {
|
||||
categories[k] = isString(val) ? val : safeJson.stringify(val);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
metrics,
|
||||
categories,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Record<string, unknown> => Record<string, string | number>
|
||||
*/
|
||||
function normalizeExtra(record: Record<string, unknown>) {
|
||||
const result: Record<string, string | number> = {};
|
||||
for (const k in record) {
|
||||
const val = record[k];
|
||||
if (isNumber(val) || isString(val)) {
|
||||
result[k] = val;
|
||||
} else {
|
||||
result[k] = safeJson.stringify(val);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export class SlardarReportClient implements LoggerReportClient {
|
||||
slardarInstance: SlardarInstance;
|
||||
|
||||
constructor(slardarInstance: SlardarInstance) {
|
||||
// 业务项目里可能有多个 slardar 版本,多个版本的类型不兼容,constructor 里约束版本会存在问题 => 放开。
|
||||
this.slardarInstance = slardarInstance;
|
||||
|
||||
if (!this.slardarInstance) {
|
||||
console.warn('expected slardarInstance but get undefined/null');
|
||||
}
|
||||
}
|
||||
|
||||
send(options: CommonLogOptions) {
|
||||
if (!options.action?.includes(LogAction.PERSIST)) {
|
||||
// 非持久化日志,不上报
|
||||
return;
|
||||
}
|
||||
|
||||
const { level, message, action, eventName, meta, error, ...rest } = options;
|
||||
|
||||
// Slardar API:
|
||||
|
||||
const resolveMeta = (inputs: Record<string, unknown>) =>
|
||||
toFlatPropertyMap(
|
||||
{
|
||||
...rest,
|
||||
...inputs,
|
||||
error: getErrorRecord(error),
|
||||
level: getLogLevel(level),
|
||||
},
|
||||
{
|
||||
maxDepth: 4,
|
||||
},
|
||||
);
|
||||
|
||||
if (level === LogLevel.ERROR && meta?.reportJsError === true) {
|
||||
const { reportJsError, reactInfo, ...restMeta } = meta || {};
|
||||
const resolvedMeta = resolveMeta({
|
||||
...restMeta,
|
||||
message,
|
||||
eventName,
|
||||
});
|
||||
|
||||
// 上报 JS 异常
|
||||
this.slardarInstance?.(
|
||||
'captureException',
|
||||
error,
|
||||
omitBy(
|
||||
mapValues(resolvedMeta, (v: unknown) =>
|
||||
isString(v) ? v : safeJson.stringify(v),
|
||||
),
|
||||
isNil,
|
||||
),
|
||||
reactInfo as {
|
||||
version: string;
|
||||
componentStack: string;
|
||||
},
|
||||
);
|
||||
} else if (eventName) {
|
||||
const resolvedMeta = resolveMeta({
|
||||
...meta,
|
||||
});
|
||||
|
||||
const { metrics, categories } = metaToMetricsCategories(resolvedMeta);
|
||||
|
||||
// 上报独立的事件
|
||||
this.slardarInstance?.('sendEvent', {
|
||||
name: eventName,
|
||||
metrics,
|
||||
categories,
|
||||
});
|
||||
} else if (message) {
|
||||
const resolvedMeta = resolveMeta({
|
||||
...meta,
|
||||
});
|
||||
|
||||
// 上报日志
|
||||
this.slardarInstance?.('sendLog', {
|
||||
level: getSlardarLevel(level),
|
||||
content: message,
|
||||
// slardar 内部会对 extra 处理分类,number 类型的字段放入 metrics,其他放入 categories
|
||||
extra: normalizeExtra(resolvedMeta),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type { SlardarInstance };
|
||||
27
frontend/packages/arch/logger/src/slardar/runtime.ts
Normal file
27
frontend/packages/arch/logger/src/slardar/runtime.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { reporter } from '../reporter';
|
||||
|
||||
export const getSlardarInstance = () => reporter.slardarInstance;
|
||||
|
||||
// 异步设置 coze 的 uid 信息
|
||||
export const setUserInfoContext = (userInfo: DataItem.UserInfo) => {
|
||||
const slardarInstance = getSlardarInstance();
|
||||
if (slardarInstance) {
|
||||
slardarInstance?.('context.set', 'coze_uid', userInfo?.user_id_str);
|
||||
}
|
||||
};
|
||||
175
frontend/packages/arch/logger/src/slardar/utils.ts
Normal file
175
frontend/packages/arch/logger/src/slardar/utils.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { LogLevel, type ApiErrorOption, ErrorType } from '../types';
|
||||
|
||||
function isObject<T>(it: T): it is object extends T
|
||||
? // Narrow the `{}` type to an unspecified object
|
||||
T & Record<string | number | symbol, unknown>
|
||||
: unknown extends T
|
||||
? // treat unknown like `{}`
|
||||
T & Record<string | number | symbol, unknown>
|
||||
: T extends object // function, array or actual object
|
||||
? T extends readonly unknown[]
|
||||
? never // not an array
|
||||
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
T extends (...args: any[]) => any
|
||||
? never // not a function
|
||||
: T // no, an actual object
|
||||
: never {
|
||||
// This is necessary because:
|
||||
// typeof null === 'object'
|
||||
// typeof [] === 'object'
|
||||
// [] instanceof Object === true
|
||||
return Object.prototype.toString.call(it) === '[object Object]';
|
||||
}
|
||||
|
||||
export function toFlatPropertyMap(
|
||||
inputObj: Record<string, unknown>,
|
||||
options?: {
|
||||
keySeparator?: string;
|
||||
maxDepth?: number;
|
||||
},
|
||||
) {
|
||||
const { keySeparator = '.', maxDepth } = options || {};
|
||||
const flattenRecursive = (
|
||||
obj: Record<string, unknown>,
|
||||
propertyMap: Record<string, unknown>,
|
||||
depth = 1,
|
||||
parentKey?: string,
|
||||
// eslint-disable-next-line max-params
|
||||
) => {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const path = parentKey ? `${parentKey}${keySeparator}${key}` : key;
|
||||
const currentDepth = depth + 1;
|
||||
if (value && isObject(value) && (!maxDepth || currentDepth <= maxDepth)) {
|
||||
flattenRecursive(value, propertyMap, currentDepth, path);
|
||||
} else {
|
||||
propertyMap[path] = value;
|
||||
}
|
||||
}
|
||||
return propertyMap;
|
||||
};
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
return flattenRecursive(inputObj, result);
|
||||
}
|
||||
|
||||
export const safeJson = (() => {
|
||||
const stringify = (sth: unknown): string => {
|
||||
try {
|
||||
return JSON.stringify(sth);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return `JSON stringify Error: ${(e as Error).message}`;
|
||||
}
|
||||
};
|
||||
|
||||
const parse = (sth?: string) => {
|
||||
try {
|
||||
return JSON.parse(sth || '');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
stringify,
|
||||
parse,
|
||||
};
|
||||
})();
|
||||
|
||||
export class ApiError extends Error {
|
||||
errorOption: ApiErrorOption;
|
||||
|
||||
constructor(option: ApiErrorOption) {
|
||||
super(
|
||||
`httpStatus=${option.httpStatus}, code=${option.code}, message=${option.message}, logId=${option.logId}`,
|
||||
);
|
||||
this.name = 'ApiError';
|
||||
this.errorOption = option;
|
||||
}
|
||||
}
|
||||
|
||||
export const getErrorType = (error?: ApiError | Error): string => {
|
||||
if (!error) {
|
||||
return ErrorType.Unknown;
|
||||
}
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
// 优先用业务给的 api error type
|
||||
if (error.errorOption?.errorType) {
|
||||
return error.errorOption.errorType;
|
||||
}
|
||||
return ErrorType.ApiError;
|
||||
}
|
||||
|
||||
return ErrorType.Unknown;
|
||||
};
|
||||
|
||||
export const getApiErrorRecord = (
|
||||
error: ApiError | Error,
|
||||
): Record<string, unknown> => {
|
||||
if (error instanceof ApiError && error.errorOption) {
|
||||
const { errorOption } = error;
|
||||
return {
|
||||
httpStatus: errorOption.httpStatus,
|
||||
code: errorOption.code,
|
||||
logId: errorOption.logId,
|
||||
response: safeJson.stringify(errorOption.response),
|
||||
requestConfig: safeJson.stringify(errorOption.requestConfig),
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const getErrorRecord = (
|
||||
error?: ApiError | Error,
|
||||
): Record<string, string | number | undefined> => {
|
||||
if (!error) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
...getApiErrorRecord(error),
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
type: getErrorType(error),
|
||||
};
|
||||
};
|
||||
|
||||
const levelMap = {
|
||||
[LogLevel.INFO]: 'info',
|
||||
[LogLevel.SUCCESS]: 'success',
|
||||
[LogLevel.WARNING]: 'warn',
|
||||
[LogLevel.ERROR]: 'error',
|
||||
[LogLevel.FATAL]: 'fatal',
|
||||
};
|
||||
export function getLogLevel(level = LogLevel.INFO) {
|
||||
return levelMap[level];
|
||||
}
|
||||
|
||||
/** Slardar 自定义事件级别,默认是 info, 可枚举项 debug | info | warn | error */
|
||||
const slardarLevelMap = {
|
||||
[LogLevel.INFO]: 'info',
|
||||
[LogLevel.SUCCESS]: 'info',
|
||||
[LogLevel.WARNING]: 'warn',
|
||||
[LogLevel.ERROR]: 'error',
|
||||
[LogLevel.FATAL]: 'error',
|
||||
};
|
||||
export function getSlardarLevel(level = LogLevel.INFO) {
|
||||
return slardarLevelMap[level];
|
||||
}
|
||||
168
frontend/packages/arch/logger/src/types/index.ts
Normal file
168
frontend/packages/arch/logger/src/types/index.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 日志级别
|
||||
*/
|
||||
export const enum LogLevel {
|
||||
/**
|
||||
* 日志
|
||||
*/
|
||||
INFO = 'info',
|
||||
/**
|
||||
* 成功日志
|
||||
*/
|
||||
SUCCESS = 'success',
|
||||
/**
|
||||
* 接口问题导致的错误
|
||||
* 不影响用户使用的边缘 case
|
||||
* 非核心功能问题
|
||||
*/
|
||||
WARNING = 'warning',
|
||||
/**
|
||||
* 严重错误
|
||||
*/
|
||||
ERROR = 'error',
|
||||
/**
|
||||
* 故障
|
||||
*/
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
/**
|
||||
* 日志动作,描述消费日志的行为
|
||||
*/
|
||||
export const enum LogAction {
|
||||
/**
|
||||
* 输出到浏览器控制台
|
||||
*/
|
||||
CONSOLE = 'console',
|
||||
/**
|
||||
* 持久化,即上报至平台
|
||||
*/
|
||||
PERSIST = 'persist',
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用日志配置
|
||||
*/
|
||||
export interface CommonLogOptions {
|
||||
/**
|
||||
* 命名空间
|
||||
*/
|
||||
namespace?: string;
|
||||
/**
|
||||
* 作用域
|
||||
* 层级:namespace > scope
|
||||
*/
|
||||
scope?: string;
|
||||
/**
|
||||
* 日志级别
|
||||
* @default LogLevel.INFO
|
||||
*/
|
||||
level?: LogLevel;
|
||||
/**
|
||||
* 日志动作,描述消费日志的行为
|
||||
* @default [LogAction.CONSOLE]
|
||||
*/
|
||||
action?: LogAction[];
|
||||
/**
|
||||
* 日志消息
|
||||
* 输出到浏览器控制台场景下必填。
|
||||
* 最终输出到浏览器控制台: ${namespace} ${scope} ${message}
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* 事件名
|
||||
* 上报事件场景下必填。
|
||||
*/
|
||||
eventName?: string;
|
||||
/**
|
||||
* 扩展信息,可用于描述日志/事件的上下文信息
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
/**
|
||||
* Error
|
||||
* 错误日志/事件场景下必填
|
||||
*/
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报平台 Client
|
||||
*/
|
||||
export interface LoggerReportClient {
|
||||
send: (options: CommonLogOptions) => void;
|
||||
}
|
||||
|
||||
export type LogOptionsResolver = (
|
||||
options: CommonLogOptions,
|
||||
) => CommonLogOptions;
|
||||
|
||||
export interface BaseLoggerOptions {
|
||||
ctx?: CommonLogOptions;
|
||||
clients?: LoggerReportClient[];
|
||||
beforeSend?: LogOptionsResolver[];
|
||||
disableConsole?: boolean;
|
||||
}
|
||||
|
||||
// Make some properties required
|
||||
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||
|
||||
/**
|
||||
* 美化输出,开发提效
|
||||
* type A = { a: string };
|
||||
* type B = { b: string };
|
||||
* type C = A & B;
|
||||
* type PrettyC = Pretty<A & B>;
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type Pretty<T extends Record<string, any>> = {
|
||||
[key in keyof T]: T[key];
|
||||
};
|
||||
|
||||
export type LogOptions = Pretty<WithRequired<CommonLogOptions, 'message'>>;
|
||||
|
||||
export const enum ErrorType {
|
||||
/**
|
||||
* API httpCode 非 200
|
||||
*/
|
||||
ApiError = 'ApiError',
|
||||
/**
|
||||
* API httpCode 200,业务 Code 存在异常
|
||||
*/
|
||||
ApiBizError = 'ApiBizError',
|
||||
/**
|
||||
* 未归类的错误
|
||||
*/
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export interface ApiErrorOption {
|
||||
httpStatus: string;
|
||||
/**
|
||||
* 业务 code
|
||||
*/
|
||||
code?: string;
|
||||
message?: string;
|
||||
logId?: string;
|
||||
requestConfig?: Record<string, unknown>;
|
||||
response?: Record<string, unknown>;
|
||||
/**
|
||||
* 错误类型,用于细化监控
|
||||
* @default ErrorType.ApiError
|
||||
*/
|
||||
errorType?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user