EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / แจ้งเตือนถี่เกินเท่ากับไม่ได้แจ้ง จูนบอทให้คนอ่านจริง
Linux

แจ้งเตือนถี่เกินเท่ากับไม่ได้แจ้ง จูนบอทให้คนอ่านจริง

โดย Ton-Or ·5 ต.ค. 2026·อ่าน 9 นาที
แจ้งเตือนถี่เกินเท่ากับไม่ได้แจ้ง จูนบอทให้คนอ่านจริง

การตรวจจับถูกทุกครั้ง แต่คนอ่านเลิกอ่านไปแล้ว ปัญหาของระบบแจ้งเตือนส่วนใหญ่ไม่ได้อยู่ที่ความแม่นยำ มันอยู่ที่จำนวนข้อความต่อวันที่ไม่มีใครยอมรับไหว

พอดีผมทำระบบเฝ้ามอนิเตอร์เครื่องทำความเย็นตัวหนึ่ง อ่านค่ากระแสจากมิเตอร์เก็บลงฐานข้อมูล แล้วให้สคริปต์คอยดูแล้วยิงข้อความเข้าแอปแชตเมื่อมีเหตุการณ์ ตอนแรกทุกคนชอบ พอผ่านไปสองสัปดาห์ คนรับเริ่มบ่นว่ามันมั่ว ผมเลยนั่งไล่ล็อกของวันเดียวออกมาดูทั้งวัน แล้วพบว่าการตรวจจับไม่ได้มั่วเลยสักข้อ ทุกเหตุการณ์ถูกหมด สิ่งที่มั่วคือจำนวนข้อความ ผมเลยเก็บวิธีจูนไว้เป็นชุด เพราะมันใช้ได้กับระบบแจ้งเตือนทุกแบบ ไม่ใช่แค่งานเครื่องทำความเย็น

ส่วนเรื่องฟิสิกส์ของการอนุมานว่าเครื่องกำลังละลายน้ำแข็งอยู่หรือเปล่า ผมเขียนไว้แล้วใน อนุมานจังหวะละลายน้ำแข็งจากความชันอุณหภูมิ ตอนนี้ว่าด้วยเรื่องเดียวคือ พอตรวจจับถูกแล้ว จะส่งอะไรให้ใครบ้าง

นับก่อนแก้ วันเดียว 28 เหตุการณ์กลายเป็น 56 ข้อความ

สิ่งแรกที่ผมทำไม่ใช่แก้โค้ด แต่คือนับ ผมดึงล็อกของวันที่คนบ่นออกมาแล้วนับตามชนิดเหตุการณ์ ถ้าไม่ทำขั้นตอนนี้ก่อน เราจะเดาว่าตัวการคือเหตุการณ์ใหญ่ๆ ทั้งที่จริงตัวการมักเป็นของเล็กๆ ที่ยิงซ้ำ

# นับเหตุการณ์ที่ยิงออกไปในวันเดียว แยกตามชนิด
grep "^$(date +%F)" /var/log/example-notify.log \
  | grep -oE '\[event\] [A-Z-]+' | sort | uniq -c | sort -rn

# นับจำนวนข้อความจริงที่ออกไป (หนึ่งเหตุการณ์ส่งหลายคนได้)
grep -c '\[send\] ok=True' /var/log/example-notify.log
28เหตุการณ์ในหนึ่งวัน
56ข้อความที่ส่งจริง
16เป็นการเตือนซ้ำ
57%สัดส่วนของการเตือนซ้ำ
5รอบละลายน้ำแข็ง
1/1เปิดและปิดเครื่อง

