↓ Chuyển đến nội dung chính

DevOps Monitoring Observability

Những công cụ giám sát DevOps tôi dùng cho hai sản phẩm, và những thứ tôi đã bỏ

Với hai sản phẩm nhỏ chạy trên Fly.io, hệ thống giám sát (monitoring) DevOps của tôi gồm: một kênh chat nhận lỗi từ cơ chế ghi nhật ký của Django, một kho nhật ký trên object storage mà tôi truy vấn bằng DuckDB, bộ chỉ số có sẵn của Fly, báo cáo sự cố (crash report) từ điện thoại gửi qua SDK của Sentry về máy chủ của chính tôi, và một bản tóm tắt mỗi sáng. Bài này nói rõ mỗi công cụ theo dõi cái gì, tốn bao nhiêu tiền, và những gì tôi đã bỏ hoặc thôi tin, trong đó có một tỷ lệ lỗi 22% mà hóa ra là do chính các yêu cầu thăm dò của Cloudflare.

Những công cụ giám sát DevOps tôi dùng cho hai sản phẩm, và những thứ tôi đã bỏ

Tôi có hai sản phẩm nhỏ: một ứng dụng sàn giao dịch kiêm cộng đồng mà tôi đang làm, và một ứng dụng báo giá cho thợ làm nghề phải đo đạc, báo giá ngay tại nhà khách. Cả hai đều chạy trên loại máy rẻ nhất của Fly.io. Hệ thống giám sát DevOps của tôi rốt cuộc gói gọn trong năm thứ: một kênh chat nhận mọi nhật ký (log) mức ERROR, toàn bộ nhật ký được lưu vào object storage và truy vấn bằng DuckDB, bộ chỉ số (metric) Prometheus có sẵn của Fly, báo cáo sự cố ứng dụng trên điện thoại gửi qua SDK mã nguồn mở của Sentry về API của chính tôi, và một bản tóm tắt mỗi sáng. Không có APM thuê ngoài, không có truy vết (tracing) trên production, cũng không có bảng điều khiển (dashboard) nào mà tôi phải nhớ để mở ra xem. Thứ duy nhất tốn thêm tiền là kho nhật ký, khoảng 2 đô la Mỹ mỗi tháng. Dưới đây là danh sách đầy đủ, cùng những thứ tôi đã bỏ hoặc thôi tin tưởng, trong đó có một thứ khiến tôi mất mấy ngày đuổi theo một sự cố mà chưa từng có người dùng nào gặp phải.

Giám sát ở đây nghĩa là gì, và quy mô nhỏ đến mức nào
#

Khi nói giám sát DevOps, ý tôi là hai việc: biết có gì đó hỏng trước khi có người báo cho mình, và tìm ra nguyên nhân mà không phải đoán mò. Mọi công cụ dưới đây đều được đánh giá theo đúng hai tiêu chí đó.

Năm ngoái tôi có viết bài Dựng hệ thống giám sát mà lập trình viên thật sự dùng tới, chủ yếu nghĩ cho các đội: đội nhỏ thì dùng công cụ giám sát có sẵn của nhà cung cấp đám mây, khi đã có hàng trăm dịch vụ thì dùng một APM thuê ngoài. Bài này là đầu nhỏ nhất của cùng một cách nghĩ. Cả hai sản phẩm đều là Django chạy trên máy shared-cpu-1x của Fly, như tôi đã kể trong Bộ công nghệ hiện tại của tôi, và cả hai đều chỉ có một lập trình viên là tôi. Nếu coi giám sát là một sản phẩm và lập trình viên là người dùng của nó, thì hệ thống của tôi chỉ có đúng một người dùng: người đọc cảnh báo cũng chính là người sửa lỗi. Khi tôi lấy số liệu nền từ chỉ số của Fly vào tháng Chín, API của sàn giao dịch dùng trung bình 1% CPU. Đây không phải bộ công nghệ cho doanh nghiệp lớn, và tôi cũng không định tô vẽ nó thành như vậy.

Trong ba trụ cột quen thuộc là chỉ số, nhật ký và truy vết, tôi có hai. Cả hai sản phẩm đều không có truy vết phân tán (distributed tracing), vì mỗi sản phẩm chỉ là một codebase Django và một cơ sở dữ liệu, không có ranh giới dịch vụ nào để một trace phải đi qua. Dữ liệu truy vết duy nhất tôi xem là của reverse proxy trên máy trạm của tôi.

