找回密码
 立即注册
首页 业界区 业界 [python]Flask - Tracking ID的设计

[python]Flask - Tracking ID的设计

翁谌缜 2026-1-17 17:55:00
前言

在实际业务中,根据 tracking_id 追溯一条请求的完整处理路径是比较常见的需求。借助 Flask 自带的全局对象 g 以及钩子函数,可以很容易地为每条请求添加 tracking_id,并在日志中自动记录。
主要内容:


  • 如何为每条请求添加 tracking_id
  • 如何为日志自动添加 tracking_id 记录
  • 如何自定义响应类,实现统一的响应格式,并在响应头中添加 tracking_id
  • 视图函数单元测试示例
  • Gunicorn 配置
项目结构

虽然内容看起来很多,但 tracking_id 的实现其实很简单。本文按照生产项目的规范组织了代码,添加了 Gunicorn 配置和单元测试代码,以及规范了日志格式和 JSON 响应格式。
  1. ├── apis
  2. │   ├── common
  3. │   │   ├── common.py
  4. │   │   └── __init__.py
  5. │   └── __init__.py
  6. ├── gunicorn.conf.py
  7. ├── handles
  8. │   └── user.py
  9. ├── logs
  10. │   ├── access.log
  11. │   └── error.log
  12. ├── main.py
  13. ├── middlewares
  14. │   ├── __init__.py
  15. │   └── tracking_id.py
  16. ├── pkgs
  17. │   └── log
  18. │       ├── app_log.py
  19. │       └── __init__.py
  20. ├── pyproject.toml
  21. ├── pytest.ini
  22. ├── README.md
  23. ├── responses
  24. │   ├── __init__.py
  25. │   └── json_response.py
  26. ├── tests
  27. │   └── apis
  28. │       └── test_common.py
  29. ├── tmp
  30. │   └── gunicorn.pid
  31. └── uv.lock
复制代码
安装依赖
  1. uv add flask
  2. uv add gunicorn gevent  # 生产环境部署一般依赖这两个
  3. uv add --dev pytest           # 测试库
复制代码
实现添加 tracking_id 的中间件

代码文件:middlewares/tracking_id.py
  1. from uuid import uuid4
  2. from flask import Flask, Response, g, request
  3. def tracking_id_middleware(app: Flask):
  4.     """
  5.     跟踪 ID 中间件
  6.     为每个请求生成或获取跟踪 ID,用于追踪请求链路
  7.     """
  8.    
  9.     @app.before_request
  10.     def tracking_id_before_request():
  11.         """
  12.         请求前处理函数
  13.         检查请求头中是否包含 X-Tracking-ID,如果没有则生成一个新的 UUID 作为跟踪 ID
  14.         并将其存储到 Flask 的全局对象 g 中,供后续处理使用
  15.         """
  16.         # 从请求头中获取 X-Tracking-ID
  17.         tracking_id = request.headers.get("X-Tracking-ID")
  18.         if not tracking_id:
  19.             # 如果请求头中没有 X-Tracking-ID,则生成一个新的 UUID
  20.             tracking_id = str(uuid4())
  21.         # 将跟踪 ID 存储到 Flask 的全局对象 g 中,供后续处理使用
  22.         g.tracking_id = tracking_id
  23.     @app.after_request
  24.     def tracking_id_after_request(response: Response):
  25.         """
  26.         请求后处理函数
  27.         将跟踪 ID 添加到响应头中,以便客户端知道本次请求的跟踪 ID
  28.         """
  29.         # 检查响应头中是否已经有 X-Tracking-ID
  30.         tracking_id = response.headers.get("X-Tracking-ID", "")
  31.         if not tracking_id:
  32.             # 如果响应头中没有 X-Tracking-ID,则从全局对象 g 中获取
  33.             tracking_id = g.get("tracking_id", "")
  34.             # 将跟踪 ID 添加到响应头中
  35.             response.headers["X-Tracking-ID"] = tracking_id
  36.         return response
  37.     # 返回应用实例
  38.     return app