ตัวเลขนี้เปลี่ยนวิธีคิดทั้งหมด เพราะเหตุการณ์ที่มีค่าจริงคือเปิดเครื่อง ปิดเครื่อง และรอบละลายน้ำแข็งห้ารอบ รวมสิบสองเหตุการณ์ ส่วนอีกสิบหกเหตุการณ์เป็นการเตือนซ้ำหลังละลายน้ำแข็งจบ ซึ่งบอกแค่ว่าเครื่องเดินมากี่นาทีแล้ว และมันคือ 57 เปอร์เซ็นต์ของทั้งหมด พูดง่ายๆ คือคนรับต้องอ่านข้อความที่ไม่มีข้อมูลใหม่มากกว่าครึ่งของทุกวัน แล้วเขาก็เลิกอ่านทั้งหมดรวมทั้งอันที่สำคัญด้วย

ที่มาของข้อความหนึ่งวันเปิดเครื่อง1->ละลายน้ำแข็ง5 รอบ นับ 10->เตือนซ้ำ16->ปิดเครื่อง1->คูณผู้รับ 2 คน= 56

แยกชั้นผู้รับก่อน ลดจำนวนข้อความได้ครึ่งหนึ่งโดยไม่ต้องแก้ตรรกะ

จุดที่ได้ผลเร็วที่สุดคือเลิกส่งทุกอย่างให้ทุกคน ระบบเดิมมีผู้รับสองคนแล้วส่งเหมือนกันทุกข้อความ ซึ่งผิดตั้งแต่ต้น เพราะสองคนนี้ต้องการคนละอย่าง คนคุมหน้างานต้องรู้ทุกจังหวะเพราะเขาต้องเดินไปดูเครื่อง ส่วนเจ้าของกิจการต้องการรู้แค่ว่าวันนี้เครื่องเปิดกี่โมง ปิดกี่โมง และสรุปตอนจบรอบว่ายังไง

# ผู้รับแยกชั้น: (ปลายทาง, สไตล์ข้อความ, ระดับที่รับ)
TIER_ALL   = {"on", "off", "defrost_start", "defrost_end", "reminder", "runtime"}
TIER_BRIEF = {"on", "off", "defrost_end"}          # เจ้าของ: เปิด ปิด และสรุปจบรอบ

RECIPIENTS = [
    ("site-lead",  "casual", TIER_ALL),
    ("owner",      "formal", TIER_BRIEF),
]

def notify(kind, build):
    """build(style) -> ข้อความตามสไตล์ของผู้รับแต่ละคน"""
    for target, style, tier in RECIPIENTS:
        if kind not in tier:
            continue
        try:
            send_message(target, build(style))
        except Exception as e:
            log("[notify] %s: %s" % (target, e))
ก่อนแยกชั้น

ทุกคนได้ทุกอย่าง

  • 28 เหตุการณ์ คูณ 2 คน เท่ากับ 56 ข้อความ
  • เจ้าของได้การเตือนซ้ำ 16 ครั้ง
  • คนที่ไม่ได้ทำอะไรกับข้อมูล ก็ต้องอ่าน
  • สุดท้ายทั้งสองคนปิดการแจ้งเตือน
หลังแยกชั้น

คนละชุดตามหน้าที่

  • คนคุมหน้างานได้ครบ เพราะต้องเดินไปดู
  • เจ้าของได้ เปิด ปิด สรุปจบรอบ รวม 7 ข้อความ
  • ปริมาณรวมลดจาก 56 เหลือ 35 ทันที
  • ไม่ได้แตะตรรกะตรวจจับเลยแม้แต่บรรทัดเดียว

การแยกชั้นยังพ่วงเรื่องสำนวนข้อความมาด้วย ผมให้ฟังก์ชันสร้างข้อความรับ style แล้วคืนคนละสำนวน เพราะคนคุมหน้างานอ่านแบบสั้นห้วนได้ ส่วนเจ้าของอยากได้แบบสุภาพเป็นทางการกว่า จุดสำคัญคือมันเป็นฟังก์ชันเดียวกัน ไม่ใช่โค้ดคนละชุด ไม่งั้นแก้ที่หนึ่งลืมอีกที่แน่นอน

การเตือนซ้ำ ลดจากหกครั้งเหลือครั้งเดียว

