feat(config-service): split config-service from iam for plugin/layout config
- new NestJS service on port 3011/gRPC 50059 (ADR-026) - owns 6 config_ tables (plugin/role-mapping/role-layout/layout-tpl/user-override/outbox) - GraphQL Federation 2 subgraph with DataLoader + RouterAuthGuard - gRPC ConfigService + admin REST CRUD + user REST API - three-layer merge: registry.defaultProps + roleMapping.widget_props + userOverride.props - Redis cache with 5min TTL - registered in apollo-router supergraph + docker-compose + port-allocation Implements M3 of v2.1 migration plan.
This commit is contained in:
108
services/config-service/src/shared/cache/config-cache.service.ts
vendored
Normal file
108
services/config-service/src/shared/cache/config-cache.service.ts
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { getRedis } from "../../config/redis.js";
|
||||
import { cacheMetrics } from "../observability/metrics.js";
|
||||
|
||||
const CACHE_TTL_SECONDS = 300; // 5 分钟
|
||||
|
||||
/**
|
||||
* config-service Redis 缓存服务。
|
||||
*
|
||||
* 缓存策略:
|
||||
* - Plugin registry: `config:plugin:{pluginId}` → JSON PluginRegistryItem
|
||||
* - User layout override: `config:user-layout:{userId}` → JSON UserLayoutOverride
|
||||
* - TTL: 5 分钟,超时自动失效重新从 DB 加载
|
||||
* - 失效:admin 修改配置或用户更新布局时主动 del
|
||||
*
|
||||
* 使用 ioredis 单例(config/redis.ts 管理),不重复创建连接。
|
||||
*/
|
||||
@Injectable()
|
||||
export class ConfigCacheService {
|
||||
private static buildPluginKey(pluginId: string): string {
|
||||
return `config:plugin:${pluginId}`;
|
||||
}
|
||||
|
||||
private static buildUserLayoutKey(userId: string): string {
|
||||
return `config:user-layout:${userId}`;
|
||||
}
|
||||
|
||||
// ============ Plugin Registry ============
|
||||
|
||||
async getPlugin(pluginId: string): Promise<string | null> {
|
||||
const redis = getRedis();
|
||||
const raw = await redis.get(ConfigCacheService.buildPluginKey(pluginId));
|
||||
if (!raw) {
|
||||
cacheMetrics.recordMiss("plugin");
|
||||
return null;
|
||||
}
|
||||
cacheMetrics.recordHit("plugin");
|
||||
return raw;
|
||||
}
|
||||
|
||||
async setPlugin(pluginId: string, json: string): Promise<void> {
|
||||
const redis = getRedis();
|
||||
await redis.set(
|
||||
ConfigCacheService.buildPluginKey(pluginId),
|
||||
json,
|
||||
"EX",
|
||||
CACHE_TTL_SECONDS,
|
||||
);
|
||||
}
|
||||
|
||||
async invalidatePlugin(pluginId: string, reason = "manual"): Promise<void> {
|
||||
const redis = getRedis();
|
||||
await redis.del(ConfigCacheService.buildPluginKey(pluginId));
|
||||
cacheMetrics.recordInvalidation("plugin", reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量失效所有 plugin 缓存(admin 全量更新时调用)。
|
||||
* 通过 SCAN 匹配 config:plugin:* 模式删除。
|
||||
*/
|
||||
async invalidateAllPlugins(reason = "admin-update"): Promise<void> {
|
||||
const redis = getRedis();
|
||||
let cursor = "0";
|
||||
do {
|
||||
const [next, keys] = await redis.scan(
|
||||
cursor,
|
||||
"MATCH",
|
||||
"config:plugin:*",
|
||||
"COUNT",
|
||||
100,
|
||||
);
|
||||
cursor = next;
|
||||
if (keys.length > 0) {
|
||||
await redis.del(...keys);
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
cacheMetrics.recordInvalidation("plugin", reason);
|
||||
}
|
||||
|
||||
// ============ User Layout Override ============
|
||||
|
||||
async getUserLayout(userId: string): Promise<string | null> {
|
||||
const redis = getRedis();
|
||||
const raw = await redis.get(ConfigCacheService.buildUserLayoutKey(userId));
|
||||
if (!raw) {
|
||||
cacheMetrics.recordMiss("user-layout");
|
||||
return null;
|
||||
}
|
||||
cacheMetrics.recordHit("user-layout");
|
||||
return raw;
|
||||
}
|
||||
|
||||
async setUserLayout(userId: string, json: string): Promise<void> {
|
||||
const redis = getRedis();
|
||||
await redis.set(
|
||||
ConfigCacheService.buildUserLayoutKey(userId),
|
||||
json,
|
||||
"EX",
|
||||
CACHE_TTL_SECONDS,
|
||||
);
|
||||
}
|
||||
|
||||
async invalidateUserLayout(userId: string, reason = "manual"): Promise<void> {
|
||||
const redis = getRedis();
|
||||
await redis.del(ConfigCacheService.buildUserLayoutKey(userId));
|
||||
cacheMetrics.recordInvalidation("user-layout", reason);
|
||||
}
|
||||
}
|
||||
107
services/config-service/src/shared/errors/application-error.ts
Normal file
107
services/config-service/src/shared/errors/application-error.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
export type ErrorType =
|
||||
| "validation"
|
||||
| "not_found"
|
||||
| "permission_denied"
|
||||
| "unauthorized"
|
||||
| "conflict"
|
||||
| "business"
|
||||
| "database"
|
||||
| "internal";
|
||||
|
||||
export interface ErrorDetails {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export abstract class ApplicationError extends Error {
|
||||
abstract readonly type: ErrorType;
|
||||
abstract readonly statusCode: number;
|
||||
readonly code: string;
|
||||
readonly details?: ErrorDetails;
|
||||
// traceId 改为可写,以便 GlobalErrorFilter 注入请求级 traceId
|
||||
traceId?: string;
|
||||
|
||||
constructor(message: string, code: string, details?: ErrorDetails) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
toJSON(): Record<string, unknown> {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: this.code,
|
||||
message: this.message,
|
||||
details: this.details,
|
||||
traceId: this.traceId,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends ApplicationError {
|
||||
readonly type = "validation" as const;
|
||||
readonly statusCode = 400;
|
||||
constructor(message: string, details?: ErrorDetails) {
|
||||
super(message, "CONFIG_VALIDATION_ERROR", details);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends ApplicationError {
|
||||
readonly type = "not_found" as const;
|
||||
readonly statusCode = 404;
|
||||
constructor(resource: string, id: string) {
|
||||
super(`${resource} not found: ${id}`, "CONFIG_NOT_FOUND", { resource, id });
|
||||
}
|
||||
}
|
||||
|
||||
export class PermissionDeniedError extends ApplicationError {
|
||||
readonly type = "permission_denied" as const;
|
||||
readonly statusCode = 403;
|
||||
constructor(permission: string) {
|
||||
super(`Permission denied: ${permission}`, "CONFIG_PERMISSION_DENIED", {
|
||||
permission,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends ApplicationError {
|
||||
readonly type = "unauthorized" as const;
|
||||
readonly statusCode = 401;
|
||||
constructor(message: string, details?: ErrorDetails) {
|
||||
super(message, "CONFIG_UNAUTHORIZED", details);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends ApplicationError {
|
||||
readonly type = "conflict" as const;
|
||||
readonly statusCode = 409;
|
||||
constructor(message: string, details?: ErrorDetails) {
|
||||
super(message, "CONFIG_CONFLICT", details);
|
||||
}
|
||||
}
|
||||
|
||||
export class BusinessError extends ApplicationError {
|
||||
readonly type = "business" as const;
|
||||
readonly statusCode = 422;
|
||||
constructor(message: string, details?: ErrorDetails) {
|
||||
super(message, "CONFIG_BUSINESS_ERROR", details);
|
||||
}
|
||||
}
|
||||
|
||||
export class DatabaseError extends ApplicationError {
|
||||
readonly type = "database" as const;
|
||||
readonly statusCode = 500;
|
||||
constructor(message: string, details?: ErrorDetails) {
|
||||
super(message, "CONFIG_DATABASE_ERROR", details);
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalError extends ApplicationError {
|
||||
readonly type = "internal" as const;
|
||||
readonly statusCode = 500;
|
||||
constructor(message: string, details?: ErrorDetails) {
|
||||
super(message, "CONFIG_INTERNAL_ERROR", details);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import type { Request, Response } from "express";
|
||||
import { ZodError } from "zod";
|
||||
import { ApplicationError } from "./application-error.js";
|
||||
|
||||
@Catch()
|
||||
export class GlobalErrorFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(GlobalErrorFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const traceIdHeader = request.headers["x-request-id"];
|
||||
const traceId =
|
||||
typeof traceIdHeader === "string" ? traceIdHeader : "unknown";
|
||||
|
||||
let statusCode = 500;
|
||||
let body: Record<string, unknown>;
|
||||
|
||||
if (exception instanceof ApplicationError) {
|
||||
exception.traceId = traceId;
|
||||
statusCode = exception.statusCode;
|
||||
body = exception.toJSON();
|
||||
} else if (exception instanceof ZodError) {
|
||||
statusCode = 400;
|
||||
body = {
|
||||
success: false,
|
||||
error: {
|
||||
code: "CONFIG_VALIDATION_ERROR",
|
||||
message: "Validation failed",
|
||||
details: exception.flatten(),
|
||||
traceId,
|
||||
},
|
||||
};
|
||||
} else if (exception instanceof HttpException) {
|
||||
statusCode = exception.getStatus();
|
||||
const res = exception.getResponse();
|
||||
const message = this.extractHttpMessage(res, exception);
|
||||
body = {
|
||||
success: false,
|
||||
error: {
|
||||
code: "HTTP_ERROR",
|
||||
message,
|
||||
traceId,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
this.logger.error(
|
||||
`Unhandled exception: ${exception}`,
|
||||
exception instanceof Error ? exception.stack : undefined,
|
||||
);
|
||||
body = {
|
||||
success: false,
|
||||
error: {
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "An unexpected error occurred",
|
||||
traceId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
response.status(statusCode).json(body);
|
||||
}
|
||||
|
||||
private extractHttpMessage(
|
||||
res: string | object,
|
||||
exception: HttpException,
|
||||
): string {
|
||||
if (typeof res === "string") {
|
||||
return res;
|
||||
}
|
||||
if (res && typeof res === "object" && "message" in res) {
|
||||
const msg = (res as { message: unknown }).message;
|
||||
return typeof msg === "string" ? msg : exception.message;
|
||||
}
|
||||
return exception.message;
|
||||
}
|
||||
}
|
||||
120
services/config-service/src/shared/health/health.controller.ts
Normal file
120
services/config-service/src/shared/health/health.controller.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Controller, Get, HttpException, HttpStatus } from "@nestjs/common";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { getDb } from "../../config/database.js";
|
||||
import { getRedis } from "../../config/redis.js";
|
||||
|
||||
const SERVICE_NAME = "config-service";
|
||||
|
||||
interface DependencyCheck {
|
||||
name: string;
|
||||
status: "ok" | "error";
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查端点。
|
||||
*
|
||||
* - GET /healthz:liveness,仅返回进程存活,不检查依赖
|
||||
* - GET /readyz:readiness,检查 4 依赖(DB/Redis/Kafka/gRPC),失败返回 503
|
||||
*
|
||||
* 与 iam 相比省略 JWKS 检查(config-service 不持有 JWT 密钥)。
|
||||
*/
|
||||
@Controller()
|
||||
export class HealthController {
|
||||
@Get("healthz")
|
||||
liveness(): { status: string; service: string; timestamp: string } {
|
||||
return {
|
||||
status: "ok",
|
||||
service: SERVICE_NAME,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("readyz")
|
||||
async readiness(): Promise<{
|
||||
status: string;
|
||||
service: string;
|
||||
timestamp: string;
|
||||
dependencies: DependencyCheck[];
|
||||
}> {
|
||||
const checks: DependencyCheck[] = [];
|
||||
|
||||
// 1. DB
|
||||
checks.push(await this.checkDb());
|
||||
|
||||
// 2. Redis
|
||||
checks.push(await this.checkRedis());
|
||||
|
||||
// 3. Kafka(检查 producer 实例存在)
|
||||
checks.push(await this.checkKafka());
|
||||
|
||||
// 4. gRPC(本进程内启动,进程存活即 gRPC 存活)
|
||||
checks.push({ name: "grpc", status: "ok" });
|
||||
|
||||
const allOk = checks.every((c) => c.status === "ok");
|
||||
if (!allOk) {
|
||||
throw new HttpException(
|
||||
{
|
||||
status: "error",
|
||||
service: SERVICE_NAME,
|
||||
timestamp: new Date().toISOString(),
|
||||
dependencies: checks,
|
||||
},
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ok",
|
||||
service: SERVICE_NAME,
|
||||
timestamp: new Date().toISOString(),
|
||||
dependencies: checks,
|
||||
};
|
||||
}
|
||||
|
||||
private async checkDb(): Promise<DependencyCheck> {
|
||||
try {
|
||||
const db = getDb();
|
||||
await db.execute(sql`SELECT 1`);
|
||||
return { name: "database", status: "ok" };
|
||||
} catch (error) {
|
||||
return {
|
||||
name: "database",
|
||||
status: "error",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async checkRedis(): Promise<DependencyCheck> {
|
||||
try {
|
||||
const redis = getRedis();
|
||||
const pong = await redis.ping();
|
||||
if (pong !== "PONG") {
|
||||
return { name: "redis", status: "error", error: `Unexpected: ${pong}` };
|
||||
}
|
||||
return { name: "redis", status: "ok" };
|
||||
} catch (error) {
|
||||
return {
|
||||
name: "redis",
|
||||
status: "error",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async checkKafka(): Promise<DependencyCheck> {
|
||||
try {
|
||||
const { getKafkaProducer } = await import("../../config/kafka.js");
|
||||
const producer = getKafkaProducer();
|
||||
void producer;
|
||||
return { name: "kafka", status: "ok" };
|
||||
} catch (error) {
|
||||
return {
|
||||
name: "kafka",
|
||||
status: "error",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
10
services/config-service/src/shared/health/health.module.ts
Normal file
10
services/config-service/src/shared/health/health.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { HealthController } from "./health.controller.js";
|
||||
|
||||
/**
|
||||
* 健康检查模块。
|
||||
*/
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class HealthModule {}
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnApplicationShutdown,
|
||||
OnModuleInit,
|
||||
} from "@nestjs/common";
|
||||
import { closeDb } from "../../config/database.js";
|
||||
import { closeRedis } from "../../config/redis.js";
|
||||
import { disconnectKafkaProducer } from "../../config/kafka.js";
|
||||
|
||||
const SERVICE_NAME = "config-service";
|
||||
|
||||
/**
|
||||
* 优雅停机服务。
|
||||
*
|
||||
* 关闭顺序:
|
||||
* 1. HTTP/gRPC server 已由 NestJS app.close() 停止
|
||||
* 2. Kafka producer 断开
|
||||
* 3. Redis 断开
|
||||
* 4. DB 连接池关闭(最后关闭)
|
||||
*
|
||||
* v2.1 ADR-032:outbox 投递由 Debezium CDC 接管,本服务不持有 OutboxPublisher。
|
||||
*/
|
||||
@Injectable()
|
||||
export class LifecycleService implements OnModuleInit, OnApplicationShutdown {
|
||||
private readonly logger = new Logger(LifecycleService.name);
|
||||
|
||||
onModuleInit(): void {
|
||||
this.logger.log(`service ${SERVICE_NAME} module initialized`);
|
||||
}
|
||||
|
||||
async onApplicationShutdown(signal?: string): Promise<void> {
|
||||
this.logger.log(
|
||||
`service ${SERVICE_NAME} shutting down (signal=${signal ?? "unknown"})`,
|
||||
);
|
||||
|
||||
// 1. Kafka producer
|
||||
try {
|
||||
await disconnectKafkaProducer();
|
||||
this.logger.log("Kafka producer disconnected");
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Kafka producer disconnect failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Redis
|
||||
try {
|
||||
await closeRedis();
|
||||
this.logger.log("Redis connection closed");
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Redis close failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 3. DB(最后关闭)
|
||||
try {
|
||||
await closeDb();
|
||||
this.logger.log("database connection closed");
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`database close failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(`service ${SERVICE_NAME} shutdown complete`);
|
||||
}
|
||||
}
|
||||
19
services/config-service/src/shared/observability/logger.ts
Normal file
19
services/config-service/src/shared/observability/logger.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { pino } from "pino";
|
||||
import { env } from "../../config/env.js";
|
||||
|
||||
export const logger = pino({
|
||||
level: env.LOG_LEVEL,
|
||||
base: {
|
||||
service: "config-service",
|
||||
version: "0.1.0",
|
||||
},
|
||||
transport:
|
||||
env.NODE_ENV === "development"
|
||||
? {
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export type Logger = typeof logger;
|
||||
79
services/config-service/src/shared/observability/metrics.ts
Normal file
79
services/config-service/src/shared/observability/metrics.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import promClient from "prom-client";
|
||||
|
||||
const registry = new promClient.Registry();
|
||||
registry.setDefaultLabels({ service: "config-service" });
|
||||
|
||||
registry.registerMetric(
|
||||
new promClient.Counter({
|
||||
name: "config_service_requests_total",
|
||||
help: "Total number of config-service requests",
|
||||
labelNames: ["method", "endpoint", "status"],
|
||||
}),
|
||||
);
|
||||
|
||||
registry.registerMetric(
|
||||
new promClient.Histogram({
|
||||
name: "config_service_request_duration_seconds",
|
||||
help: "Config-service request duration in seconds",
|
||||
labelNames: ["method", "endpoint"],
|
||||
buckets: [0.01, 0.05, 0.1, 0.3, 0.5, 1, 3, 5],
|
||||
}),
|
||||
);
|
||||
|
||||
// 自动收集 Node.js 进程级指标
|
||||
promClient.collectDefaultMetrics({ register: registry });
|
||||
|
||||
// Redis 缓存指标(plugin / user-layout 缓存可观测性)
|
||||
registry.registerMetric(
|
||||
new promClient.Counter({
|
||||
name: "config_service_cache_hits_total",
|
||||
help: "Total number of config cache hits (Redis)",
|
||||
labelNames: ["kind"],
|
||||
}),
|
||||
);
|
||||
|
||||
registry.registerMetric(
|
||||
new promClient.Counter({
|
||||
name: "config_service_cache_misses_total",
|
||||
help: "Total number of config cache misses (Redis)",
|
||||
labelNames: ["kind"],
|
||||
}),
|
||||
);
|
||||
|
||||
registry.registerMetric(
|
||||
new promClient.Counter({
|
||||
name: "config_service_cache_invalidations_total",
|
||||
help: "Total number of config cache invalidations (Redis)",
|
||||
labelNames: ["kind", "reason"],
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* 缓存指标访问器:供 ConfigCacheService 使用.
|
||||
*/
|
||||
export const cacheMetrics = {
|
||||
recordHit(kind: string): void {
|
||||
const metric = registry.getSingleMetric("config_service_cache_hits_total");
|
||||
if (metric && "inc" in metric) {
|
||||
(metric as promClient.Counter).inc({ kind });
|
||||
}
|
||||
},
|
||||
recordMiss(kind: string): void {
|
||||
const metric = registry.getSingleMetric(
|
||||
"config_service_cache_misses_total",
|
||||
);
|
||||
if (metric && "inc" in metric) {
|
||||
(metric as promClient.Counter).inc({ kind });
|
||||
}
|
||||
},
|
||||
recordInvalidation(kind: string, reason: string): void {
|
||||
const metric = registry.getSingleMetric(
|
||||
"config_service_cache_invalidations_total",
|
||||
);
|
||||
if (metric && "inc" in metric) {
|
||||
(metric as promClient.Counter).inc({ kind, reason });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export { registry as metricsRegistry };
|
||||
27
services/config-service/src/shared/observability/tracer.ts
Normal file
27
services/config-service/src/shared/observability/tracer.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||
import { env } from "../../config/env.js";
|
||||
|
||||
let sdk: NodeSDK | null = null;
|
||||
|
||||
export function initTracer(): void {
|
||||
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
||||
|
||||
sdk = new NodeSDK({
|
||||
serviceName: "config-service",
|
||||
traceExporter: new OTLPTraceExporter({
|
||||
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
||||
}),
|
||||
instrumentations: [getNodeAutoInstrumentations()],
|
||||
});
|
||||
|
||||
sdk.start();
|
||||
console.log("Tracer initialized with auto-instrumentations");
|
||||
}
|
||||
|
||||
export async function shutdownTracer(): Promise<void> {
|
||||
if (sdk) {
|
||||
await sdk.shutdown();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user