统一 Node 22 与后端安全运行基线(07-15-production-trust-closeout-cleanup)
这个提交包含在:
+20
-5
@@ -1,7 +1,7 @@
|
||||
require("dotenv/config");
|
||||
|
||||
const { getConfig } = require("./config");
|
||||
const { initTracing } = require("./observability/tracing");
|
||||
const { initTracing, shutdownTracing } = require("./observability/tracing");
|
||||
|
||||
const config = getConfig();
|
||||
initTracing(config);
|
||||
@@ -35,22 +35,37 @@ async function main() {
|
||||
}, timeoutMs);
|
||||
forceTimer.unref?.();
|
||||
|
||||
let exitCode = 0;
|
||||
try {
|
||||
await app.close();
|
||||
app.log.info("Graceful shutdown completed");
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
app.log.error({ err: error }, "Graceful shutdown failed");
|
||||
process.exit(1);
|
||||
exitCode = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
await shutdownTracing();
|
||||
} catch (error) {
|
||||
app.log.error({ err: error }, "OTel shutdown failed");
|
||||
exitCode = 1;
|
||||
}
|
||||
|
||||
clearTimeout(forceTimer);
|
||||
app.log.info({ exitCode }, "Graceful shutdown completed");
|
||||
process.exit(exitCode);
|
||||
};
|
||||
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
main().catch(async (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
try {
|
||||
await shutdownTracing();
|
||||
} catch {
|
||||
// The startup error remains authoritative; shutdown is best effort here.
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,42 +1,56 @@
|
||||
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
||||
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node');
|
||||
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
||||
const { Resource } = require('@opentelemetry/resources');
|
||||
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
||||
const { NodeSDK } = require("@opentelemetry/sdk-node");
|
||||
const { ConsoleSpanExporter } = require("@opentelemetry/sdk-trace-node");
|
||||
const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node");
|
||||
const { resourceFromAttributes } = require("@opentelemetry/resources");
|
||||
const { ATTR_SERVICE_NAME } = require("@opentelemetry/semantic-conventions");
|
||||
|
||||
let sdk = null;
|
||||
|
||||
function initTracing(config) {
|
||||
if (config.otelTracesEnabled !== true) {
|
||||
return;
|
||||
}
|
||||
function initTracing(config, overrides = {}) {
|
||||
if (config.otelTracesEnabled !== true) {
|
||||
return null;
|
||||
}
|
||||
if (sdk) {
|
||||
return sdk;
|
||||
}
|
||||
|
||||
const exporter = new ConsoleSpanExporter();
|
||||
try {
|
||||
const NodeSDKImpl = overrides.NodeSDK ?? NodeSDK;
|
||||
const ConsoleSpanExporterImpl = overrides.ConsoleSpanExporter ?? ConsoleSpanExporter;
|
||||
const createResource = overrides.resourceFromAttributes ?? resourceFromAttributes;
|
||||
const buildInstrumentations = overrides.getNodeAutoInstrumentations ?? getNodeAutoInstrumentations;
|
||||
const logger = overrides.logger ?? console;
|
||||
const serviceName = overrides.serviceName ?? "hyb-oa-server";
|
||||
|
||||
sdk = new NodeSDK({
|
||||
resource: new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: 'hyb-oa-server',
|
||||
sdk = new NodeSDKImpl({
|
||||
resource: createResource({
|
||||
[ATTR_SERVICE_NAME]: serviceName,
|
||||
}),
|
||||
traceExporter: new ConsoleSpanExporterImpl(),
|
||||
instrumentations: [
|
||||
buildInstrumentations({
|
||||
"@opentelemetry/instrumentation-fs": { enabled: false },
|
||||
}),
|
||||
traceExporter: exporter,
|
||||
instrumentations: [getNodeAutoInstrumentations({
|
||||
'@opentelemetry/instrumentation-fs': { enabled: false },
|
||||
})]
|
||||
});
|
||||
|
||||
try {
|
||||
sdk.start();
|
||||
console.log('OTel tracing initialized');
|
||||
} catch (error) {
|
||||
console.error('Error initializing OTel tracing', error);
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
if (sdk) {
|
||||
sdk.shutdown()
|
||||
.then(() => console.log('OTel tracing terminated'))
|
||||
.catch((error) => console.log('Error terminating OTel tracing', error));
|
||||
}
|
||||
],
|
||||
});
|
||||
sdk.start();
|
||||
logger.log("OTel tracing initialized");
|
||||
return sdk;
|
||||
} catch (error) {
|
||||
sdk = null;
|
||||
(overrides.logger ?? console).error("Error initializing OTel tracing", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { initTracing };
|
||||
async function shutdownTracing() {
|
||||
if (!sdk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeSdk = sdk;
|
||||
sdk = null;
|
||||
await activeSdk.shutdown();
|
||||
}
|
||||
|
||||
module.exports = { initTracing, shutdownTracing };
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
require("dotenv/config");
|
||||
|
||||
const { Client } = require("pg");
|
||||
|
||||
const LOCAL_DATABASE_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
||||
|
||||
class DevSeedSafetyError extends Error {
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
this.name = "DevSeedSafetyError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function refuse(code, message) {
|
||||
throw new DevSeedSafetyError(code, message);
|
||||
}
|
||||
|
||||
function validateDevSeedSafety(env = process.env) {
|
||||
if (String(env.NODE_ENV || "").trim().toLowerCase() === "production") {
|
||||
refuse("production_forbidden", "seed:dev is permanently disabled in production");
|
||||
}
|
||||
if (String(env.SEED_DEV_ALLOW || "").trim() !== "1") {
|
||||
refuse("explicit_allow_required", "seed:dev requires the explicit local allow switch");
|
||||
}
|
||||
|
||||
let databaseUrl;
|
||||
try {
|
||||
databaseUrl = new URL(String(env.DATABASE_URL || ""));
|
||||
} catch {
|
||||
refuse("database_url_invalid", "seed:dev requires a valid local PostgreSQL target");
|
||||
}
|
||||
if (!["postgres:", "postgresql:"].includes(databaseUrl.protocol)) {
|
||||
refuse("database_protocol_invalid", "seed:dev requires a local PostgreSQL target");
|
||||
}
|
||||
|
||||
let effectiveTarget;
|
||||
try {
|
||||
const client = new Client({ connectionString: databaseUrl.toString() });
|
||||
effectiveTarget = {
|
||||
host: String(client.host || "").toLowerCase(),
|
||||
databaseName: String(client.database || "").trim(),
|
||||
};
|
||||
} catch {
|
||||
refuse("database_url_invalid", "seed:dev requires a valid local PostgreSQL target");
|
||||
}
|
||||
if (!LOCAL_DATABASE_HOSTS.has(effectiveTarget.host)) {
|
||||
refuse("database_host_not_local", "seed:dev only accepts a loopback database target");
|
||||
}
|
||||
|
||||
const databaseName = decodeURIComponent(databaseUrl.pathname.replace(/^\/+/, "")).trim();
|
||||
if (!databaseName || databaseName.includes("/") || effectiveTarget.databaseName !== databaseName) {
|
||||
refuse("database_name_invalid", "seed:dev requires one explicit database name");
|
||||
}
|
||||
const confirmation = String(env.SEED_DEV_DATABASE_CONFIRM || "").trim();
|
||||
if (!confirmation || confirmation !== databaseName) {
|
||||
refuse("database_confirmation_mismatch", "seed:dev database confirmation does not match the target");
|
||||
}
|
||||
|
||||
return {
|
||||
databaseName,
|
||||
resetPasswords: String(env.SEED_DEV_RESET_PASSWORDS || "").trim() === "1",
|
||||
};
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
const result = validateDevSeedSafety();
|
||||
const passwordMode = result.resetPasswords ? "one_time_reset" : "preserve_existing";
|
||||
console.log(`[seed-dev] safety check passed; passwordMode=${passwordMode}`);
|
||||
} catch (error) {
|
||||
const code = error instanceof DevSeedSafetyError ? error.code : "unexpected_error";
|
||||
console.error(`[seed-dev][REFUSED] ${code}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DevSeedSafetyError,
|
||||
validateDevSeedSafety,
|
||||
};
|
||||
+49
-48
@@ -2,6 +2,7 @@ require("dotenv/config");
|
||||
|
||||
const { createPrismaClient } = require("../prismaClient");
|
||||
const { hashPassword } = require("../utils/crypto");
|
||||
const { validateDevSeedSafety } = require("./dev-seed-safety");
|
||||
|
||||
async function ensureDepartment(prisma, payload) {
|
||||
const now = new Date();
|
||||
@@ -330,9 +331,8 @@ async function ensureWorkspaceSetting(prisma) {
|
||||
return prisma.workspaceSetting.create({ data });
|
||||
}
|
||||
|
||||
async function ensureEmployee(prisma, payload) {
|
||||
async function ensureEmployee(prisma, payload, { resetPasswords = false } = {}) {
|
||||
const now = new Date();
|
||||
const passwordHash = await hashPassword(payload.password);
|
||||
|
||||
const existing = await prisma.employee.findFirst({
|
||||
where: { email: payload.email },
|
||||
@@ -340,6 +340,12 @@ async function ensureEmployee(prisma, payload) {
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
const passwordData = resetPasswords
|
||||
? {
|
||||
password_hash: await hashPassword(payload.password),
|
||||
must_change_password: true,
|
||||
}
|
||||
: {};
|
||||
const employee = await prisma.employee.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
@@ -353,11 +359,10 @@ async function ensureEmployee(prisma, payload) {
|
||||
status: "active",
|
||||
bank_account_encrypted: "ENC_DEV_ONLY",
|
||||
bank_name: "DEV_BANK",
|
||||
password_hash: passwordHash,
|
||||
must_change_password: false,
|
||||
...passwordData,
|
||||
updated_at: now,
|
||||
},
|
||||
select: { id: true, email: true },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
await prisma.employeeRole.deleteMany({
|
||||
@@ -373,9 +378,13 @@ async function ensureEmployee(prisma, payload) {
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
return employee;
|
||||
return {
|
||||
...employee,
|
||||
seedPasswordAction: resetPasswords ? "reset" : "preserved",
|
||||
};
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(payload.password);
|
||||
const employee = await prisma.employee.create({
|
||||
data: {
|
||||
employee_no: payload.employeeNo,
|
||||
@@ -391,11 +400,11 @@ async function ensureEmployee(prisma, payload) {
|
||||
bank_account_encrypted: "ENC_DEV_ONLY",
|
||||
bank_name: "DEV_BANK",
|
||||
password_hash: passwordHash,
|
||||
must_change_password: false,
|
||||
must_change_password: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
select: { id: true, email: true },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
await prisma.employeeRole.createMany({
|
||||
@@ -407,10 +416,11 @@ async function ensureEmployee(prisma, payload) {
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
return employee;
|
||||
return { ...employee, seedPasswordAction: "created" };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const safety = validateDevSeedSafety();
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
await prisma.$connect();
|
||||
|
||||
@@ -451,6 +461,9 @@ async function main() {
|
||||
});
|
||||
|
||||
await ensureWorkspaceSetting(prisma);
|
||||
const ensureDevEmployee = (payload) => ensureEmployee(prisma, payload, {
|
||||
resetPasswords: safety.resetPasswords,
|
||||
});
|
||||
|
||||
for (const flagKey of [
|
||||
"portal_home_v1",
|
||||
@@ -477,7 +490,7 @@ async function main() {
|
||||
});
|
||||
}
|
||||
|
||||
const ceo = await ensureEmployee(prisma, {
|
||||
const ceo = await ensureDevEmployee({
|
||||
employeeNo: process.env.SEED_CEO_EMPLOYEE_NO ?? "HYD000001",
|
||||
name: "张总",
|
||||
phone: process.env.SEED_CEO_PHONE ?? "13900000000",
|
||||
@@ -491,7 +504,7 @@ async function main() {
|
||||
hireDate,
|
||||
});
|
||||
|
||||
const admin = await ensureEmployee(prisma, {
|
||||
const admin = await ensureDevEmployee({
|
||||
employeeNo: process.env.SEED_ADMIN_EMPLOYEE_NO ?? "HYD000006",
|
||||
name: "王顺利",
|
||||
phone: process.env.SEED_ADMIN_PHONE ?? "13800000000",
|
||||
@@ -505,7 +518,7 @@ async function main() {
|
||||
hireDate,
|
||||
});
|
||||
|
||||
const manager = await ensureEmployee(prisma, {
|
||||
const manager = await ensureDevEmployee({
|
||||
employeeNo: process.env.SEED_MANAGER_EMPLOYEE_NO ?? "HYD000002",
|
||||
name: "李研发",
|
||||
phone: process.env.SEED_MANAGER_PHONE ?? "13700000000",
|
||||
@@ -519,7 +532,7 @@ async function main() {
|
||||
hireDate,
|
||||
});
|
||||
|
||||
const employee = await ensureEmployee(prisma, {
|
||||
const employee = await ensureDevEmployee({
|
||||
employeeNo: process.env.SEED_EMPLOYEE_EMPLOYEE_NO ?? "HYD000003",
|
||||
name: "陈工程",
|
||||
phone: process.env.SEED_EMPLOYEE_PHONE ?? "13600000000",
|
||||
@@ -533,7 +546,7 @@ async function main() {
|
||||
hireDate,
|
||||
});
|
||||
|
||||
const finance = await ensureEmployee(prisma, {
|
||||
const finance = await ensureDevEmployee({
|
||||
employeeNo: process.env.SEED_FINANCE_EMPLOYEE_NO ?? "HYD000004",
|
||||
name: "赵财务",
|
||||
phone: process.env.SEED_FINANCE_PHONE ?? "13500000000",
|
||||
@@ -547,7 +560,7 @@ async function main() {
|
||||
hireDate,
|
||||
});
|
||||
|
||||
const financeMgr = await ensureEmployee(prisma, {
|
||||
const financeMgr = await ensureDevEmployee({
|
||||
employeeNo: process.env.SEED_FINANCE_MGR_EMPLOYEE_NO ?? "HYD000005",
|
||||
name: "钱主管",
|
||||
phone: process.env.SEED_FINANCE_MGR_PHONE ?? "13400000000",
|
||||
@@ -561,7 +574,7 @@ async function main() {
|
||||
hireDate,
|
||||
});
|
||||
|
||||
const hr = await ensureEmployee(prisma, {
|
||||
const hr = await ensureDevEmployee({
|
||||
employeeNo: process.env.SEED_HR_EMPLOYEE_NO ?? "HYD000007",
|
||||
name: "孙人事",
|
||||
phone: process.env.SEED_HR_PHONE ?? "13300000000",
|
||||
@@ -575,7 +588,7 @@ async function main() {
|
||||
hireDate,
|
||||
});
|
||||
|
||||
const administration = await ensureEmployee(prisma, {
|
||||
const administration = await ensureDevEmployee({
|
||||
employeeNo: process.env.SEED_ADMINISTRATION_EMPLOYEE_NO ?? "HYD000008",
|
||||
name: "周行政",
|
||||
phone: process.env.SEED_ADMINISTRATION_PHONE ?? "13200000000",
|
||||
@@ -659,40 +672,28 @@ async function main() {
|
||||
warningThresholdPercent: 75,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Seeded dev users:");
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- 负责人: ${ceo.email} / ${process.env.SEED_CEO_PASSWORD ?? "CeoPassword1234"}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- 管理员: ${admin.email} / ${process.env.SEED_ADMIN_PASSWORD ?? "AdminPassword1234"}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- 研发负责人: ${manager.email} / ${process.env.SEED_MANAGER_PASSWORD ?? "ManagerPassword1234"}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- 员工: ${employee.email} / ${process.env.SEED_EMPLOYEE_PASSWORD ?? "EmployeePassword1234"}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- 财务: ${finance.email} / ${process.env.SEED_FINANCE_PASSWORD ?? "FinancePassword1234"}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- 财务主管: ${financeMgr.email} / ${process.env.SEED_FINANCE_MGR_PASSWORD ?? "FinanceMgrPassword1234"}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- 人事: ${hr.email} / ${process.env.SEED_HR_PASSWORD ?? "HrPassword1234"}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- 行政: ${administration.email} / ${process.env.SEED_ADMINISTRATION_PASSWORD ?? "AdministrationPassword1234"}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- Legal Entity: ${legalEntity.code} / ${legalEntity.name}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- HolidayCalendar: ${holidayCalendar.code} / ${holidayCalendar.name}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- Supplier: ${supplier.code} / ${supplier.name}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- Budget Pools: ${managementBudgetPool.code}, ${rdBudgetPool.code}`);
|
||||
const users = [ceo, admin, manager, employee, finance, financeMgr, hr, administration];
|
||||
const countAction = (action) => users.filter((item) => item.seedPasswordAction === action).length;
|
||||
console.log(
|
||||
`[seed-dev] completed; users=${users.length} created=${countAction("created")} ` +
|
||||
`passwordsPreserved=${countAction("preserved")} oneTimeResets=${countAction("reset")}`,
|
||||
);
|
||||
console.log("[seed-dev] reference data synchronized; credential values were not printed");
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
const code = typeof error?.code === "string" ? error.code : error?.name || "unexpected_error";
|
||||
console.error(`[seed-dev][FAIL] ${code}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureEmployee,
|
||||
main,
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ function resolveEmailBaseUrl(config) {
|
||||
let cachedTransportKey = null;
|
||||
let cachedTransporter = null;
|
||||
|
||||
function getTransporter(config) {
|
||||
function getTransporter(config, createTransport = nodemailer.createTransport) {
|
||||
if (!config.emailEnabled) {
|
||||
return null;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ function getTransporter(config) {
|
||||
}
|
||||
|
||||
cachedTransportKey = key;
|
||||
cachedTransporter = nodemailer.createTransport({
|
||||
cachedTransporter = createTransport({
|
||||
host: config.smtpHost,
|
||||
port: config.smtpPort,
|
||||
secure: config.smtpSecure,
|
||||
@@ -86,6 +86,36 @@ function uniq(values) {
|
||||
return Array.from(new Set((values ?? []).filter(Boolean)));
|
||||
}
|
||||
|
||||
const SAFE_SMTP_COMMANDS = new Set([
|
||||
'CONN',
|
||||
'EHLO',
|
||||
'HELO',
|
||||
'MAIL FROM',
|
||||
'RCPT TO',
|
||||
'DATA',
|
||||
'AUTH',
|
||||
]);
|
||||
|
||||
function buildEmailErrorLog(error) {
|
||||
const result = { errorCode: 'email_delivery_failed' };
|
||||
const providerCode = String(error?.code ?? '').trim();
|
||||
if (/^[A-Za-z0-9_-]{1,40}$/.test(providerCode)) {
|
||||
result.providerCode = providerCode;
|
||||
}
|
||||
|
||||
const smtpCommand = String(error?.command ?? '').trim().toUpperCase();
|
||||
if (SAFE_SMTP_COMMANDS.has(smtpCommand)) {
|
||||
result.smtpCommand = smtpCommand;
|
||||
}
|
||||
|
||||
const smtpResponseCode = Number(error?.responseCode);
|
||||
if (Number.isInteger(smtpResponseCode) && smtpResponseCode >= 100 && smtpResponseCode <= 999) {
|
||||
result.smtpResponseCode = smtpResponseCode;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function sendNotificationEmailSafe(params) {
|
||||
try {
|
||||
const emailResult = await sendNotificationEmail(params);
|
||||
@@ -95,7 +125,7 @@ async function sendNotificationEmailSafe(params) {
|
||||
} catch (error) {
|
||||
params.logger?.error?.(
|
||||
{
|
||||
err: error,
|
||||
...buildEmailErrorLog(error),
|
||||
requestId: params.requestId,
|
||||
notificationType: params.type,
|
||||
recipients: params.recipientEmployeeIds?.length ?? 0,
|
||||
@@ -118,12 +148,13 @@ async function sendNotificationEmail({
|
||||
body,
|
||||
type = 'notification',
|
||||
targetPath,
|
||||
createTransport,
|
||||
}) {
|
||||
if (!config.emailEnabled) {
|
||||
return { skipped: true, sent: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const transporter = getTransporter(config);
|
||||
const transporter = getTransporter(config, createTransport);
|
||||
if (!transporter) {
|
||||
logger?.warn?.({ requestId }, 'Email enabled but transporter not configured');
|
||||
return { skipped: true, sent: 0, failed: 0 };
|
||||
@@ -184,10 +215,9 @@ async function sendNotificationEmail({
|
||||
failed += 1;
|
||||
logger?.error?.(
|
||||
{
|
||||
err: error,
|
||||
...buildEmailErrorLog(error),
|
||||
requestId,
|
||||
employeeId: employee.id,
|
||||
email: employee.email,
|
||||
notificationType: type,
|
||||
},
|
||||
'Email send failed',
|
||||
|
||||
+27
-5
@@ -1,3 +1,11 @@
|
||||
require("dotenv/config");
|
||||
|
||||
const { getConfig } = require("./config");
|
||||
const { initTracing, shutdownTracing } = require("./observability/tracing");
|
||||
|
||||
const config = getConfig();
|
||||
initTracing(config, { serviceName: "hyb-oa-worker" });
|
||||
|
||||
const { buildApp } = require("./app");
|
||||
|
||||
async function main() {
|
||||
@@ -24,23 +32,37 @@ async function main() {
|
||||
}, timeoutMs);
|
||||
forceTimer.unref?.();
|
||||
|
||||
let exitCode = 0;
|
||||
try {
|
||||
await app.close();
|
||||
app.log.info("Worker graceful shutdown completed");
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
app.log.error({ err: error }, "Worker graceful shutdown failed");
|
||||
process.exit(1);
|
||||
exitCode = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
await shutdownTracing();
|
||||
} catch (error) {
|
||||
app.log.error({ err: error }, "Worker OTel shutdown failed");
|
||||
exitCode = 1;
|
||||
}
|
||||
|
||||
clearTimeout(forceTimer);
|
||||
app.log.info({ exitCode }, "Worker graceful shutdown completed");
|
||||
process.exit(exitCode);
|
||||
};
|
||||
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
main().catch(async (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
try {
|
||||
await shutdownTracing();
|
||||
} catch {
|
||||
// The startup error remains authoritative; shutdown is best effort here.
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户