> ตอนนี้ฉันมีบล็อกใหม่ที่สวยงามแล้ว อ่านบทความนี้พร้อมอัปเดตล่าสุดที่นั่น https://blog.goncharov.page/nodejs-logging-made-right

อะไรที่จู้จี้คุณมากที่สุดเมื่อคุณคิดถึงการเข้าสู่ระบบ NodeJS? หากคุณถามฉัน ฉันจะบอกว่าขาดมาตรฐานอุตสาหกรรมในการสร้างรหัสการติดตาม ภายในบทความนี้ เราจะอธิบายภาพรวมว่าเราสามารถสร้าง ID ติดตามเหล่านี้ได้อย่างไร (หมายความว่าเราจะตรวจสอบโดยย่อว่าที่เก็บข้อมูลในเครื่องต่อเนื่องหรือที่รู้จักในชื่อ CLS ทำงานอย่างไร) และเจาะลึกถึงวิธีที่เราสามารถใช้ Proxy เพื่อให้มันทำงานกับ ANY ได้ คนตัดไม้

เหตุใดการมี ID ติดตามสำหรับแต่ละคำขอใน NodeJS จึงเป็นปัญหา

บนแพลตฟอร์มที่ใช้มัลติเธรดและสร้างเธรดใหม่สำหรับแต่ละคำขอ มีสิ่งที่เรียกว่า "thread-local storage a.k.a. TLS" ซึ่งช่วยให้สามารถเก็บข้อมูลใดๆ ก็ตามที่ใช้ได้กับทุกสิ่งภายในเธรดได้ หากคุณมี Native API ที่จะทำเช่นนั้น การสร้าง ID แบบสุ่มสำหรับแต่ละคำขอไม่ใช่เรื่องง่าย ให้ใส่ไว้ใน TLS และใช้ในคอนโทรลเลอร์หรือบริการของคุณในภายหลัง แล้วข้อตกลงกับ NodeJS คืออะไร? อย่างที่คุณทราบ NodeJS เป็นแพลตฟอร์มแบบเธรดเดียว (ไม่เป็นความจริงอีกต่อไปเนื่องจากตอนนี้เรามีคนงาน แต่นั่นไม่ได้เปลี่ยนภาพรวม) ซึ่งทำให้ TLS ล้าสมัย แทนที่จะใช้งานเธรดที่แตกต่างกัน NodeJS จะเรียกใช้การโทรกลับที่แตกต่างกันภายในเธรดเดียวกัน (มี บทความดีๆ มากมาย เกี่ยวกับลูปเหตุการณ์ใน NodeJS หากคุณสนใจ) และ NodeJS ให้วิธีการแก่เราในการระบุการโทรกลับเหล่านี้โดยไม่ซ้ำกันและติดตามความสัมพันธ์ของพวกเขาด้วย กันและกัน.

ย้อนกลับไปในสมัยก่อน (v0.11.11) เรามี addAsyncListener ซึ่งทำให้เราสามารถติดตามเหตุการณ์แบบอะซิงโครนัสได้ โดยใช้พื้นฐานจาก "Forrest Norvell" ได้สร้างการใช้งาน "continuation local storage a.k.a. CLS" เป็นครั้งแรก เราจะไม่กล่าวถึงการใช้งาน CLS นั้น เนื่องจากเราในฐานะนักพัฒนาได้ถอด API นั้นในเวอร์ชัน 0.12 ออกไปแล้ว

จนกระทั่ง NodeJS 8 เราไม่มีวิธีอย่างเป็นทางการในการเชื่อมต่อกับการประมวลผลเหตุการณ์ async ของ NodeJS และในที่สุด NodeJS 8 ก็มอบพลังที่เราสูญเสียไปให้กับเราผ่านทาง async_hooks (หากคุณต้องการทำความเข้าใจ async_hooks ให้ดีขึ้น โปรดดูที่ บทความนี้) สิ่งนี้นำเราไปสู่การใช้งาน CLS แบบ async_hooks สมัยใหม่ — cls-hooked

ภาพรวมของซีแอลเอส

ต่อไปนี้คือขั้นตอนง่ายๆ ของวิธีการทำงานของ CLS:

