2.7 · Cross-cutting concerns
บทนี้เล่าเรื่องที่ตัดผ่านทุก feature — logging, error handling, observability, config
เป็นเรื่องที่ไม่มีใครคิดถึงตอนเขียน feature แต่ตอน debug ตอน 3 ทุ่มจะอยากรู้ว่า "แกะรอยอย่างไร"
Logging — 2 ระบบคู่กัน
vtrc-api มี logger คู่กัน 2 ตัว เขียนลงคนละที่
1. wlog.js — เขียนลงไฟล์ flat text
ที่มา — vtrc-api/api/src/lib/wlog.js:1-73
logs/
└── <yyyy>/
└── <mm>/
└── <dd>/
├── info_<date>.log
├── warning_<date>.log
├── error_<date>.log
├── query_<date>.log
├── slipError_<date>.log
├── verify_<date>.log
└── ...ทุก entry format เดียวกัน
<th-TH locale date> - {"CODE": "...", "SOURCE": "...", "TYPE": "...", "text": "...", ...}helper functions 10 ตัว (wlog.js:36-72) — info, warning, error, query, notificationLog, notificationLogError, slipError, verify, forceLogs แต่ละตัวแค่เลือก prefix ของชื่อไฟล์ต่างกัน
2. logging.js — Apollo plugin เฉพาะ GraphQL
ที่มา — vtrc-api/api/src/lib/logging.js:1-108
เป็น Apollo plugin ที่ log ทุก GraphQL request + response — skip IntrospectionQuery
{
query: "...",
variables: {...},
headers: {...},
from: { appId, userId }
}redact list (logging.js:5-9) — แทนที่ field เหล่านี้ด้วย _REPLACED_
- variables:
password,confirmPassword,oldPassword - headers:
authorization,apiKey
ระวัง — redact ไม่ครบ identityCard, token, refreshToken, empCode ยังถูก log ใน cleartext
Log รวมกัน — อ่านยาก
เพราะทุก replica เขียน log ลง directory เดียวกัน (docker-compose.yml:59, 72, 84, 98 — mount ./api-logs เหมือนกัน)
api-logs/
└── logs/2025/07/09/
├── info_09-07-2025.log ← ทุก replica เขียนที่นี่
└── error_09-07-2025.log ← ไม่มี replica tagจะรู้ว่า log entry มาจาก replica ไหน ต้อง grep เนื้อหา (ถ้ามี APP_NO ใน entry นั้น) — ไม่มี guaranteed ว่ามี
console.log กระจัดกระจาย
นอกจาก logger ทั้ง 2 ตัว ยังมี console.log ในโค้ดจำนวนมาก — เช่น index.js:55, 62, 99 (formatError log ทุก error ด้วย console.log)
ผลคือ stdout กับไฟล์ log มีเนื้อหาไม่ตรงกัน ตอน debug ต้องดูทั้งคู่
Frontend ปิด console ไว้หมด
อย่าแปลกใจที่ console.log ใน frontend ไม่พิมพ์อะไร — vtrc-web/src/containers/App/index.js:15 เขียนทับไว้
console.log = console.warn = console.error = () => {}ถ้าต้องการ debug frontend ต้อง comment บรรทัดนี้ก่อน (แล้วอย่าลืม uncomment กลับ)
Error handling — responError เป็นศูนย์กลาง
vtrc-api/api/src/lib/responErrors.js:3-29 — function เดียวที่ throw ทุก error
function responError(code) {
throw new ApolloError(i18nMessage(code), code)
}code เป็น string ที่ match กับ message catalog (i18nMessage) — ตัวอย่าง
TOKEN_EXPIREDTOKEN_INVALIDSESSION_EXPIREDACCESS_KEY_INVALIDINTERNAL_SERVER_ERROR
ระวัง — caller หลายที่เรียก responError(...) โดยไม่ return (session.js:39, 44, 62, 68) พึ่งพาว่ามัน throw ถ้าวันหนึ่งเปลี่ยนให้ไม่ throw → code path พังทันที
formatError ใน Apollo
vtrc-api/api/src/index.js:62-74 — format error ก่อนส่งกลับ frontend
formatError: (err) => {
console.log(err) // log ทุก error
return err
}ทุก error จะถูก console.log — ในกรณี debug ดี แต่ใน production คือ noise จำนวนมาก
debug: !IS_PRODUCTION ควบคุม stack trace — ถ้า NODE_ENV=production จะไม่ส่ง stack ออกไป
Observability — แทบไม่มี
VTRC ไม่มีระบบ monitoring ที่ครบครัน — ไม่มี Prometheus, ไม่มี Grafana, ไม่มี distributed tracing
สิ่งที่มี
console.log+wlog→ text files/ais-smsgwendpoint (routes.js:990-993) — stub ที่ echo OK ไม่ได้ทำอะไรplayground: true+introspection: trueใน Apollo — เปิดไว้ทุก env รวมถึง production
ไม่มี health check endpoint — ไม่มี /health หรือ /readiness ที่ Docker หรือ load balancer ใช้ตรวจสอบได้
ถ้าทำ modernization — สิ่งแรกที่ควรเพิ่มคือ
/health+/readinessendpoint- Prometheus metrics
- OpenTelemetry trace
Config — ทุกอย่างใน config.js
vtrc-api/api/src/config.js (522 บรรทัด) — เป็น single file ที่ dotenv.config() แล้ว destructure process.env
หมวดหมู่
- runtime identity (
APP_NO,APP_PORT,NODE_ENV, paths, timezone) - secrets (
JWT_SECRET, DB creds, Redis creds) - centralize bridge (
END_POINT_CENTRALIZE,END_POINT_CENTRALIZE_AUTH,API_CENTRALIZE_KEY) - inline device-key registry
API_DEVICE_KEY(prod + uat arrays) — บรรทัด 41-149 - access-role/menu constants — บรรทัด 150-370
- HR status lookup tables (
relationHrmi,statusHrmi,statusKcash) — บรรทัด 392-519
ระวัง
JWT_SECRET='secret'default (config.js:12) — ถ้า env ไม่ตั้ง จะใช้ string'secret'ที่ใครก็รู้- prod device keys commit อยู่ใน source (sensitive — ห้าม log ห้าม paste)
Centralize-api ใช้ logger คนละแบบ
centralize-api/src/libs/log/logs.middleware.ts:1-68 — I3GatewayLogger คือ class ที่ implement LoggerService ของ NestJS
logs/<DD-MM-YYYY>.logระวัง — เขียน ANSI color escape (\x1b[32m) ลงไฟล์ด้วย (logs.middleware.ts:22-24) — ทำให้อ่านไฟล์ยาก
stream เปิดทุกครั้งที่สร้าง instance — logs.middleware.ts:12-17 — แต่ไม่ปิด ถ้า service รันนาน → leak file descriptor
Error filter ของ centralize-api
centralize-api/src/libs/exception/http-exception.filter.ts:1-26
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse()
const status = exception.getStatus()
const message = exception.getResponse()
this.logger.log(message) // log(), ไม่ใช่ error()
response.status(status).json({
statusCode: status,
message: message['message'] || message['error']
})
}
}ระวัง 2 จุด
- log ทุก error ที่ระดับ
log(info) ไม่ใช่error— ตอน filter log จะหาผิดระดับ - จับแค่
HttpException— driver error ของ mssql ที่ไม่ใช่ HTTP จะ escape filter → response เป็น default 500 ของ NestJS
เคล็ดลับตอนทำงานจริง
ตอน debug หา "เกิดอะไรขึ้น"
อย่าเริ่มที่ log แบบสุ่ม — ไล่ตามลำดับนี้
- รู้
userIdของคนที่เจอปัญหา - grep
userIdในlogs/.../<date>/ทุกไฟล์ - ดู error log ก่อน ถ้าไม่เจอ ค่อยดู info log
- ถ้าเป็น GraphQL request → ดู entry ในรูปแบบ
{ query, variables, headers, from: { appId, userId } }
ถ้าเจอ "log ไม่ครบ"
อาจจะเพราะ
- request ตกที่ replica ที่ mount volume เสีย → log ไม่ถูกเขียน
- format ไม่ใช่ JSON → ไม่ match grep pattern
- error เกิดก่อน resolver → ไม่ผ่าน logging.js
ลองดู stdout ของ container ด้วย (docker logs vtrc-api-nodejs-2) เพราะบางทีมีแค่ console.log
อย่าเชื่อ message ของ error ใน frontend
frontend แสดง message ตาม code ของ Apollo error บางครั้ง code เดียวกันใช้หลายที่ → context ไม่ชัดเจน
ตัวอย่าง — TOKEN_INVALID เกิดได้จาก
- JWT ผิดรูปแบบ
- session ไม่มีใน DB
- apiKey header ผิด
ต้องดู log ใน backend ถึงรู้สาเหตุจริง
"ทำไม NODE_ENV=production แต่ยังเห็น playground"
เพราะ vtrc-api/docker-compose.yml:54 — NODE_ENV=production ถูก comment ไว้! container รันด้วย default 'development' จึงเปิด playground + introspection + debug
อย่าลืม uncomment ตอน build ใหม่