simple-channel-log 1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Unnamed great master <gqylpy@outlook.com>.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.2
2
+ Name: simple-channel-log
3
+ Version: 1.0
4
+ Summary: 轻量高效的日志库,支持多级别日志记录、日志轮转、流水日志追踪及埋点日志功能,深度集成 Flask 和 Requests 框架。
5
+ Author: Unnamed great master
6
+ Author-email: <gqylpy@outlook.com>
7
+ License: MIT
8
+ Project-URL: Source, https://github.com/2018-11-27/simple-channel-log
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Natural Language :: Chinese (Simplified)
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Artistic Software
17
+ Classifier: Topic :: Internet :: Log Analysis
18
+ Classifier: Topic :: Text Processing
19
+ Classifier: Programming Language :: Python :: 2.7
20
+ Classifier: Programming Language :: Python :: 3.0
21
+ Classifier: Programming Language :: Python :: 3.1
22
+ Classifier: Programming Language :: Python :: 3.2
23
+ Classifier: Programming Language :: Python :: 3.3
24
+ Classifier: Programming Language :: Python :: 3.4
25
+ Classifier: Programming Language :: Python :: 3.5
26
+ Classifier: Programming Language :: Python :: 3.6
27
+ Classifier: Programming Language :: Python :: 3.7
28
+ Classifier: Programming Language :: Python :: 3.8
29
+ Classifier: Programming Language :: Python :: 3.9
30
+ Classifier: Programming Language :: Python :: 3.10
31
+ Classifier: Programming Language :: Python :: 3.11
32
+ Classifier: Programming Language :: Python :: 3.12
33
+ Classifier: Programming Language :: Python :: 3.13
34
+ Requires-Python: >=2.7
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: gqylpy_log==0.0a1
38
+ Provides-Extra: flask
39
+ Requires-Dist: Flask>=0.10; extra == "flask"
40
+ Provides-Extra: requests
41
+ Requires-Dist: requests>=2.19; extra == "requests"
42
+ Dynamic: author
43
+ Dynamic: author-email
44
+ Dynamic: classifier
45
+ Dynamic: description
46
+ Dynamic: description-content-type
47
+ Dynamic: license
48
+ Dynamic: project-url
49
+ Dynamic: provides-extra
50
+ Dynamic: requires-dist
51
+ Dynamic: requires-python
52
+ Dynamic: summary
53
+
54
+ # simple-channel-log
55
+
56
+ [![Release](https://img.shields.io/github/release/2018-11-27/simple-channel-log.svg?style=flat-square")](https://github.com/2018-11-27/simple-channel-log/releases/latest)
57
+ [![Python Version](https://img.shields.io/badge/python-2.7+/3.6+-blue.svg)](https://github.com/2018-11-27/simple-channel-log)
58
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
59
+ [![Downloads](https://pepy.tech/badge/simple-channel-log)](https://pepy.tech/project/simple-channel-log)
60
+
61
+ 轻量高效的日志库,支持多级别日志记录、日志轮转、流水日志追踪及埋点日志功能,深度集成 Flask 和 Requests 框架。
62
+
63
+ ## 主要特性
64
+
65
+ - 📅 支持按时间轮转日志(天/小时/分钟等)
66
+ - 📊 提供多级别日志记录(DEBUG/INFO/WARNING/ERROR/CRITICAL)
67
+ - 🌐 内置Flask中间件记录请求/响应流水日志(Journallog-in)
68
+ - 📡 集成Requests会话记录外部调用流水日志(Journallog-out)
69
+ - 🔍 智能处理长字符串截断(超过1000字符自动标记)
70
+ - 📁 自动创建分级日志目录结构
71
+ - 💻 支持终端输出与文件存储分离控制
72
+
73
+ ## 安装
74
+
75
+ ```bash
76
+ pip install simple_channel_log
77
+ ```
78
+
79
+ ## 快速入门
80
+
81
+ ### 基础配置
82
+
83
+ ```python
84
+ # coding:utf-8
85
+ import simple_channel_log as log
86
+
87
+ # 初始化日志配置
88
+ log.__init__("<your_appname>", "<your_syscode>", logdir="/app/logs")
89
+
90
+ # 初始化后可直接调用日志方法,日志将记录到参数 `logdir` 指定的目录中
91
+ log.debug("调试信息", extra_field="value")
92
+ log.info("业务日志", user_id=123)
93
+ log.warning("异常预警", error_code=500)
94
+ log.error("系统错误", stack_trace="...")
95
+
96
+ # 埋点日志
97
+ log.trace(
98
+ user_id=123,
99
+ action="purchase",
100
+ item_count=2
101
+ )
102
+ ```
103
+
104
+ ### Flask 流水日志
105
+
106
+ ```python
107
+ # coding:utf-8
108
+ import simple_channel_log as log
109
+
110
+ # 初始化日志配置
111
+ log.__init__(..., enable_journallog_in=True)
112
+ ```
113
+
114
+ 设置 `enable_journallog_in=True` 表示启用 Flask 流水日志,将自动记录每个接口的调用信息。
115
+
116
+ ### Requests 外部调用追踪
117
+
118
+ ```python
119
+ # coding:utf-8
120
+ import simple_channel_log as log
121
+
122
+ # 初始化日志配置
123
+ log.__init__(..., enable_journallog_out=True)
124
+ ```
125
+
126
+ 设置 `enable_journallog_out=True` 表示启用 Requests 外部调用追踪,将自动记录每个请求的调用信息。
127
+
128
+ ## 详细配置
129
+
130
+ ### 初始化参数
131
+
132
+ | 参数名 | 类型 | 默认值 | 说明 |
133
+ |-----------------------|------|----------|-------------------------------|
134
+ | appname | str | 必填 | 应用名称,建议与CI配置一致 |
135
+ | syscode | str | 必填 | 系统编码(大写) |
136
+ | logdir | str | 系统相关默认路径 | 日志存储根目录 |
137
+ | when | str | 'D' | 轮转周期:W(周)/D(天)/H(时)/M(分)/S(秒) |
138
+ | interval | int | 1 | 轮转频率 |
139
+ | backup_count | int | 7 | 历史日志保留数量(0=永久) |
140
+ | output_to_terminal | bool | False | 启用后日志将同时输出到控制台 |
141
+ | enable_journallog_in | bool | False | 启用Flask请求流水日志 |
142
+ | enable_journallog_out | bool | False | 启用Requests外部调用日志 |
@@ -0,0 +1,89 @@
1
+ # simple-channel-log
2
+
3
+ [![Release](https://img.shields.io/github/release/2018-11-27/simple-channel-log.svg?style=flat-square")](https://github.com/2018-11-27/simple-channel-log/releases/latest)
4
+ [![Python Version](https://img.shields.io/badge/python-2.7+/3.6+-blue.svg)](https://github.com/2018-11-27/simple-channel-log)
5
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
6
+ [![Downloads](https://pepy.tech/badge/simple-channel-log)](https://pepy.tech/project/simple-channel-log)
7
+
8
+ 轻量高效的日志库,支持多级别日志记录、日志轮转、流水日志追踪及埋点日志功能,深度集成 Flask 和 Requests 框架。
9
+
10
+ ## 主要特性
11
+
12
+ - 📅 支持按时间轮转日志(天/小时/分钟等)
13
+ - 📊 提供多级别日志记录(DEBUG/INFO/WARNING/ERROR/CRITICAL)
14
+ - 🌐 内置Flask中间件记录请求/响应流水日志(Journallog-in)
15
+ - 📡 集成Requests会话记录外部调用流水日志(Journallog-out)
16
+ - 🔍 智能处理长字符串截断(超过1000字符自动标记)
17
+ - 📁 自动创建分级日志目录结构
18
+ - 💻 支持终端输出与文件存储分离控制
19
+
20
+ ## 安装
21
+
22
+ ```bash
23
+ pip install simple_channel_log
24
+ ```
25
+
26
+ ## 快速入门
27
+
28
+ ### 基础配置
29
+
30
+ ```python
31
+ # coding:utf-8
32
+ import simple_channel_log as log
33
+
34
+ # 初始化日志配置
35
+ log.__init__("<your_appname>", "<your_syscode>", logdir="/app/logs")
36
+
37
+ # 初始化后可直接调用日志方法,日志将记录到参数 `logdir` 指定的目录中
38
+ log.debug("调试信息", extra_field="value")
39
+ log.info("业务日志", user_id=123)
40
+ log.warning("异常预警", error_code=500)
41
+ log.error("系统错误", stack_trace="...")
42
+
43
+ # 埋点日志
44
+ log.trace(
45
+ user_id=123,
46
+ action="purchase",
47
+ item_count=2
48
+ )
49
+ ```
50
+
51
+ ### Flask 流水日志
52
+
53
+ ```python
54
+ # coding:utf-8
55
+ import simple_channel_log as log
56
+
57
+ # 初始化日志配置
58
+ log.__init__(..., enable_journallog_in=True)
59
+ ```
60
+
61
+ 设置 `enable_journallog_in=True` 表示启用 Flask 流水日志,将自动记录每个接口的调用信息。
62
+
63
+ ### Requests 外部调用追踪
64
+
65
+ ```python
66
+ # coding:utf-8
67
+ import simple_channel_log as log
68
+
69
+ # 初始化日志配置
70
+ log.__init__(..., enable_journallog_out=True)
71
+ ```
72
+
73
+ 设置 `enable_journallog_out=True` 表示启用 Requests 外部调用追踪,将自动记录每个请求的调用信息。
74
+
75
+ ## 详细配置
76
+
77
+ ### 初始化参数
78
+
79
+ | 参数名 | 类型 | 默认值 | 说明 |
80
+ |-----------------------|------|----------|-------------------------------|
81
+ | appname | str | 必填 | 应用名称,建议与CI配置一致 |
82
+ | syscode | str | 必填 | 系统编码(大写) |
83
+ | logdir | str | 系统相关默认路径 | 日志存储根目录 |
84
+ | when | str | 'D' | 轮转周期:W(周)/D(天)/H(时)/M(分)/S(秒) |
85
+ | interval | int | 1 | 轮转频率 |
86
+ | backup_count | int | 7 | 历史日志保留数量(0=永久) |
87
+ | output_to_terminal | bool | False | 启用后日志将同时输出到控制台 |
88
+ | enable_journallog_in | bool | False | 启用Flask请求流水日志 |
89
+ | enable_journallog_out | bool | False | 启用Requests外部调用日志 |
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,58 @@
1
+ # coding:utf-8
2
+ import sys
3
+ import codecs
4
+ import setuptools
5
+
6
+ if sys.version_info.major < 3:
7
+ open = codecs.open
8
+
9
+ setuptools.setup(
10
+ name='simple-channel-log',
11
+ version='1.0',
12
+ author='Unnamed great master',
13
+ author_email='<gqylpy@outlook.com>',
14
+ license='MIT',
15
+ project_urls={
16
+ 'Source': 'https://github.com/2018-11-27/simple-channel-log'
17
+ },
18
+ description='''
19
+ 轻量高效的日志库,支持多级别日志记录、日志轮转、流水日志追踪及埋点日志功能,深度集成
20
+ Flask 和 Requests 框架。
21
+ '''.strip().replace('\n ', ''),
22
+ long_description=open('README.md', encoding='utf8').read(),
23
+ long_description_content_type='text/markdown',
24
+ packages=['simple_channel_log'],
25
+ python_requires='>=2.7',
26
+ install_requires=['gqylpy_log==0.0a1'],
27
+ extras_require={
28
+ 'flask': ['Flask>=0.10'],
29
+ 'requests': ['requests>=2.19']
30
+ },
31
+ classifiers=[
32
+ 'Development Status :: 3 - Alpha',
33
+ 'Intended Audience :: Developers',
34
+ 'License :: OSI Approved :: Apache Software License',
35
+ 'Natural Language :: Chinese (Simplified)',
36
+ 'Natural Language :: English',
37
+ 'Operating System :: OS Independent',
38
+ 'Topic :: Software Development :: Libraries :: Python Modules',
39
+ 'Topic :: Artistic Software',
40
+ 'Topic :: Internet :: Log Analysis',
41
+ 'Topic :: Text Processing',
42
+ 'Programming Language :: Python :: 2.7',
43
+ 'Programming Language :: Python :: 3.0',
44
+ 'Programming Language :: Python :: 3.1',
45
+ 'Programming Language :: Python :: 3.2',
46
+ 'Programming Language :: Python :: 3.3',
47
+ 'Programming Language :: Python :: 3.4',
48
+ 'Programming Language :: Python :: 3.5',
49
+ 'Programming Language :: Python :: 3.6',
50
+ 'Programming Language :: Python :: 3.7',
51
+ 'Programming Language :: Python :: 3.8',
52
+ 'Programming Language :: Python :: 3.9',
53
+ 'Programming Language :: Python :: 3.10',
54
+ 'Programming Language :: Python :: 3.11',
55
+ 'Programming Language :: Python :: 3.12',
56
+ 'Programming Language :: Python :: 3.13'
57
+ ]
58
+ )
@@ -0,0 +1,71 @@
1
+ # coding:utf-8
2
+
3
+
4
+ def __init__(
5
+ appname, # type: str
6
+ syscode, # type: str
7
+ logdir =None, # type: str
8
+ when =None, # type: str
9
+ interval =None, # type: int
10
+ backup_count =None, # type: int
11
+ output_to_terminal =None, # type: bool
12
+ enable_journallog_in =None, # type: bool
13
+ enable_journallog_out=None # type: bool
14
+ ):
15
+ """
16
+ 初始化日志配置。
17
+
18
+ @param appname:
19
+ 你的应用名称,通常与 GitLab CI 文件中的 APPNAME 字段一致。
20
+ @param syscode:
21
+ 你的应用分配的系统编码(大写),应与 GitLab CI 文件中的 SVCCODE 字段一致。
22
+ @param logdir:
23
+ 指定日志目录,默认为 "/app/logs/<syscode>_<appname>"(如果你的系统是 Windows
24
+ 则为 "C:\\BllLogs\\<syscode>_<appname>")。
25
+ @param when:
26
+ 控制日志轮转周期,默认为 "D"。支持按天/小时/分钟等单位滚动。可选值有:W:周, D:天,
27
+ H:小时, M:分钟, S:秒, 等等。
28
+ @param interval:
29
+ 日志轮转频率,默认为 1 。同参数 `when` 一起使用(如:`when="D"` 且
30
+ `interval=1` 表示每天滚动一次)。
31
+ @param backup_count:
32
+ 日志保留策略,控制最大历史版本数量,默认为 7。设为 0 则永久保留。
33
+ @param output_to_terminal:
34
+ 设为 True 日志将同时输出到终端,默认为 False。流水日志和埋点日志除外。
35
+ @param enable_journallog_in:
36
+ 设为 True 表示启用内部流水日志,默认为 False。目前仅支持 Flask 框架。
37
+ @param enable_journallog_out:
38
+ 设为 True 表示启用外部流水日志,默认为 False。目前仅支持 requests 框架。
39
+ """
40
+
41
+
42
+ def debug (msg, *args, **extra): pass
43
+ def info (msg, *args, **extra): pass
44
+ def warning (msg, *args, **extra): pass
45
+ def warn (msg, *args, **extra): pass
46
+ def error (msg, *args, **extra): pass
47
+ def exception(msg, *args, **extra): pass
48
+ def critical (msg, *args, **extra): pass
49
+ def fatal (msg, *args, **extra): pass
50
+
51
+ def trace(**extra): pass # 埋点日志
52
+
53
+
54
+ class _xe6_xad_x8c_xe7_x90_xaa_xe6_x80_xa1_xe7_x8e_xb2_xe8_x90_x8d_xe4_xba_x91:
55
+ import sys
56
+
57
+ ipath = __name__ + '.i ' + __name__
58
+ __import__(ipath)
59
+
60
+ ipack = sys.modules[__name__]
61
+ icode = globals()['i ' + __name__]
62
+
63
+ for iname in globals():
64
+ if iname[0] != '_':
65
+ ifunc = getattr(icode, iname, None)
66
+ if ifunc:
67
+ ifunc.__module__ = __package__
68
+ ifunc.__doc__ = getattr(ipack, iname).__doc__
69
+ setattr(ipack, iname, ifunc)
70
+
71
+ ipack.__init__ = icode.__init__
@@ -0,0 +1,555 @@
1
+ # coding:utf-8
2
+ import os
3
+ import re
4
+ import sys
5
+ import time
6
+ import uuid
7
+ import json as jsonx
8
+ import socket
9
+ import inspect
10
+ import warnings
11
+ import functools
12
+ import threading
13
+
14
+ from datetime import datetime
15
+
16
+ if os.path.basename(sys.argv[0]) != 'setup.py':
17
+ import gqylpy_log as glog
18
+
19
+ try:
20
+ from flask import Flask
21
+ except ImportError:
22
+ Flask = None
23
+ else:
24
+ from flask import g
25
+ from flask import request
26
+ from flask import current_app
27
+ from flask import has_request_context
28
+
29
+ def __new__(cls, *a, **kw):
30
+ app = super(Flask, cls).__new__(cls)
31
+ cls.__apps__.append(app)
32
+ return app
33
+
34
+ if sys.version_info.major < 3:
35
+ __new__ = staticmethod(__new__)
36
+
37
+ Flask.__apps__, Flask.__new__ = [], __new__
38
+
39
+ try:
40
+ import requests
41
+ except ImportError:
42
+ requests = None
43
+
44
+ if sys.version_info.major < 3:
45
+ from urlparse import urlparse
46
+ from urlparse import parse_qs
47
+ else:
48
+ from urllib.parse import urlparse
49
+ from urllib.parse import parse_qs
50
+
51
+ unicode = str
52
+
53
+ co_qualname = 'co_qualname' if sys.version_info >= (3, 11) else 'co_name'
54
+
55
+ that = sys.modules[__package__]
56
+ this = sys.modules[__name__]
57
+
58
+ unique = object()
59
+
60
+
61
+ def __init__(
62
+ appname,
63
+ syscode,
64
+ logdir=r'C:\BllLogs' if sys.platform == 'win32' else '/app/logs',
65
+ when='D',
66
+ interval=1,
67
+ backup_count=7,
68
+ stream=unique,
69
+ output_to_terminal=None,
70
+ enable_journallog_in=False,
71
+ enable_journallog_out=False
72
+ ):
73
+ if hasattr(this, 'appname'):
74
+ raise RuntimeError('repeat initialization.')
75
+
76
+ if re.match(r'[A-Za-z]\d{9}$', syscode) is None:
77
+ raise ValueError('parameter syscode "%s" is illegal' % syscode)
78
+
79
+ prefix = re.match(r'[a-zA-Z]\d{9}[_-]', appname)
80
+ if prefix is None:
81
+ appname = syscode.lower() + '_' + appname
82
+ elif prefix.group()[:-1].upper() != syscode.upper():
83
+ raise ValueError('parameter appname "%s" is illegal' % syscode)
84
+
85
+ if stream is not unique:
86
+ warnings.warn(
87
+ 'parameter "stream" will be deprecated soon, replaced to '
88
+ '"output_to_terminal".', category=DeprecationWarning,
89
+ stacklevel=2
90
+ )
91
+ if output_to_terminal is None:
92
+ output_to_terminal = stream
93
+
94
+ that.appname = this.appname = appname = \
95
+ appname[0].lower() + appname[1:10] + '_' + appname[11:]
96
+ that.syscode = this.syscode = syscode.upper()
97
+ this.output_to_terminal = output_to_terminal
98
+
99
+ handlers = [{
100
+ 'name': 'TimedRotatingFileHandler',
101
+ 'level': 'DEBUG',
102
+ 'filename': '%s/%s/debug/%s_code-debug.log' %
103
+ (logdir, appname, appname),
104
+ 'encoding': 'UTF-8',
105
+ 'when': when,
106
+ 'interval': interval,
107
+ 'backupCount': backup_count,
108
+ 'options': {'onlyRecordCurrentLevel': True}
109
+ }]
110
+
111
+ for level in 'info', 'warning', 'error', 'critical':
112
+ handlers.append({
113
+ 'name': 'TimedRotatingFileHandler',
114
+ 'level': level.upper(),
115
+ 'filename': '%s/%s/%s_code-%s.log' %
116
+ (logdir, appname, appname, level),
117
+ 'encoding': 'UTF-8',
118
+ 'when': when,
119
+ 'interval': interval,
120
+ 'backupCount': backup_count,
121
+ 'options': {'onlyRecordCurrentLevel': True}
122
+ })
123
+
124
+ glog.__init__('code', handlers=handlers, gname='code')
125
+
126
+ if output_to_terminal:
127
+ glog.__init__(
128
+ 'stream',
129
+ formatter={
130
+ 'fmt': '[%(asctime)s] [%(levelname)s] %(message)s',
131
+ 'datefmt': '%Y-%m-%d %H:%M:%S'
132
+ },
133
+ handlers=[{'name': 'StreamHandler'}],
134
+ gname='stream'
135
+ )
136
+
137
+ enable_journallog = False
138
+
139
+ if enable_journallog_in and Flask is not None:
140
+ enable_journallog = True
141
+ thread = threading.Thread(target=register_flask_middleware)
142
+ thread.daemon = True
143
+ thread.start()
144
+
145
+ if enable_journallog_out and requests is not None:
146
+ enable_journallog = True
147
+ requests.Session.request = journallog_out(requests.Session.request)
148
+
149
+ if enable_journallog:
150
+ glog.__init__(
151
+ 'info',
152
+ handlers=[{
153
+ 'name': 'TimedRotatingFileHandler',
154
+ 'level': 'INFO',
155
+ 'filename': '%s/%s/%s_info-info.log' %
156
+ (logdir, appname, appname),
157
+ 'encoding': 'UTF-8',
158
+ 'when': when,
159
+ 'interval': interval,
160
+ 'backupCount': backup_count,
161
+ }],
162
+ gname='info_'
163
+ )
164
+
165
+ glog.__init__(
166
+ 'trace',
167
+ handlers=[{
168
+ 'name': 'TimedRotatingFileHandler',
169
+ 'level': 'DEBUG',
170
+ 'filename': '%s/%s/trace/%s_trace-trace.log' %
171
+ (logdir, appname, appname),
172
+ 'encoding': 'UTF-8',
173
+ 'when': when,
174
+ 'interval': interval,
175
+ 'backupCount': backup_count,
176
+ }],
177
+ gname='trace'
178
+ )
179
+
180
+
181
+ def register_flask_middleware():
182
+ start = time.time()
183
+ while not Flask.__apps__ and time.time() - start < 300:
184
+ time.sleep(.02)
185
+
186
+ for app in Flask.__apps__:
187
+ app.before_request(journallog_in_before)
188
+ app.after_request(journallog_in)
189
+
190
+
191
+ def logger(msg, *args, **extra):
192
+ args = tuple(OmitLongString(v) for v in args)
193
+ extra = OmitLongString(extra)
194
+
195
+ if sys.version_info.major < 3 and isinstance(msg, str):
196
+ msg = msg.decode('utf8')
197
+
198
+ if isinstance(msg, unicode):
199
+ msg = (msg % args)[:1000]
200
+ elif isinstance(msg, (dict, list, tuple)):
201
+ msg = OmitLongString(msg)
202
+
203
+ transaction_id = g.transaction_id \
204
+ if Flask is not None and has_request_context() else uuid.uuid4().hex
205
+
206
+ f_back = inspect.currentframe().f_back
207
+ level = f_back.f_code.co_name
208
+ f_back = f_back.f_back
209
+
210
+ data = {
211
+ 'app_name': this.appname + '_code',
212
+ 'level': level.upper(),
213
+ 'log_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
214
+ 'logger': __package__,
215
+ 'thread': threading.current_thread().ident,
216
+ 'code_message': msg,
217
+ 'transaction_id': transaction_id,
218
+ 'method_code': None,
219
+ 'method_name': getattr(f_back.f_code, co_qualname),
220
+ 'error_code': None,
221
+ 'tag': None,
222
+ 'host_name': socket.gethostname(),
223
+ 'filename': f_back.f_code.co_filename,
224
+ 'line': f_back.f_lineno
225
+ }
226
+
227
+ for k, v in extra.items():
228
+ if data.get(k) is None:
229
+ data[k] = v
230
+
231
+ getattr(glog, level)(jsonx.dumps(data, ensure_ascii=False), gname='code')
232
+
233
+ if this.output_to_terminal:
234
+ getattr(glog, level)(msg, gname='stream')
235
+
236
+
237
+ def debug(msg, *args, **extra):
238
+ logger(msg, *args, **extra)
239
+
240
+
241
+ def info(msg, *args, **extra):
242
+ logger(msg, *args, **extra)
243
+
244
+
245
+ def warning(msg, *args, **extra):
246
+ logger(msg, *args, **extra)
247
+
248
+
249
+ warn = warning
250
+
251
+
252
+ def error(msg, *args, **extra):
253
+ logger(msg, *args, **extra)
254
+
255
+
256
+ exception = error
257
+
258
+
259
+ def critical(msg, *args, **extra):
260
+ logger(msg, *args, **extra)
261
+
262
+
263
+ fatal = critical
264
+
265
+
266
+ def trace(**extra):
267
+ extra = OmitLongString(extra)
268
+ extra.update({'app_name': this.appname + '_trace', 'level': 'TRACE'})
269
+ glog.debug(jsonx.dumps(extra, ensure_ascii=False), gname='trace')
270
+
271
+
272
+ def journallog_in_before():
273
+ if request.path == '/healthcheck':
274
+ return
275
+
276
+ g.request_time = datetime.now()
277
+
278
+ if request.args:
279
+ request_data = request.args.to_dict()
280
+ elif request.form:
281
+ request_data = request.form.to_dict()
282
+ else:
283
+ request_body = request.data
284
+ try:
285
+ request_data = jsonx.loads(request_body) if request_body else None
286
+ except ValueError:
287
+ request_data = None
288
+
289
+ g.request_data = request_data
290
+
291
+ g.transaction_id = (
292
+ FindTransactionID(dict(request.headers)).result or
293
+ FindTransactionID(request_data).result or
294
+ uuid.uuid4().hex
295
+ )
296
+
297
+
298
+ def journallog_in(response):
299
+ if request.path == '/healthcheck':
300
+ return response
301
+
302
+ parsed_url = urlparse(request.url)
303
+ address = parsed_url.scheme + '://' + parsed_url.netloc + parsed_url.path
304
+
305
+ view_func = current_app.view_functions.get(request.endpoint)
306
+ method_name = view_func.__name__ if view_func else None
307
+
308
+ try:
309
+ response_data = jsonx.loads(response.get_data())
310
+ except ValueError:
311
+ response_data = response_code = order_id = \
312
+ province_code = city_code = \
313
+ account_type = account_num = \
314
+ response_account_type = response_account_num = None
315
+ else:
316
+ if isinstance(response_data, dict):
317
+ head = response_data.get('head')
318
+ x = head if isinstance(head, dict) else response_data
319
+ response_code = x.get('code')
320
+ order_id = x.get('order_id')
321
+ province_code = x.get('province_code')
322
+ city_code = x.get('city_code')
323
+ account_type = x.get('account_type')
324
+ account_num = x.get('account_num')
325
+ response_account_type = x.get('response_account_type')
326
+ response_account_num = x.get('response_account_num')
327
+ else:
328
+ response_code = order_id = \
329
+ province_code = city_code = \
330
+ account_type = account_num = \
331
+ response_account_type = response_account_num = None
332
+
333
+ response_time = datetime.now()
334
+ response_time_str = response_time.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
335
+
336
+ total_time = round((response_time - g.request_time).total_seconds(), 3)
337
+
338
+ glog.info(jsonx.dumps({
339
+ 'app_name': this.appname + '_info',
340
+ 'level': 'INFO',
341
+ 'log_time': response_time_str,
342
+ 'logger': __package__,
343
+ 'thread': threading.current_thread().ident,
344
+ 'transaction_id': g.transaction_id,
345
+ 'dialog_type': 'in',
346
+ 'address': address,
347
+ 'fcode': request.headers.get('User-Agent'),
348
+ 'tcode': this.syscode,
349
+ 'method_code': None,
350
+ 'method_name': method_name,
351
+ 'http_method': request.method,
352
+ 'request_time': g.request_time.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
353
+ 'request_headers': dict(request.headers),
354
+ 'request_payload': OmitLongString(g.request_data),
355
+ 'response_time': response_time_str,
356
+ 'response_headers': dict(response.headers),
357
+ 'response_payload': OmitLongString(response_data),
358
+ 'response_code': response_code,
359
+ 'response_remark': None,
360
+ 'http_status_code': response.status_code,
361
+ 'order_id': order_id,
362
+ 'province_code': province_code,
363
+ 'city_code': city_code,
364
+ 'total_time': total_time,
365
+ 'error_code': response_code,
366
+ 'request_ip': request.remote_addr,
367
+ 'host_ip': parsed_url.hostname,
368
+ 'host_name': socket.gethostname(),
369
+ 'account_type': account_type,
370
+ 'account_num': account_num,
371
+ 'response_account_type': response_account_type,
372
+ 'response_account_num': response_account_num,
373
+ 'user': None,
374
+ 'tag': None,
375
+ 'service_line': None
376
+ }, ensure_ascii=False), gname='info_')
377
+
378
+ return response
379
+
380
+
381
+ def journallog_out(func):
382
+
383
+ @functools.wraps(func)
384
+ def inner(
385
+ self, method, url,
386
+ headers=None, params=None, data=None, json=None,
387
+ **kw
388
+ ):
389
+ if headers is None:
390
+ headers = {'User-Agent': this.syscode}
391
+ else:
392
+ headers['User-Agent'] = this.syscode
393
+
394
+ f_back = inspect.currentframe().f_back.f_back
395
+ if f_back.f_back is not None:
396
+ f_back = f_back.f_back
397
+ method_name = getattr(f_back.f_code, co_qualname)
398
+
399
+ parse_url = urlparse(url)
400
+ address = parse_url.scheme + '://' + parse_url.netloc + parse_url.path
401
+ query_string = {k: v[0] for k, v in parse_qs(parse_url.query).items()}
402
+
403
+ if params is not None:
404
+ params.update(query_string)
405
+ request_data = params
406
+ elif query_string:
407
+ request_data = query_string
408
+ elif data:
409
+ request_data = data
410
+ if isinstance(request_data, (str, unicode)):
411
+ try:
412
+ request_data = jsonx.loads(request_data)
413
+ except ValueError:
414
+ pass
415
+ elif json:
416
+ request_data = json
417
+ else:
418
+ request_data = None
419
+
420
+ if Flask is not None and has_request_context():
421
+ transaction_id = g.transaction_id
422
+ else:
423
+ transaction_id = (
424
+ FindTransactionID(headers).result or
425
+ FindTransactionID(request_data).result or
426
+ uuid.uuid4().hex
427
+ )
428
+
429
+ for name in headers.copy():
430
+ namex = name.replace('-', '').replace('_', '').lower()
431
+ if namex == 'transactionid':
432
+ del headers[name]
433
+ headers['Transaction-ID'] = transaction_id
434
+
435
+ request_time = datetime.now()
436
+ response = func(
437
+ self, method, url,
438
+ headers=headers, params=params, data=data, json=json,
439
+ **kw
440
+ )
441
+ response_time = datetime.now()
442
+ response_time_str = response_time.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
443
+
444
+ try:
445
+ response_data = response.json()
446
+ except ValueError:
447
+ response_data = response_code = order_id = \
448
+ province_code = city_code = \
449
+ account_type = account_num = \
450
+ response_account_type = response_account_num = None
451
+ else:
452
+ if isinstance(response_data, dict):
453
+ head = response_data.get('head')
454
+ x = head if isinstance(head, dict) else response_data
455
+ response_code = x.get('code')
456
+ order_id = x.get('order_id')
457
+ province_code = x.get('province_code')
458
+ city_code = x.get('city_code')
459
+ account_type = x.get('account_type')
460
+ account_num = x.get('account_num')
461
+ response_account_type = x.get('response_account_type')
462
+ response_account_num = x.get('response_account_num')
463
+ else:
464
+ response_code = order_id = \
465
+ province_code = city_code = \
466
+ account_type = account_num = \
467
+ response_account_type = response_account_num = None
468
+
469
+ total_time = round((response_time - request_time).total_seconds(), 3)
470
+
471
+ glog.info(jsonx.dumps({
472
+ 'app_name': this.appname + '_info',
473
+ 'level': 'INFO',
474
+ 'log_time': response_time_str,
475
+ 'logger': __package__,
476
+ 'thread': threading.current_thread().ident,
477
+ 'transaction_id': transaction_id,
478
+ 'dialog_type': 'out',
479
+ 'address': address,
480
+ 'fcode': this.syscode,
481
+ 'tcode': this.syscode,
482
+ 'method_code': None,
483
+ 'method_name': method_name,
484
+ 'http_method': response.request.method,
485
+ 'request_time': request_time.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
486
+ 'request_headers': dict(response.request.headers),
487
+ 'request_payload': OmitLongString(request_data),
488
+ 'response_time': response_time_str,
489
+ 'response_headers': dict(response.headers),
490
+ 'response_payload': OmitLongString(response_data),
491
+ 'response_code': response_code,
492
+ 'response_remark': None,
493
+ 'http_status_code': response.status_code,
494
+ 'order_id': order_id,
495
+ 'province_code': province_code,
496
+ 'city_code': city_code,
497
+ 'total_time': total_time,
498
+ 'error_code': response_code,
499
+ 'request_ip': parse_url.hostname,
500
+ 'host_ip': socket.gethostbyname(socket.gethostname()),
501
+ 'host_name': socket.gethostname(),
502
+ 'account_type': account_type,
503
+ 'account_num': account_num,
504
+ 'response_account_type': response_account_type,
505
+ 'response_account_num': response_account_num,
506
+ 'user': None,
507
+ 'tag': None,
508
+ 'service_line': None
509
+ }, ensure_ascii=False), gname='info_')
510
+
511
+ return response
512
+
513
+ return inner
514
+
515
+
516
+ class OmitLongString(dict):
517
+
518
+ def __init__(self, data):
519
+ for name, value in data.items():
520
+ dict.__setitem__(self, name, OmitLongString(value))
521
+
522
+ def __new__(cls, data):
523
+ if isinstance(data, dict):
524
+ return dict.__new__(cls)
525
+
526
+ if isinstance(data, (list, tuple)):
527
+ return data.__class__(cls(v) for v in data)
528
+
529
+ if sys.version_info.major < 3 and isinstance(data, str):
530
+ data = data.decode('utf8')
531
+
532
+ if isinstance(data, (unicode, str)) and len(data) > 1000:
533
+ data = '<Ellipsis>'
534
+
535
+ return data
536
+
537
+
538
+ class FindTransactionID(dict):
539
+ result = None
540
+
541
+ def __init__(self, data, root=None):
542
+ if root is None:
543
+ root = self
544
+ for k, v in data.items():
545
+ if k.replace('-', '').replace('_', '').lower() == 'transactionid':
546
+ root.result = data[k]
547
+ break
548
+ dict.__setitem__(self, k, FindTransactionID(v, root=root))
549
+
550
+ def __new__(cls, data, root=None):
551
+ if isinstance(data, dict):
552
+ return dict.__new__(cls)
553
+ if isinstance(data, (list, tuple)):
554
+ return data.__class__(cls(v) for v in data)
555
+ return cls
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.2
2
+ Name: simple-channel-log
3
+ Version: 1.0
4
+ Summary: 轻量高效的日志库,支持多级别日志记录、日志轮转、流水日志追踪及埋点日志功能,深度集成 Flask 和 Requests 框架。
5
+ Author: Unnamed great master
6
+ Author-email: <gqylpy@outlook.com>
7
+ License: MIT
8
+ Project-URL: Source, https://github.com/2018-11-27/simple-channel-log
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Natural Language :: Chinese (Simplified)
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Artistic Software
17
+ Classifier: Topic :: Internet :: Log Analysis
18
+ Classifier: Topic :: Text Processing
19
+ Classifier: Programming Language :: Python :: 2.7
20
+ Classifier: Programming Language :: Python :: 3.0
21
+ Classifier: Programming Language :: Python :: 3.1
22
+ Classifier: Programming Language :: Python :: 3.2
23
+ Classifier: Programming Language :: Python :: 3.3
24
+ Classifier: Programming Language :: Python :: 3.4
25
+ Classifier: Programming Language :: Python :: 3.5
26
+ Classifier: Programming Language :: Python :: 3.6
27
+ Classifier: Programming Language :: Python :: 3.7
28
+ Classifier: Programming Language :: Python :: 3.8
29
+ Classifier: Programming Language :: Python :: 3.9
30
+ Classifier: Programming Language :: Python :: 3.10
31
+ Classifier: Programming Language :: Python :: 3.11
32
+ Classifier: Programming Language :: Python :: 3.12
33
+ Classifier: Programming Language :: Python :: 3.13
34
+ Requires-Python: >=2.7
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: gqylpy_log==0.0a1
38
+ Provides-Extra: flask
39
+ Requires-Dist: Flask>=0.10; extra == "flask"
40
+ Provides-Extra: requests
41
+ Requires-Dist: requests>=2.19; extra == "requests"
42
+ Dynamic: author
43
+ Dynamic: author-email
44
+ Dynamic: classifier
45
+ Dynamic: description
46
+ Dynamic: description-content-type
47
+ Dynamic: license
48
+ Dynamic: project-url
49
+ Dynamic: provides-extra
50
+ Dynamic: requires-dist
51
+ Dynamic: requires-python
52
+ Dynamic: summary
53
+
54
+ # simple-channel-log
55
+
56
+ [![Release](https://img.shields.io/github/release/2018-11-27/simple-channel-log.svg?style=flat-square")](https://github.com/2018-11-27/simple-channel-log/releases/latest)
57
+ [![Python Version](https://img.shields.io/badge/python-2.7+/3.6+-blue.svg)](https://github.com/2018-11-27/simple-channel-log)
58
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
59
+ [![Downloads](https://pepy.tech/badge/simple-channel-log)](https://pepy.tech/project/simple-channel-log)
60
+
61
+ 轻量高效的日志库,支持多级别日志记录、日志轮转、流水日志追踪及埋点日志功能,深度集成 Flask 和 Requests 框架。
62
+
63
+ ## 主要特性
64
+
65
+ - 📅 支持按时间轮转日志(天/小时/分钟等)
66
+ - 📊 提供多级别日志记录(DEBUG/INFO/WARNING/ERROR/CRITICAL)
67
+ - 🌐 内置Flask中间件记录请求/响应流水日志(Journallog-in)
68
+ - 📡 集成Requests会话记录外部调用流水日志(Journallog-out)
69
+ - 🔍 智能处理长字符串截断(超过1000字符自动标记)
70
+ - 📁 自动创建分级日志目录结构
71
+ - 💻 支持终端输出与文件存储分离控制
72
+
73
+ ## 安装
74
+
75
+ ```bash
76
+ pip install simple_channel_log
77
+ ```
78
+
79
+ ## 快速入门
80
+
81
+ ### 基础配置
82
+
83
+ ```python
84
+ # coding:utf-8
85
+ import simple_channel_log as log
86
+
87
+ # 初始化日志配置
88
+ log.__init__("<your_appname>", "<your_syscode>", logdir="/app/logs")
89
+
90
+ # 初始化后可直接调用日志方法,日志将记录到参数 `logdir` 指定的目录中
91
+ log.debug("调试信息", extra_field="value")
92
+ log.info("业务日志", user_id=123)
93
+ log.warning("异常预警", error_code=500)
94
+ log.error("系统错误", stack_trace="...")
95
+
96
+ # 埋点日志
97
+ log.trace(
98
+ user_id=123,
99
+ action="purchase",
100
+ item_count=2
101
+ )
102
+ ```
103
+
104
+ ### Flask 流水日志
105
+
106
+ ```python
107
+ # coding:utf-8
108
+ import simple_channel_log as log
109
+
110
+ # 初始化日志配置
111
+ log.__init__(..., enable_journallog_in=True)
112
+ ```
113
+
114
+ 设置 `enable_journallog_in=True` 表示启用 Flask 流水日志,将自动记录每个接口的调用信息。
115
+
116
+ ### Requests 外部调用追踪
117
+
118
+ ```python
119
+ # coding:utf-8
120
+ import simple_channel_log as log
121
+
122
+ # 初始化日志配置
123
+ log.__init__(..., enable_journallog_out=True)
124
+ ```
125
+
126
+ 设置 `enable_journallog_out=True` 表示启用 Requests 外部调用追踪,将自动记录每个请求的调用信息。
127
+
128
+ ## 详细配置
129
+
130
+ ### 初始化参数
131
+
132
+ | 参数名 | 类型 | 默认值 | 说明 |
133
+ |-----------------------|------|----------|-------------------------------|
134
+ | appname | str | 必填 | 应用名称,建议与CI配置一致 |
135
+ | syscode | str | 必填 | 系统编码(大写) |
136
+ | logdir | str | 系统相关默认路径 | 日志存储根目录 |
137
+ | when | str | 'D' | 轮转周期:W(周)/D(天)/H(时)/M(分)/S(秒) |
138
+ | interval | int | 1 | 轮转频率 |
139
+ | backup_count | int | 7 | 历史日志保留数量(0=永久) |
140
+ | output_to_terminal | bool | False | 启用后日志将同时输出到控制台 |
141
+ | enable_journallog_in | bool | False | 启用Flask请求流水日志 |
142
+ | enable_journallog_out | bool | False | 启用Requests外部调用日志 |
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ simple_channel_log/__init__.py
5
+ simple_channel_log/i simple_channel_log.py
6
+ simple_channel_log.egg-info/PKG-INFO
7
+ simple_channel_log.egg-info/SOURCES.txt
8
+ simple_channel_log.egg-info/dependency_links.txt
9
+ simple_channel_log.egg-info/requires.txt
10
+ simple_channel_log.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ gqylpy_log==0.0a1
2
+
3
+ [flask]
4
+ Flask>=0.10
5
+
6
+ [requests]
7
+ requests>=2.19
@@ -0,0 +1 @@
1
+ simple_channel_log