返回文章列表

Sentry異常監控與Flask應用測試

本文介紹如何使用 Sentry 監控 Flask 應用程式的異常,並示範如何設定 Sentry 並整合到 Flask 應用中。文章涵蓋了 Sentry 的基本組態、錯誤記錄、使用 pdb 進行除錯,以及如何使用工廠模式建立應用程式物件並編寫測試案例,包含產品列表、分類別建立、產品建立和產品搜尋等功能測試。

Web 開發 測試

Sentry 提供了錯誤分類別、計數以及嚴重程度評估等功能,能有效協助開發者找出潛藏的錯誤。整合 Sentry SDK 後,Flask 應用程式便能將錯誤資訊傳送到 Sentry 伺服器,方便追蹤及除錯。設定 Sentry 時,需要提供專案的 DSN,並將 FlaskIntegration 加入 integrations 中以確保正確捕捉 Flask 應用程式中的錯誤。文章也說明瞭如何使用 Python 內建的 pdb 進行程式碼除錯,設定斷點並逐步執行程式碼,觀察變數變化。

除了異常監控,本文也介紹了使用工廠模式建立 Flask 應用程式物件的優點和實作方式。透過 create_app() 函式,可以根據不同的設定建立多個應用程式例項,方便測試和佈署。同時,使用 create_db() 函式可以初始化資料函式庫並建立表格,確保應用程式在不同環境下都能正常運作。此外,文章也示範瞭如何在藍圖中使用 current_app 代理物件來存取應用程式物件,以及如何編寫測試案例,包含產品列表、分類別建立、產品建立和產品搜尋等功能測試,確保程式碼的品質和穩定性。最後,文章也簡要介紹了 nose2 測試框架,說明其在測試收集和執行方面的優勢。

使用Sentry監控異常

Sentry是一個簡化異常監控流程的工具,並提供應用程式使用者在使用過程中遇到的錯誤洞察。很可能在日誌檔案中有被人類眼睛忽略的錯誤。Sentry將錯誤分類別並記錄錯誤的重複次數,幫助我們根據多個標準瞭解錯誤的嚴重程度以及如何相應地處理它們。它具有良好的GUI介面,方便所有這些功能的實作。在本篇中,我們將設定Sentry並將其用作有效的錯誤監控工具。

準備工作

Sentry可作為雲端服務使用,對開發者和基本使用者免費。為了本篇的目的,這個免費的雲端服務就足夠了。前往https://sentry.io/signup/ 並開始註冊流程。話雖如此,我們需要安裝Sentry的Python SDK:

$ pip install 'sentry-sdk[flask]'

內容解密:

  • 上述命令安裝了Sentry的Python SDK,並包含了對Flask框架的整合支援。
  • sentry-sdk 是Sentry的Python客戶端,用於將錯誤和事件傳送到Sentry伺服器。
  • [flask] 是一個額外的依賴包,提供了與Flask框架的整合。

操作步驟

一旦完成Sentry註冊,將顯示一個畫面,要求選擇需要與Sentry整合的專案型別。接著,另一個畫面將顯示如何組態Flask應用程式以將事件傳送到新建立的Sentry例項的步驟。

組態Sentry

在完成前面的設定後,將以下程式碼新增到Flask應用程式的my_app/__init__.py中,將https://1234:5678@fake-sentry-server/1替換為Sentry專案URI:

import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration

sentry_sdk.init(
    dsn="https://1234:5678@fake-sentry-server/1",
    integrations=[FlaskIntegration()]
)

內容解密:

  • sentry_sdk.init() 初始化Sentry客戶端。
  • dsn 引數是Sentry專案的URI,用於將事件傳送到正確的Sentry專案。
  • integrations=[FlaskIntegration()] 啟用了對Flask框架的支援,使得Sentry能夠自動捕捉Flask應用程式中的錯誤。

工作原理

在Sentry中記錄的錯誤將如下圖所示:

