refactor: pipeline run log

This commit is contained in:
xiaojunnuo
2022-10-28 16:08:12 +08:00
parent 25d5a99b3d
commit 3dfdd3c31c
24 changed files with 381 additions and 153 deletions
+86 -43
View File
@@ -1,94 +1,136 @@
import { ConcurrencyStrategy, Pipeline, Runnable, Stage, Step, Task } from "../d.ts/pipeline";
import { ConcurrencyStrategy, HistoryResult, HistoryResultGroup, Pipeline, ResultType, Runnable, RunStrategy, Stage, Step, Task } from "../d.ts";
import _ from "lodash";
import { RunHistory } from "./run-history";
import { pluginRegistry, TaskPlugin } from "../plugin";
import { IAccessService } from "../access/access-service";
import { ContextFactory } from "./context";
import { ContextFactory, IContext } from "./context";
import { IStorage } from "./storage";
import { logger } from "../utils/util.log";
import { Logger } from "log4js";
export class Executor {
userId: any;
pipeline: Pipeline;
runtime: RunHistory = new RunHistory();
lastSuccessHistory: RunHistory;
runtime!: RunHistory;
accessService: IAccessService;
contextFactory: ContextFactory;
logger: Logger;
pipelineContext: IContext;
onChanged: (history: RunHistory) => void;
constructor(options: {
userId: any;
pipeline: Pipeline;
storage: IStorage;
onChanged: (history: RunHistory) => void;
lastSuccessHistory?: RunHistory;
accessService: IAccessService;
}) {
constructor(options: { userId: any; pipeline: Pipeline; storage: IStorage; onChanged: (history: RunHistory) => void; accessService: IAccessService }) {
this.pipeline = options.pipeline;
this.lastSuccessHistory = options.lastSuccessHistory ?? new RunHistory();
this.onChanged = options.onChanged;
this.accessService = options.accessService;
this.userId = options.userId;
this.contextFactory = new ContextFactory(options.storage);
this.logger = logger;
this.pipelineContext = this.contextFactory.getContext("pipeline", this.pipeline.id);
}
async run() {
await this.runWithHistory(this.pipeline, async () => {
return await this.runStages();
});
}
async runWithHistory(runnable: Runnable, run: () => Promise<any>) {
this.runtime.start(runnable);
this.onChanged(this.runtime);
async run(runtimeId: any = 0) {
try {
await run();
this.runtime = new RunHistory(runtimeId);
this.logger.info(`pipeline.${this.pipeline.id} start`);
await this.runWithHistory(this.pipeline, "pipeline", async () => {
return await this.runStages();
});
} finally {
this.logger.info(`pipeline.${this.pipeline.id} end`);
}
}
async runWithHistory(runnable: Runnable, runnableType: string, run: (result: HistoryResult) => Promise<any>) {
const result = this.runtime.start(runnable, runnableType);
this.onChanged(this.runtime);
const contextKey = `status.${runnable.id}`;
if (runnable.strategy?.runStrategy === RunStrategy.SkipWhenSucceed) {
const lastResult = await this.pipelineContext.get(contextKey);
if (lastResult != null && lastResult === ResultType.success) {
this.runtime.log(runnable, result, "跳过");
return ResultType.skip;
}
}
try {
await run(result);
this.runtime.success(runnable);
this.onChanged(this.runtime);
await this.pipelineContext.set(contextKey, ResultType.success);
return ResultType.success;
} catch (e: any) {
logger.error(e);
this.logger.error(e);
this.runtime.error(runnable, e);
this.onChanged(this.runtime);
await this.pipelineContext.set(contextKey, ResultType.error);
throw e;
} finally {
this.runtime.finally(runnable);
}
}
private async runStages() {
const resList: ResultType[] = [];
for (const stage of this.pipeline.stages) {
await this.runWithHistory(stage, async () => {
return await this.runStage(stage);
const res: ResultType = await this.runWithHistory(stage, "stage", async (result) => {
return await this.runStage(stage, result);
});
resList.push(res);
}
return this.compositionResultType(resList);
}
async runStage(stage: Stage) {
async runStage(stage: Stage, result: HistoryResult) {
const runnerList = [];
for (const task of stage.tasks) {
const runner = this.runWithHistory(task, async () => {
return await this.runTask(task);
const runner = this.runWithHistory(task, "task", async (result) => {
return await this.runTask(task, result);
});
runnerList.push(runner);
}
let resList: ResultType[] = [];
if (stage.concurrency === ConcurrencyStrategy.Parallel) {
await Promise.all(runnerList);
resList = await Promise.all(runnerList);
} else {
for (const runner of runnerList) {
await runner;
for (let i = 0; i < runnerList.length; i++) {
const runner = runnerList[i];
resList[i] = await runner;
}
}
return this.compositionResultType(resList);
}
private async runTask(task: Task) {
for (const step of task.steps) {
await this.runWithHistory(step, async () => {
return await this.runStep(step);
});
compositionResultType(resList: ResultType[]) {
let hasSuccess = false;
for (const type of resList) {
if (type === ResultType.error) {
return ResultType.error;
}
if (type === ResultType.success) {
hasSuccess = true;
}
}
if (hasSuccess) {
return ResultType.success;
}
return ResultType.error;
}
private async runStep(step: Step) {
//执行任务
const taskPlugin: TaskPlugin = await this.getPlugin(step.type);
private async runTask(task: Task, result: HistoryResult) {
const resList: ResultType[] = [];
for (const step of task.steps) {
const res: ResultType = await this.runWithHistory(step, "step", async (result) => {
await this.runStep(step, result);
});
resList.push(res);
}
return this.compositionResultType(resList);
}
private async runStep(step: Step, result: HistoryResult) {
//执行任务
const taskPlugin: TaskPlugin = await this.getPlugin(step.type, result.logger);
// @ts-ignore
delete taskPlugin.define;
const define = taskPlugin.getDefine();
//从outputContext读取输入参数
_.forEach(define.input, (item, key) => {
@@ -109,14 +151,15 @@ export class Executor {
});
}
private async getPlugin(type: string): Promise<TaskPlugin> {
private async getPlugin(type: string, logger: Logger): Promise<TaskPlugin> {
const pluginClass = pluginRegistry.get(type);
// @ts-ignore
const plugin = new pluginClass();
await plugin.doInit({
accessService: this.accessService,
pipelineContext: this.contextFactory.getContext("pipeline", this.pipeline.id),
pipelineContext: this.pipelineContext,
userContext: this.contextFactory.getContext("user", this.userId),
logger,
});
return plugin;
}
+33 -21
View File
@@ -1,62 +1,74 @@
import { Context, HistoryResult, Log, Runnable } from "../d.ts/pipeline";
import { Context, HistoryResult, Log, Runnable } from "../d.ts";
import _ from "lodash";
import { buildLogger, logger } from "../utils/util.log";
export class RunHistory {
constructor(runtimeId: any) {
this.id = runtimeId;
}
id: any;
logs: Log[] = [];
context: Context = {};
results: {
[key: string]: HistoryResult;
} = {};
logger = logger;
start(runnable: Runnable) {
const status = "ing";
start(runnable: Runnable, runnableType: string) {
const status = "start";
const now = new Date().getTime();
_.merge(runnable, { status, lastTime: now });
this.results[runnable.id] = {
const result: HistoryResult = {
type: runnableType,
startTime: new Date().getTime(),
title: runnable.title,
status,
logs: [],
logger: buildLogger((text) => {
result.logs.push(text);
}),
};
this.log(runnable, `${runnable.title}<${runnable.id}> 开始执行`);
this.results[runnable.id] = result;
this.log(runnable, result, `${runnable.title}<id:${runnable.id}> 开始执行`);
return result;
}
success(runnable: Runnable, result?: any) {
success(runnable: Runnable, res?: any) {
const status = "success";
const now = new Date().getTime();
_.merge(runnable, { status, lastTime: now });
_.merge(this.results[runnable.id], { status, endTime: now }, result);
this.log(
runnable,
`${this.results[runnable.id].title}<${runnable.id}> 执行成功`
);
const result = this.results[runnable.id];
_.merge(result, { status, endTime: now }, res);
this.log(runnable, result, `${result.title}<id:${runnable.id}> 执行成功`);
}
error(runnable: Runnable, e: Error) {
const status = "error";
const now = new Date().getTime();
_.merge(runnable, { status, lastTime: now });
_.merge(this.results[runnable.id], {
const result = this.results[runnable.id];
_.merge(result, {
status,
endTime: now,
errorMessage: e.message,
});
this.log(
runnable,
`${this.results[runnable.id].title}<${runnable.id}> 执行异常:${
e.message
}`,
status
);
this.log(runnable, result, `${result.title}<id:${runnable.id}> 执行异常:${e.message}`, status);
}
log(runnable: Runnable, text: string, level = "info") {
log(runnable: Runnable, result: HistoryResult, text: string, level = "info") {
const now = new Date().getTime();
result.logger.info(`[${runnable.title}] [${result.type}]`, text);
this.logs.push({
time: new Date().getTime(),
time: now,
level,
title: runnable.title,
text,
});
}
finally(runnable: Runnable) {
//
}
}
+4 -9
View File
@@ -7,11 +7,6 @@ export interface IStorage {
}
export class FileStorage implements IStorage {
/**
* 范围: user / pipeline / runtime / task
*/
scope: any;
namespace: any;
root: string;
constructor(rootDir?: string) {
if (rootDir == null) {
@@ -42,17 +37,17 @@ export class FileStorage implements IStorage {
}
async get(scope: string, namespace: string, key: string): Promise<string | null> {
const path = `${this.root}/${this.scope}/${namespace}/${key}`;
const path = this.buildPath(scope, namespace, key);
return this.readFile(path);
}
async set(scope: string, namespace: string, key: string, value: string): Promise<void> {
const path = this.buildPath(namespace, key);
const path = this.buildPath(scope, namespace, key);
this.writeFile(path, value);
}
private buildPath(namespace: string, key: string) {
return `${this.root}/${this.scope}/${namespace}/${key}`;
private buildPath(scope: string, namespace: string, key: string) {
return `${this.root}/${scope}/${namespace}/${key}`;
}
}
+20 -3
View File
@@ -1,3 +1,5 @@
import { Logger } from "log4js";
export enum RunStrategy {
AlwaysRun,
SkipWhenSucceed,
@@ -27,9 +29,9 @@ export type EventHandler = {
};
export type RunnableStrategy = {
runStrategy: RunStrategy;
onSuccess: EventHandler[];
onError: EventHandler[];
runStrategy?: RunStrategy;
onSuccess?: EventHandler[];
onError?: EventHandler[];
};
export type Step = Runnable & {
@@ -79,7 +81,20 @@ export type Log = {
text: string;
};
export enum ResultType {
success,
error,
skip,
}
export type HistoryResultGroup = {
[key: string]: {
runnable: Runnable;
res: HistoryResult;
};
};
export type HistoryResult = {
type: string;
title: string;
/**
* 任务状态
@@ -92,4 +107,6 @@ export type HistoryResult = {
*/
result?: string;
errorMessage?: string;
logs: string[];
logger: Logger;
};
@@ -1,12 +1,13 @@
import { AbstractRegistrable } from "../registry";
import { CreateRecordOptions, IDnsProvider, DnsProviderDefine, RemoveRecordOptions } from "./api";
import { AbstractAccess } from "../access";
import { Logger } from "log4js";
export abstract class AbstractDnsProvider extends AbstractRegistrable<DnsProviderDefine> implements IDnsProvider {
// @ts-ignore
access: AbstractAccess;
doInit(options: { access: AbstractAccess }) {
access!: AbstractAccess;
logger!: Logger;
doInit(options: { access: AbstractAccess; logger: Logger }) {
this.access = options.access;
this.logger = options.logger;
this.onInit();
}
@@ -1,5 +1,6 @@
import { Registrable } from "../registry";
import { dnsProviderRegistry } from "./registry";
import { AbstractDnsProvider } from "./abstract-dns-provider";
export type DnsProviderDefine = Registrable & {
accessType: string;
@@ -21,9 +22,13 @@ export interface IDnsProvider {
removeRecord(options: RemoveRecordOptions): Promise<any>;
}
export function IsDnsProvider(define: DnsProviderDefine) {
return function (target: any) {
target.prototype.define = define;
export function IsDnsProvider(define: (() => DnsProviderDefine) | DnsProviderDefine) {
return function (target: typeof AbstractDnsProvider) {
if (define instanceof Function) {
target.prototype.define = define();
} else {
target.prototype.define = define;
}
dnsProviderRegistry.install(target);
};
}
@@ -14,7 +14,6 @@ export class AliyunDnsProvider extends AbstractDnsProvider implements IDnsProvid
constructor() {
super();
}
async onInit() {
const access: any = this.access;
this.client = new Core({
@@ -1,12 +1,11 @@
import { AbstractRegistrable } from "../registry";
import { Logger } from "log4js";
import { logger } from "../utils/util.log";
import { IAccessService } from "../access/access-service";
import { IContext } from "../core/context";
import { PluginDefine, TaskInput, TaskOutput, TaskPlugin } from "./api";
export abstract class AbstractPlugin extends AbstractRegistrable<PluginDefine> implements TaskPlugin {
logger: Logger = logger;
logger!: Logger;
// @ts-ignore
accessService: IAccessService;
// @ts-ignore
@@ -14,10 +13,11 @@ export abstract class AbstractPlugin extends AbstractRegistrable<PluginDefine> i
// @ts-ignore
userContext: IContext;
async doInit(options: { accessService: IAccessService; pipelineContext: IContext; userContext: IContext }) {
async doInit(options: { accessService: IAccessService; pipelineContext: IContext; userContext: IContext; logger: Logger }) {
this.accessService = options.accessService;
this.pipelineContext = options.pipelineContext;
this.userContext = options.userContext;
this.logger = options.logger;
await this.onInit();
}
@@ -1,18 +1,19 @@
// @ts-ignore
import * as acme from "@certd/acme-client";
import _ from "lodash";
import { logger } from "../../../utils/util.log";
import { AbstractDnsProvider } from "../../../dns-provider/abstract-dns-provider";
import { IContext } from "../../../core/context";
import { IDnsProvider } from "../../../dns-provider";
import { Challenge } from "@certd/acme-client/types/rfc8555";
console.log("acme", acme);
import { Logger } from "log4js";
export class AcmeService {
userContext: IContext;
constructor(options: { userContext: IContext }) {
logger: Logger;
constructor(options: { userContext: IContext; logger: Logger }) {
this.userContext = options.userContext;
this.logger = options.logger;
acme.setLogger((text: string) => {
logger.info(text);
this.logger.info(text);
});
}
@@ -64,27 +65,27 @@ export class AcmeService {
}
async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider) {
logger.info("Triggered challengeCreateFn()");
this.logger.info("Triggered challengeCreateFn()");
/* http-01 */
if (challenge.type === "http-01") {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
const fileContents = keyAuthorization;
logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`);
this.logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`);
/* Replace this */
logger.info(`Would write "${fileContents}" to path "${filePath}"`);
this.logger.info(`Would write "${fileContents}" to path "${filePath}"`);
// await fs.writeFileAsync(filePath, fileContents);
} else if (challenge.type === "dns-01") {
/* dns-01 */
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
const recordValue = keyAuthorization;
logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`);
this.logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`);
/* Replace this */
logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
this.logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
return await dnsProvider.createRecord({
fullRecord: dnsRecord,
@@ -106,25 +107,25 @@ export class AcmeService {
*/
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordItem: any, dnsProvider: IDnsProvider) {
logger.info("Triggered challengeRemoveFn()");
this.logger.info("Triggered challengeRemoveFn()");
/* http-01 */
if (challenge.type === "http-01") {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`);
this.logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`);
/* Replace this */
logger.info(`Would remove file on path "${filePath}"`);
this.logger.info(`Would remove file on path "${filePath}"`);
// await fs.unlinkAsync(filePath);
} else if (challenge.type === "dns-01") {
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
const recordValue = keyAuthorization;
logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`);
this.logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`);
/* Replace this */
logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
this.logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
await dnsProvider.removeRecord({
fullRecord: dnsRecord,
type: "TXT",
@@ -169,9 +170,9 @@ export class AcmeService {
csr: csr.toString(),
};
/* Done */
logger.debug(`CSR:\n${cert.csr}`);
logger.debug(`Certificate:\n${cert.crt}`);
logger.info("证书申请成功");
this.logger.debug(`CSR:\n${cert.csr}`);
this.logger.debug(`Certificate:\n${cert.crt}`);
this.logger.info("证书申请成功");
return cert;
}
@@ -1,6 +1,6 @@
import { AbstractPlugin } from "../../abstract-plugin";
import forge from "node-forge";
import { ContextScope, IsTask, TaskInput, TaskOutput, TaskPlugin } from "../../api";
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../../api";
import dayjs from "dayjs";
import { dnsProviderRegistry } from "../../../dns-provider";
import { AbstractDnsProvider } from "../../../dns-provider/abstract-dns-provider";
@@ -39,53 +39,49 @@ export type CertInfo = {
dnsProviderType: {
title: "DNS提供商",
component: {
name: "a-select",
name: "pi-dns-provider-selector",
},
helper: "请选择dns解析提供商",
},
dnsProviderAccess: {
title: "DNS解析授权",
component: {
name: "access-selector",
name: "pi-access-selector",
},
helper: "请选择dns解析提供商授权",
},
renewDays: {
title: "更新天数",
value: 20,
component: {
name: "a-input-number",
value: 20,
},
helper: "到期前多少天后更新证书",
},
forceUpdate: {
title: "强制更新",
value: false,
component: {
name: "a-switch",
vModel: "checked",
value: false,
},
helper: "强制重新申请证书",
helper: "是否强制重新申请证书",
},
},
output: {
cert: {
key: "cert",
type: "CertInfo",
title: "证书",
scope: ContextScope.pipeline,
title: "域名证书",
},
},
};
})
export class CertPlugin extends AbstractPlugin implements TaskPlugin {
export class CertApplyPlugin extends AbstractPlugin implements TaskPlugin {
// @ts-ignore
acme: AcmeService;
constructor() {
super();
}
protected async onInit() {
this.acme = new AcmeService({ userContext: this.userContext });
this.acme = new AcmeService({ userContext: this.userContext, logger: this.logger });
}
async execute(input: TaskInput): Promise<TaskOutput> {
@@ -139,7 +135,7 @@ export class CertPlugin extends AbstractPlugin implements TaskPlugin {
const access = await this.accessService.getById(dnsProviderAccessId);
// @ts-ignore
const dnsProvider: AbstractDnsProvider = new dnsProviderClass();
dnsProvider.doInit({ access });
dnsProvider.doInit({ access, logger: this.logger });
const cert = await this.acme.order({
email,
@@ -10,33 +10,34 @@ import { CertInfo } from "../cert-plugin";
return {
name: "DeployCertToAliyunCDN",
title: "部署证书至阿里云CDN",
desc: "依赖证书申请前置任务,自动部署域名证书至阿里云CDN",
input: {
domainName: {
title: "cdn加速域名",
title: "CDN加速域名",
component: {
placeholder: "cdn加速域名",
placeholder: "你在阿里云上配置的CDN加速域名,比如certd.docmirror.cn",
},
required: true,
},
certName: {
title: "证书名称",
component: {
placeholder: "上传后将以此名称作为前缀",
placeholder: "上传后将以此名称作为前缀备注",
},
},
cert: {
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
name: "pi-output-selector",
},
required: true,
},
accessId: {
title: "Access提供者",
helper: "access授权",
title: "Access授权",
helper: "阿里云授权AccessKeyId、AccessKeySecret",
component: {
name: "access-selector",
name: "pi-access-selector",
type: "aliyun",
},
required: true,
@@ -46,16 +47,13 @@ import { CertInfo } from "../cert-plugin";
};
})
export class DeployCertToAliyunCDN extends AbstractPlugin implements TaskPlugin {
constructor() {
super();
}
async execute(input: TaskInput): Promise<TaskOutput> {
console.log("开始部署证书到阿里云cdn");
const access = (await this.accessService.getById(input.accessId)) as AliyunAccess;
const client = this.getClient(access);
const params = await this.buildParams(input);
await this.doRequest(client, params);
console.log("部署完成");
return {};
}
@@ -70,7 +68,7 @@ export class DeployCertToAliyunCDN extends AbstractPlugin implements TaskPlugin
async buildParams(input: TaskInput) {
const { certName, domainName } = input;
const CertName = certName + "-" + dayjs().format("YYYYMMDDHHmmss");
const CertName = (certName ?? "certd") + "-" + dayjs().format("YYYYMMDDHHmmss");
const cert = input.cert as CertInfo;
return {
RegionId: "cn-hangzhou",
@@ -4,12 +4,12 @@ import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../api";
@IsTask(() => {
return {
name: "EchoPlugin",
title: "测试插件回声",
title: "测试插件【echo】",
input: {
cert: {
title: "cert",
component: {
name: "output-selector",
name: "pi-output-selector",
},
helper: "输出选择",
},
@@ -20,7 +20,7 @@ import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../api";
export class EchoPlugin extends AbstractPlugin implements TaskPlugin {
async execute(input: TaskInput): Promise<TaskOutput> {
for (const key in input) {
console.log("input :", key, input[key]);
this.logger.info("input :", key, input[key]);
}
return input;
}
@@ -1,5 +1,4 @@
import { Logger } from "log4js";
import { logger } from "../utils/util.log";
export type Registrable = {
name: string;
@@ -8,7 +7,6 @@ export type Registrable = {
};
export abstract class AbstractRegistrable<T extends Registrable> {
logger: Logger = logger;
// @ts-ignore
define: T;
@@ -21,18 +19,20 @@ export class Registry<T extends typeof AbstractRegistrable> {
[key: string]: T;
} = {};
install(target: T) {
if (target == null) {
install(pluginClass: T) {
if (pluginClass == null) {
return;
}
// @ts-ignore
const define = new target().define;
const plugin = new pluginClass();
delete plugin.define;
const define = plugin.getDefine();
let defineName = define.name;
if (defineName == null) {
defineName = target.name;
defineName = plugin.name;
}
this.register(defineName, target);
this.register(defineName, pluginClass);
}
register(key: string, value: T) {
@@ -57,4 +57,15 @@ export class Registry<T extends typeof AbstractRegistrable> {
getStorage() {
return this.storage;
}
getDefineList() {
const list = [];
for (const key in this.storage) {
const PluginClass = this.storage[key];
// @ts-ignore
const plugin = new PluginClass();
list.push({ ...plugin.define, key });
}
return list;
}
}
+32 -4
View File
@@ -1,6 +1,34 @@
import log4js from "log4js";
import log4js, { Appender, Logger, LoggingEvent } from "log4js";
const OutputAppender = {
configure: (config: any, layouts: any, findAppender: any, levels: any) => {
let layout = layouts.colouredLayout;
if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout);
}
function customAppender(layout: any, timezoneOffset: any) {
return (loggingEvent: LoggingEvent) => {
if (loggingEvent.context.outputHandler?.write) {
const text = `${layout(loggingEvent, timezoneOffset)}\n`;
loggingEvent.context.outputHandler.write(text);
}
};
}
return customAppender(layout, config.timezoneOffset);
},
};
// @ts-ignore
log4js.configure({
appenders: { std: { type: "stdout" } },
categories: { default: { appenders: ["std"], level: "info" } },
appenders: { std: { type: "stdout" }, output: { type: OutputAppender } },
categories: { default: { appenders: ["std"], level: "info" }, pipeline: { appenders: ["std", "output"], level: "info" } },
});
export const logger = log4js.getLogger("pipeline");
export const logger = log4js.getLogger("default");
export function buildLogger(write: (text: string) => void) {
const logger = log4js.getLogger("pipeline");
logger.addContext("outputHandler", {
write,
});
return logger;
}