ตัวการหลักคือบล็อกเตือนซ้ำหลังละลายน้ำแข็งจบ ที่ตั้งไว้ว่ายิงทุก 30 นาที สูงสุด 6 ครั้ง เจตนาเดิมดี คืออยากรู้ว่าหลังละลายน้ำแข็งแล้วห้องกลับมาเย็นทันหรือเปล่า แต่ในทางปฏิบัติ ข้อความครั้งที่สองถึงครั้งที่หกไม่ได้เพิ่มข้อมูลอะไร มันบอกแค่ว่าเวลาผ่านไปอีกสามสิบนาที

ON_REPORT_EVERY = 1800     # วินาที: จังหวะรายงานระหว่างเครื่องเดิน
PD_INTERVAL     = 1800     # วินาที: จังหวะเตือนซ้ำหลังละลายน้ำแข็งจบ
PD_MAX          = 1        # เดิม 6 -> เหลือ 1 (สรุปหนึ่งครั้งพอ)

now = int(time.time())
if S["state"] == "ON" and not S["def_active"] and S.get("pd_end") and now >= S.get("pd_next", 0):
    stat = read_status(db)
    mins = max(1, round((now - S["pd_end"]) / 60))
    notify("reminder", lambda style: msg_post_defrost(mins, stat, style))

    S["pd_count"] += 1
    S["pd_next"]   = now + PD_INTERVAL
    if S["pd_count"] >= PD_MAX:
        S["pd_end"] = 0          # ปิดชุดนี้ รอรอบละลายน้ำแข็งครั้งหน้า
    save_state()
ตั้งเพดานจำนวนครั้งไว้เสมอ อย่าปล่อยให้เตือนจนกว่าจะหาย

ระบบแจ้งเตือนที่ยิงซ้ำจนกว่าสถานะจะกลับปกติ ฟังดูสมเหตุสมผลบนกระดาษ แต่ในความจริงมันคือกรณีที่สร้างข้อความมากที่สุดในวันที่แย่ที่สุด ซึ่งเป็นวันที่คนรับต้องการสมาธิที่สุดพอดี ผมตั้งเพดานไว้เสมอ แล้วให้ข้อความสุดท้ายในชุดบอกตรงๆ ว่า นี่คือครั้งสุดท้ายของเรื่องนี้ ถ้าอยากรู้สถานะให้ถามกลับมา คนรับจะได้รู้ว่าความเงียบต่อจากนี้ไม่ได้แปลว่าระบบตาย

อีกจุดหนึ่งที่ผมเพิ่มคือ รายงานระหว่างเครื่องเดินกับการเตือนซ้ำหลังละลายน้ำแข็ง ต้องไม่ทับกัน เดิมทั้งสองตัวมีนาฬิกาของตัวเอง แล้วบางช่วงมันตรงกันพอดี คนรับเลยได้สองข้อความติดกันที่พูดเรื่องเดียวกัน วิธีแก้คือให้ตัวหนึ่งเลื่อนนาฬิกาของอีกตัวทุกครั้งที่ยิง S["on_next"] = now + ON_REPORT_EVERY ทำให้สองตัวไม่มีวันชนกัน

รอบที่ซ้อนกัน ต้องรวมเป็นรอบเดียว ไม่ใช่นับสองรอบ

ในล็อกวันนั้นมีรอบละลายน้ำแข็งสองรอบห่างกันแค่ 8 นาที ตอน 10:12 กับ 10:27 ซึ่งดูแล้วไม่สมเหตุสมผล พอไล่ค่ากระแสจริงถึงเข้าใจ คือรอบแรกฮีตเตอร์ดับไปประมาณ 7 นาทีแล้วติดกลับมาใหม่ ซึ่งเป็นพฤติกรรมปกติของเครื่อง แต่ค่ารอที่ผมตั้งไว้ตอนนั้นคือ 120 วินาที เจ็ดนาทีมากกว่าสองนาที ระบบเลยฟันไปแล้วว่ารอบแรกจบ พอฮีตเตอร์ติดใหม่ก็นับเป็นรอบที่สอง กลายเป็นข้อความเริ่มและจบสองชุดติดกัน

