feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
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];
|
||||
}
|
||||
Reference in New Issue
Block a user