为什么要使用uuid标记日志?

在分布式系统中,一个请求可能会经过多个服务,每个服务都会生成自己的日志。如果我们只使用普通的日志记录,那么很难将这些日志串联在一起,以至难以跟踪一个请求的完整生命周期。

如果能够使用uuid标记日志,为每个请求生成一个唯一的uuid,且这个日志可以在不同的系统间传递,就可以解决这个问题。

FastAPI和Loguru是两个非常流行的Python库。FastAPI是一个现代的Web框架,具有高性能和易于使用的特点。Loguru是一个灵活的日志记录库,具有简单、易用和强大的特点。在这篇文章中,我们将介绍如何使用FastAPI和Loguru进行日志记录,并使用同一个uuid标记来标记每个请求的日志链路。

安装FastAPI和Loguru

首先,我们需要安装FastAPI和Loguru。可以使用pip命令来安装它们:

pip install fastapi
pip install loguru

创建FastAPI应用程序

接下来,我们需要创建一个FastAPI应用程序。在这个例子中,我们将创建一个简单的应用程序,它对外提供一个根路径访问,我们在根路径被访问时会记录日志,并且会调用hello函数,hello函数自身也添加了日志记录。

import uvicorn
from fastapi import FastAPI
from loguru import logger app = FastAPI() logger.add("log.log") @app.get("/{name}")
async def root(name):
logger.info("Hello from the root path")
hello()
return {"message": f"Hello {name}"} def hello():
logger.info("hello() called!") if __name__ == '__main__':
uvicorn.run("main:app", port=10001, reload=True)

tips uvicorn.run("main:app", port=10001, reload=True) 中的 main 表示执行 main.py 文件中的 app

服务启动后,如果我们访问 http://localhost:10001/Bingo,会收到响应 {"message":"Hello Bingo"}, log.log文件中会记录

2023-06-01 21:24:13.471 | INFO     | main:root:19 - Root path is Visited!
2023-06-01 21:24:13.472 | INFO | main:hello:26 - hello() called!
2023-06-01 21:24:13.472 | INFO | main:root:21 - Bingo Visited!

添加uuid标记

上面的三条日志虽然都是因为 我们访问一个地址产生的,但是他们之间除了时间,没有其他的管理;如果同一时间有多个请求,我们就难以跟踪随调用了 hello()做了什么 ,那么如何才能将他们关联起来呢?

这需要用到 with 上下文管理函数 和** FastAPI中间件** 的概念。

with 上下文管理函数

with 是 Python 中的一个上下文管理器,它可以在代码块执行完毕后自动释放资源,避免资源泄漏等问题,提高代码的可读性和可维护性。

with 语句的语法如下:

with expression [as variable]:
with-block

其中,expression 是一个上下文管理器对象,可以是一个函数或一个类,必须实现 __enter____exit__ 方法。as variable 是可选的,用来指定一个变量名,将 expression.__enter__() 方法的返回值赋值给该变量。with-block 是一个代码块,用来执行需要被管理的代码。

with 语句的执行流程如下:

  1. 执行 expression.__enter__() 方法,获取上下文管理器对象。
  2. 如果指定了 as variable,将 __enter__() 方法的返回值赋值给该变量。
  3. 执行 with-block 中的代码。
  4. 如果 with-block 中的代码执行过程中抛出异常,则执行 expression.__exit__(exc_type, exc_value, traceback) 方法,释放资源。
  5. 如果 with-block 中的代码执行完毕,则执行 expression.__exit__(None, None, None) 方法,释放资源。

with 语句可以用来管理文件、网络连接、锁等资源,例如:

with open('file.txt', 'r') as f:
content = f.read()
print(content)

上述代码使用 with 语句来管理文件资源,当代码块执行完毕后,会自动关闭文件句柄,释放资源。

FastAPI中间件

FastAPI中间件是一种机制,允许我们在请求到达应用程序之前或之后执行一些操作。它们可以用于添加请求头、验证身份、记录日志等。在FastAPI中,中间件是使用装饰器实现的。我们可以使用@app.middleware()装饰器来定义中间件。中间件函数接收一个Request对象和一个call_next函数作为参数。它可以在请求到达应用程序之前或之后执行一些操作,并调用call_next函数来继续处理请求。

代码实现

改造后的代码如下:

import uvicorn
import uuid
from fastapi import FastAPI
from loguru import logger
from starlette.responses import JSONResponse app = FastAPI() logger.add(
"log.log",
format="{time:YYYY-MM-DD HH:mm:ss.ms} [{extra[request_id]}] | {level} | {module}.{function}:{line} : {message}"
) @app.middleware("http")
async def request_middleware(request, call_next):
request_id = str(uuid.uuid4())
with logger.contextualize(request_id=request_id):
logger.info("Request started")
try:
return await call_next(request) except Exception as ex:
logger.error(f"Request failed: {ex}")
return JSONResponse(content={"success": False}, status_code=500) finally:
logger.info("Request ended") @app.get("/{name}")
async def root(name):
logger.info("Root path is Visited!")
hello()
logger.info(f"{name} Visited!")
return {"message": f"Hello {name}"} def hello():
logger.info("hello() called!") if __name__ == '__main__':
uvicorn.run("main:app", port=10001, reload=True)