复制代码
代码文件 middlewares/__init__.py,方便其他模块导入
  1. from .tracking_id import tracking_id_middleware
  2. __all__ = [
  3.     "tracking_id_middleware",
  4. ]
复制代码
日志模块 - 自动记录 tracking_id

实现一个简单的输出到控制台的日志模块,日志格式为 JSON,自动添加 tracking_id 到日志中,避免手动在 logger.info() 这类方法中传入 tracking_id。
代码文件 pkgs/log/app_log.py
  1. import json
  2. import logging
  3. import sys
  4. from flask import g
  5. class JSONFormatter(logging.Formatter):
  6.     """日志格式化器,输出 JSON 格式的日志。"""
  7.     def format(self, record: logging.LogRecord) -> str:
  8.         log_record = {
  9.             "@timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
  10.             "level": record.levelname,
  11.             "name": record.name,
  12.             # "processName": record.processName,  # 如需记录进程名可取消注释
  13.             "tracking_id": getattr(record, "tracking_id", None),
  14.             "loc": "%s:%d" % (record.filename, record.lineno),
  15.             "func": record.funcName,
  16.             "message": record.getMessage(),
  17.         }
  18.         return json.dumps(log_record, ensure_ascii=False, default=str)
  19. class TrackingIDFilter(logging.Filter):
  20.     """日志过滤器,为日志记录添加 tracking_id。"""
  21.     def filter(self, record):
  22.         record.tracking_id = g.get("tracking_id", None)
  23.         return True
  24. def _setup_console_handler(level: int) -> logging.StreamHandler:
  25.     """设置控制台日志处理器。
  26.     Args:
  27.         level (int): 日志级别。
  28.     """
  29.     handler = logging.StreamHandler(sys.stdout)
  30.     handler.setLevel(level)
  31.     handler.setFormatter(JSONFormatter())
  32.     return handler
  33. def setup_app_logger(level: int = logging.INFO, name: str = "app") -> logging.Logger:
  34.     logger = logging.getLogger(name)
  35.     if logger.hasHandlers():
  36.         return logger
  37.     logger.setLevel(level)
  38.     logger.propagate = False
  39.     logger.addHandler(_setup_console_handler(level))
  40.     logger.addFilter(TrackingIDFilter())
  41.     return logger
复制代码
在 pkgs/log/__init__.py 中初始化 logger,实现单例调用。
  1. from .app_log import setup_app_logger
  2. logger = setup_app_logger()
  3. __all__ = ["logger"]
复制代码
自定义响应类