Danh sách
#

Công cụTheo dõi cái gìGiữ hay bỏ, và vì saoChi phí
Kênh chat của đội, nhận dữ liệu từ một logging handler của DjangoMọi lỗi ERROR từ API, máy chủ WebSocket và worker; các lần triển khai; nhập sai mã đăng nhập dồn dậpGiữ. Đây là nơi duy nhất tôi tự xem mà không cần ai nhắcMiễn phí
Log shipper của Fly đẩy sang một bucket tương thích S3, truy vấn bằng DuckDBMọi dòng nhật ký của các ứng dụng thuộc sàn giao dịchGiữ. Bộ đệm của Fly chỉ giữ vài phút; chỗ này giữ tất cả, lại truy vấn được bằng SQLKhoảng 2 đô la/tháng, cộng phí lưu trữ
Chỉ số có sẵn của Fly (Prometheus API)CPU, bộ nhớ, hạn mức burst, số phản hồi; exporter của Postgres được quản lýGiữ. Nó cho thấy các lần cơ sở dữ liệu bị treo không phải do code của tôiCó sẵn trong Fly
Kiểm tra tình trạng (health check) của Fly trên /api/health/ (cả hai ứng dụng)Một máy có nên nhận lưu lượng hay khôngGiữ để định tuyến. Bỏ, nếu coi nó là bằng chứng người dùng vẫn ổnCó sẵn trong Fly
SDK của Sentry, với DSN trỏ về API của tôiỨng dụng bị crash trên điện thoạiGiữ SDK. Chưa bao giờ đăng ký sentry.ioKhông tốn thêm gì ngoài API
Bản tóm tắt buổi sáng trong kênh chat của độiLỗi, mức sử dụng, tìm kiếm, tin nhắn và bảng công việc của hôm trướcGiữ. Chuyển từ một tác vụ cron trên CI sang máy Mac của tôiMiễn phí
SDK phân tích ứng dụng (cả hai ứng dụng), và công cụ quản trị trang web của công cụ tìm kiếmMức sử dụng trên web, iOS và Android; công cụ tìm kiếm nhìn trang web ra saoGiữ, đọc qua APIMiễn phí
Phân tích GraphQL của CloudflareAi đứng sau một mã trạng thái hay một đường dẫnGiữ, nhưng gom nhóm theo user agent trước khi tinGói miễn phí, mỗi truy vấn một ngày
Dashboard của một dịch vụ cập nhật OTA thuê ngoàiĐiện thoại đang chạy bản phát hành OTA nào; các lần tự quay lui khi crashChuyển sang đây từ một danh sách trong trang quản trị chẳng cho biết gìNằm trong dịch vụ cập nhật
Một script thăm dò nhỏCác yêu cầu bị lỗi trong lúc triển khai hoặc khởi động lạiGiữ. Nó gọi thẳng vào origin nên bộ nhớ đệm không che được khoảng hởMiễn phí
flyctl logsVài phút đầu ra gần nhất của một ứng dụngGiữ để xem trực tiếp. Bỏ, nếu dùng làm nơi lưu trữCó sẵn trong Fly
Cloudflare Early HintsKhông theo dõi gì cả; đây là tính năng tăng tốcĐã tắt. Các yêu cầu thăm dò của nó chính là toàn bộ lỗi 504 của zoneMiễn phí

Những dòng không ghi “cả hai ứng dụng” đều là của sàn giao dịch. Ứng dụng báo giá có ít hơn: health check, nhật ký và chỉ số của Fly, phân tích ứng dụng trên iOS, và một tác vụ xuất cơ sở dữ liệu ra tệp Parquet mỗi ngày để tôi truy vấn bằng DuckDB. Nó chưa có báo cáo crash, cũng chưa có kênh cảnh báo, và đó là chỗ hổng lớn nhất trong bài này. (Xin nói rõ về dòng dịch vụ cập nhật: công ty làm ra dịch vụ đó trả tiền cho tôi để viết bài trên blog của họ.)

Lỗi được gửi tới nơi tôi vốn đã ở đó
#