当我们再次访问 http://localhost:10001/Bingo,收到的响应 {"message":"Hello Bingo"}不会有变化, 但是log.log文件中会记录:

2023-06-01 21:35:55.3555 [b61c693f-97c0-4c84-9f44-3b855bea2568] | INFO | main.request_middleware:29 : Request started
2023-06-01 21:35:55.3555 [b61c693f-97c0-4c84-9f44-3b855bea2568] | INFO | main.root:43 : Root path is Visited!
2023-06-01 21:35:55.3555 [b61c693f-97c0-4c84-9f44-3b855bea2568] | INFO | main.hello:50 : hello() called!
2023-06-01 21:35:55.3555 [b61c693f-97c0-4c84-9f44-3b855bea2568] | INFO | main.root:45 : Bingo Visited!
2023-06-01 21:35:55.3555 [b61c693f-97c0-4c84-9f44-3b855bea2568] | INFO | main.request_middleware:38 : Request ended

再次请求时,uuid 会发生变化:

2023-06-01 21:35:55.3555 [b0e02e34-51ee-4cbb-838b-8222ec8f0483] | INFO | main.request_middleware:29 : Request started
2023-06-01 21:35:55.3555 [b0e02e34-51ee-4cbb-838b-8222ec8f0483] | INFO | main.root:43 : Root path is Visited!
2023-06-01 21:35:55.3555 [b0e02e34-51ee-4cbb-838b-8222ec8f0483] | INFO | main.hello:50 : hello() called!
2023-06-01 21:35:55.3555 [b0e02e34-51ee-4cbb-838b-8222ec8f0483] | INFO | main.root:45 : Bingo Visited!
2023-06-01 21:35:55.3555 [b0e02e34-51ee-4cbb-838b-8222ec8f0483] | INFO | main.request_middleware:38 : Request ended

实现原理是我们在每个请求的状态中存储uuid,并在日志记录中使用它。

代码解释:

  1. @app.middleware("http") 表示注册一个 HTTP 中间件,用于处理 HTTP 请求。
  2. request 是一个请求对象,call_next 是一个回调函数,用于调用下一个中间件或路由处理函数。
  3. uuid.uuid4() 生成一个唯一的请求 ID,用于在日志中标识该请求。
  4. logger.contextualize(request_id=request_id) 用于在日志中添加请求 ID 上下文。
  5. logger.info("Request started") 记录请求开始的日志信息。
  6. await call_next(request) 调用下一个中间件或路由处理函数,并返回响应对象。
  7. logger.error(f"Request failed: {ex}") 记录请求失败的日志信息,其中 ex 是捕获到的异常对象。
  8. JSONResponse(content={"success": False}, status_code=500) 返回一个 HTTP 500 错误响应,表示请求处理失败。
  9. logger.info("Request ended") 记录请求结束的日志信息。

这个中间件只用实现一次,对后续所有其他接口的开发没有任何的侵入,一劳永逸。

tips: 为什么使用uuid.uuid4()而不是uuid.uuid()?

uuid.uuid()生成的UUID是根据主机ID、序列号和当前时间生成的。这意味着在同一台计算机上生成的UUID可能会重复,尤其是在高负载情况下。为了避免这种情况,我们可以使用uuid.uuid4()生成随机UUID。uuid.uuid4()生成的UUID是完全随机的,几乎不可能重复。因此,它是生成唯一标识符的最佳选择。

总结

在这个例子中,我们使用FastAPI和Loguru进行了日志记录,并使用同一个uuid标记来标记每个请求的日志链路。这使得我们能够轻松地跟踪每个请求的日志,并识别它们的来源,如果上游也使用了这样方式记录日志, 那做到跟踪一个请求的完整生命周期就不难办到了。

