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 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\"{request.method} {request.path} - {request.remote_addr}\")\n@app.after_request\ndef after_request(response):\n \"\"\"请求后处理 - 添加缓存控制\"\"\"\n # API 端点不缓存\n if request.path.startswith('/api/'):",
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 load_book_data():\n \"\"\"加载书籍数据\"\"\"\n global BOOK_DATA",
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 load_book_data():\n \"\"\"加载书籍数据\"\"\"\n global BOOK_DATA\n try:\n with open('book_10242.json', 'r', encoding='utf-8') as f:\n BOOK_DATA = json.load(f)\n app.logger.info(f'✅ 成功加载书籍数据')\n return True\n except Exception as e:",
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
- CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:7860", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "app:app"]
 
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(f"{request.method} {request.path} - {request.remote_addr}")
 
 
 
 
 
 
 
 
 
 
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
+