Spaces:
Sleeping
Sleeping
eithney commited on
Commit ·
16fa939
1
Parent(s): b0a1454
Add client IP logging functionality: Implement `get_client_ip` to retrieve real client IP addresses, enhancing request logging in `before_request`. Update Dockerfile to use Gunicorn with configuration for accurate IP logging. Update VSCode completion for new function.
Browse files- .vscode/PythonImportHelper-v2-Completion.json +219 -3
- Dockerfile +2 -1
- IP_LOGGING_GUIDE.md +297 -0
- app.py +42 -3
- gunicorn_config.py +93 -0
- test_ip_logging.py +115 -0
.vscode/PythonImportHelper-v2-Completion.json
CHANGED
|
@@ -233,6 +233,15 @@
|
|
| 233 |
"detail": "requests",
|
| 234 |
"documentation": {}
|
| 235 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
{
|
| 237 |
"label": "OpenSearch",
|
| 238 |
"importPath": "opensearchpy",
|
|
@@ -266,6 +275,15 @@
|
|
| 266 |
"detail": "app",
|
| 267 |
"documentation": {}
|
| 268 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
{
|
| 270 |
"label": "load_book_data",
|
| 271 |
"kind": 2,
|
|
@@ -397,7 +415,7 @@
|
|
| 397 |
"kind": 2,
|
| 398 |
"importPath": "app",
|
| 399 |
"description": "app",
|
| 400 |
-
"peekOfCode": "def before_request():\n \"\"\"请求前处理\"\"\"\n # 记录请求信息\n if request.path.startswith('/api/'):\n app.logger.info(f\"{
|
| 401 |
"detail": "app",
|
| 402 |
"documentation": {}
|
| 403 |
},
|
|
@@ -469,7 +487,7 @@
|
|
| 469 |
"kind": 5,
|
| 470 |
"importPath": "app",
|
| 471 |
"description": "app",
|
| 472 |
-
"peekOfCode": "OPENSEARCH_CONFIG = {\n 'host': os.environ.get('OPENSEARCH_HOST', '192.168.3.33'),\n 'port': int(os.environ.get('OPENSEARCH_PORT', 9200)),\n 'use_ssl': os.environ.get('OPENSEARCH_USE_SSL', 'False').lower() == 'true',\n}\n# 全局变量:存储学习数据\nBOOK_DATA = None\ndef
|
| 473 |
"detail": "app",
|
| 474 |
"documentation": {}
|
| 475 |
},
|
|
@@ -478,7 +496,7 @@
|
|
| 478 |
"kind": 5,
|
| 479 |
"importPath": "app",
|
| 480 |
"description": "app",
|
| 481 |
-
"peekOfCode": "BOOK_DATA = None\ndef
|
| 482 |
"detail": "app",
|
| 483 |
"documentation": {}
|
| 484 |
},
|
|
@@ -617,6 +635,186 @@
|
|
| 617 |
"detail": "download_resources",
|
| 618 |
"documentation": {}
|
| 619 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
{
|
| 621 |
"label": "OpenSearchClient",
|
| 622 |
"kind": 6,
|
|
@@ -661,5 +859,23 @@
|
|
| 661 |
"peekOfCode": "def main():\n with open('book_10242.json', 'r', encoding='utf-8') as f:\n data = json.load(f)\n pages_data = json.loads(data['Data'])\n # 检查第4页(索引3)和第5页(索引4)\n for i in [3, 4]:\n if i < len(pages_data):\n page = pages_data[i]\n print(f\"=== 第{page['pageNumber']}页 ===\")\n print(f\"pageId: {page['pageId']}\")",
|
| 662 |
"detail": "quick_check",
|
| 663 |
"documentation": {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
}
|
| 665 |
]
|
|
|
|
| 233 |
"detail": "requests",
|
| 234 |
"documentation": {}
|
| 235 |
},
|
| 236 |
+
{
|
| 237 |
+
"label": "multiprocessing",
|
| 238 |
+
"kind": 6,
|
| 239 |
+
"isExtraImport": true,
|
| 240 |
+
"importPath": "multiprocessing",
|
| 241 |
+
"description": "multiprocessing",
|
| 242 |
+
"detail": "multiprocessing",
|
| 243 |
+
"documentation": {}
|
| 244 |
+
},
|
| 245 |
{
|
| 246 |
"label": "OpenSearch",
|
| 247 |
"importPath": "opensearchpy",
|
|
|
|
| 275 |
"detail": "app",
|
| 276 |
"documentation": {}
|
| 277 |
},
|
| 278 |
+
{
|
| 279 |
+
"label": "get_client_ip",
|
| 280 |
+
"kind": 2,
|
| 281 |
+
"importPath": "app",
|
| 282 |
+
"description": "app",
|
| 283 |
+
"peekOfCode": "def get_client_ip():\n \"\"\"\n 获取客户端真实 IP 地址\n 在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部\n \"\"\"\n # 按优先级检查各种代理头部\n headers_to_check = [\n 'X-Forwarded-For',\n 'X-Real-IP',\n 'CF-Connecting-IP', # Cloudflare",
|
| 284 |
+
"detail": "app",
|
| 285 |
+
"documentation": {}
|
| 286 |
+
},
|
| 287 |
{
|
| 288 |
"label": "load_book_data",
|
| 289 |
"kind": 2,
|
|
|
|
| 415 |
"kind": 2,
|
| 416 |
"importPath": "app",
|
| 417 |
"description": "app",
|
| 418 |
+
"peekOfCode": "def before_request():\n \"\"\"请求前处理\"\"\"\n # 获取客户端真实 IP\n client_ip = get_client_ip()\n # 记录所有请求信息(包括静态文件)\n user_agent = request.headers.get('User-Agent', 'Unknown')[:100] # 限制长度\n # API 请求记录详细信息\n if request.path.startswith('/api/'):\n app.logger.info(\n f\"[{client_ip}] {request.method} {request.path} \"",
|
| 419 |
"detail": "app",
|
| 420 |
"documentation": {}
|
| 421 |
},
|
|
|
|
| 487 |
"kind": 5,
|
| 488 |
"importPath": "app",
|
| 489 |
"description": "app",
|
| 490 |
+
"peekOfCode": "OPENSEARCH_CONFIG = {\n 'host': os.environ.get('OPENSEARCH_HOST', '192.168.3.33'),\n 'port': int(os.environ.get('OPENSEARCH_PORT', 9200)),\n 'use_ssl': os.environ.get('OPENSEARCH_USE_SSL', 'False').lower() == 'true',\n}\n# 全局变量:存储学习数据\nBOOK_DATA = None\ndef get_client_ip():\n \"\"\"\n 获取客户端真实 IP 地址",
|
| 491 |
"detail": "app",
|
| 492 |
"documentation": {}
|
| 493 |
},
|
|
|
|
| 496 |
"kind": 5,
|
| 497 |
"importPath": "app",
|
| 498 |
"description": "app",
|
| 499 |
+
"peekOfCode": "BOOK_DATA = None\ndef get_client_ip():\n \"\"\"\n 获取客户端真实 IP 地址\n 在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部\n \"\"\"\n # 按优先级检查各种代理头部\n headers_to_check = [\n 'X-Forwarded-For',\n 'X-Real-IP',",
|
| 500 |
"detail": "app",
|
| 501 |
"documentation": {}
|
| 502 |
},
|
|
|
|
| 635 |
"detail": "download_resources",
|
| 636 |
"documentation": {}
|
| 637 |
},
|
| 638 |
+
{
|
| 639 |
+
"label": "on_starting",
|
| 640 |
+
"kind": 2,
|
| 641 |
+
"importPath": "gunicorn_config",
|
| 642 |
+
"description": "gunicorn_config",
|
| 643 |
+
"peekOfCode": "def on_starting(server):\n \"\"\"服务器启动时执行\"\"\"\n print(\"=\" * 60)\n print(\"🚀 Gunicorn 服务器启动\")\n print(f\"📍 绑定地址: {bind}\")\n print(f\"👷 工作进程数: {workers}\")\n print(f\"🧵 每进程线程数: {threads}\")\n print(f\"⏱️ 超时时间: {timeout}s\")\n print(\"=\" * 60)\ndef worker_int(worker):",
|
| 644 |
+
"detail": "gunicorn_config",
|
| 645 |
+
"documentation": {}
|
| 646 |
+
},
|
| 647 |
+
{
|
| 648 |
+
"label": "worker_int",
|
| 649 |
+
"kind": 2,
|
| 650 |
+
"importPath": "gunicorn_config",
|
| 651 |
+
"description": "gunicorn_config",
|
| 652 |
+
"peekOfCode": "def worker_int(worker):\n \"\"\"工作进程被中断时执行\"\"\"\n print(f\"⚠️ 工作进程 {worker.pid} 被中断\")\ndef worker_abort(worker):\n \"\"\"工作进程异常退出时执行\"\"\"\n print(f\"❌ 工作进程 {worker.pid} 异常退出\")\ndef post_worker_init(worker):\n \"\"\"工作进程初始化后执行\"\"\"\n print(f\"✅ 工作进程 {worker.pid} 初始化完成\")",
|
| 653 |
+
"detail": "gunicorn_config",
|
| 654 |
+
"documentation": {}
|
| 655 |
+
},
|
| 656 |
+
{
|
| 657 |
+
"label": "worker_abort",
|
| 658 |
+
"kind": 2,
|
| 659 |
+
"importPath": "gunicorn_config",
|
| 660 |
+
"description": "gunicorn_config",
|
| 661 |
+
"peekOfCode": "def worker_abort(worker):\n \"\"\"工作进程异常退出时执行\"\"\"\n print(f\"❌ 工作进程 {worker.pid} 异常退出\")\ndef post_worker_init(worker):\n \"\"\"工作进程初始化后执行\"\"\"\n print(f\"✅ 工作进程 {worker.pid} 初始化完成\")",
|
| 662 |
+
"detail": "gunicorn_config",
|
| 663 |
+
"documentation": {}
|
| 664 |
+
},
|
| 665 |
+
{
|
| 666 |
+
"label": "post_worker_init",
|
| 667 |
+
"kind": 2,
|
| 668 |
+
"importPath": "gunicorn_config",
|
| 669 |
+
"description": "gunicorn_config",
|
| 670 |
+
"peekOfCode": "def post_worker_init(worker):\n \"\"\"工作进程初始化后执行\"\"\"\n print(f\"✅ 工作进程 {worker.pid} 初始化完成\")",
|
| 671 |
+
"detail": "gunicorn_config",
|
| 672 |
+
"documentation": {}
|
| 673 |
+
},
|
| 674 |
+
{
|
| 675 |
+
"label": "bind",
|
| 676 |
+
"kind": 5,
|
| 677 |
+
"importPath": "gunicorn_config",
|
| 678 |
+
"description": "gunicorn_config",
|
| 679 |
+
"peekOfCode": "bind = f\"0.0.0.0:{os.environ.get('PORT', 7860)}\"\n# 工作进程数(根据 CPU 核心数自动调整)\nworkers = int(os.environ.get('GUNICORN_WORKERS', multiprocessing.cpu_count() * 2 + 1))\n# 如果资源受限,可以手动设置较小的值\nworkers = min(workers, 4)\n# 工作进程类型\nworker_class = 'sync'\n# 每个工作进程的线程数\nthreads = 2\n# 超时时间(秒)",
|
| 680 |
+
"detail": "gunicorn_config",
|
| 681 |
+
"documentation": {}
|
| 682 |
+
},
|
| 683 |
+
{
|
| 684 |
+
"label": "workers",
|
| 685 |
+
"kind": 5,
|
| 686 |
+
"importPath": "gunicorn_config",
|
| 687 |
+
"description": "gunicorn_config",
|
| 688 |
+
"peekOfCode": "workers = int(os.environ.get('GUNICORN_WORKERS', multiprocessing.cpu_count() * 2 + 1))\n# 如果资源受限,可以手动设置较小的值\nworkers = min(workers, 4)\n# 工作进程类型\nworker_class = 'sync'\n# 每个工作进程的线程数\nthreads = 2\n# 超时时间(秒)\ntimeout = 120\n# 保持活动连接的时间",
|
| 689 |
+
"detail": "gunicorn_config",
|
| 690 |
+
"documentation": {}
|
| 691 |
+
},
|
| 692 |
+
{
|
| 693 |
+
"label": "workers",
|
| 694 |
+
"kind": 5,
|
| 695 |
+
"importPath": "gunicorn_config",
|
| 696 |
+
"description": "gunicorn_config",
|
| 697 |
+
"peekOfCode": "workers = min(workers, 4)\n# 工作进程类型\nworker_class = 'sync'\n# 每个工作进程的线程数\nthreads = 2\n# 超时时间(秒)\ntimeout = 120\n# 保持活动连接的时间\nkeepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)",
|
| 698 |
+
"detail": "gunicorn_config",
|
| 699 |
+
"documentation": {}
|
| 700 |
+
},
|
| 701 |
+
{
|
| 702 |
+
"label": "worker_class",
|
| 703 |
+
"kind": 5,
|
| 704 |
+
"importPath": "gunicorn_config",
|
| 705 |
+
"description": "gunicorn_config",
|
| 706 |
+
"peekOfCode": "worker_class = 'sync'\n# 每个工作进程的线程数\nthreads = 2\n# 超时时间(秒)\ntimeout = 120\n# 保持活动连接的时间\nkeepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)\nmax_requests = 1000\nmax_requests_jitter = 50",
|
| 707 |
+
"detail": "gunicorn_config",
|
| 708 |
+
"documentation": {}
|
| 709 |
+
},
|
| 710 |
+
{
|
| 711 |
+
"label": "threads",
|
| 712 |
+
"kind": 5,
|
| 713 |
+
"importPath": "gunicorn_config",
|
| 714 |
+
"description": "gunicorn_config",
|
| 715 |
+
"peekOfCode": "threads = 2\n# 超时时间(秒)\ntimeout = 120\n# 保持活动连接的时间\nkeepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)\nmax_requests = 1000\nmax_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout",
|
| 716 |
+
"detail": "gunicorn_config",
|
| 717 |
+
"documentation": {}
|
| 718 |
+
},
|
| 719 |
+
{
|
| 720 |
+
"label": "timeout",
|
| 721 |
+
"kind": 5,
|
| 722 |
+
"importPath": "gunicorn_config",
|
| 723 |
+
"description": "gunicorn_config",
|
| 724 |
+
"peekOfCode": "timeout = 120\n# 保持活动连接的时间\nkeepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)\nmax_requests = 1000\nmax_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'",
|
| 725 |
+
"detail": "gunicorn_config",
|
| 726 |
+
"documentation": {}
|
| 727 |
+
},
|
| 728 |
+
{
|
| 729 |
+
"label": "keepalive",
|
| 730 |
+
"kind": 5,
|
| 731 |
+
"importPath": "gunicorn_config",
|
| 732 |
+
"description": "gunicorn_config",
|
| 733 |
+
"peekOfCode": "keepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)\nmax_requests = 1000\nmax_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址",
|
| 734 |
+
"detail": "gunicorn_config",
|
| 735 |
+
"documentation": {}
|
| 736 |
+
},
|
| 737 |
+
{
|
| 738 |
+
"label": "max_requests",
|
| 739 |
+
"kind": 5,
|
| 740 |
+
"importPath": "gunicorn_config",
|
| 741 |
+
"description": "gunicorn_config",
|
| 742 |
+
"peekOfCode": "max_requests = 1000\nmax_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间",
|
| 743 |
+
"detail": "gunicorn_config",
|
| 744 |
+
"documentation": {}
|
| 745 |
+
},
|
| 746 |
+
{
|
| 747 |
+
"label": "max_requests_jitter",
|
| 748 |
+
"kind": 5,
|
| 749 |
+
"importPath": "gunicorn_config",
|
| 750 |
+
"description": "gunicorn_config",
|
| 751 |
+
"peekOfCode": "max_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间\n# %(m)s - 请求方法",
|
| 752 |
+
"detail": "gunicorn_config",
|
| 753 |
+
"documentation": {}
|
| 754 |
+
},
|
| 755 |
+
{
|
| 756 |
+
"label": "accesslog",
|
| 757 |
+
"kind": 5,
|
| 758 |
+
"importPath": "gunicorn_config",
|
| 759 |
+
"description": "gunicorn_config",
|
| 760 |
+
"peekOfCode": "accesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间\n# %(m)s - 请求方法\n# %(U)s - URL 路径\n# %(q)s - 查询字符串",
|
| 761 |
+
"detail": "gunicorn_config",
|
| 762 |
+
"documentation": {}
|
| 763 |
+
},
|
| 764 |
+
{
|
| 765 |
+
"label": "errorlog",
|
| 766 |
+
"kind": 5,
|
| 767 |
+
"importPath": "gunicorn_config",
|
| 768 |
+
"description": "gunicorn_config",
|
| 769 |
+
"peekOfCode": "errorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间\n# %(m)s - 请求方法\n# %(U)s - URL 路径\n# %(q)s - 查询字符串\n# %(s)s - 状态码",
|
| 770 |
+
"detail": "gunicorn_config",
|
| 771 |
+
"documentation": {}
|
| 772 |
+
},
|
| 773 |
+
{
|
| 774 |
+
"label": "loglevel",
|
| 775 |
+
"kind": 5,
|
| 776 |
+
"importPath": "gunicorn_config",
|
| 777 |
+
"description": "gunicorn_config",
|
| 778 |
+
"peekOfCode": "loglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间\n# %(m)s - 请求方法\n# %(U)s - URL 路径\n# %(q)s - 查询字符串\n# %(s)s - 状态码\n# %(b)s - 响应大小",
|
| 779 |
+
"detail": "gunicorn_config",
|
| 780 |
+
"documentation": {}
|
| 781 |
+
},
|
| 782 |
+
{
|
| 783 |
+
"label": "access_log_format",
|
| 784 |
+
"kind": 5,
|
| 785 |
+
"importPath": "gunicorn_config",
|
| 786 |
+
"description": "gunicorn_config",
|
| 787 |
+
"peekOfCode": "access_log_format = (\n '[%(t)s] '\n 'Client: %({X-Forwarded-For}i)s (Remote: %(h)s) '\n '%(m)s %(U)s%(q)s '\n 'Status: %(s)s '\n 'Size: %(b)s bytes '\n 'Time: %(D)s μs '\n 'UA: \"%({User-Agent}i)s\"'\n)\n# 进程名称前缀",
|
| 788 |
+
"detail": "gunicorn_config",
|
| 789 |
+
"documentation": {}
|
| 790 |
+
},
|
| 791 |
+
{
|
| 792 |
+
"label": "proc_name",
|
| 793 |
+
"kind": 5,
|
| 794 |
+
"importPath": "gunicorn_config",
|
| 795 |
+
"description": "gunicorn_config",
|
| 796 |
+
"peekOfCode": "proc_name = 'english_learning_app'\n# 守护进程(通常设为 False,让容器管理进程)\ndaemon = False\n# 预加载应用(减少内存占用)\npreload_app = True\n# 在请求处理前后执行的钩子\ndef on_starting(server):\n \"\"\"服务器启动时执行\"\"\"\n print(\"=\" * 60)\n print(\"🚀 Gunicorn 服务器启动\")",
|
| 797 |
+
"detail": "gunicorn_config",
|
| 798 |
+
"documentation": {}
|
| 799 |
+
},
|
| 800 |
+
{
|
| 801 |
+
"label": "daemon",
|
| 802 |
+
"kind": 5,
|
| 803 |
+
"importPath": "gunicorn_config",
|
| 804 |
+
"description": "gunicorn_config",
|
| 805 |
+
"peekOfCode": "daemon = False\n# 预加载应用(减少内存占用)\npreload_app = True\n# 在请求处理前后执行的钩子\ndef on_starting(server):\n \"\"\"服务器启动时执行\"\"\"\n print(\"=\" * 60)\n print(\"🚀 Gunicorn 服务器启动\")\n print(f\"📍 绑定地址: {bind}\")\n print(f\"👷 工作进程数: {workers}\")",
|
| 806 |
+
"detail": "gunicorn_config",
|
| 807 |
+
"documentation": {}
|
| 808 |
+
},
|
| 809 |
+
{
|
| 810 |
+
"label": "preload_app",
|
| 811 |
+
"kind": 5,
|
| 812 |
+
"importPath": "gunicorn_config",
|
| 813 |
+
"description": "gunicorn_config",
|
| 814 |
+
"peekOfCode": "preload_app = True\n# 在请求处理前后执行的钩子\ndef on_starting(server):\n \"\"\"服务器启动时执行\"\"\"\n print(\"=\" * 60)\n print(\"🚀 Gunicorn 服务器启动\")\n print(f\"📍 绑定地址: {bind}\")\n print(f\"👷 工作进程数: {workers}\")\n print(f\"🧵 每进程线程数: {threads}\")\n print(f\"⏱️ 超时时间: {timeout}s\")",
|
| 815 |
+
"detail": "gunicorn_config",
|
| 816 |
+
"documentation": {}
|
| 817 |
+
},
|
| 818 |
{
|
| 819 |
"label": "OpenSearchClient",
|
| 820 |
"kind": 6,
|
|
|
|
| 859 |
"peekOfCode": "def main():\n with open('book_10242.json', 'r', encoding='utf-8') as f:\n data = json.load(f)\n pages_data = json.loads(data['Data'])\n # 检查第4页(索引3)和第5页(索引4)\n for i in [3, 4]:\n if i < len(pages_data):\n page = pages_data[i]\n print(f\"=== 第{page['pageNumber']}页 ===\")\n print(f\"pageId: {page['pageId']}\")",
|
| 860 |
"detail": "quick_check",
|
| 861 |
"documentation": {}
|
| 862 |
+
},
|
| 863 |
+
{
|
| 864 |
+
"label": "test_ip_logging",
|
| 865 |
+
"kind": 2,
|
| 866 |
+
"importPath": "test_ip_logging",
|
| 867 |
+
"description": "test_ip_logging",
|
| 868 |
+
"peekOfCode": "def test_ip_logging():\n \"\"\"测试 IP 地址记录\"\"\"\n base_url = \"http://localhost:7860\"\n print(\"=\" * 60)\n print(\"🧪 测试客户端 IP 记录功能\")\n print(\"=\" * 60)\n # 测试场景\n test_cases = [\n {\n \"name\": \"正常请求(无代理)\",",
|
| 869 |
+
"detail": "test_ip_logging",
|
| 870 |
+
"documentation": {}
|
| 871 |
+
},
|
| 872 |
+
{
|
| 873 |
+
"label": "show_example_logs",
|
| 874 |
+
"kind": 2,
|
| 875 |
+
"importPath": "test_ip_logging",
|
| 876 |
+
"description": "test_ip_logging",
|
| 877 |
+
"peekOfCode": "def show_example_logs():\n \"\"\"显示日志示例\"\"\"\n print(\"\\n\" + \"=\" * 60)\n print(\"📝 日志输出示例\")\n print(\"=\" * 60)\n examples = [\n \"[2025-10-16 18:58:23] INFO in app: [203.0.113.1] GET /api/health | UA: Mozilla/5.0 (Windows NT 10.0)\",\n \"[2025-10-16 18:58:24] INFO in app: [198.51.100.1] POST /api/search | UA: Mozilla/5.0 (iPhone; CPU iPhone OS)\",\n \"[2025-10-16 18:58:25] INFO in app: [192.0.2.1] GET /api/book/info | UA: curl/7.68.0\",\n ]",
|
| 878 |
+
"detail": "test_ip_logging",
|
| 879 |
+
"documentation": {}
|
| 880 |
}
|
| 881 |
]
|
Dockerfile
CHANGED
|
@@ -35,7 +35,8 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
| 35 |
CMD curl -f http://localhost:7860/api/health || exit 1
|
| 36 |
|
| 37 |
# 使用 Gunicorn 启动 Flask 应用(生产环境推荐)
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
# 如果想使用 Flask 内置服务器(开发/测试用),使用下面这行:
|
| 41 |
# CMD ["python3.12", "app.py"]
|
|
|
|
| 35 |
CMD curl -f http://localhost:7860/api/health || exit 1
|
| 36 |
|
| 37 |
# 使用 Gunicorn 启动 Flask 应用(生产环境推荐)
|
| 38 |
+
# 使用配置文件以显示客户端真实 IP
|
| 39 |
+
CMD ["gunicorn", "-c", "gunicorn_config.py", "app:app"]
|
| 40 |
|
| 41 |
# 如果想使用 Flask 内置服务器(开发/测试用),使用下面这行:
|
| 42 |
# CMD ["python3.12", "app.py"]
|
IP_LOGGING_GUIDE.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📋 客户端 IP 地址日志指南
|
| 2 |
+
|
| 3 |
+
## ✅ 已完成的功能
|
| 4 |
+
|
| 5 |
+
现在 Flask 应用已经配置为在日志中记录客户端的真实 IP 地址。
|
| 6 |
+
|
| 7 |
+
## 🔍 日志格式
|
| 8 |
+
|
| 9 |
+
### Flask 应用日志
|
| 10 |
+
|
| 11 |
+
```
|
| 12 |
+
[2025-10-16 18:58:23] INFO in app: [203.0.113.1] GET /api/health | UA: Mozilla/5.0 (Windows NT 10.0)
|
| 13 |
+
^^^^^^^^^^^^
|
| 14 |
+
客户端真实 IP
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
**日志包含:**
|
| 18 |
+
- 时间戳
|
| 19 |
+
- 日志级别
|
| 20 |
+
- 客户端 IP 地址
|
| 21 |
+
- HTTP 方法和路径
|
| 22 |
+
- User-Agent(浏览器信息)
|
| 23 |
+
|
| 24 |
+
### Gunicorn 访问日志
|
| 25 |
+
|
| 26 |
+
```
|
| 27 |
+
[16/Oct/2025:18:58:23 +0000] Client: 203.0.113.1 (Remote: 172.17.0.1) GET /api/health Status: 200 Size: 123 bytes Time: 5234 μs UA: "Mozilla/5.0..."
|
| 28 |
+
^^^^^^^^^^^^
|
| 29 |
+
真实客户端 IP
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
**日志包含:**
|
| 33 |
+
- 时间戳
|
| 34 |
+
- 客户端真实 IP(从 X-Forwarded-For 获取)
|
| 35 |
+
- 远程地址(可能是代理服务器)
|
| 36 |
+
- 请求信息
|
| 37 |
+
- 响应状态码和大小
|
| 38 |
+
- 处理时间
|
| 39 |
+
- User-Agent
|
| 40 |
+
|
| 41 |
+
## 🎯 IP 获取逻辑
|
| 42 |
+
|
| 43 |
+
应用会按以下优先级检查 HTTP 头部来获取真实 IP:
|
| 44 |
+
|
| 45 |
+
1. **X-Forwarded-For** - 最常见的代理头部
|
| 46 |
+
2. **X-Real-IP** - Nginx 等使用
|
| 47 |
+
3. **CF-Connecting-IP** - Cloudflare 使用
|
| 48 |
+
4. **True-Client-IP** - 某些 CDN 使用
|
| 49 |
+
5. **X-Client-IP** - 其他代理使用
|
| 50 |
+
6. **request.remote_addr** - 直连时的 IP
|
| 51 |
+
|
| 52 |
+
### 代码实现
|
| 53 |
+
|
| 54 |
+
```python
|
| 55 |
+
def get_client_ip():
|
| 56 |
+
"""获取客户端真实 IP 地址"""
|
| 57 |
+
headers_to_check = [
|
| 58 |
+
'X-Forwarded-For',
|
| 59 |
+
'X-Real-IP',
|
| 60 |
+
'CF-Connecting-IP',
|
| 61 |
+
'True-Client-IP',
|
| 62 |
+
'X-Client-IP',
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
for header in headers_to_check:
|
| 66 |
+
ip = request.headers.get(header)
|
| 67 |
+
if ip:
|
| 68 |
+
# X-Forwarded-For 可能包含多个 IP,取第一个
|
| 69 |
+
return ip.split(',')[0].strip()
|
| 70 |
+
|
| 71 |
+
return request.remote_addr or 'Unknown'
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## 📁 日志文件位置
|
| 75 |
+
|
| 76 |
+
### 本地开发环境
|
| 77 |
+
|
| 78 |
+
- **控制台输出**: 实时显示
|
| 79 |
+
- **文件日志**: `logs/app.log`(自动滚动,保留10个备份)
|
| 80 |
+
|
| 81 |
+
### Hugging Face Spaces
|
| 82 |
+
|
| 83 |
+
- **控制台输出**: Space 页面的 "Logs" 标签
|
| 84 |
+
- **文件日志**: 不持久化(容器重启后丢失)
|
| 85 |
+
|
| 86 |
+
## 🔍 查看日志
|
| 87 |
+
|
| 88 |
+
### 查看实时日志(本地)
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
# 启动应用(控制台输出)
|
| 92 |
+
python3.12 app.py
|
| 93 |
+
|
| 94 |
+
# 或使用 Gunicorn
|
| 95 |
+
gunicorn -c gunicorn_config.py app:app
|
| 96 |
+
|
| 97 |
+
# 实时监控日志文件
|
| 98 |
+
tail -f logs/app.log
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### 查看历史日志
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
# 查看最近 100 条日志
|
| 105 |
+
tail -n 100 logs/app.log
|
| 106 |
+
|
| 107 |
+
# 搜索特定 IP 的访问记录
|
| 108 |
+
grep "203.0.113.1" logs/app.log
|
| 109 |
+
|
| 110 |
+
# 统计各 IP 的访问次数
|
| 111 |
+
grep -oP '\[\K[0-9.]+(?=\])' logs/app.log | sort | uniq -c | sort -rn
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### Hugging Face Spaces
|
| 115 |
+
|
| 116 |
+
1. 进入你的 Space 页面
|
| 117 |
+
2. 点击 "Logs" 标签
|
| 118 |
+
3. 查看实时日志输出
|
| 119 |
+
4. 可以搜索特定 IP 或时间段
|
| 120 |
+
|
| 121 |
+
## 📊 日志分析示例
|
| 122 |
+
|
| 123 |
+
### 统计最活跃的 IP 地址
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
# 提取所有 IP 并统计
|
| 127 |
+
grep -oP '\[\K[0-9.]+(?=\])' logs/app.log | sort | uniq -c | sort -rn | head -10
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
输出示例:
|
| 131 |
+
```
|
| 132 |
+
245 203.0.113.1
|
| 133 |
+
156 198.51.100.1
|
| 134 |
+
89 192.0.2.1
|
| 135 |
+
45 203.0.113.50
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
### 查看特定 IP 的访问历史
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
grep "\[203.0.113.1\]" logs/app.log
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
输出示例:
|
| 145 |
+
```
|
| 146 |
+
[2025-10-16 18:58:23] INFO in app: [203.0.113.1] GET /api/health | UA: Mozilla/5.0
|
| 147 |
+
[2025-10-16 18:58:24] INFO in app: [203.0.113.1] GET /api/book/info | UA: Mozilla/5.0
|
| 148 |
+
[2025-10-16 18:58:25] INFO in app: [203.0.113.1] POST /api/search | UA: Mozilla/5.0
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
### 分析访问时间分布
|
| 152 |
+
|
| 153 |
+
```bash
|
| 154 |
+
# 按小时统计访问量
|
| 155 |
+
grep -oP '\[.*?\s+\K\d{2}' logs/app.log | sort | uniq -c
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### 统计不同浏览器的使用情况
|
| 159 |
+
|
| 160 |
+
```bash
|
| 161 |
+
grep "UA:" logs/app.log | grep -oP 'UA: \K[^/]+' | sort | uniq -c | sort -rn
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
## 🧪 测试 IP 记录
|
| 165 |
+
|
| 166 |
+
### 运行测试脚本
|
| 167 |
+
|
| 168 |
+
```bash
|
| 169 |
+
# 确保应用正在运行
|
| 170 |
+
python3.12 app.py
|
| 171 |
+
|
| 172 |
+
# 在另一个终端运行测试
|
| 173 |
+
python3.12 test_ip_logging.py
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
### 手动测试
|
| 177 |
+
|
| 178 |
+
```bash
|
| 179 |
+
# 普通请求
|
| 180 |
+
curl http://localhost:7860/api/health
|
| 181 |
+
|
| 182 |
+
# 模拟代理请求(带 X-Forwarded-For)
|
| 183 |
+
curl -H "X-Forwarded-For: 203.0.113.1" http://localhost:7860/api/health
|
| 184 |
+
|
| 185 |
+
# 查看日志中记录的 IP
|
| 186 |
+
tail logs/app.log
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
## 🔒 隐私和安全
|
| 190 |
+
|
| 191 |
+
### IP 地址处理
|
| 192 |
+
|
| 193 |
+
- IP 地址仅用于日志记录和调试
|
| 194 |
+
- 不会永久存储在数据库中
|
| 195 |
+
- 日志文件会自动滚动(最多保留10个备份)
|
| 196 |
+
|
| 197 |
+
### 隐私保护建议
|
| 198 |
+
|
| 199 |
+
如果需要遵守 GDPR 等隐私法规:
|
| 200 |
+
|
| 201 |
+
1. **匿名化 IP**: 可以只记录 IP 的前缀
|
| 202 |
+
```python
|
| 203 |
+
# 例如:203.0.113.1 -> 203.0.113.0
|
| 204 |
+
ip_parts = ip.split('.')
|
| 205 |
+
anonymized_ip = '.'.join(ip_parts[:3]) + '.0'
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
2. **定期清理日志**: 设置日志保留期限
|
| 209 |
+
```python
|
| 210 |
+
# 在 gunicorn_config.py 中
|
| 211 |
+
max_log_age_days = 30 # 保留30天
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
3. **限制日志访问**: 确保只有授权人员可以访问日志
|
| 215 |
+
|
| 216 |
+
## 📈 日志性能影响
|
| 217 |
+
|
| 218 |
+
### 性能考虑
|
| 219 |
+
|
| 220 |
+
- IP 获取:几乎无性能影响(只是读取 HTTP 头部)
|
| 221 |
+
- 日志写入:异步处理,不影响请求响应时间
|
| 222 |
+
- 文件大小:每个日志文件最大 10MB,自动滚动
|
| 223 |
+
|
| 224 |
+
### 优化建议
|
| 225 |
+
|
| 226 |
+
如果日志量很大:
|
| 227 |
+
|
| 228 |
+
1. **减少日志级别**(只记录重要信息)
|
| 229 |
+
```python
|
| 230 |
+
app.logger.setLevel(logging.WARNING) # 只记录警告和错误
|
| 231 |
+
```
|
| 232 |
+
|
| 233 |
+
2. **只记录 API 请求**(跳过静态资源)
|
| 234 |
+
```python
|
| 235 |
+
# 已在 before_request 中实现
|
| 236 |
+
if request.path.startswith('/api/'):
|
| 237 |
+
app.logger.info(...)
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
3. **使用日志聚合服务**(如 ELK、Datadog)
|
| 241 |
+
|
| 242 |
+
## 🛠️ 自定义日志格式
|
| 243 |
+
|
| 244 |
+
### 修改 Flask 日志格式
|
| 245 |
+
|
| 246 |
+
编辑 `app.py` 中的 `setup_logging()` 函数:
|
| 247 |
+
|
| 248 |
+
```python
|
| 249 |
+
file_handler.setFormatter(logging.Formatter(
|
| 250 |
+
'[%(asctime)s] [%(levelname)s] [IP:%(client_ip)s] %(message)s'
|
| 251 |
+
))
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
### 修改 Gunicorn 日志格式
|
| 255 |
+
|
| 256 |
+
编辑 `gunicorn_config.py` 中的 `access_log_format`:
|
| 257 |
+
|
| 258 |
+
```python
|
| 259 |
+
access_log_format = (
|
| 260 |
+
'%(h)s - %({X-Forwarded-For}i)s '
|
| 261 |
+
'[%(t)s] "%(r)s" %(s)s %(b)s '
|
| 262 |
+
'"%(f)s" "%(a)s"'
|
| 263 |
+
)
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
## 📚 相关文档
|
| 267 |
+
|
| 268 |
+
- [Flask 日志文档](https://flask.palletsprojects.com/en/latest/logging/)
|
| 269 |
+
- [Gunicorn 配置文档](https://docs.gunicorn.org/en/stable/settings.html)
|
| 270 |
+
- [HTTP 头部参考](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
|
| 271 |
+
|
| 272 |
+
## 🎉 总结
|
| 273 |
+
|
| 274 |
+
现在您的 Flask 应用已经可以:
|
| 275 |
+
|
| 276 |
+
✅ 记录客户端真实 IP 地址
|
| 277 |
+
✅ 支持多种代理环境(Hugging Face Spaces、Cloudflare 等)
|
| 278 |
+
✅ 提供详细的访问日志
|
| 279 |
+
✅ 支持日志分析和统计
|
| 280 |
+
|
| 281 |
+
查看日志的快速命令:
|
| 282 |
+
```bash
|
| 283 |
+
# 实时监控
|
| 284 |
+
tail -f logs/app.log
|
| 285 |
+
|
| 286 |
+
# 搜索特定 IP
|
| 287 |
+
grep "203.0.113.1" logs/app.log
|
| 288 |
+
|
| 289 |
+
# 统计访问量
|
| 290 |
+
grep -c "GET /api/" logs/app.log
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
---
|
| 294 |
+
|
| 295 |
+
**最后更新**: 2025-10-16
|
| 296 |
+
**相关文件**: `app.py`, `gunicorn_config.py`, `test_ip_logging.py`
|
| 297 |
+
|
app.py
CHANGED
|
@@ -70,6 +70,29 @@ OPENSEARCH_CONFIG = {
|
|
| 70 |
# 全局变量:存储学习数据
|
| 71 |
BOOK_DATA = None
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
def load_book_data():
|
| 74 |
"""加载书籍数据"""
|
| 75 |
global BOOK_DATA
|
|
@@ -294,9 +317,25 @@ def internal_error(error):
|
|
| 294 |
@app.before_request
|
| 295 |
def before_request():
|
| 296 |
"""请求前处理"""
|
| 297 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
if request.path.startswith('/api/'):
|
| 299 |
-
app.logger.info(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
@app.after_request
|
| 302 |
def after_request(response):
|
|
@@ -352,7 +391,7 @@ def main():
|
|
| 352 |
print("=" * 60)
|
| 353 |
print("🎉 应用已准备就绪!")
|
| 354 |
print("=" * 60)
|
| 355 |
-
|
| 356 |
try:
|
| 357 |
# 启动 Flask 应用
|
| 358 |
app.run(
|
|
|
|
| 70 |
# 全局变量:存储学习数据
|
| 71 |
BOOK_DATA = None
|
| 72 |
|
| 73 |
+
def get_client_ip():
|
| 74 |
+
"""
|
| 75 |
+
获取客户端真实 IP 地址
|
| 76 |
+
在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部
|
| 77 |
+
"""
|
| 78 |
+
# 按优先级检查各种代理头部
|
| 79 |
+
headers_to_check = [
|
| 80 |
+
'X-Forwarded-For',
|
| 81 |
+
'X-Real-IP',
|
| 82 |
+
'CF-Connecting-IP', # Cloudflare
|
| 83 |
+
'True-Client-IP',
|
| 84 |
+
'X-Client-IP',
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
for header in headers_to_check:
|
| 88 |
+
ip = request.headers.get(header)
|
| 89 |
+
if ip:
|
| 90 |
+
# X-Forwarded-For 可能包含多个 IP,取第一个
|
| 91 |
+
return ip.split(',')[0].strip()
|
| 92 |
+
|
| 93 |
+
# 如果没有代理头部,使用 remote_addr
|
| 94 |
+
return request.remote_addr or 'Unknown'
|
| 95 |
+
|
| 96 |
def load_book_data():
|
| 97 |
"""加载书籍数据"""
|
| 98 |
global BOOK_DATA
|
|
|
|
| 317 |
@app.before_request
|
| 318 |
def before_request():
|
| 319 |
"""请求前处理"""
|
| 320 |
+
# 获取客户端真实 IP
|
| 321 |
+
client_ip = get_client_ip()
|
| 322 |
+
|
| 323 |
+
# 记录所有请求信息(包括静态文件)
|
| 324 |
+
user_agent = request.headers.get('User-Agent', 'Unknown')[:100] # 限制长度
|
| 325 |
+
|
| 326 |
+
# API 请求记录详细信息
|
| 327 |
if request.path.startswith('/api/'):
|
| 328 |
+
app.logger.info(
|
| 329 |
+
f"[{client_ip}] {request.method} {request.path} "
|
| 330 |
+
f"| UA: {user_agent}"
|
| 331 |
+
)
|
| 332 |
+
# 静态资源只记录简要信息
|
| 333 |
+
elif app.debug:
|
| 334 |
+
app.logger.debug(f"[{client_ip}] {request.method} {request.path}")
|
| 335 |
+
|
| 336 |
+
# 将 IP 存储到 g 对象,方便在其他地方使用
|
| 337 |
+
from flask import g
|
| 338 |
+
g.client_ip = client_ip
|
| 339 |
|
| 340 |
@app.after_request
|
| 341 |
def after_request(response):
|
|
|
|
| 391 |
print("=" * 60)
|
| 392 |
print("🎉 应用已准备就绪!")
|
| 393 |
print("=" * 60)
|
| 394 |
+
|
| 395 |
try:
|
| 396 |
# 启动 Flask 应用
|
| 397 |
app.run(
|
gunicorn_config.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Gunicorn 配置文件
|
| 5 |
+
用于在生产环境(如 Hugging Face Spaces)中运行 Flask 应用
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import multiprocessing
|
| 10 |
+
|
| 11 |
+
# 服务器绑定
|
| 12 |
+
bind = f"0.0.0.0:{os.environ.get('PORT', 7860)}"
|
| 13 |
+
|
| 14 |
+
# 工作进程数(根据 CPU 核心数自动调整)
|
| 15 |
+
workers = int(os.environ.get('GUNICORN_WORKERS', multiprocessing.cpu_count() * 2 + 1))
|
| 16 |
+
# 如果资源受限,可以手动设置较小的值
|
| 17 |
+
workers = min(workers, 4)
|
| 18 |
+
|
| 19 |
+
# 工作进程类型
|
| 20 |
+
worker_class = 'sync'
|
| 21 |
+
|
| 22 |
+
# 每个工作进程的线程数
|
| 23 |
+
threads = 2
|
| 24 |
+
|
| 25 |
+
# 超时时间(秒)
|
| 26 |
+
timeout = 120
|
| 27 |
+
|
| 28 |
+
# 保持活动连接的时间
|
| 29 |
+
keepalive = 5
|
| 30 |
+
|
| 31 |
+
# 最大请求数(处理后重启进程,防止内存泄漏)
|
| 32 |
+
max_requests = 1000
|
| 33 |
+
max_requests_jitter = 50
|
| 34 |
+
|
| 35 |
+
# 日志配置
|
| 36 |
+
accesslog = '-' # 输出到 stdout
|
| 37 |
+
errorlog = '-' # 输出到 stderr
|
| 38 |
+
loglevel = 'info'
|
| 39 |
+
|
| 40 |
+
# 自定义访问日志格式 - 包含真实客户端 IP
|
| 41 |
+
# %(h)s - 远程地址
|
| 42 |
+
# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)
|
| 43 |
+
# %(t)s - 时间
|
| 44 |
+
# %(m)s - 请求方法
|
| 45 |
+
# %(U)s - URL 路径
|
| 46 |
+
# %(q)s - 查询字符串
|
| 47 |
+
# %(s)s - 状态码
|
| 48 |
+
# %(b)s - 响应大小
|
| 49 |
+
# %(D)s - 请求处理时间(微秒)
|
| 50 |
+
# %({User-Agent}i)s - User-Agent
|
| 51 |
+
|
| 52 |
+
access_log_format = (
|
| 53 |
+
'[%(t)s] '
|
| 54 |
+
'Client: %({X-Forwarded-For}i)s (Remote: %(h)s) '
|
| 55 |
+
'%(m)s %(U)s%(q)s '
|
| 56 |
+
'Status: %(s)s '
|
| 57 |
+
'Size: %(b)s bytes '
|
| 58 |
+
'Time: %(D)s μs '
|
| 59 |
+
'UA: "%({User-Agent}i)s"'
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# 进程名称前缀
|
| 63 |
+
proc_name = 'english_learning_app'
|
| 64 |
+
|
| 65 |
+
# 守护进程(通常设为 False,让容器管理进程)
|
| 66 |
+
daemon = False
|
| 67 |
+
|
| 68 |
+
# 预加载应用(减少内存占用)
|
| 69 |
+
preload_app = True
|
| 70 |
+
|
| 71 |
+
# 在请求处理前后执行的钩子
|
| 72 |
+
def on_starting(server):
|
| 73 |
+
"""服务器启动时执行"""
|
| 74 |
+
print("=" * 60)
|
| 75 |
+
print("🚀 Gunicorn 服务器启动")
|
| 76 |
+
print(f"📍 绑定地址: {bind}")
|
| 77 |
+
print(f"👷 工作进程数: {workers}")
|
| 78 |
+
print(f"🧵 每进程线程数: {threads}")
|
| 79 |
+
print(f"⏱️ 超时时间: {timeout}s")
|
| 80 |
+
print("=" * 60)
|
| 81 |
+
|
| 82 |
+
def worker_int(worker):
|
| 83 |
+
"""工作进程被中断时执行"""
|
| 84 |
+
print(f"⚠️ 工作进程 {worker.pid} 被中断")
|
| 85 |
+
|
| 86 |
+
def worker_abort(worker):
|
| 87 |
+
"""工作进程异常退出时执行"""
|
| 88 |
+
print(f"❌ 工作进程 {worker.pid} 异常退出")
|
| 89 |
+
|
| 90 |
+
def post_worker_init(worker):
|
| 91 |
+
"""工作进程初始化后执行"""
|
| 92 |
+
print(f"✅ 工作进程 {worker.pid} 初始化完成")
|
| 93 |
+
|
test_ip_logging.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
测试 IP 地址获取和日志记录功能
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
import requests
|
| 9 |
+
import json
|
| 10 |
+
|
| 11 |
+
def test_ip_logging():
|
| 12 |
+
"""测试 IP 地址记录"""
|
| 13 |
+
base_url = "http://localhost:7860"
|
| 14 |
+
|
| 15 |
+
print("=" * 60)
|
| 16 |
+
print("🧪 测试客户端 IP 记录功能")
|
| 17 |
+
print("=" * 60)
|
| 18 |
+
|
| 19 |
+
# 测试场景
|
| 20 |
+
test_cases = [
|
| 21 |
+
{
|
| 22 |
+
"name": "正常请求(无代理)",
|
| 23 |
+
"headers": {},
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"name": "通过代理请求(X-Forwarded-For)",
|
| 27 |
+
"headers": {
|
| 28 |
+
"X-Forwarded-For": "203.0.113.1, 198.51.100.1"
|
| 29 |
+
},
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"name": "通过代理请求(X-Real-IP)",
|
| 33 |
+
"headers": {
|
| 34 |
+
"X-Real-IP": "192.0.2.1"
|
| 35 |
+
},
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"name": "Cloudflare 代理",
|
| 39 |
+
"headers": {
|
| 40 |
+
"CF-Connecting-IP": "198.51.100.50"
|
| 41 |
+
},
|
| 42 |
+
},
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
for i, test_case in enumerate(test_cases, 1):
|
| 46 |
+
print(f"\n{i}. {test_case['name']}")
|
| 47 |
+
print("-" * 40)
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
response = requests.get(
|
| 51 |
+
f"{base_url}/api/health",
|
| 52 |
+
headers=test_case['headers'],
|
| 53 |
+
timeout=5
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
if response.status_code == 200:
|
| 57 |
+
data = response.json()
|
| 58 |
+
print(f" ✅ 状态: {response.status_code}")
|
| 59 |
+
print(f" 📝 响应: {json.dumps(data, ensure_ascii=False, indent=2)}")
|
| 60 |
+
print(f" 💡 提示: 查看服务器日志以确认记录的 IP 地址")
|
| 61 |
+
else:
|
| 62 |
+
print(f" ⚠️ 状态码: {response.status_code}")
|
| 63 |
+
|
| 64 |
+
except requests.exceptions.ConnectionError:
|
| 65 |
+
print(f" ❌ 连接失败 - 请确保应用正在运行")
|
| 66 |
+
print(f" 💡 启动命令: python3.12 app.py")
|
| 67 |
+
break
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f" ❌ 错误: {e}")
|
| 70 |
+
|
| 71 |
+
print("\n" + "=" * 60)
|
| 72 |
+
print("📊 测试完成")
|
| 73 |
+
print("\n💡 查看日志:")
|
| 74 |
+
print(" - 控制台输出")
|
| 75 |
+
print(" - logs/app.log 文件")
|
| 76 |
+
print("\n日志格式示例:")
|
| 77 |
+
print(" [203.0.113.1] GET /api/health | UA: python-requests/2.31.0")
|
| 78 |
+
print("=" * 60)
|
| 79 |
+
|
| 80 |
+
def show_example_logs():
|
| 81 |
+
"""显示日志示例"""
|
| 82 |
+
print("\n" + "=" * 60)
|
| 83 |
+
print("📝 日志输出示例")
|
| 84 |
+
print("=" * 60)
|
| 85 |
+
|
| 86 |
+
examples = [
|
| 87 |
+
"[2025-10-16 18:58:23] INFO in app: [203.0.113.1] GET /api/health | UA: Mozilla/5.0 (Windows NT 10.0)",
|
| 88 |
+
"[2025-10-16 18:58:24] INFO in app: [198.51.100.1] POST /api/search | UA: Mozilla/5.0 (iPhone; CPU iPhone OS)",
|
| 89 |
+
"[2025-10-16 18:58:25] INFO in app: [192.0.2.1] GET /api/book/info | UA: curl/7.68.0",
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
print("\nFlask 应用日志:")
|
| 93 |
+
for example in examples:
|
| 94 |
+
print(f" {example}")
|
| 95 |
+
|
| 96 |
+
print("\nGunicorn 访问日志:")
|
| 97 |
+
print(" [16/Oct/2025:18:58:23 +0000] Client: 203.0.113.1 (Remote: 172.17.0.1)")
|
| 98 |
+
print(" GET /api/health Status: 200 Size: 123 bytes Time: 5234 μs")
|
| 99 |
+
print("=" * 60)
|
| 100 |
+
|
| 101 |
+
if __name__ == '__main__':
|
| 102 |
+
# 显示日志示例
|
| 103 |
+
show_example_logs()
|
| 104 |
+
|
| 105 |
+
# 询问是否进行实际测试
|
| 106 |
+
print("\n是否进行实际测试? (需要应用正在运行)")
|
| 107 |
+
response = input("输入 'y' 继续,其他键跳过: ")
|
| 108 |
+
|
| 109 |
+
if response.lower() == 'y':
|
| 110 |
+
test_ip_logging()
|
| 111 |
+
else:
|
| 112 |
+
print("\n跳过实际测试")
|
| 113 |
+
print("\n💡 启动应用后可以运行:")
|
| 114 |
+
print(" python3.12 test_ip_logging.py")
|
| 115 |
+
|