มาแยกย่อยกันทีละขั้นตอน:

  1. สมมติว่าเรามีเว็บเซิร์ฟเวอร์ทั่วไป ก่อนอื่นเราต้องสร้างเนมสเปซ CLS หนึ่งครั้งตลอดอายุการสมัครของเรา
  2. ประการที่สอง เราต้องกำหนดค่ามิดเดิลแวร์เพื่อสร้างบริบท CLS ใหม่สำหรับแต่ละคำขอ เพื่อความง่าย สมมติว่ามิดเดิลแวร์นี้เป็นเพียงการโทรกลับที่ถูกเรียกใช้เมื่อได้รับคำขอใหม่
  3. ดังนั้นเมื่อมีคำขอใหม่มาถึง เราจะเรียกฟังก์ชันการโทรกลับนั้น
  4. ภายในฟังก์ชันนั้น เราสร้างบริบท CLS ใหม่ (วิธีหนึ่งคือใช้การเรียก API run)
  5. ณ จุดนี้ CLS จะวางบริบทใหม่ในแผนผังบริบทโดย ID การดำเนินการปัจจุบัน
  6. แต่ละเนมสเปซ CLS มีคุณสมบัติ active ในขั้นตอนนี้ CLS จะกำหนด active ให้กับบริบท
  7. ภายในบริบท เราทำการเรียกไปยังทรัพยากรแบบอะซิงโครนัส เช่น เราขอข้อมูลบางอย่างจากฐานข้อมูล เราส่งการโทรกลับไปยังการโทร ซึ่งจะดำเนินการเมื่อคำขอไปยังฐานข้อมูลเสร็จสมบูรณ์
  8. ฮุคอะซิงโครนัส init เริ่มทำงานสำหรับการดำเนินการแบบอะซิงโครนัสใหม่ โดยจะเพิ่มบริบทปัจจุบันลงในแผนที่ของบริบทด้วย async ID (พิจารณาว่าเป็นตัวระบุของการดำเนินการแบบอะซิงโครนัสใหม่) ในแผนที่นั้น เรามี ID สองอันที่ชี้ไปยังบริบทเดียวกัน
  9. เนื่องจากเราไม่มีเหตุผลในการเรียกกลับครั้งแรกอีกต่อไป มันจึงออกจากการทำงานแบบอะซิงโครนัสครั้งแรกอย่างมีประสิทธิภาพ
  10. หลังจาก async hook เริ่มทำงานสำหรับการโทรกลับครั้งแรก โดยจะตั้งค่าบริบทที่ใช้งานอยู่บนเนมสเปซเป็น undefined (ซึ่งไม่เป็นความจริงเสมอไป เนื่องจากเราอาจมีหลายบริบทที่ซ้อนกัน แต่สำหรับกรณีง่าย ๆ แล้วมันเป็นจริง)
  11. ตะขอ ทำลาย จะถูกยิงในการดำเนินการครั้งแรก มันจะลบบริบทออกจากแผนที่บริบทของเราด้วย async ID (เหมือนกับ ID การดำเนินการปัจจุบันของการโทรกลับครั้งแรกของเรา)
  12. คำขอไปยังฐานข้อมูลเสร็จสิ้นแล้ว และการโทรกลับครั้งที่สองของเรากำลังจะเริ่มทำงาน
  13. ณ จุดนี้ "ก่อน" ฮุกอะซิงก์จะเข้ามามีบทบาท รหัสการดำเนินการปัจจุบันเหมือนกับรหัส async ของการดำเนินการที่สอง (คำขอฐานข้อมูล) มันตั้งค่าคุณสมบัติ active ของเนมสเปซเป็นบริบทที่พบโดยรหัสการดำเนินการปัจจุบัน เป็นบริบทที่เราสร้างขึ้นมาก่อน
  14. ตอนนี้เราดำเนินการโทรกลับครั้งที่สอง เรียกใช้ตรรกะทางธุรกิจภายใน ภายในฟังก์ชันนั้น เราสามารถ "รับค่าใดๆ ด้วยคีย์" จาก CLS และมันจะคืนค่าอะไรก็ตามที่ค้นพบด้วยคีย์ในบริบทที่เราสร้างไว้ก่อนหน้านี้
  15. สมมติว่าเป็นการสิ้นสุดการประมวลผลคำขอที่ฟังก์ชันของเราส่งคืน
  16. หลังจาก async hook เริ่มทำงานสำหรับการโทรกลับครั้งที่สอง มันตั้งค่าบริบทที่ใช้งานบนเนมสเปซเป็น undefined
  17. destroy hook เริ่มทำงานสำหรับการดำเนินการอะซิงโครนัสครั้งที่สอง มันจะลบบริบทของเราออกจากแผนที่ของบริบทด้วย async ID ปล่อยให้มันว่างเปล่าอย่างแน่นอน
  18. เนื่องจากเราไม่ได้มีการอ้างอิงใด ๆ กับวัตถุบริบทอีกต่อไป ตัวรวบรวมขยะของเราจึงทำให้หน่วยความจำที่เกี่ยวข้องว่างมากขึ้น

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