Trước tháng Chín, một lỗi 500 chưa được xử lý, một thông báo đẩy (push notification) gửi thất bại hay một frame WebSocket bị hỏng chỉ xuất hiện ở đúng một chỗ là flyctl logs, mà luồng đó thì chẳng ai tự giác ngồi đọc. Giờ đây một logging handler gửi mọi bản ghi từ mức ERROR trở lên vào một kênh chat, dù nó đến từ API, máy chủ WebSocket hay worker chạy nền. Cảnh báo mức WARNING thì tôi cố ý không gửi.

Handler gửi tin từ một luồng nền, nên không bao giờ làm chậm một yêu cầu. Bên trong nó không ghi nhật ký gì, nên không thể tự gọi đệ quy. Và cùng một lỗi chỉ được gửi tối đa mười phút một lần, giới hạn 30 cảnh báo mỗi giờ, nên khi có sự cố bạn chỉ đọc một tin nhắn chứ không phải một nghìn. Kênh này cũng chứa những thứ tôi muốn đặt cạnh lỗi để đối chiếu: các lần triển khai đều báo lúc bắt đầu, lúc chạy xong và chỗ bị hỏng, kèm commit và người chạy; các bản cập nhật OTA và các lần phát hành giao diện web cũng vậy. Khi một tài khoản bị nhập sai mã đăng nhập dồn dập, hệ thống cũng ghi ở mức ERROR, nên ai đó đang dò mã sẽ hiện ra ngay cạnh các sự cố.

Hồi tháng Mười có một cảnh báo đến mà không khớp với dòng nào trong bất kỳ nhật ký nào. Từ đó, mỗi cảnh báo mang theo một id ngẫu nhiên ngắn, tên máy và lệnh khởi động tiến trình, đồng thời ghi đúng dòng đó vào nhật ký của tiến trình. Nếu tôi không tìm thấy id ở đâu cả, thì cảnh báo đến từ một tiến trình không ghi nhật ký vào đó, chẳng hạn một phiên SSH. Mọi thứ vào kênh cũng được đẩy vào một luồng dữ liệu mà agent lập trình tôi làm việc cùng đọc được, nên agent đang làm việc với code cũng thấy lỗi cùng lúc với tôi.

Nhật ký: Fly giữ vài phút, kho lưu trữ giữ tất cả
#

Bộ đệm nhật ký của Fly chỉ giữ vài phút đầu ra, khoảng một trăm dòng gần nhất, đủ để theo dõi một lần triển khai nhưng vô dụng với chuyện của ngày hôm qua. Vì vậy một ứng dụng Fly thứ ba chạy log shipper của Fly, tức là Vector đăng ký nghe luồng nhật ký của cả tổ chức, và tôi chỉ đổi một chỗ ở sink S3 của nó:

[sinks.aws_s3]
  type = "aws_s3"
  bucket = "myapp-logs"
  compression = "gzip"
  key_prefix = "logs/{{fly.app.name}}/%F/"   # one folder per app per day
  batch.timeout_secs = 300                  # a quiet app still lands its lines

Mọi dòng nhật ký của các ứng dụng thuộc sàn giao dịch đều được ghi vào một bucket tương thích S3 dưới dạng JSON nén gzip. Shipper chạy trên một máy shared-cpu-1x 256 MB, khoảng 2 đô la mỗi tháng, phí lưu trữ chỉ vài xu. DuckDB đọc thẳng các tệp tại chỗ thông qua một macro, và dữ liệu của một ngày trả về trong khoảng một giây:

CREATE MACRO logs(app, day := '*') AS TABLE
  SELECT * FROM read_json_auto('s3://myapp-logs/logs/' || app || '/' || day || '/*.log.gz',
                               union_by_name := true);

SELECT timestamp, message FROM logs('myapp-api', '2026-09-06') WHERE message LIKE '% 500 %';

Tôi đã dùng chính macro này để đo thời gian khởi động nguội (cold start) trong bài về scale to zero: script khởi động báo từng giai đoạn, dòng nào cũng mang id của máy, phần còn lại chỉ là một câu GROUP BY.

Nói gọn lại, đó là ba tầng của cùng một luồng, chia theo người đọc: kho lưu trữ giữ mọi thứ từ INFO trở lên (cũng là lý do các ứng dụng ghi nhật ký ở mức INFO trên production), một script xem trực tiếp chỉ hiện cảnh báo và lỗi, còn kênh chat nhận mức ERROR. Có điều, cả ba đều không biết ai là người gọi. Nhật ký truy cập không có IP hay user agent của client, nên muốn biết điều đó tôi phải sang Cloudflare.