ฮีตเตอร์ดับ 7 นาที เทียบค่ารอสองแบบ ความกว้างตามเวลาจริงฮีตเตอร์ติดดับประมาณ 7 นาทีติดอีกครั้งgrace เดิม120 วินาทีฟันว่าจบติดใหม่ถูกนับเป็นรอบใหม่grace ใหม่900 วินาทียังรอไม่ครบถ้าไม่กลับ ต้องรอถึงตรงนี้ฮีตเตอร์กลับมาก่อนครบ 900 วินาที ยังเป็นรอบเดิมแท่งวาดสัดส่วนตามเวลาจริง 120 วินาที กับ 900 วินาที เทียบกับช่วงดับ 7 นาที
ค่ารอเดิม 120 วินาทีหมดก่อนฮีตเตอร์กลับมา รอบเดียวเลยถูกนับเป็นสอง ค่ารอ 900 วินาทียังไม่หมดตอนฮีตเตอร์ติดใหม่ รวมเป็นรอบเดิม
ค่ารอสั้นเกิน ทำให้รอบเดียวถูกนับเป็นสองฮีตเตอร์ติด10:12->ดับ 7 นาที> grace 120s->ฟันว่าจบผิด->ติดใหม่ 10:27นับเป็นรอบใหม่->4 ข้อความ
DEF_THR       = 100    # เกณฑ์กระแสฮีตเตอร์ ที่ถือว่าเป็นการละลายน้ำแข็งจริง
DEF_END_GRACE = 900    # เดิม 120 วินาที -> 900 วินาที (ใกล้เคียงความยาวรอบปกติ)

if heater_current > DEF_THR:
    if not S["def_active"]:
        S["def_active"] = True
        S["def_start"]  = ts
        S["pd_end"]     = 0          # รอบใหม่ ยกเลิกชุดเตือนซ้ำของรอบก่อน
    S["def_off_since"] = 0           # ฮีตเตอร์กลับมา = ยังเป็นรอบเดิม ไม่ split
    S["def_last"] = ts

elif S["def_active"]:
    if not S.get("def_off_since"):
        S["def_off_since"] = ts
    if ts - S["def_off_since"] >= DEF_END_GRACE:
        S["def_active"] = False
        S["ndef"] += 1
        notify("defrost_end", lambda style: msg_defrost_end(S, style))
        S["pd_end"]  = S["def_last"]        # เริ่มนับชุดเตือนซ้ำจากตรงนี้
        S["pd_next"] = S["def_last"] + PD_INTERVAL
        S["pd_count"] = 0

จุดที่ต้องระวังคือ เวลาสรุปความยาวรอบ ต้องนับถึง ครั้งสุดท้ายที่เห็นฮีตเตอร์ทำงาน ไม่ใช่นับถึงเวลาที่ตัดสินว่าจบ เพราะไม่งั้นทุกรอบจะยาวเกินจริงไป 15 นาทีเท่ากับค่ารอที่ตั้งไว้ ผมเลยเก็บ def_last ไว้แยกจากเวลาที่ตัดสิน แล้วรายงานจาก def_last เสมอ

ค่ารอที่ถูก มาจากพฤติกรรมของเครื่อง ไม่ได้มาจากความรู้สึก

ผมเลือก 900 วินาทีเพราะไปดูข้อมูลย้อนหลังแล้วพบว่ารอบละลายน้ำแข็งปกติของเครื่องตัวนี้ยาวประมาณ 5 ถึง 19 นาที ค่ารอที่ใกล้เคียงความยาวรอบปกติ ทำให้รอบที่ติดกันถูกรวมเป็นรอบเดียวโดยอัตโนมัติ ข้อแลกเปลี่ยนคือข้อความสรุปจะมาช้าลงประมาณ 15 นาทีหลังรอบจบจริง ซึ่งยอมรับได้ เพราะข้อความสรุปไม่ใช่ข้อความที่ต้องรีบ ต่างจากข้อความเปิดเครื่องที่ต้องมาทันที

