edgesync360-edgehub-logbook-nodejs-sdk
v2.0.1
Published
EdgeHub LogBook SDK for Node.js — structured audit logging for microservices (IEC 62443 / EU CRA)
Downloads
121
Maintainers
Readme
EdgeHub LogBook — Node.js SDK
v2.0.1 · A structured audit-log SDK for microservices, designed for IEC 62443 / EU CRA compliance.
API behaviour is consistent across Go (common-library/logbook), Java (dc-edge-sdk-logbook-java), and Node.js.
Table of Contents
- Installation
- Overview — Three API Flavours
- Quick Start
- Minimal usage
- Scope entry points
- Registering Actions
- resource() — Type Auto-Inference
- AuditTask — Multi-step Audit
- AuditEvent — Single-step Audit
- logBook — General System Log
- Severity Methods
- Cross-service MQ Trace Propagation
- Express Middleware
- Constants Reference
- Custom Transport
- i18n Action Names
Installation
npm install edgesync360-edgehub-logbook-nodejs-sdkPeer dependency: requires mongodb >= 5 (only when using the built-in MongoTransport).
Overview — Three API Flavours
SDK 提供三種 log 記錄方式,依使用情境選擇:
| API | 用途 | 典型場景 |
|-----|------|----------|
| auditTask() | 多步驟稽核流程 | 使用者操作需記錄 start → progress → close |
| auditEvent() | 單步驟稽核 | 一次性事件,一行鏈結即完成 |
| logBook() | 系統層級 log (tp=sys) | 背景排程、流量統計等無明確操作者的紀錄 |
核心名詞
| 名詞 | 說明 |
|------|------|
| Scope | 上下文物件,攜帶 tenantID、accountName、traceID、spanID |
| TraceID | 32 字元小寫 hex,用來串聯同一操作在不同服務間的所有 log |
| Action | 操作代碼(如 GROUP.CREATE),透過 registerActions 預先註冊 |
| Resource | 被操作的目標資源(id + name),type 由註冊的 action 自動推斷 |
Quick Start
const {
LogClient, MongoTransport,
SERVICE, SOURCE, RESOURCETYPE
} = require("edgesync360-edgehub-logbook-nodejs-sdk");
// 1. 建立 Transport
const transport = new MongoTransport("mongodb://localhost:27017", "my_db");
// 2. 初始化 Client
const client = new LogClient(transport, {
serviceName: SERVICE.DPM, // 預設 service(可在鏈中覆寫)
source: SOURCE.EDGE // 預設 source
});
// 3. 註冊 Action(啟動時呼叫一次)
await client.registerActions({
svc: SERVICE.DM,
acts: [
{ code: "GROUP.CREATE", name: "Create Group", res_type: RESOURCETYPE.GROUP },
{ code: "DEVICE.CREATE", name: "Create Device", res_type: RESOURCETYPE.DEVICE },
],
v: "1.0"
});
// 4. 寫一筆 audit log
await client.auditEvent()
.accountName("[email protected]")
.tenant("t-001", "MyTenant")
.source(SOURCE.WEB)
.action("GROUP.CREATE")
.resource("g-001", "MyGroup")
.info("Done")
.reportSuccess();Scope entry points
auditTask() / auditEvent() 的 scope(tenantID、accountName、traceID、spanID 等)可用下列三種方式之一帶入,擇一即可,不必併用。
| 方式 | 行為摘要 | 何時用 |
|------|----------|--------|
| fromRequest(req) | 從 cookie(EIToken JWT → accountName、IFPTenant → tenantID、TenantName → 租戶顯示名)與 header(x-logbook-trace-id / traceparent、x-span-id / x-logbook-span-id)組 scope;無則自動產生 trace / span | 標準 Web 請求,與 Go / Java SDK 解析慣例一致 |
| fromScopeContext() | 從 AsyncLocalStorage 讀取目前 async 鏈上的 scope | 已掛 middleware() 或 runWithScope(),深層程式拿不到 req 時 |
| 手動鏈結 | .accountName()、.tenant()、.traceID()、.source()、.service() … 自行帶欄位 | 無 HTTP、自訂登入、或欄位來源與預設 cookie/header 不同 |
fromScopeContext() 的前提:必須在具有 ALS store 的上下文內呼叫。Express 專案通常:
// 進入點:middleware 用 fromRequest + runWithScope 包住後續處理
app.use(client.middleware());
// 深層:不需 req
const task = client.auditTask().fromScopeContext()
.action("GROUP.CREATE")
.resource("g-001", "MyGroup");手動建立 ALS 上下文(不依賴 Express):可傳 plain scope 物件 或 ScopedClient(與 middleware() 相同)。
await client.runWithScope(
{
tenantID: "t-001",
tenant: "MyTenant",
accountName: "[email protected]",
source: SOURCE.WEB,
},
async () => {
await client.auditEvent().fromScopeContext()
.action("GROUP.CREATE")
.resource("g-001", "MyGroup")
.info("inside ALS")
.reportSuccess();
}
);與 Express Middleware、Cross-service MQ 搭配時,建議先讀本節再選進入點。
Registering Actions
在服務啟動時呼叫一次,將 action 定義寫入 dc_log_action collection。
註冊後,鏈中呼叫 .action(code) 會自動帶入 name、svc、res_type,不需手動輸入。
await client.registerActions({
svc: SERVICE.DM,
acts: [
{ code: "GROUP.CREATE", name: "Create Group", res_type: RESOURCETYPE.GROUP },
{ code: "DEVICE.CREATE", name: "Create Device", res_type: RESOURCETYPE.DEVICE },
{ code: "OTA.UPDATE", name: "OTA Update", res_type: RESOURCETYPE.DEVICE },
{ code: "USAGE.REPORT", name: "Report Usage", res_type: RESOURCETYPE.USAGE },
],
v: "1.0"
});resource() — Type Auto-Inference
所有 API 的 resource() 統一只接受兩個參數 (id, name),resource type 由 registerActions 自動推斷:
// 先呼叫 .action(),再呼叫 .resource()
.action("DEVICE.CREATE") // 從註冊資料帶入 res_type = DEVICE
.resource("dev-001", "My-Device") // type 自動為 DEVICE,無需手動傳入只要
registerActions有定義res_type,SDK 會自動對應。無需也無法手動指定 type。
AuditTask — Multi-step Audit
適用於需要記錄「開始 → 處理中 → 結案」的多步驟操作。
方式一:從 HTTP Request 自動取得 Scope
從 cookie(EIToken JWT / IFPTenant / TenantName)和 x-logbook-trace-id header 自動解析使用者與租戶資訊。
const task = client.auditTask().fromRequest(req)
.source(SOURCE.WEB)
.action("GROUP.CREATE")
.resource("g-001", "MyGroup");
await task.info("Starting group creation").start();
await task.info("Data written to database").progress();
await task.info("Group created successfully").reportSuccess();
// 其他結案方式:.reportFail() / .reportCancel()附帶 i18n 多語訊息:
await task.info("Starting group creation")
.withMsgs("開始建立群組", "开始新增群组", "グループの追加を開始します") // tw, cn, jp
.start();方式二:手動建構 Scope(背景任務)
未帶 source 時預設為 SOURCE.INTERNAL。
const task = client.auditTask()
.accountName("[email protected]")
.tenant("t-worker", "BackgroundWorkerTenant")
.action("OTA.UPDATE")
.resource("dev-002", "Smart-Gateway-01");
await task.info("Start background OTA sync").start();
await task.info("Syncing metadata...").progress();
await task.info("OTA sync completed").reportSuccess();方式三:從 AsyncLocalStorage 繼承 Scope
需先掛載 client.middleware()(見 Express Middleware)。
middleware 會在每個 request 進來時自動解析 scope 並存入背景,之後任何深度的程式碼都能用 fromScopeContext() 取得,不需要層層傳遞 req。
// service layer — 完全不需要 req 參數
async function createDevice(device) {
const task = client.auditTask().fromScopeContext()
.action("DEVICE.CREATE")
.resource(device.id, device.name);
await task.info("Creating device").start();
await db.insert(device);
await task.info("Device created").reportSuccess();
}適合 audit log 寫在較深層的 service / helper 中,不想一路傳
req的情境。
AuditEvent — Single-step Audit
適用於一次性完成的事件,不需 start/progress,一行鏈結即結案。resource() 是可選的 — 可以在 action() 之後直接呼叫 severity 方法。
// 帶 resource
await client.auditEvent().fromRequest(req)
.source(SOURCE.WEB)
.action("GROUP.CREATE")
.resource("g-001", "MyGroup")
.info("Group created")
.reportSuccess();
// 不帶 resource(直接接 severity)
await client.auditEvent()
.accountName("[email protected]")
.tenant("t-001", "test tenant")
.action("GROUP.CREATE")
.info("Group created (no resource)")
.reportSuccess();MQ 消費端已有 ScopedClient 時:
const scopedB = client.fromMessageAttrs(mqAttrs, SOURCE.INTERNAL);
await scopedB.auditEvent()
.action("DEVICE.CREATE")
.resource(payload.device_id, payload.device_name)
.info("Service B processed and logged")
.reportSuccess();logBook — General System Log
用於系統層級紀錄(tp = sys),例如排程回報、流量統計等無明確操作者的場景。
Tenant 為必填;accountName 可省略(省略時自動帶 "-")。resource() 是可選的。
// 最簡形式:tenant + action + severity + result
await client.logBook()
.tenant("t-001", "MyTenant")
.action("USAGE.REPORT")
.info("Device reported usage")
.reportSuccess();
// 帶 resource(type 由 registerActions 自動推斷)
await client.logBook()
.tenant("t-001", "MyTenant")
.action("USAGE.REPORT")
.resource("epn-001", "Edge-Device")
.info("EPN reported traffic")
.reportSuccess();
// 帶 accountName
await client.logBook()
.tenant("t-001", "MyTenant")
.accountName("[email protected]")
.action("USAGE.REPORT")
.info("Usage confirmed")
.reportSuccess();forLogBook(scope) — 直接傳入 scope 物件:
await client.forLogBook({ tenantID: "t-001", tenant: "MyTenant" })
.action("USAGE.REPORT")
.info("Report complete")
.reportSuccess();LogBookEntryBuilder 方法
| Method | Description |
|--------|-------------|
| .withCtx({ key: "value" }) | 附加 key-value 上下文(value 必須為字串) |
| .reportSuccess() | result = success |
| .reportFail() | result = failed |
| .reportCancel() | result = cancel |
Severity Methods
所有 API(AuditTask / AuditEvent / logBook)均共用以下 severity 方法:
| Method | 用途 |
|--------|------|
| .info(msg) | 一般資訊 |
| .warn(msg) | 警告 |
| .error(msg) | 錯誤 |
| .critical(msg) | 嚴重錯誤 |
| .debug(msg) | 除錯 |
呼叫後回傳 EntryBuilder,再以 .reportSuccess() / .reportFail() / .reportCancel() 結案。
AuditTask 額外提供 .start() 和 .progress()。
Cross-service MQ Trace Propagation
跨服務時透過 MQ header 傳遞 traceID 等 scope 資訊,讓不同服務的 log 可以串聯。
MQTT 5 / RabbitMQ / AMQP
Service A(Publisher)
const scoped = client.auditTask().fromRequest(req).source(SOURCE.WEB);
const task = scoped.action("DEVICE.CREATE").resource("dev-001", "MyDevice");
await task.info("User initiated device creation").start();
const body = JSON.stringify(businessPayload); // 純商業資料
const attrs = scoped.toMQHeaders(); // { "x-logbook-trace-id": ..., ... }
// MQTT 5: msg.userProperties = scoped.toMQTT5UserProperties()
// AMQP/RabbitMQ: message headers = attrs
await task.info("Message sent to broker").reportSuccess();Service B(Subscriber)
const scopedB = client.fromMessageAttrs(attrs, SOURCE.INTERNAL);
await scopedB.auditEvent()
.action("DEVICE.CREATE")
.resource(payload.device_id, payload.device_name)
.info("Service B completed processing")
.reportSuccess();
// traceID 與 Service A 一致,可在 audit UI 中串聯查詢MQTT 3 — 將 trace 嵌入 payload envelope
MQTT 3 不支援 user properties,需將 trace 資訊包進 payload。
Service A
const {
MQTT3_ENVELOPE_KEY_LOGBOOK,
MQTT3_ENVELOPE_KEY_DATA
} = require("edgesync360-edgehub-logbook-nodejs-sdk");
// 方式一:手動組裝 envelope
const envelope = {
[MQTT3_ENVELOPE_KEY_LOGBOOK]: scoped.toMQHeaders(),
[MQTT3_ENVELOPE_KEY_DATA]: businessPayload,
};
publish(JSON.stringify(envelope));
// 方式二:SDK 自動包裝
const wrappedBuffer = scoped.wrapPayloadWithTrace(
Buffer.from(JSON.stringify(businessPayload))
);
publish(wrappedBuffer);Service B
const { unwrapPayloadAndScope } = require("edgesync360-edgehub-logbook-nodejs-sdk");
const { scopeMap, dataBytes } = unwrapPayloadAndScope(rawPayloadBuffer);
const scopedB = client.fromMessageAttrs(scopeMap, SOURCE.INTERNAL);
const data = JSON.parse(dataBytes.toString());
await scopedB.auditEvent()
.action("DEVICE.CREATE")
.resource(data.device_id, data.device_name)
.info("Service B completed processing (MQTT 3)")
.reportSuccess();Propagation Key Constants
const {
KEY_TRACE_ID, // "x-logbook-trace-id"
KEY_SPAN_ID, // "x-logbook-span-id"
KEY_ACCOUNT_NAME, // "x-logbook-account-name"
KEY_TENANT_ID, // "x-logbook-tenant-id"
KEY_TENANT_NAME, // "x-logbook-tenant-name"
} = require("edgesync360-edgehub-logbook-nodejs-sdk");Express Middleware
client.middleware() 是 SDK 提供的 Express middleware,作用:
- 從 request 的 cookie(
EIToken/IFPTenant/TenantName)解析使用者與租戶資訊 - 從 header(
x-logbook-trace-id)取得 traceID;若沒有則自動產生 - 將解析出的 scope 存入
AsyncLocalStorage,讓後續程式碼可用fromScopeContext()取得
const express = require("express");
const cookieParser = require("cookie-parser");
const app = express();
app.use(cookieParser()); // 解析 cookie 字串 → req.cookies 物件
app.use(client.middleware()); // 解析 scope → 存入 AsyncLocalStorage
app.post("/groups", async (req, res) => {
const task = client.auditTask().fromScopeContext()
.action("GROUP.CREATE")
.resource("g-001", "MyGroup");
await task.info("Group creation started").start();
// ... business logic ...
await task.info("Group created successfully").reportSuccess();
res.json({ ok: true });
});服務間呼叫(無 cookie)
服務間 API 呼叫通常沒有 cookie,middleware 不會報錯,只是 accountName / tenantID 會是空的。
兩種處理方式:
方式 A:呼叫端在 header 帶 trace 資訊
// Service A → 呼叫 Service B 時帶上 trace header
const headers = scoped.toMQHeaders();
await fetch("http://service-b/internal/devices", {
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
// Service B 的 middleware 會從 header 讀到 traceID,串聯兩端 log方式 B:handler 裡手動建構 scope(不依賴 middleware)
await client.auditEvent()
.accountName("service-a@internal")
.tenant(req.body.tenantID, req.body.tenantName)
.action("DEVICE.CREATE")
.info("Internal call from Service A")
.reportSuccess();Constants Reference
LOG_TYPE
| Key | Value | Description |
|-----|-------|-------------|
| OP | "op" | 操作稽核(AuditTask / AuditEvent) |
| SEC | "sec" | 安全事件 |
| SYS | "sys" | 系統 log(logBook) |
SEVERITY
| Key | Value |
|-----|-------|
| I | "info" |
| W | "warn" |
| E | "error" |
| C | "critical" |
| D | "debug" |
RESULT
| Key | Value |
|-----|-------|
| SUCCESS | "success" |
| FAIL | "failed" |
| PARTIAL | "partial_success" |
| CANCEL | "cancel" |
SOURCE
| Key | Value | Description |
|-----|-------|-------------|
| WEB | "web" | 來自前端操作 |
| EDGE | "edge" | 來自邊緣裝置 |
| INTERNAL | "internal" | 內部服務 / 背景任務 |
SERVICE
DPM / DM / UM / CC / AE / DA / TM / SS / CM / TFM
RESOURCETYPE
DEVICE / GROUP / USER / TENANT / FILE / ALARM / ROLE / CONTAINER / USAGE … (完整列表見 enums.js)
Custom Transport
如不使用內建 MongoTransport,可自行實作以下介面:
const customTransport = {
// 必要:寫入一筆 log entry
async writeLog(entry, dateForPartition) { /* ... */ },
// 必要:更新 resource 索引(不需要的話傳空函式)
async updateResourceIndex(entry) { /* ... */ },
// 選用:前處理 entry(加 hash、轉換欄位等),省略則跳過
async prepareLog(entry) { return entry; },
// 選用:註冊 action 定義(無 MongoDB 時可省略)
async registerActions(input, cache) { /* ... */ },
};
const client = new LogClient(customTransport, { version: "1.0" });i18n Action Names
使用 Names.of(tw, cn, jp) 為 action 註冊多語顯示名稱:
const { Names } = require("edgesync360-edgehub-logbook-nodejs-sdk");
await client.registerActions({
svc: SERVICE.DM,
acts: [{
code: "GROUP.CREATE",
name: "Create Group",
names: Names.of("建立群組", "新增群组", "グループの追加"), // tw, cn, jp
res_type: RESOURCETYPE.GROUP,
}],
v: "1.0"
});License
MIT