Bộ chỉ số tôi không phải tự dựng
#

Fly chạy sẵn một Prometheus được quản lý cho mỗi tổ chức, nên tôi không phải vận hành hệ thống chỉ số riêng. Khi có câu hỏi, tôi gọi thẳng API của nó: bộ nhớ, CPU và hạn mức burst theo từng máy (trên CPU dùng chung thì fly_instance_cpu_balance rất quan trọng), số phản hồi và số yêu cầu đồng thời theo từng ứng dụng. Có hai điều tôi ước mình biết sớm hơn. Nó đòi Authorization: FlyV1 <token>, còn header Bearer hay token chỉ đọc đều nhận về “something went wrong resolving organization”. Và dữ liệu chỉ được giữ khoảng mười hai ngày.

Nó chứng minh giá trị của mình vào tháng Chín, khi nhật ký xuất hiện “Could not open a database connection” vài lần mỗi ngày, trông y hệt một lỗi của ứng dụng. Cụm Postgres được quản lý cũng nằm trong cùng Prometheus đó, và ở mỗi phút có lỗi, thời gian scrape của máy chủ chính (primary) nhảy từ khoảng 80 mili giây lên một đến mười giây, trong khi bản sao (replica) vẫn bình thường và thời điểm khởi động của Postgres không hề đổi. Tức là Postgres không khởi động lại; chính máy chủ vật lý bên dưới bị treo. Tôi gửi các mốc thời gian cho bộ phận hỗ trợ của Fly, và sau một lần khởi động lại để bảo trì vào đầu tháng Mười đưa primary sang máy khác, tình trạng treo hằng ngày đã chấm dứt.

Báo cáo crash từ điện thoại
#

Trước đây, khi ứng dụng crash trên điện thoại thì tôi không thấy dấu vết gì ở bất cứ đâu. Câu trả lời hiển nhiên là Sentry, và tôi lấy một nửa của nó: ứng dụng dùng SDK mã nguồn mở của Sentry, nhưng DSN trỏ vào một endpoint trong chính API Django của chúng tôi. Endpoint này nhận định dạng envelope của Sentry, gom nhóm các lần crash và cảnh báo qua đúng handler gửi vào kênh chat ở trên. Truy vết, phiên (session) và replay đều tắt; chỉ nhận lỗi.

Tôi không đăng ký sentry.io vì với vài báo cáo mỗi ngày, làm vậy nghĩa là thêm một nhà cung cấp nắm dữ liệu người dùng, thêm một dòng trong chính sách quyền riêng tư và thêm một hóa đơn. Cái giá phải trả là symbolication (giải mã stack trace về mã nguồn), thứ tôi vẫn chưa thay thế được. Stack trace hoặc là JavaScript đã bị rút gọn (minify), hoặc là địa chỉ native thô, nên một issue cho tôi biết cái gì crash, trên bản phát hành và thiết bị nào, người dùng vừa làm gì ngay trước đó, nhưng không cho biết lỗi nằm ở đâu trong mã nguồn.

Bảng điều khiển duy nhất là một tin nhắn
#

Tôi không dựng bảng điều khiển cho ứng dụng nào cả. Bài năm ngoái có một phép thử cho dashboard: nếu không ai tự mở nó ra, nó chưa chứng minh được giá trị. Một tin nhắn tự gửi đến thì không cần ai mở, nên thứ gần với dashboard nhất mà tôi có là một bài đăng trong kênh chat của đội mỗi sáng.

Phần đầu là các lỗi production trong 24 giờ qua, đọc từ kho nhật ký chứ không phải từ kênh chat, vì cảnh báo đã bị giới hạn tần suất nên không thể cho biết một chuyện xảy ra bao nhiêu lần. Số, id và địa chỉ được che đi trước khi gom nhóm, nên một trăm lần cùng một lỗi chỉ là một dòng kèm số lần, và không địa chỉ email nào lọt vào kênh. Tiếp theo là các lần health check thất bại, mức sử dụng hôm qua so với cùng thứ đó của tuần trước (Chủ nhật thì không bao giờ giống thứ Hai), lượt truy cập từ tìm kiếm của tuần gần nhất, các cuộc trò chuyện đang chờ trả lời, bảng công việc và khoản chi tiêu. Phần nào không tải được thì ghi rõ như vậy, còn bài đăng vẫn được gửi đi.