规范 JSON 类型的响应格式,并在响应头中添加 X-Tracking-ID 和 X-DateTime。
代码文件 responses/json_response.py
  1. import json
  2. from datetime import datetime
  3. from http import HTTPStatus
  4. from typing import Any
  5. from flask import Response, g, request
  6. class JsonResponse(Response):
  7.     def __init__(
  8.         self,
  9.         data: Any = None,
  10.         code: HTTPStatus = HTTPStatus.OK,
  11.         msg: str = "this is a json response",
  12.     ):
  13.         x_tracking_id = g.get("tracking_id", "")
  14.         x_datetime = datetime.now().astimezone().isoformat(timespec="seconds")
  15.         resp_headers = {
  16.             "Content-Type": "application/json",
  17.             "X-Tracking-ID": x_tracking_id,
  18.             "X-DateTime": x_datetime,
  19.         }
  20.         try:
  21.             resp = json.dumps(
  22.                 {
  23.                     "code": code.value,
  24.                     "msg": msg,
  25.                     "data": data,
  26.                 },
  27.                 ensure_ascii=False,
  28.                 default=str,
  29.             )
  30.         except Exception as e:
  31.             resp = json.dumps(
  32.                 {
  33.                     "code": HTTPStatus.INTERNAL_SERVER_ERROR.value,
  34.                     "msg": f"Response serialization error: {str(e)}",
  35.                     "data": None,
  36.                 }
  37.             )
  38.         super().__init__(response=resp, status=code.value, headers=resp_headers)
  39. class Success(JsonResponse):
  40.     def __init__(self, data: Any = None, msg: str = ""):
  41.         if not msg:
  42.             msg = f"{request.method} {request.path} success"
  43.         super().__init__(data=data, code=HTTPStatus.OK, msg=msg)
  44. class Fail(JsonResponse):
  45.     def __init__(self, msg: str = "", data: Any = None):
  46.         if not msg:
  47.             msg = f"{request.method} {request.path} failed"
  48.         super().__init__(data=data, code=HTTPStatus.INTERNAL_SERVER_ERROR, msg=msg)
  49. class ArgumentNotFound(JsonResponse):
  50.     def __init__(self, msg: str = "", data: Any = None):
  51.         if not msg:
  52.             msg = f"{request.method} {request.path} argument not found"
  53.         super().__init__(data=data, code=HTTPStatus.BAD_REQUEST, msg=msg)
  54. class ArgumentInvalid(JsonResponse):
  55.     def __init__(self, msg: str = "", data: Any = None):
  56.         if not msg:
  57.             msg = f"{request.method} {request.path} argument invalid"
  58.         super().__init__(data=data, code=HTTPStatus.BAD_REQUEST, msg=msg)
  59. class AuthFailed(JsonResponse):
  60.     """HTTP 状态码: 401"""
  61.     def __init__(self, msg: str = "", data: Any = None):
  62.         if not msg:
  63.             msg = f"{request.method} {request.path} auth failed"
  64.         super().__init__(data=data, code=HTTPStatus.UNAUTHORIZED, msg=msg)
  65. class ResourceConflict(JsonResponse):
  66.     """HTTP 状态码: 409"""
  67.     def __init__(self, msg: str = "", data: Any = None):
  68.         if not msg:
  69.             msg = f"{request.method} {request.path} resource conflict"
  70.         super().__init__(data=data, code=HTTPStatus.CONFLICT, msg=msg)
  71. class ResourceNotFound(JsonResponse):
  72.     """HTTP 状态码: 404"""
  73.     def __init__(self, msg: str = "", data: Any = None):
  74.         if not msg:
  75.             msg = f"{request.method} {request.path} resource not found"
  76.         super().__init__(data=data, code=HTTPStatus.NOT_FOUND, msg=msg)
  77. class ResourceForbidden(JsonResponse):
  78.     """HTTP 状态码: 403"""
  79.     def __init__(self, msg: str = "", data: Any = None):
  80.         if not msg:
  81.             msg = f"{request.method} {request.path} resource forbidden"
  82.         super().__init__(data=data, code=HTTPStatus.FORBIDDEN, msg=msg)
复制代码
代码文件 responses/__init__.py,方便其他模块调用。
  1. from .json_response import (
  2.     ArgumentInvalid,
  3.     ArgumentNotFound,
  4.     AuthFailed,
  5.     Fail,
  6.     JsonResponse,
  7.     ResourceConflict,
  8.     ResourceForbidden,
  9.     ResourceNotFound,
  10.     Success,
  11. )
  12. __all__ = [
  13.     "JsonResponse",
  14.     "Success",
  15.     "Fail",
  16.     "ArgumentNotFound",
  17.     "ArgumentInvalid",
  18.     "AuthFailed",
  19.     "ResourceConflict",
  20.     "ResourceNotFound",
  21.     "ResourceForbidden",
  22. ]
复制代码
编写视图函数