สถานะต้องรอดจากการรีสตาร์ต ไม่งั้นการนับเริ่มใหม่ทุกครั้ง

ในไทม์ไลน์วันนั้นมีการรีสตาร์ตบริการกลางวันด้วยตอน 19:43 แล้วชุดเตือนซ้ำนับต่อจากเดิมได้ถูกต้อง เพราะสถานะทั้งหมดถูกเขียนลงไฟล์ ไม่ได้อยู่แค่ในหน่วยความจำ ถ้าไม่ทำแบบนี้ พอรีสตาร์ตเมื่อไหร่ ตัวนับกลับเป็นศูนย์ แล้วคนรับก็จะได้ชุดเตือนซ้ำใหม่อีกหกครั้งทั้งที่เพิ่งได้ไปแล้ว

STATE = "/var/lib/example/notify.state"

S = {"state": "OFF", "last_ts": 0, "on_since": None, "day_start": 0, "ndef": 0,
     "last_active": 0, "def_active": False, "def_start": 0, "def_last": 0,
     "def_off_since": 0, "pd_end": 0, "pd_next": 0, "pd_count": 0, "on_next": 0}

def load_state():
    try:
        S.update(json.load(open(STATE)))
    except Exception:
        pass                       # ไฟล์ยังไม่มี = เริ่มจากค่าตั้งต้น ไม่ใช่ข้อผิดพลาด

def save_state():
    try:
        json.dump(S, open(STATE, "w"))
    except Exception as e:
        log("[state] %s" % e)

มีอีกข้อที่สำคัญไม่แพ้กัน คือตอนเริ่มทำงานครั้งแรกหลังรีสตาร์ต ต้อง อ่านค่าปัจจุบันมาตั้งสถานะแล้วเงียบไว้ ห้ามยิงข้อความย้อนหลัง เพราะไม่งั้นทุกครั้งที่รีสตาร์ตในขณะเครื่องเดินอยู่ คนรับจะได้ข้อความเปิดเครื่องอีกรอบ ทั้งที่เครื่องเปิดมาตั้งแต่เช้า ผมเขียนบรรทัดล็อกกำกับไว้ตรงนี้ตรงๆ ว่าเริ่มแบบเงียบ เพื่อให้ตัวเองอ่านล็อกทีหลังแล้วรู้ว่าช่วงนั้นไม่มีข้อความเพราะอะไร

ยกเลิกอัตโนมัติเมื่อเครื่องดับ แต่ให้ข้ามรอบ ไม่ใช่ยกเลิกทั้งชุด

ปัญหาที่ตามมาหลังตั้งชุดเตือนซ้ำคือ ถ้าเครื่องถูกปิดระหว่างที่ชุดยังไม่หมด ระบบจะยิงข้อความว่าเครื่องทำงานมากี่นาทีแล้วต่อไป ทั้งที่ตู้ดับไปแล้ว ทางแก้คือก่อนยิงทุกครั้งให้ดูค่ากระแสจริงก่อน ถ้าไม่มีกระแสไหลเลยก็ไม่ต้องยิง

แต่ตรงนี้ผมเคยแก้ผิดมาแล้วรอบหนึ่ง ตอนแรกผมสั่งให้ยกเลิกชุดทั้งชุดเมื่อเจอกระแสเป็นศูนย์ ผลคือเวลาคอมเพรสเซอร์ตัดตามเทอร์โมสตัทตามปกติ ซึ่งกระแสเป็นศูนย์ชั่วคราวสองสามนาที ชุดรายงานก็หายไปทั้งชุด แล้วคนรับบ่นว่ารายงานไม่ครบ ผมเลยเปลี่ยนเป็น ข้ามรอบนี้แล้วรอรอบหน้า แทน

running = (stat.get("comp", 0) > COMP_THR) or (stat.get("heat", 0) > 0) \
          or (stat.get("line", 0) > 0)