Đọc số liệu phân tích ứng dụng qua API dạy bạn nghi ngờ con số một chút. Tháng Tám có 121 lượt mở ứng dụng Android lần đầu từ Azerbaijan, kéo theo 119 lượt gỡ cài đặt, và tôi đọc tổng số Android của tháng đó sau khi đã trừ chúng ra.

Các bản phát hành có màn hình riêng. Hồi bản cập nhật OTA còn được phát từ máy chủ của tôi, cách duy nhất để xem một bản phát hành là một danh sách trong trang quản trị có ô “is active”, và tôi hoàn toàn không biết có bao nhiêu điện thoại đã nhận bản cập nhật. Dashboard của dịch vụ mới cho thấy các thiết bị thật sự đang chạy bản nào, việc cài đặt diễn ra ra sao, và cả lần quay lui khi một bundle lỗi bị crash và SDK đưa bản trước đó quay lại. Tôi đã viết riêng về việc chuyển sang một dịch vụ cập nhật thuê ngoài.

Những thứ tôi đã bỏ, hoặc thôi tin
#

Không có thứ nào trong số này là công cụ trả phí. Phần lớn là những thứ tôi vẫn coi là tín hiệu nhưng hóa ra không phải, và một tác vụ chạy sai chỗ.

Tỷ lệ 504 của Cloudflare, cho tới khi tôi gom nhóm theo user agent
#

Tháng Chín, dashboard của Cloudflare cho thấy có vẻ như 22% số yêu cầu trong một ngày thất bại với lỗi 504, dồn vào các endpoint mà ứng dụng thăm dò (poll) mỗi 30 giây, trong khi health check vẫn xanh hết và nhật ký của chính ứng dụng cho thấy các yêu cầu đó được xử lý rất nhanh. Tôi mất mấy ngày với nó, phát hiện máy cần 25 đến 33 giây để khởi động nguội và phải làm thế 92 lần mỗi ngày, sửa xong, rồi viết trong bài về scale to zero rằng đó là nguyên nhân.

Sáng hôm sau tôi gom nhóm chính những lỗi 504 đó theo user agent. Toàn bộ 7,344 lỗi từ ngày 5 đến ngày 9 tháng Chín đều đến từ các yêu cầu thăm dò Early Hints của chính Cloudflare (bastion early hints và nginx-ssl early hints); chúng không nhận được phản hồi từ origin nên bị ghi thành 504. Chúng bắt chước đường dẫn của yêu cầu thật, nên rơi đúng vào những endpoint bận nhất và trông y như người dùng đang gặp lỗi. Ứng dụng, trình duyệt, curl và trình thu thập dữ liệu của công cụ tìm kiếm không có lấy một lỗi nào. Origin vốn không gửi header preload, nên Early Hints thực ra chẳng làm được gì, và tôi đã tắt nó.

Công sức tối ưu khởi động nguội vẫn có ích, vì triển khai và đánh thức máy đều nhanh hơn, nhưng sự cố mà tôi tưởng mình đang sửa thì chưa từng chạm tới người dùng nào. Vì thế, quy tắc tôi muốn chia sẻ nhất là: trước khi tin vào tỷ lệ lỗi của CDN, hãy gom nhóm theo user agent và kiểm tra xem đó có phải do chính cơ chế của CDN sinh ra không. Trên gói miễn phí của Cloudflare, đó là một truy vấn GraphQL vào httpRequestsAdaptiveGroups, mỗi lần một ngày. Lần hoảng tiếp theo cũng được giải quyết bằng đúng truy vấn đó, khi một endpoint thăm dò tăng từ 70–260 yêu cầu mỗi ngày lên 2,880. Không phải công cụ cào dữ liệu (scraper) nào cả. Đó là chính máy Mac của tôi, với ứng dụng vẫn đang chạy trong trình giả lập iOS sau khi kiểm thử.

Một health check báo xanh
#

