@ziky/error-event-contract
v1.0.0
Published
Versioned error event contract for Mixi backend services
Readme
@ziky/error-event-contract
Package định nghĩa contract dùng chung và runtime validation cho error event của hệ thống Mixi. Producer và consumer nên phụ thuộc vào package này thay vì tự sao chép interface hoặc ép kiểu payload nhận từ RabbitMQ.
Cài đặt
Package được phát hành công khai trên npm dưới scope @ziky.
npm install @ziky/error-event-contractVí dụ .npmrc khi package nằm trên npmjs:
@ziky:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=${NPM_TOKEN}Không commit token thật vào repository.
Public API
import {
ERROR_EVENT_EXCHANGE,
ERROR_EVENT_ROUTING_PREFIX,
ERROR_EVENT_SPEC_VERSION,
ERROR_EVENT_TYPE,
InvalidErrorEventError,
errorRoutingKey,
parseErrorEvent,
type ErrorEventEnvironment,
type ErrorEventV1,
} from "@ziky/error-event-contract";| Export | Giá trị/vai trò |
| ---------------------------- | ----------------------------------------------------- |
| ERROR_EVENT_SPEC_VERSION | Phiên bản contract hiện tại: "1.0" |
| ERROR_EVENT_TYPE | Loại event: "com.mixi.backend.error.v1" |
| ERROR_EVENT_EXCHANGE | Topic exchange mặc định: "mixi.events" |
| ERROR_EVENT_ROUTING_PREFIX | Routing prefix mặc định: "observability.error" |
| ErrorEventV1 | TypeScript interface của event v1 |
| parseErrorEvent() | Validate dữ liệu không tin cậy tại runtime |
| errorRoutingKey() | Tạo routing key chuẩn từ tên service |
| InvalidErrorEventError | Lỗi được ném khi event hoặc service name không hợp lệ |
Cấu trúc ErrorEventV1
Ví dụ event đầy đủ:
{
"specVersion": "1.0",
"eventType": "com.mixi.backend.error.v1",
"eventId": "0198a530-bd10-7ac2-8d84-7fa021dce33c",
"occurredAt": "2026-07-27T08:30:00.000Z",
"service": {
"name": "mixi-music-api",
"version": "2.4.1",
"environment": "production",
"instanceId": "mixi-music-api-7f69b6f4d9-x2kq8"
},
"request": {
"requestId": "req-01",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"correlationId": "order-123",
"method": "POST",
"path": "/api/playlists",
"statusCode": 500,
"userId": "user-42",
"tenantId": "tenant-01"
},
"error": {
"name": "DatabaseError",
"code": "DB_TIMEOUT",
"message": "Query timed out",
"stack": "DatabaseError: Query timed out ...",
"fingerprint": "database-query-timeout"
},
"metadata": {
"operation": "playlist.create",
"retryCount": 2
}
}Trường bắt buộc
| Trường | Quy tắc |
| --------------------- | ------------------------------------------ |
| specVersion | Phải bằng "1.0" |
| eventType | Phải bằng "com.mixi.backend.error.v1" |
| eventId | Chuỗi khác rỗng, tối đa 100 ký tự |
| occurredAt | Chuỗi ngày giờ parse được, tối đa 64 ký tự |
| service.name | Chuỗi khác rỗng, tối đa 120 ký tự |
| service.environment | development, staging hoặc production |
| error.name | Chuỗi khác rỗng, tối đa 255 ký tự |
| error.message | Chuỗi khác rỗng, tối đa 4.096 ký tự |
Trường tùy chọn và giới hạn
| Trường | Giới hạn |
| ------------------------------ | ------------------------ |
| service.version | 80 ký tự |
| service.instanceId | 160 ký tự |
| request.requestId, traceId | 160 ký tự |
| request.correlationId | 160 ký tự |
| request.userId, tenantId | 160 ký tự |
| request.method | 20 ký tự |
| request.path | 2.048 ký tự |
| request.statusCode | Số nguyên từ 100 đến 599 |
| error.code | 120 ký tự |
| error.stack | 32.768 ký tự |
| error.fingerprint | 128 ký tự |
| metadata | Object |
parseErrorEvent() chỉ lấy các trường đã định nghĩa ở service, request và
error. metadata được giữ dưới dạng object và không được package này tự
redact; producer phải loại bỏ secret trước khi publish.
Producer: tạo và publish event
Khi tự xây producer, dùng các constant của package để tránh hard-code:
import { randomUUID } from "node:crypto";
import {
ERROR_EVENT_EXCHANGE,
ERROR_EVENT_SPEC_VERSION,
ERROR_EVENT_TYPE,
errorRoutingKey,
type ErrorEventV1,
} from "@ziky/error-event-contract";
const event: ErrorEventV1 = {
specVersion: ERROR_EVENT_SPEC_VERSION,
eventType: ERROR_EVENT_TYPE,
eventId: randomUUID(),
occurredAt: new Date().toISOString(),
service: {
name: "mixi-music-api",
version: process.env.APP_VERSION,
environment: "production",
},
error: {
name: "PlaylistError",
code: "PLAYLIST_CREATE_FAILED",
message: "Could not create playlist",
},
metadata: {
operation: "playlist.create",
},
};
channel.publish(
ERROR_EVENT_EXCHANGE,
errorRoutingKey(event.service.name),
Buffer.from(JSON.stringify(event)),
{
contentType: "application/json",
messageId: event.eventId,
persistent: true,
type: event.eventType,
},
);errorRoutingKey("Mixi-Music-API") trả về
observability.error.mixi-music-api. Tên service sau khi trim và chuyển thành
chữ thường phải có dạng kebab-case: chỉ gồm chữ, số và dấu -, không có hai dấu
- liên tiếp.
Consumer: parse message an toàn
Payload từ queue là dữ liệu không tin cậy. Luôn JSON.parse rồi gọi
parseErrorEvent() trước khi xử lý:
import {
InvalidErrorEventError,
parseErrorEvent,
} from "@ziky/error-event-contract";
import type { ConsumeMessage } from "amqplib";
async function handleMessage(message: ConsumeMessage): Promise<void> {
try {
const input: unknown = JSON.parse(message.content.toString("utf8"));
const event = parseErrorEvent(input);
await errorStore.save(event);
channel.ack(message);
} catch (error) {
if (
error instanceof InvalidErrorEventError ||
error instanceof SyntaxError
) {
// Payload hỏng không nên retry vô hạn. Chuyển sang DLQ theo policy của queue.
channel.nack(message, false, false);
return;
}
// Lỗi hạ tầng tạm thời có thể retry/requeue theo policy đã giới hạn.
channel.nack(message, false, true);
}
}Ví dụ bind queue nhận lỗi của tất cả service:
await channel.assertExchange("mixi.events", "topic", { durable: true });
await channel.assertQueue("observability.error-store", {
durable: true,
arguments: {
"x-dead-letter-exchange": "mixi.events.dlx",
},
});
await channel.bindQueue(
"observability.error-store",
"mixi.events",
"observability.error.*",
);Consumer phải có chiến lược idempotency dựa trên eventId, retry hữu hạn và
dead-letter queue. Không log toàn bộ payload nếu event có thể chứa dữ liệu cá
nhân hoặc secret.
Xử lý lỗi validation
import {
InvalidErrorEventError,
parseErrorEvent,
} from "@ziky/error-event-contract";
try {
const event = parseErrorEvent(input);
// input đã được validate và event có kiểu ErrorEventV1
} catch (error) {
if (error instanceof InvalidErrorEventError) {
logger.warn({ reason: error.message }, "Rejected invalid error event");
}
}Các lỗi thường gặp:
specVersion is unsupported: producer/consumer đang dùng contract khác phiên bản.eventType is unsupported: message không phải error event v1.occurredAt must be an ISO date-time: timestamp không parse được.service.environment is invalid: environment nằm ngoài ba giá trị hỗ trợ.request.statusCode is invalid: status không phải số nguyên HTTP 100–599.Service name is invalid: tên service không thể chuyển thành routing key chuẩn.
Versioning và tương thích
- Thêm trường optional hoặc nới giới hạn có thể phát hành minor version.
- Sửa lỗi không thay đổi contract phát hành patch version.
- Xóa/đổi tên trường, đổi kiểu hoặc siết validation là breaking change và phải tăng major version.
- Khi chuyển major version, producer và consumer nên hỗ trợ song song contract cũ/mới trong thời gian migration.
Kiểm tra local
Từ root monorepo:
npm ci
npm run build --workspace @ziky/error-event-contract
npm run typecheck --workspace @ziky/error-event-contract
npm test --workspace @ziky/error-event-contract
npm pack --workspace @ziky/error-event-contract --dry-run