กำลังสร้าง ID ติดตาม

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

สำหรับ ด่วน มิดเดิลแวร์นี้อาจมีลักษณะดังนี้:

จากนั้นในคอนโทรลเลอร์ของเรา เราสามารถสร้าง ID การติดตามที่สร้างขึ้นดังนี้:

ID การติดตามนี้ไม่ค่อยมีการใช้มากนัก เว้นแต่เราจะเพิ่มลงในบันทึกของเรา

มาเพิ่มลงใน "winston" ของเรากันดีกว่า

ถ้าตัวบันทึกทั้งหมดรองรับฟอร์แมตเตอร์ในรูปแบบของฟังก์ชัน (หลายตัวไม่ทำอย่างนั้นด้วยเหตุผลที่ดี) บทความนี้ก็จะไม่มีอยู่จริง แล้วเราจะเพิ่ม ID ติดตามให้กับ "pino" อันเป็นที่รักของฉันได้อย่างไร? พร็อกซี่ เพื่อช่วยเหลือ!

การรวมพร็อกซีและ CLS

พร็อกซีเป็นออบเจ็กต์ที่ล้อมรอบออบเจ็กต์ดั้งเดิมของเรา ทำให้เราสามารถแทนที่พฤติกรรมของมันได้ในบางสถานการณ์ รายการสถานการณ์เหล่านี้ (จริงๆ แล้วเรียกว่ากับดัก) มีจำกัด และคุณสามารถดูชุดทั้งหมดได้ "ที่นี่" แต่เราสนใจเฉพาะกับดัก "รับ" เท่านั้น มันช่วยให้เราสามารถสกัดกั้นการเข้าถึงทรัพย์สินได้ หมายความว่าหากเรามีวัตถุ const a = { prop: 1 } และรวมมันไว้ใน Proxy โดยมีกับดัก get เราสามารถส่งคืนทุกสิ่งที่เราต้องการสำหรับ a.prop

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

ในสถานการณ์สมมตินี้ Proxy ของเราอาจมีลักษณะดังนี้:

มิดเดิลแวร์ของเราจะแปลงร่างเป็นดังนี้:

และเราสามารถใช้ตัวบันทึกดังนี้:

cls-พรอกซี

ตามแนวคิดข้างต้น ไลบรารีขนาดเล็กที่เรียกว่า cls-proxify ได้ถูกสร้างขึ้น มีการผสานรวมกับ "ด่วน", "koa" และ "fastify" ได้ทันที มันไม่เพียงแต่ใช้กับดัก get กับวัตถุต้นฉบับ แต่ยังใช้ "กับดักอื่นๆ อีกมากมาย" ด้วยเช่นกัน จึงมีแอปพลิเคชันที่เป็นไปได้มากมายไม่รู้จบ คุณสามารถเรียกใช้ฟังก์ชันพร็อกซี การสร้างคลาส หรืออะไรก็ได้เกือบหมด! คุณถูกจำกัดด้วยจินตนาการของคุณเท่านั้น! ดูการสาธิตสดการใช้งานกับ pino และ fastify, pino และ express

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

คอยติดตามบทความใหม่ ๆ โดยติดตามฉันที่ "Twitter" หรือ "LinkedIn"! สมัครรับ "จดหมายข่าว" หรือ "RSS" ของฉัน ส่งอีเมลถึงฉันหากคุณมีคำถามใดๆ