Fly kiểm tra /api/health/ của cả hai ứng dụng mỗi 30 giây và dùng kết quả để quyết định máy nào được nhận lưu lượng, và nó làm việc này tốt. Nhưng health check chạy trên một máy đã khởi động xong, nên nó không thấy những gì xảy ra trước đó: một lần khởi động nguội, một hàng đợi ở proxy, một yêu cầu không bao giờ tới nơi. Giờ tôi chỉ nhờ nó định tuyến. Khi triển khai, tôi dùng script thăm dò thay thế: nó gọi vào origin suốt quá trình và thoát với mã 1 nếu có bất kỳ yêu cầu nào lỗi. Script này đã bắt được một lần flyctl deploy thông thường để lại khoảng hở 7.3 giây, rồi ghi nhận không lỗi nào trong 2,514 yêu cầu khi chạy qua script triển khai, vốn đánh thức một máy dự phòng trước.

Bản thân health check cũng từng gây ra hai sự cố. Tháng Bảy, thiết lập SECURE_SSL_REDIRECT của Django trả lời yêu cầu thăm dò HTTP thường của Fly bằng mã 301, mọi máy đều trượt kiểm tra, và ai cũng nhận về lỗi 503. Tháng Tám, việc thu hẹp một except Exception trong view health check khiến lỗi “connection already closed” lọt ra thành 500, và Fly gỡ máy ra đúng trong trường hợp mà cơ chế thử lại của view được viết ra để xử lý. Giờ health check chỉ hỏi một câu: tiến trình này có phục vụ được yêu cầu không, và khi cơ sở dữ liệu bị treo thì cả hai ứng dụng không còn bị gỡ khỏi định tuyến nữa.

Bản tóm tắt buổi sáng cũng phải học bài học tương tự về cách đếm. Sàn giao dịch tạm ngưng (suspend) các máy đang rảnh, nên một máy bị đánh thức bởi yêu cầu sẽ trượt lần kiểm tra đầu tiên vài trăm mili giây sau khi khởi động và qua ở lần kế tiếp. Đếm cả những lần đó khiến sáng nào đọc cũng như có sự cố, nên giờ lỗi nào qua lại được trong vòng một phút sẽ được liệt kê riêng, ghi là máy thức dậy và các lần triển khai.

Một tác vụ định kỳ trên CI
#

Bản tóm tắt buổi sáng ban đầu là một workflow CI chạy theo lịch cron, và chỉ trong vòng một ngày đã phải chuyển đi. Khi nó cần đọc số liệu phân tích và tìm kiếm, khóa cần dùng là khóa tài khoản dịch vụ (service account) của dự án phân tích, thứ làm được mọi việc trong dự án đó, và tôi không muốn để nó trong secrets của CI. Giờ một trình lập lịch trên máy Mac của tôi chạy nó mỗi 20 phút, và nó chỉ đăng bài khi đã qua tám giờ sáng theo giờ Auckland mà bài của hôm nay chưa được gửi, nên một chiếc Mac đang ngủ lúc tám giờ sẽ đăng khi nó thức dậy. Workflow vẫn được giữ làm phương án dự phòng chạy tay, chỉ thiếu hai phần phân tích và tìm kiếm.

Nếu được bắt đầu lại
#

Tôi sẽ dựng ba thứ trước khi có người dùng đầu tiên, chứ không đợi tới sau sự cố đầu tiên: gửi lỗi vào một kênh chat mà đội vốn đã đọc, có giới hạn tần suất; một kho nhật ký trên object storage truy vấn được bằng SQL; và những chỉ số mà nền tảng vốn đã thu thập sẵn. Tất cả chỉ tốn vài đô la mỗi tháng. Báo cáo crash thì thêm vào đúng tuần ứng dụng lên điện thoại, vì nếu không, một lần crash trên điện thoại chẳng báo cho ai biết điều gì. Bản tóm tắt buổi sáng thì thêm vào khi số chỗ phải kiểm tra đã nhiều tới mức tôi sắp chẳng buồn kiểm tra chỗ nào nữa.

Truy vết thì tôi để dành cho tới khi có dịch vụ thứ hai để một yêu cầu phải đi qua, còn APM dạng dịch vụ thuê ngoài thì đợi tới khi có một đội cần dashboard dùng chung, lịch trực (on-call) và ngân sách để trả hóa đơn cho người khác. Đó là lúc những công cụ trong bài năm ngoái bắt đầu đáng đồng tiền. Trước lúc đó, thói quen hữu ích nhất lại miễn phí: khi một con số trông đáng sợ, hãy tìm xem cái gì tạo ra nó trước khi kết luận cái gì bị hỏng. Không ít lần hoảng hốt trong bài này đến từ chính hệ thống giám sát, chứ không phải từ sản phẩm.