代码文件 apis/common/common.py。以下定义了 5 个路由,主要用于测试响应类是否正常返回 JSON 格式。
  1. from datetime import datetime
  2. from flask import Blueprint
  3. from handles import user as user_handle
  4. from pkgs.log import logger
  5. from responses import Success
  6. route = Blueprint("common_apis", __name__, url_prefix="/api")
  7. @route.get("/health")
  8. def health_check():
  9.     # print(g.get("tracking_id", "no-tracking-id"))
  10.     logger.info("Health check")
  11.     return Success(data="OK")
  12. @route.get("/users")
  13. def get_users():
  14.     users = user_handle.get_users()
  15.     return Success(data=users)
  16. @route.get("/names")
  17. def get_names():
  18.     names = ["Alice", "Bob", "Charlie"]
  19.     return Success(data=names)
  20. @route.get("/item")
  21. def get_item():
  22.     item = {"id": 101, "name": "Sample Item", "price": 29.99, "now": datetime.now()}
  23.     return Success(data=item)
  24. @route.get("/error")
  25. def get_error():
  26.     raise Exception("This is a test exception")
复制代码
GET /api/users 调用了 handles/ 中的代码,模拟查询数据库。handles/user.py 中的代码如下:
  1. import time
  2. from typing import Any, Dict, List
  3. def get_users() -> List[Dict[str, Any]]:
  4.     # 模拟查询用户数据
  5.     time.sleep(0.1)  # 模拟延迟
  6.     users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
  7.     return users
复制代码
代码文件 apis/common/__init__.py 中导入各个蓝图并统一暴露。由于示例代码只定义了一个蓝图,所以这里写得很简单。如果有多个蓝图,可以把蓝图都添加到一个列表中,在 Flask 应用中一次性遍历注册。
  1. from .common import route
  2. # from .common import route as common_route
  3. # routes = [
  4. #     common_route,
  5. # ]
  6. __all__ = ["route"]
复制代码
代码文件 apis/__init__.py 中提供 Flask 应用的工厂函数。
  1. import traceback
  2. from flask import Flask
  3. from apis.common import route as common_route
  4. from middlewares import tracking_id_middleware
  5. from responses import Fail, ResourceNotFound
  6. from pkgs.log import logger
  7. # 错误处理器
  8. def error_handler_notfound(error):
  9.     return ResourceNotFound()
  10. def error_handler_generic(error):
  11.     logger.error(traceback.format_exc())
  12.     return Fail(data=str(error))
  13. def create_app() -> Flask:
  14.     app = Flask(__name__)
  15.     # 注册中间件
  16.     app = tracking_id_middleware(app)
  17.     # 注册错误处理器
  18.     app.errorhandler(Exception)(error_handler_generic)
  19.     app.errorhandler(404)(error_handler_notfound)
  20.     # 注册蓝图
  21.     app.register_blueprint(common_route)
  22.     return app
  23. __all__ = [
  24.     "create_app",
  25. ]
复制代码
入口代码文件 main.py
  1. from apis import create_app
  2. app = create_app()
  3. if __name__ == "__main__":
  4.     app.run(host="127.0.0.1", port=8000, debug=False)
复制代码
简单运行测试


  • 启动应用
  1. # 方式1, 直接启动, 用于简单测试
  2. python main.py
  3. # 方式2, 使用 gunicorn, 这是生产环境启动方式. 配置文件默认路径即 ./gunicorn.conf.py
  4. gunicorn main:app