【Python】如何在FastAPI中使用UUID标记日志,以跟踪一个请求的完整生命周期的更多相关文章

  1. 如何在golang中打印grpc详细日志

    最近捣鼓fabric,在一个tls证书问题上纠结挺久,连接orderer服务时候,grpc日志总是冷冰冰的显示这个信息 Orderer Client Status Code: (2) CONNECTI ...

  2. java之线程(线程的创建方式、java中的Thread类、线程的同步、线程的生命周期、线程之间的通信)

    CPU:10核 主频100MHz 1核  主频    3GHz 那么哪一个CPU比较好呢? CPU核不是越多越好吗?并不一定.主频用于衡量GPU处理速度的快慢,举个例子10头牛运送货物快还是1架飞机运 ...

  3. [C++/Python] 如何在C++中使用一个Python类? (Use Python-defined class in C++)

    最近在做基于OpenCV的车牌识别, 其中需要用到深度学习的一些代码(Python), 所以一开始的时候开发语言选择了Python(祸患之源). 固然现在Python的速度不算太慢, 但你一定要用Py ...

  4. Python 如何在csv中定位非数字和字母的符号

    在数据清洗过程中,有时不仅希望去掉脏数据,更希望定位脏数据的位置,例如从csv里面定位非数字和字母单元格的位置,在使用isdigit().isalpha().isalnum()时无法判断浮点数,会将浮 ...

  5. python 如何在 command 中能够找到 其他module

    部分代码如下: __author__ = 'norsd' # coding=utf8 # 上句说明使用utf8编码 try: import os import sys import time #关键语 ...

  6. python的函数定义中99%的人会遇到的一个坑

    列表是一种经常使用的数据类型.在函数的定义中,常常会使用列表作为参数. 比如,要测试一个接口的数据,接口返回的数据格式如下: { "code": "20000" ...

  7. Android中Context的理解及使用(二)——Application的用途和生命周期

    实现数据共享功能: 多个Activity里面,可以使用Application来实现数据的共享,因为对于同一个应用程序来说,Application是唯一的. 1.实现全局共享的数据App.java继承自 ...

  8. 《React Native 精解与实战》书籍连载「React Native 中的生命周期」

    此文是我的出版书籍<React Native 精解与实战>连载分享,此书由机械工业出版社出版,书中详解了 React Native 框架底层原理.React Native 组件布局.组件与 ...

  9. 如何在WORD2010中取消自动编号?

    如何在WORD2010中取消自动编号? 使用WORD2010有一个很大的问题就是WORD2010的自动编号问题,WORD2010的自动编号是符合外国人的写作习惯的,对中国人来说不适用. WORD201 ...

  10. 如何在 Matlab 中绘制带箭头的坐标系

    如何在 Matlab 中绘制带箭头的坐标系 如何在 Matlab 中绘制带箭头的坐标系 实现原理 演示效果 完整代码 实现原理 使用 matlab 的绘制函数时,默认设置为一个方框形的坐标系, 图1 ...

随机推荐

  1. Cesium官方教程——Fabric

    1.简介 Fabric 是Cesium中定义的描述材质Material的JSON 结构体.Material代表了一个物体的外观. 材质Material可以是比较简单的,比如直接将一张图片赋予表面,或者 ...

  2. 云原生K8S精选的分布式可靠的键值存储etcd原理和实践

    @ 目录 概述 定义 应用场景 特性 为何使用etcd 术语 架构 原理 读操作 写操作 日志复制 部署 单示例快速部署 多实例集群部署 静态 etcd 动态发现 常见命令 概述 定义 etcd 官网 ...

  3. 剑指 offer 第 24 天

    第 24 天 数学(中等) 剑指 Offer 14- I. 剪绳子 给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m.n都是整数,n>1并且m>1),每段绳子的长度记为 k[ ...

  4. ChatGPT与码农的机会

    之前一篇博客已经写了有关AI在博客编写方面的优势与对未来博客的编写方面的思考.这篇文档我继续分享一个我在开发中的一个案例和相关的感想. 事件还原 我发现ChatGPT也可以帮助我编写OData,于是我 ...

  5. Django笔记十之values_list指定字段取值及distinct去重处理

    这篇笔记将介绍 reverse.distinct.values 和 values_list 的用法. 本篇笔记目录如下: reverse() values() values_list() distin ...

  6. 一连串div跟随鼠标移动

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. GKCTF X DASCTF应急挑战杯-Maple_root-Writeup

    GKCTF X DASCTF应急挑战杯-Maple_root-Writeup 参赛队员: b4tteRy, x0r, f1oat 最终成绩:2285 最终排名:27 总结 经过最近几次类线下的演练,感 ...

  8. 常用脚本学习手册——Bat脚本

    常用脚本学习手册--Bat脚本 我们在日常工作中常常会遇到一些需要重复进行的工作,又或者我们的项目在转交客户时需要去简化配置过程 这时我们就需要使用到一些自动化部署操作,我们常常会采用脚本来完成这部分 ...

  9. 数据挖掘关联分析—R实现

    关联分析 关联分析又称关联挖掘,就是在交易数据.关系数据或其他信息载体中,查找存在于项目集合或对象集合之间的频繁模式.关联.相关性或因果结构.或者说,关联分析是发现交易数据库中不同商品(项)之间的联系 ...

  10. 如何通过Java代码将添加页码到PDF文档?

    页码可以清楚了解总页数.定位页数快速寻找自己所要的文段.打印时不会分不清头中尾.很多人在使用办公软件的时候就会选择PDF进行文档的保存,因为PDF在保存之后不会出现乱码甚至是格式改变的情况.在PDF中 ...