if not running:
    # คอมตัดชั่วคราวตามเทอร์โมสตัท = ข้ามรอบนี้พอ ห้ามยกเลิกทั้งชุด
    # ถ้าเครื่องดับจริง ตัวตรวจจับ OFF จะเคลียร์ pd_end ให้เองอยู่แล้ว
    S["pd_next"] = now + PD_INTERVAL
    save_state()
    log("[pd] ไม่มีกระแสไหล ข้ามรอบนี้ รอรอบหน้า")
else:
    ...ยิงข้อความตามปกติ...

หลักคิดคือ ให้ตัวตรวจจับสถานะเป็นคนตัดสินใจว่าจบหรือไม่จบ ส่วนตัวยิงข้อความมีหน้าที่แค่ตัดสินใจว่ารอบนี้ควรยิงหรือไม่ ถ้าเอาสองหน้าที่นี้มาปนกันเมื่อไหร่ ก็จะได้พฤติกรรมแปลกๆ แบบที่ผมเจอ

ห้ามใส่ keepalive หรือ poll ถี่กับ session ของแอปแชต

ข้อนี้สำคัญที่สุดในบทความ และเป็นข้อที่คนแก้ผิดทางกันมากที่สุด อาการที่เจอคือคนรับบ่นว่าข้อความมาช้าประมาณหนึ่งนาที คนส่วนใหญ่จะแก้ด้วยการเพิ่มการเช็คสถานะการเชื่อมต่อให้ถี่ขึ้น หรือเติม keepalive เข้าไป เพราะคิดว่าปัญหาอยู่ที่การเชื่อมต่อหลับ

การเช็ค session ถี่ๆ ทำให้ session ค้าง แล้วข้อความหายทั้งชุด

ฝั่งบริการแชตมีระบบป้องกันการใช้งานผิดปกติของตัวเอง การเรียกดูสถานะหรือดึงโปรไฟล์ซ้ำๆ ในความถี่สูง ถูกนับเป็นพฤติกรรมผิดปกติ ผลที่ผมเจอคือ session ค้างไปเฉยๆ ทั้งที่บริการยังทำงานอยู่และตอบสถานะว่าปกติ ข้อความที่ยิงในช่วงนั้นหายเงียบโดยไม่มีข้อผิดพลาดใดๆ ให้จับ ซึ่งแย่กว่าข้อความมาช้าหนึ่งนาทีมาก อย่าไปแตะฝั่งการเชื่อมต่อเพื่อแก้ปัญหาความหน่วง ให้ปล่อยการทำงานตามปกติเลี้ยงการเชื่อมต่อไปเอง แล้วไปลดความหน่วงที่ฝั่งของเราแทน

ความหน่วงหนึ่งนาทีนั้น พอผมไล่ดูจริงมันมาจากฝั่งเราทั้งหมด และแก้ได้โดยไม่ต้องแตะการเชื่อมต่อเลย

# ก่อนแก้: หน่วงรวมประมาณ 61 วินาที
POLL       = 10    # อ่านฐานข้อมูลทุก 10 วินาที
ON_CONFIRM = 30    # ต้องเห็นค่ากระแสต่อเนื่อง 30 วินาที ถึงจะฟันว่าเปิดเครื่อง

# หลังแก้: หน่วงรวมเหลือประมาณ 3 ถึง 4 วินาที
POLL       = 1     # ฐานข้อมูลอยู่บนเครื่องเดียวกัน อ่านทุกวินาทีไม่กระทบใคร
ON_CONFIRM = 3     # กระแสเดินคือเครื่องเปิดแล้ว ไม่ต้องหน่วงยืนยัน

เหตุผลที่ลดค่ายืนยันจาก 30 เหลือ 3 ได้ คือการเปิดเครื่องเป็นเหตุการณ์ที่ต่อเนื่องอยู่แล้วโดยธรรมชาติ มันไม่ใช่สัญญาณกระพริบที่ต้องกรอง พอกระแสเริ่มเดินก็คือเปิดแล้วจริงๆ ส่วนการลดจังหวะอ่านจาก 10 เหลือ 1 วินาที ไม่กระทบใครเลยเพราะเป็นการอ่านฐานข้อมูลบนเครื่องเดียวกัน ไม่ได้ไปยิงบริการภายนอก หลักที่ผมสรุปไว้คือ เวลาจะลดความหน่วง ให้ลดค่ายืนยันกับจังหวะอ่านของเรา ไม่ใช่เพิ่มความถี่ในการรบกวนบริการปลายทาง