复制代码

  • curl 请求 /api/health。可以看到响应头中已经有了 X-Tracking-ID 和 X-DateTime
  1. $ curl -v http://127.0.0.1:8000/api/health
  2. *   Trying 127.0.0.1:8000...
  3. * Connected to 127.0.0.1 (127.0.0.1) port 8000
  4. * using HTTP/1.x
  5. > GET /api/health HTTP/1.1
  6. > Host: 127.0.0.1:8000
  7. > User-Agent: curl/8.14.1
  8. > Accept: */*
  9. >
  10. * Request completely sent off
  11. < HTTP/1.1 200 OK
  12. < Server: gunicorn
  13. < Date: Sat, 17 Jan 2026 08:41:07 GMT
  14. < Connection: keep-alive
  15. < Content-Type: application/json
  16. < X-Tracking-ID: 1f0adb8d-9bee-49d4-873f-31aa1437da60
  17. < X-DateTime: 2026-01-17T16:41:07+08:00
  18. < Content-Length: 61
  19. <
  20. * Connection #0 to host 127.0.0.1 left intact
  21. {"code": 200, "msg": "GET /api/health success", "data": "OK"}
复制代码

  • curl 请求 /api/users。手动指定请求头中的 X-Tracking-ID,响应时也会保持相同的 ID。
  1. $ curl -v http://127.0.0.1:8000/api/users -H 'X-Tracking-ID:123456'
  2. *   Trying 127.0.0.1:8000...
  3. * Connected to 127.0.0.1 (127.0.0.1) port 8000
  4. * using HTTP/1.x
  5. > GET /api/users HTTP/1.1
  6. > Host: 127.0.0.1:8000
  7. > User-Agent: curl/8.14.1
  8. > Accept: */*
  9. > X-Tracking-ID:123456
  10. >
  11. * Request completely sent off
  12. < HTTP/1.1 200 OK
  13. < Server: gunicorn
  14. < Date: Sat, 17 Jan 2026 08:44:37 GMT
  15. < Connection: keep-alive
  16. < Content-Type: application/json
  17. < X-Tracking-ID: 123456
  18. < X-DateTime: 2026-01-17T16:44:37+08:00
  19. < Content-Length: 110
  20. <
  21. * Connection #0 to host 127.0.0.1 left intact
  22. {"code": 200, "msg": "GET /api/users success", "data": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}
复制代码
编写单元测试

使用 pytest 进行单元测试,这里只是一个简单的示例
配置 pytest

配置文件 pytest.ini
  1. [pytest]
  2. testpaths = "tests"
  3. pythonpath = "."
复制代码
测试代码

代码文件 tests/apis/test_common.py
  1. from typing import Generator
  2. from unittest.mock import MagicMock, patch
  3. import pytest
  4. from flask import Flask
  5. from flask.testing import FlaskClient
  6. from apis.common import route as common_route
  7. @pytest.fixture
  8. def app() -> Generator[Flask, None, None]:
  9.     app = Flask(__name__)
  10.     app.config.update(
  11.         {
  12.             "TESTING": True,
  13.             "DEBUG": False,
  14.         }
  15.     )
  16.     app.register_blueprint(common_route)
  17.     yield app
  18. @pytest.fixture
  19. def client(app: Flask) -> FlaskClient:
  20.     return app.test_client()
  21. class TestGetHealth:
  22.     def test_get_health_success(self, client: FlaskClient) -> None:
  23.         resp = client.get("/api/health")
  24.         assert resp.status_code == 200
  25.         resp_headers = resp.headers
  26.         assert resp_headers.get("Content-Type") == "application/json"
  27.         assert "X-Tracking-ID" in resp_headers
  28.         assert "X-DateTime" in resp_headers
  29.         resp_body = resp.json
  30.         assert resp_body == {
  31.             "code": 200,
  32.             "msg": "GET /api/health success",
  33.             "data": "OK",
  34.         }
  35. class TestGetUsers:
  36.     @patch("apis.common.common.user_handle.get_users")
  37.     def test_get_users(self, mock_get_users: MagicMock, client: FlaskClient) -> None:
  38.         # mock user.get_users() 的返回值
  39.         mock_get_users.return_value = [
  40.             {"id": 1, "name": "Alice123"},
  41.             {"id": 2, "name": "Bob456"},
  42.         ]
  43.         # 发送请求
  44.         resp = client.get("/api/users")
  45.         assert resp.status_code == 200
  46.         resp_headers = resp.headers
  47.         assert resp_headers.get("Content-Type") == "application/json"
  48.         assert "X-Tracking-ID" in resp_headers
  49.         assert "X-DateTime" in resp_headers
  50.         # resp_body = resp.json
  51.         mock_get_users.assert_called_once()
复制代码
执行测试

  1. pytest -vv
复制代码
配置 Gunicorn

代码文件 gunicorn.conf.py。简单配置了一些启动参数,以及请求日志的格式。
  1. # Gunicorn 配置文件
  2. from pathlib import Path
  3. from multiprocessing import cpu_count
  4. import gunicorn.glogging
  5. from datetime import datetime
  6. class CustomLogger(gunicorn.glogging.Logger):
  7.     def atoms(self, resp, req, environ, request_time):
  8.         """
  9.         重写 atoms 方法来自定义日志占位符
  10.         """
  11.         # 获取默认的所有占位符数据
  12.         atoms = super().atoms(resp, req, environ, request_time)
  13.         
  14.         # 自定义 't' (时间戳) 的格式
  15.         now = datetime.now().astimezone()
  16.         atoms['t'] = now.isoformat(timespec="seconds")
  17.         
  18.         return atoms
  19.    
  20. # 预加载应用代码
  21. preload_app = True
  22. # 工作进程数量:通常是 CPU 核心数的 2 倍加 1
  23. # workers = int(cpu_count() * 2 + 1)
  24. workers = 2
  25. # 使用 gevent 异步 worker 类型,适合 I/O 密集型应用
  26. # 注意:gevent worker 不使用 threads 参数,而是使用协程进行并发处理
  27. worker_class = "gevent"
  28. # 每个 gevent worker 可处理的最大并发连接数
  29. worker_connections = 2000
  30. # 绑定地址和端口
  31. bind = "127.0.0.1:8000"
  32. # 进程名称
  33. proc_name = "flask-dev"
  34. # PID 文件路径
  35. pidfile = str(Path(__file__).parent / "tmp" / "gunicorn.pid")
  36. logger_class = CustomLogger
  37. access_log_format = (
  38.     '{"@timestamp": "%(t)s", '
  39.     '"remote_addr": "%(h)s", '
  40.     '"protocol": "%(H)s", '
  41.     '"host": "%({host}i)s", '
  42.     '"request_method": "%(m)s", '
  43.     '"request_path": "%(U)s", '
  44.     '"status_code": %(s)s, '
  45.     '"response_length": %(b)s, '
  46.     '"referer": "%(f)s", '
  47.     '"user_agent": "%(a)s", '
  48.     '"x_tracking_id": "%({x-tracking-id}i)s", '
  49.     '"request_time": %(L)s}'
  50. )
  51. # 访问日志路径
  52. accesslog = str(Path(__file__).parent / "logs" / "access.log")
  53. # 错误日志路径
  54. errorlog = str(Path(__file__).parent / "logs" / "error.log")
  55. # 日志级别
  56. loglevel = "debug"
复制代码
输出的日志格式。可以看到日志格式符合 JSON 规范,便于 Filebeat 收集后在 Kibana 上检索。
  1. $ tail -n 1 logs/access.log | python3 -m json.tool
  2. {
  3.     "@timestamp": "2026-01-17T16:44:37+08:00",
  4.     "remote_addr": "127.0.0.1",
  5.     "protocol": "HTTP/1.1",
  6.     "host": "127.0.0.1:8000",
  7.     "request_method": "GET",
  8.     "request_path": "/api/users",
  9.     "status_code": 200,
  10.     "response_length": 110,
  11.     "referer": "-",
  12.     "user_agent": "curl/8.14.1",
  13.     "x_tracking_id": "123456",
  14.     "request_time": 0.102042
  15. }
复制代码
补充

全局对象 g 的注意事项


  • g 不是进程或线程共享的全局变量,请只在请求处理流程中使用 g。
  • 如果视图函数中启动了后台线程或异步任务,在子线程中直接访问 g 通常会报错或获取不到数据。这时建议显式传递数据。
  • 不要在 g 中存储大文件或数据对象,否则会占用过高内存。
  • g 不是 session。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

2026-1-18 08:50:00

举报

您需要登录后才可以回帖 登录 | 立即注册