記錄的錯誤畫面顯示了錯誤的詳細資訊,包括錯誤型別、發生次數等。

使用pdb進行除錯

大多數閱讀本文的Python開發者可能已經熟悉了Python偵錯程式(pdb)的使用。對於那些不熟悉的人來說,pdb是Python程式的互動式原始碼偵錯程式。我們可以在需要的地方設定斷點,在原始碼層級單步除錯,並檢查堆積疊幀。

使用pdb

使用pdb非常簡單。在需要插入斷點的地方插入以下陳述式:

import pdb; pdb.set_trace()

這將觸發應用程式在此處中斷執行,然後我們可以使用偵錯程式命令逐步遍歷堆積疊幀。

例如,在products處理函式中插入斷點:

@catalog.route('/<lang>/products')
@catalog.route('/<lang>/products/<int:page>')
def products(page=1):
    products = Product.query.paginate(page=page, per_page=10)
    import pdb; pdb.set_trace()
    return render_template('products.html', products=products)

內容解密:

  • import pdb; pdb.set_trace() 匯入pdb模組並設定斷點。
  • 當控制流到達此行時,偵錯程式提示將啟動,允許我們逐步除錯。

pdb除錯過程

當控制流到達斷點時,偵錯程式提示將如下所示:

> /Users/apple/workspace/flask-cookbook-3/Chapter-10/Chapter-10/my_app/catalog/views.py(93)products()
-> return render_template('products.html', products=products)
(Pdb) u
> /Users/apple/workspace/flask-cookbook-3/Chapter-10/lib/python3.10/site-packages/flask/app.py(1796)dispatch_request()
-> return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
(Pdb) u
...

(Pdb)提示符下,可以使用各種除錯命令,如u(up)移動到堆積疊幀的上層,檢查變數、引數和屬性。

內容解密:

  • (Pdb) 是pdb的命令提示符。
  • u 命令用於在堆積疊跟蹤中向上移動一層。
  • 可以使用不同的除錯命令來導航和檢查程式狀態。

參考資料

有關各種除錯命令的詳細資訊,請參閱pdb模組檔案:https://docs.python.org/3/library/pdb.html#debugger-commands。

使用工廠模式建立應用程式物件

在開發 Flask 應用程式時,使用工廠模式是一種很好的實踐方式,可以讓我們建立多個具有不同設定的應用程式物件。這種模式不僅能夠在同一個應用程式行程中建立多個應用程式物件,還能幫助我們在測試時更加靈活地建立不同設定的應用程式物件。

為什麼使用工廠模式

使用工廠模式可以帶來以下好處:

  • 可以建立多個具有不同設定的應用程式物件。
  • 可以在同一個應用程式行程中建立多個應用程式物件。
  • 有助於測試,可以為每個測試案例建立一個新的或不同的應用程式物件。

如何實作工廠模式

1. 建立 create_app() 函式

首先,我們需要在 my_app/__init__.py 中建立一個名為 create_app() 的函式,用於建立應用程式物件。

def create_app(alt_config={}):
    app = Flask(__name__, template_folder=alt_config.get('TEMPLATE_FOLDER', 'templates'))
    app.config['UPLOAD_FOLDER'] = os.path.realpath('.') + '/my_app/static/uploads'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
    app.config['WTF_CSRF_SECRET_KEY'] = 'random key for form'
    app.config['LOG_FILE'] = 'application.log'
    app.config.update(alt_config)
    
    if not app.debug:
        import logging
        from logging import FileHandler, Formatter
        from logging.handlers import SMTPHandler
        file_handler = FileHandler(app.config['LOG_FILE'])
        app.logger.setLevel(logging.INFO)
        app.logger.addHandler(file_handler)
        mail_handler = SMTPHandler(
            ("smtp.gmail.com", 587),
            '[email protected]', RECEPIENTS,
            'Error occurred in your application',
            ('[email protected]', 'some_gmail_password'), secure=())
        mail_handler.setLevel(logging.ERROR)
        # app.logger.addHandler(mail_handler)
        for handler in [file_handler, mail_handler]:
            handler.setFormatter(Formatter(
                '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
            ))
    app.secret_key = 'some_random_key'
    return app

2. 建立 create_db() 函式

接下來,我們需要建立一個名為 create_db() 的函式,用於初始化資料函式庫並建立表格。

db = SQLAlchemy()

def create_db(app):
    db.init_app(app)
    with app.app_context():
        db.create_all()
    return db

3. 呼叫 create_app()create_db() 函式

最後,我們需要在 my_app/__init__.py 中呼叫 create_app()create_db() 函式,並註冊藍圖。

def get_locale():
    return g.get('current_lang', 'en')

app = create_app()
babel = Babel(app)
babel.init_app(app, locale_selector=get_locale)
from my_app.catalog.views import catalog
app.register_blueprint(catalog)
db = create_db(app)

在藍圖中使用 current_app

由於工廠模式的關係,我們無法在匯入藍圖時使用應用程式物件。但是,我們可以使用 current_app 代理物件來存取目前的應用程式物件。

from flask import current_app

@catalog.before_request
def before():
    # 現有的程式碼

@catalog.context_processor
def inject_url_for():
    # 現有的程式碼

編寫第一個簡單的測試

為了確保我們的應用程式正確運作,我們需要編寫測試。以下是如何編寫第一個簡單的測試:

1. 建立測試檔案

首先,我們需要建立一個名為 app_tests.py 的測試檔案。

import os
from my_app import create_app, db, babel
import unittest
import tempfile

class CatalogTestCase(unittest.TestCase):
    def setUp(self):
        test_config = {}
        self.test_db_file = tempfile.mkstemp()[1]
        test_config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + self.test_db_file
        test_config['TESTING'] = True
        self.app = create_app(test_config)
        db.init_app(self.app)
        babel.init_app(self.app)
        with self.app.app_context():
            db.create_all()
        from my_app.catalog.views import catalog
        self.app.register_blueprint(catalog)
        self.client = self.app.test_client()

    def tearDown(self):
        os.remove(self.test_db_file)

    def test_home(self):
        rv = self.client.get('/')
        self.assertEqual(rv.status_code, 200)

2. 執行測試

最後,我們可以執行測試檔案來檢視測試結果。

$ python app_tests.py

內容解密:

此段落說明瞭如何使用工廠模式建立 Flask 應用程式物件,並編寫簡單的測試。首先,建立了一個 create_app() 函式來建立應用程式物件,並使用 create_db() 函式初始化資料函式庫。然後,註冊了藍圖並使用 current_app 代理物件來存取目前的應用程式物件。接下來,編寫了一個簡單的測試,使用 unittest 框架來測試應用程式的首頁是否正確運作。最後,執行測試檔案來檢視測試結果。

測試檢視與邏輯

在前面的章節中,我們開始為Flask應用程式撰寫測試。本章節將繼續擴充套件測試檔案,並為應用程式新增更多測試,特別是針對檢視的行為和邏輯進行測試。

準備工作

我們將在前一章節建立的app_tests.py測試檔案基礎上進行擴充套件。

步驟解析

在撰寫任何測試之前,我們需要在setUp()方法中新增一些組態,以停用CSRF(跨站請求偽造)令牌,因為在測試環境中預設不會生成這些令牌:

test_config['WTF_CSRF_ENABLED'] = False

以下是本章節中建立的一些測試範例,每個測試都會被詳細描述:

  1. 測試產品列表頁面
def test_products(self):
    "測試產品列表頁面"
    rv = self.client.get('/en/products')
    self.assertEqual(rv.status_code, 200)
    self.assertTrue('No Previous Page' in rv.data.decode("utf-8"))
    self.assertTrue('No Next Page' in rv.data.decode("utf-8"))

該測試向/products端點傳送GET請求,並斷言回應的狀態碼為200。同時,它還檢查頁面中是否包含「No Previous Page」和「No Next Page」的文字。

內容解密:

  • self.client.get('/en/products'):模擬使用者對/en/products的GET請求。
  • self.assertEqual(rv.status_code, 200):驗證請求是否成功,狀態碼是否為200。
  • self.assertTrue:檢查回應內容是否包含預期的文字。
  1. 測試建立分類別
def test_create_category(self):
    "測試建立新分類別"
    rv = self.client.get('/en/category-create')
    self.assertEqual(rv.status_code, 200)
    rv = self.client.post('/en/category-create')
    self.assertEqual(rv.status_code, 200)
    self.assertTrue('This field is required.' in rv.data.decode("utf-8"))
    # ...

該測試首先檢查建立分類別頁面的GET請求是否成功,然後嘗試在沒有填寫必要欄位的情況下POST請求建立分類別,並驗證錯誤提示資訊。

內容解密:

  • self.client.post('/en/category-create'):在沒有填寫任何資料的情況下嘗試建立分類別。
  • self.assertTrue('This field is required.' in rv.data.decode("utf-8")):檢查是否正確顯示了欄位必填的錯誤提示。
  1. 測試建立產品
def test_create_product(self):
    "測試建立新產品"
    rv = self.client.get('/en/product-create')
    self.assertEqual(rv.status_code, 200)
    # ...
    rv = self.client.post('/en/product-create', data={'name': 'iPhone 5', 'price': 549.49, 'company': 'Apple', 'category': 1, 'image': tempfile.NamedTemporaryFile()})
    self.assertEqual(rv.status_code, 302)
    # ...

該測試檢查建立產品頁面的GET請求是否成功,然後嘗試建立一個新產品,並驗證建立成功後是否重定向到正確的頁面。

內容解密:

  • self.client.post('/en/product-create', data={...}):使用提供的資料建立新產品。
  • self.assertEqual(rv.status_code, 302):驗證建立成功後是否重定向(狀態碼302)。
  1. 測試搜尋產品
def test_search_product(self):
    "測試搜尋產品"
    # 先建立分類別和產品
    rv = self.client.post('/en/category-create', data={'name': 'Phones'})
    self.assertEqual(rv.status_code, 302)
    rv = self.client.post('/en/product-create', data={'name': 'iPhone 5', 'price': 549.49, 'company': 'Apple', 'category': 1, 'image': tempfile.NamedTemporaryFile()})
    self.assertEqual(rv.status_code, 302)
    # ...
    rv = self.client.get('/en/product-search?name=iPhone')
    self.assertEqual(rv.status_code, 200)
    self.assertTrue('iPhone 5' in rv.data.decode("utf-8"))
    self.assertFalse('Galaxy S5' in rv.data.decode("utf-8"))

該測試首先建立必要的分類別和產品,然後搜尋特定的產品,並驗證搜尋結果是否正確。

內容解密:

  • self.client.get('/en/product-search?name=iPhone'):搜尋名稱包含「iPhone」的產品。
  • self.assertTrue('iPhone 5' in rv.data.decode("utf-8")):驗證搜尋結果中是否包含預期的產品。

執行測試

要執行測試檔案,只需在終端機中執行以下命令:

$ python app_tests.py -v

這將輸出測試結果,顯示每個測試是否透過。

使用nose2函式庫進行測試

nose2是一個使測試變得更加容易和有趣的函式庫。它提供了許多工具來增強我們的測試。在本章節中,我們將重點介紹如何使用nose2來執行單個測試,而不是每次都執行所有的測試。

為什麼使用nose2?

nose2可以自動從Python原始檔、目錄和包中收集測試,使得測試的組織和執行變得更加靈活。雖然nose2可以用於多種目的,但它最重要的用途仍然是作為測試收集器和執行器。