เช็คสุขภาพก่อนส่งทุกครั้ง แล้วบันทึกไว้เมื่อข้าม

ข้อสุดท้ายเป็นเรื่องกันข้อความหายเงียบ ถ้าตัวรับส่งข้อความตอบกลับมาเป็นแบบยิงแล้วจบ คือตอบว่าสำเร็จทันทีโดยยังไม่ได้ส่งจริง เราจะไม่มีทางรู้ว่าข้อความไปถึงหรือเปล่า วิธีที่ผมใช้คือเรียกจุดตรวจสุขภาพก่อนทุกครั้ง แล้วถ้าไม่ผ่านก็ข้ามพร้อมเขียนบันทึกไว้ ดีกว่ายิงทิ้งแล้วไม่รู้ตัว

<?php
// เรียก /health ก่อน /send ทุกครั้ง แล้วบันทึกเหตุผลถ้าข้าม
$ctx  = stream_context_create(['http' => ['timeout' => 8, 'ignore_errors' => true]]);
$raw  = @file_get_contents('http://127.0.0.1:8080/health', false, $ctx);

$ok = false; $alive = false; $err = '';
if ($raw !== false && ($h = json_decode($raw, true)) && is_array($h)) {
    $ok    = !empty($h['ok']);
    $alive = !empty($h['session_alive']);
    $err   = $h['session_last_error'] ?? '';
}

if (!$ok || !$alive) {
    $reason = ($raw === false) ? 'ติดต่อบริการไม่ได้'
              : (!$alive ? "session ตาย ({$err})" : 'health ไม่ผ่าน');
    file_put_contents($logFile,
        sprintf("[%s] SKIP — %s\n", date('Y-m-d H:i:s'), $reason), FILE_APPEND);
    exit(1);
}

// ผ่านแล้วค่อยยิงจริง
foreach ($recipients as $name => $target) { send_message($target, $text); }
  1. นับก่อน ดึงล็อกหนึ่งวันมาแยกตามชนิดเหตุการณ์ แล้วดูว่าชนิดไหนกินสัดส่วนเกินครึ่ง
  2. แยกชั้นผู้รับ ให้แต่ละคนได้เฉพาะสิ่งที่เขาต้องทำอะไรกับมันได้
  3. ตั้งเพดานจำนวนครั้ง ของทุกการเตือนซ้ำ และให้ข้อความสุดท้ายบอกว่าเป็นครั้งสุดท้าย
  4. ตั้งค่ารอให้รวมรอบที่ติดกัน โดยดูจากความยาวรอบจริงย้อนหลัง ไม่ใช่ตั้งจากความรู้สึก
  5. เก็บสถานะลงไฟล์ และเริ่มทำงานหลังรีสตาร์ตแบบเงียบเสมอ
  6. ลดความหน่วงที่ฝั่งเรา ห้ามเพิ่มความถี่ในการรบกวนบริการปลายทาง
  7. เช็คสุขภาพก่อนส่ง และบันทึกทุกครั้งที่ตัดสินใจข้าม

ส่วนตัวผมคิดว่าคุณภาพของระบบแจ้งเตือน วัดกันที่จำนวนข้อความที่คนรับยังยอมเปิดอ่าน ไม่ได้วัดที่ความแม่นยำของการตรวจจับ ระบบที่ตรวจจับถูกร้อยเปอร์เซ็นต์แต่ยิงวันละห้าสิบข้อความ มีค่าเท่ากับระบบที่ปิดอยู่ เพราะสุดท้ายคนก็ปิดการแจ้งเตือนทิ้งเหมือนกัน ใครจะเอาชุดค่าพวกนี้ไปปรับตามงานของตัวเองก็ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย