logmachine 2.0.0__tar.gz → 2.2.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.
File without changes
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: logmachine
3
+ Version: 2.2.0
4
+ Summary: Collaborative, beautiful logging system for distributed developers
5
+ Author-email: Mugabo Gusenga <mugabo@bufferpunk.com>
6
+ Project-URL: Homepage, https://logmachine.bufferpunk.com
7
+ Project-URL: Documentation, https://github.com/logmachine/python
8
+ Project-URL: Source, https://github.com/logmachine/python
9
+ Project-URL: Tracker, https://github.com/logmachine/python/issues
10
+ Keywords: logging,devtools,collaborative,open source,cli,json,ansi
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.7
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # 🧠 LogMachine
20
+
21
+ > Collaborative, beautiful logging system for distributed developers
22
+
23
+ **logmachine** helps teams log smarter. It’s a fully pluggable logging system that supports colored output, JSON parsing, structured log forwarding via **HTTP or Socket.IO**, and log centralization — all from a standard Python logging interface.
24
+
25
+ ---
26
+
27
+ ## 🚀 Features
28
+
29
+ - 🔥 **Color-coded terminal logs** (DEBUG, INFO, WARNING, ERROR, SUCCESS)
30
+ - 📤 **Log forwarding** to a central HTTP or Socket.IO server
31
+ - 🪵 **Custom log levels** (add your own with `.new_level(...)`)
32
+ - 👥 **User identity tracking** for team-based logs
33
+ - 🧩 **Pluggable backends**: send logs to a central server or local files
34
+ - 📦 **Simple JSON output** for web dashboards or collectors
35
+ - 🧽 Strips ANSI escape codes from logs for clean parsing
36
+ - 🧠 Automatically resolves usernames and saves them in `~/.cl_username`
37
+
38
+ ---
39
+
40
+ ## ⚙️ Installation
41
+
42
+ ```bash
43
+ pip install logmachine
44
+ ```
45
+
46
+ ---
47
+
48
+ ## 🧰 Usage
49
+
50
+ ### Basic Setup
51
+
52
+ ```python
53
+ from logmachine import LogMachine
54
+
55
+ # Create a simple logger without central logging
56
+ # Providing a non-empty string initializes the logger with that name, else the root logger is used to collect every single log in the python process.
57
+ logger = LogMachine("myapp", debug_level=1)
58
+
59
+ logger.info("Hello, world!")
60
+ logger.error("An error occurred!")
61
+ logger.success("Operation completed successfully!")
62
+ logger.debug("Debugging information here.")
63
+ logger.warning("This is a warning message.")
64
+ ```
65
+
66
+ ### With Central Logging (HTTP or Socket.IO)
67
+
68
+ You can use the default logger with central logging pointing to "https://logmachine.bufferpunk.com"
69
+
70
+ ```python
71
+ from logmachine import default_logger
72
+ logger = default_logger()
73
+ logger.info("This log is sent to the LogMachine default central server!")
74
+ ```
75
+
76
+ This is the default central logging server for logmachine, and you can create your own room there for free.
77
+ To use your own central logging server, provide the configuration as shown below:
78
+
79
+ ```python
80
+ logger_config = {
81
+ "url": "https://logmachine.bufferpunk.com", # Base server URL
82
+ "room": "team_alpha", # Your organization or room
83
+ "endpoint": "/api/logs", # Optional, defaults to /api/logs
84
+ "headers": {"Authorization": "Bearer token"},
85
+ "socketio": True, # Set False to use HTTP
86
+ "socketio_path": "/api/socket.io/" # Optional
87
+ }
88
+ logger = LogMachine("with_central", debug_level=0, central=logger_config, socketio=True)
89
+ logger.success("Central logging is working!")
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 🎨 Log Format
95
+
96
+ Every log includes:
97
+
98
+ * ✅ Username (resolved automatically or via server)
99
+ * 📁 Module directory
100
+ * ⏱️ Timestamp
101
+ * 📦 Level (INFO, ERROR, etc.)
102
+ * 📝 Message
103
+
104
+ Sample (terminal):
105
+
106
+ ```
107
+ (username @ myapp) 🤌 CL Timing: [ 2025-08-04T11:23:52 ]
108
+ [ INFO ] Server started on port 8000
109
+ 🏁
110
+ ```
111
+
112
+ ---
113
+
114
+ ## 🛠️ Advanced
115
+
116
+ ### Add Your Own Log Level
117
+
118
+ ```python
119
+ logger.new_level("CRITICAL_HACK", 60)
120
+ logger.new_level("CRITICAL_HACK", 60, color="\033[38;5;13m") # Optional color... does your girlfriend love pink? Maybe you should be in a relationship with your terminal.
121
+ logger.critical_hack("Zero day found!")
122
+ ```
123
+
124
+ ---
125
+
126
+ ## 📤 Parse & Export
127
+
128
+ ### Convert Logs to JSON
129
+
130
+ This is useful for sending logs to web dashboards or log collectors that expect JSON.
131
+ It reads the your log files, parses the log entries, and outputs them as JSON objects.
132
+
133
+ ```python
134
+ json_logs = log.jsonifier()
135
+ for entry in json_logs:
136
+ print(entry)
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 📡 Central Server Compatibility
142
+
143
+ To use Socket.IO, your central server must support these events:
144
+
145
+ * `log`: Receives log payloads: `{ room: string, data: object }`
146
+ * `GET /api/get_username?base=localname`: Returns `{ "username": "..." }`
147
+
148
+ ---
149
+
150
+ ## 🤖 Environment Variables
151
+
152
+ * `CL_USERNAME`: Manually override detected username
153
+ * Automatically stored in `~/.cl_username` for persistent identity
154
+
155
+ ---
156
+
157
+ ## 🔐 Security
158
+
159
+ * HTTP headers (e.g. `Authorization`) can be injected
160
+ * Central log transmission is fully customizable
161
+
162
+ ---
163
+
164
+ ## 🔧 Configuration Summary
165
+
166
+ | Param | Type | Description |
167
+ | --------------- | ------ | -------------------------------------------------- |
168
+ | `url` | `str` | Central server base URL |
169
+ | `room` | `str` | Logical group or org name |
170
+ | `endpoint` | `str` | HTTP endpoint for POST logs (default: `/api/logs`) |
171
+ | `headers` | `dict` | Extra headers to send (e.g. auth token) |
172
+ | `socketio` | `bool` | Whether to use Socket.IO instead of HTTP |
173
+ | `socketio_path` | `str` | Path to socket.io on the server |
174
+
175
+ ---
176
+
177
+ ## 📄 License
178
+
179
+ MIT License
180
+
181
+ ---
182
+
183
+ ## 🙋‍♂️ Author
184
+
185
+ Mugabo Gusenga
186
+ [logmachine.bufferpunk.com](https://logmachine.bufferpunk.com)
187
+ [GitHub](https://github.com/Scion-Kin/logmachine)
188
+
189
+ ---
190
+
191
+ ## ❤️ Contribute
192
+
193
+ PRs and issues are welcome!
194
+ This tool is built for devs who want **beautiful logs with distributed brains**.
195
+ Let’s make debugging fun again.
@@ -0,0 +1,177 @@
1
+ # 🧠 LogMachine
2
+
3
+ > Collaborative, beautiful logging system for distributed developers
4
+
5
+ **logmachine** helps teams log smarter. It’s a fully pluggable logging system that supports colored output, JSON parsing, structured log forwarding via **HTTP or Socket.IO**, and log centralization — all from a standard Python logging interface.
6
+
7
+ ---
8
+
9
+ ## 🚀 Features
10
+
11
+ - 🔥 **Color-coded terminal logs** (DEBUG, INFO, WARNING, ERROR, SUCCESS)
12
+ - 📤 **Log forwarding** to a central HTTP or Socket.IO server
13
+ - 🪵 **Custom log levels** (add your own with `.new_level(...)`)
14
+ - 👥 **User identity tracking** for team-based logs
15
+ - 🧩 **Pluggable backends**: send logs to a central server or local files
16
+ - 📦 **Simple JSON output** for web dashboards or collectors
17
+ - 🧽 Strips ANSI escape codes from logs for clean parsing
18
+ - 🧠 Automatically resolves usernames and saves them in `~/.cl_username`
19
+
20
+ ---
21
+
22
+ ## ⚙️ Installation
23
+
24
+ ```bash
25
+ pip install logmachine
26
+ ```
27
+
28
+ ---
29
+
30
+ ## 🧰 Usage
31
+
32
+ ### Basic Setup
33
+
34
+ ```python
35
+ from logmachine import LogMachine
36
+
37
+ # Create a simple logger without central logging
38
+ # Providing a non-empty string initializes the logger with that name, else the root logger is used to collect every single log in the python process.
39
+ logger = LogMachine("myapp", debug_level=1)
40
+
41
+ logger.info("Hello, world!")
42
+ logger.error("An error occurred!")
43
+ logger.success("Operation completed successfully!")
44
+ logger.debug("Debugging information here.")
45
+ logger.warning("This is a warning message.")
46
+ ```
47
+
48
+ ### With Central Logging (HTTP or Socket.IO)
49
+
50
+ You can use the default logger with central logging pointing to "https://logmachine.bufferpunk.com"
51
+
52
+ ```python
53
+ from logmachine import default_logger
54
+ logger = default_logger()
55
+ logger.info("This log is sent to the LogMachine default central server!")
56
+ ```
57
+
58
+ This is the default central logging server for logmachine, and you can create your own room there for free.
59
+ To use your own central logging server, provide the configuration as shown below:
60
+
61
+ ```python
62
+ logger_config = {
63
+ "url": "https://logmachine.bufferpunk.com", # Base server URL
64
+ "room": "team_alpha", # Your organization or room
65
+ "endpoint": "/api/logs", # Optional, defaults to /api/logs
66
+ "headers": {"Authorization": "Bearer token"},
67
+ "socketio": True, # Set False to use HTTP
68
+ "socketio_path": "/api/socket.io/" # Optional
69
+ }
70
+ logger = LogMachine("with_central", debug_level=0, central=logger_config, socketio=True)
71
+ logger.success("Central logging is working!")
72
+ ```
73
+
74
+ ---
75
+
76
+ ## 🎨 Log Format
77
+
78
+ Every log includes:
79
+
80
+ * ✅ Username (resolved automatically or via server)
81
+ * 📁 Module directory
82
+ * ⏱️ Timestamp
83
+ * 📦 Level (INFO, ERROR, etc.)
84
+ * 📝 Message
85
+
86
+ Sample (terminal):
87
+
88
+ ```
89
+ (username @ myapp) 🤌 CL Timing: [ 2025-08-04T11:23:52 ]
90
+ [ INFO ] Server started on port 8000
91
+ 🏁
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 🛠️ Advanced
97
+
98
+ ### Add Your Own Log Level
99
+
100
+ ```python
101
+ logger.new_level("CRITICAL_HACK", 60)
102
+ logger.new_level("CRITICAL_HACK", 60, color="\033[38;5;13m") # Optional color... does your girlfriend love pink? Maybe you should be in a relationship with your terminal.
103
+ logger.critical_hack("Zero day found!")
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 📤 Parse & Export
109
+
110
+ ### Convert Logs to JSON
111
+
112
+ This is useful for sending logs to web dashboards or log collectors that expect JSON.
113
+ It reads the your log files, parses the log entries, and outputs them as JSON objects.
114
+
115
+ ```python
116
+ json_logs = log.jsonifier()
117
+ for entry in json_logs:
118
+ print(entry)
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 📡 Central Server Compatibility
124
+
125
+ To use Socket.IO, your central server must support these events:
126
+
127
+ * `log`: Receives log payloads: `{ room: string, data: object }`
128
+ * `GET /api/get_username?base=localname`: Returns `{ "username": "..." }`
129
+
130
+ ---
131
+
132
+ ## 🤖 Environment Variables
133
+
134
+ * `CL_USERNAME`: Manually override detected username
135
+ * Automatically stored in `~/.cl_username` for persistent identity
136
+
137
+ ---
138
+
139
+ ## 🔐 Security
140
+
141
+ * HTTP headers (e.g. `Authorization`) can be injected
142
+ * Central log transmission is fully customizable
143
+
144
+ ---
145
+
146
+ ## 🔧 Configuration Summary
147
+
148
+ | Param | Type | Description |
149
+ | --------------- | ------ | -------------------------------------------------- |
150
+ | `url` | `str` | Central server base URL |
151
+ | `room` | `str` | Logical group or org name |
152
+ | `endpoint` | `str` | HTTP endpoint for POST logs (default: `/api/logs`) |
153
+ | `headers` | `dict` | Extra headers to send (e.g. auth token) |
154
+ | `socketio` | `bool` | Whether to use Socket.IO instead of HTTP |
155
+ | `socketio_path` | `str` | Path to socket.io on the server |
156
+
157
+ ---
158
+
159
+ ## 📄 License
160
+
161
+ MIT License
162
+
163
+ ---
164
+
165
+ ## 🙋‍♂️ Author
166
+
167
+ Mugabo Gusenga
168
+ [logmachine.bufferpunk.com](https://logmachine.bufferpunk.com)
169
+ [GitHub](https://github.com/Scion-Kin/logmachine)
170
+
171
+ ---
172
+
173
+ ## ❤️ Contribute
174
+
175
+ PRs and issues are welcome!
176
+ This tool is built for devs who want **beautiful logs with distributed brains**.
177
+ Let’s make debugging fun again.
@@ -0,0 +1 @@
1
+ from .main import LogMachine, default_logger
@@ -1,9 +1,13 @@
1
+ import atexit
1
2
  import os
2
3
  import re
3
4
  import json
4
5
  import logging
5
6
  import requests
6
7
  import socketio
8
+ import queue
9
+ from logging.handlers import QueueHandler, QueueListener
10
+
7
11
 
8
12
  def get_login():
9
13
  """
@@ -11,12 +15,17 @@ def get_login():
11
15
  :return: The login name of the current user.
12
16
  """
13
17
  try:
14
- return os.getlogin()
18
+ if not os.path.exists(os.path.expanduser("~/.cl_username")):
19
+ return os.getlogin()
20
+ else:
21
+ with open(os.path.expanduser("~/.cl_username"), 'r') as f:
22
+ os.environ['CL_USERNAME'] = f.read().strip()
23
+ return os.environ['CL_USERNAME']
15
24
  except Exception:
16
25
  return os.environ.get('USER', 'unknown')
17
26
 
18
27
 
19
- class RequestsTransporter(logging.StreamHandler):
28
+ class HTTPTransporter(logging.StreamHandler):
20
29
  """
21
30
  A class to handle the transport of log messages using HTTP requests.
22
31
  This class sends log messages to a central server via HTTP POST requests.
@@ -36,22 +45,19 @@ class RequestsTransporter(logging.StreamHandler):
36
45
  try:
37
46
  msg = self.format(record)
38
47
  super().emit(record)
39
- if self.central:
40
- if not self.central.get('room'):
41
- raise ValueError("""
42
- Central configuration must include 'room' for log transport.
43
- Example: {'url': 'http://central-server/api/logs', 'room': 'my_organization_name'}
44
- """)
45
-
46
- log_data = self.parse_log(msg)
47
- if log_data:
48
- response = requests.post(
49
- f"{self.central.get('url', '') + self.central.get('endpoint', '/api/logs')}?room={self.central.get('room', '')}",
50
- json=log_data,
51
- headers={"Content-Type": "application/json", **self.central.get('headers', {})}
52
- )
53
- if response.status_code != 200:
54
- raise Exception(f"Failed to send log to central: {response.text}")
48
+ if not self.central or not self.central.get('room'):
49
+ raise ValueError("""Central configuration must include 'room' for log transport.
50
+ Example: {'url': 'http://central-server/api/logs', 'room': 'my_organization_name'}""")
51
+
52
+ log_data = self.parse_log(msg)
53
+ if log_data:
54
+ response = requests.post(
55
+ f"{self.central.get('url', '') + self.central.get('endpoint', '/api/logs')}?room={self.central.get('room', '')}",
56
+ json=log_data,
57
+ headers={"Content-Type": "application/json", **self.central.get('headers', {})}
58
+ )
59
+ if response.status_code != 200:
60
+ raise Exception(f"Failed to send log to central: {response.text}")
55
61
 
56
62
  except Exception:
57
63
  self.handleError(record)
@@ -68,7 +74,7 @@ class SocketIOTransporter(logging.StreamHandler):
68
74
  self.central = kwargs.get('central', None)
69
75
  self.sio = socketio.Client()
70
76
  if self.central:
71
- self.sio.connect(self.central.get('url', ''), headers=self.central.get('headers', {}), socketio_path=self.central.get('socketio_path', '/api/socket.io'))
77
+ self.sio.connect(self.central.get('url', ''), headers=self.central.get('headers', {}), socketio_path=self.central.get('socketio_path', '/api/socket.io/'))
72
78
 
73
79
  def emit(self, record):
74
80
  try:
@@ -90,57 +96,67 @@ class SocketIOTransporter(logging.StreamHandler):
90
96
 
91
97
 
92
98
  class CustomFormatter(logging.Formatter):
93
- COLORS = {
94
- 'DEBUG': '\x1b[36m',
95
- 'INFO': '\x1b[34m',
96
- 'WARNING': '\x1b[33m',
97
- 'ERROR': '\x1b[31m',
98
- 'SUCCESS': '\x1b[32m'
99
- }
100
- RESET = '\x1b[0m'
101
- BOLD = '\x1b[1m'
102
- LEVEL_FORMATS = {
103
- 'DEBUG': BOLD + '[ DEBUG ]' + RESET,
104
- 'INFO': BOLD + '[ INFO ]' + RESET,
105
- 'WARNING': BOLD + '[ WARNING ]' + RESET,
106
- 'ERROR': BOLD + '[ ERROR ]' + RESET,
107
- 'SUCCESS': BOLD + '[ SUCCESS ]' + RESET
108
- }
99
+ def __init__(self, *args, **kwargs):
100
+ super().__init__(*args, **kwargs)
101
+ self.colors = {
102
+ 'DEBUG': '\x1b[36m',
103
+ 'INFO': '\x1b[34m',
104
+ 'WARNING': '\x1b[33m',
105
+ 'ERROR': '\x1b[31m',
106
+ 'SUCCESS': '\x1b[32m'
107
+ }
108
+ self.reset = '\x1b[0m'
109
+ self.bold = '\x1b[1m'
110
+ self.level_formats = {
111
+ 'DEBUG': self.bold + '[ DEBUG ]' + self.reset,
112
+ 'INFO': self.bold + '[ INFO ]' + self.reset,
113
+ 'WARNING': self.bold + '[ WARNING ]' + self.reset,
114
+ 'ERROR': self.bold + '[ ERROR ]' + self.reset,
115
+ 'SUCCESS': self.bold + '[ SUCCESS ]' + self.reset
116
+ }
117
+
118
+ def set_color(self, levelname: str, color_code: str):
119
+ """
120
+ Set a custom color for a specific log level.
121
+ :param levelname: The name of the log level (e.g., 'DEBUG', 'INFO').
122
+ :param color_code: The ANSI color code to use for the specified log level.
123
+ """
124
+ self.colors[levelname] = color_code
125
+ self.level_formats[levelname] = f"{self.bold}[ {levelname} ]{self.reset}"
109
126
 
110
127
  def format(self, record) -> str:
111
128
  username = os.environ.get('CL_USERNAME') or get_login()
112
129
 
113
130
  levelname = record.levelname
114
- color = self.COLORS.get(levelname, '')
115
- level_fmt = self.LEVEL_FORMATS.get(levelname, f'{levelname}')
116
- level_fmt = f"{color}{level_fmt}{self.RESET}"
131
+ color = self.colors.get(levelname, '')
132
+ level_fmt = self.level_formats.get(levelname, f'{levelname}')
133
+ level_fmt = f"{color}{level_fmt}{self.reset}"
117
134
  record.asctime = self.formatTime(record, self.datefmt)
118
135
  module_file = record.pathname
119
136
  parent_dir = os.path.basename(os.path.dirname(record.pathname)) if module_file != '<stdin>' else 'stdin'
120
137
 
121
- return f"""{self.COLORS.get('DEBUG')}({username}{self.RESET} @ {self.COLORS.get('WARNING') + parent_dir + self.RESET}) 🤌 CL Timing: {color}[ {record.asctime} ]{self.RESET}
138
+ return f"""{self.colors.get('DEBUG')}({username}{self.reset} @ {self.colors.get('WARNING') + parent_dir + self.reset}) 🤌 CL Timing: {color}[ {record.asctime} ]{self.reset}
122
139
  {level_fmt} {record.getMessage()}
123
140
  🏁"""
124
141
 
125
- class ContribLog(logging.Logger):
126
- SUCCESS = 25
127
-
128
- def __init__(self, *args, **kwargs) -> None:
129
- super().__init__(*args, level=logging.DEBUG)
130
- logging.addLevelName(self.SUCCESS, "SUCCESS")
142
+ class LogMachine(logging.Logger):
143
+ def __init__(self, name="", *args, **kwargs) -> None:
144
+ super().__init__(name, *args, level=int(kwargs.get('debug_level', 0)))
145
+ logging.addLevelName(25, "SUCCESS")
131
146
  self.log_file = kwargs.get('log_file', 'logs.log')
132
147
  self.error_file = kwargs.get('error_file', 'errors.log')
133
148
  self.debug_level = int(kwargs.get('debug_level', 0))
134
149
  self.verbose = kwargs.get('verbose', False)
135
150
  self.central = kwargs.get('central', None)
136
- self.setLevel(logging.DEBUG)
151
+ self.queue = queue.Queue()
137
152
 
138
153
  # Remove existing handlers
139
- self.handlers = []
154
+ for h in self.handlers[:]:
155
+ self.removeHandler(h)
140
156
 
141
157
  # File handlers
142
158
  fh = logging.FileHandler(self.log_file)
143
- fh.setLevel(logging.DEBUG)
159
+ fh.setLevel(self.debug_level)
144
160
  eh = logging.FileHandler(self.error_file)
145
161
  eh.setLevel(logging.ERROR)
146
162
 
@@ -148,7 +164,7 @@ class ContribLog(logging.Logger):
148
164
  if self.central:
149
165
  """
150
166
  The central uses usernames to group logs.
151
- OS usernames are used to identify the use, meaning names can clash.
167
+ OS usernames are used to identify the user, meaning names can clash.
152
168
  Therefore, we avoid a user having to define a username, rather, ask the central server to provide it.
153
169
  After getting the username, we store it in the user's home directory in a file named `.cl_username`.
154
170
  This way, the user can change it at any time, and it will be used in all future logs without needing to request it again.
@@ -158,42 +174,37 @@ class ContribLog(logging.Logger):
158
174
  login = get_login()
159
175
  response = requests.get(f"{self.central.get('url', '')}/api/get_username?base={login}")
160
176
  if response.status_code == 200:
161
- os.environ['CL_USERNAME'] = response.json().get('username', 'unknown') # Unknown will probably never be reached, but it's a fallback.
177
+ os.environ['CL_USERNAME'] = response.json().get('username') or 'unknown' # Unknown will probably never be reached, but it's a fallback.
162
178
  if os.environ.get('CL_USERNAME') != 'unknown':
163
179
  with open(os.path.expanduser("~/.cl_username"), 'w') as f:
164
180
  f.write(os.environ['CL_USERNAME'])
165
181
  else:
166
182
  os.environ['CL_USERNAME'] = 'unknown'
167
- except Exception as e:
183
+ except Exception:
168
184
  os.environ['CL_USERNAME'] = 'unknown'
169
185
  else:
170
- with open(os.path.expanduser("~/.cl_username"), 'r') as f:
171
- os.environ['CL_USERNAME'] = f.read().strip()
186
+ get_login()
172
187
 
173
- if not kwargs.get('attached', False):
174
- ch = RequestsTransporter(log_parser=self.parse_log, central=self.central)
188
+ if not kwargs.get('attached', False) and not self.central.get('socketio', False):
189
+ ch = HTTPTransporter(log_parser=self.parse_log, central=self.central)
175
190
  else:
176
191
  ch = SocketIOTransporter(log_parser=self.parse_log, central=self.central)
177
-
178
192
  else:
179
193
  ch = logging.StreamHandler()
180
194
 
181
195
  ch.setLevel(logging.DEBUG)
182
196
 
183
- formatter = CustomFormatter('%(asctime)s %(levelname)s %(message)s', datefmt='%Y-%m-%dT%H:%M:%S%z')
184
- fh.setFormatter(formatter)
185
- eh.setFormatter(formatter)
186
- ch.setFormatter(formatter)
187
-
188
- self.addHandler(fh)
189
- self.addHandler(eh)
190
- self.addHandler(ch)
197
+ self.formatter = CustomFormatter('%(asctime)s %(levelname)s %(message)s', datefmt='%Y-%m-%dT%H:%M:%S%z')
198
+ fh.setFormatter(self.formatter)
199
+ eh.setFormatter(self.formatter)
200
+ ch.setFormatter(self.formatter)
201
+ self.addHandler(QueueHandler(self.queue))
191
202
 
192
203
  # Filter console output based on debug_level
193
204
  class DebugLevelFilter(logging.Filter):
194
205
  def __init__(self, debug_level):
195
206
  super().__init__()
196
- self.debug_level = int(debug_level)
207
+ self.debug_level = debug_level
197
208
 
198
209
  def filter(self, record):
199
210
  if self.debug_level == 0:
@@ -212,12 +223,22 @@ class ContribLog(logging.Logger):
212
223
  return record.levelname in allowed
213
224
 
214
225
  ch.addFilter(DebugLevelFilter(self.debug_level if not self.verbose else 0))
226
+ self.listener = QueueListener(self.queue, fh, eh, ch)
227
+ self.listener.start()
228
+ atexit.register(self.listener.stop)
229
+ self.info("LogMachine initialized with debug level {} with{}".format(
230
+ self.debug_level,
231
+ self.central and
232
+ f" central logging to {self.central.get('url', '')}" or
233
+ "out central logging"
234
+ )
235
+ )
215
236
 
216
237
  def success(self, msg, *args, **kwargs) -> None:
217
- if self.isEnabledFor(self.SUCCESS):
218
- self._log(self.SUCCESS, msg, args, stacklevel=2, **kwargs)
238
+ if self.isEnabledFor(25):
239
+ self._log(25, msg, args, stacklevel=2, **kwargs)
219
240
 
220
- def new_level(self, level_name: str, level_num: int):
241
+ def new_level(self, level_name: str, level_num: int, ansi_color="\x1b[37m") -> None:
221
242
  """
222
243
  Dynamically add a new logging level.
223
244
  :param level_name: Name of the new logging level.
@@ -228,8 +249,9 @@ class ContribLog(logging.Logger):
228
249
  logging.addLevelName(level_num, level_name)
229
250
  setattr(self, level_name.lower(), lambda msg, *args, **kwargs: self._log(level_num, msg, args, stacklevel=2, **kwargs))
230
251
  self.setLevel(min(self.level, level_num)) # Ensure the logger's level is set appropriately
252
+ self.formatter.set_color(level_name, ansi_color) # Add color formatting for the new level
231
253
 
232
- def parse_log(self, log_text) -> dict:
254
+ def parse_log(self, log_text) -> dict | None:
233
255
  log_text = log_text.strip()
234
256
  ansi_escape = re.compile(r'\x1b\[[0-9;]*m')
235
257
  end_escape = re.compile(r'🏁')
@@ -240,7 +262,7 @@ class ContribLog(logging.Logger):
240
262
  header_match = re.search(header_pattern, clean)
241
263
 
242
264
  if not header_match:
243
- return None
265
+ return
244
266
 
245
267
  user, module, timestamp = header_match.groups()
246
268
  lines = clean.splitlines()
@@ -257,7 +279,7 @@ class ContribLog(logging.Logger):
257
279
  "timestamp": timestamp,
258
280
  "message": end_escape.sub('', message).strip()
259
281
  }
260
-
282
+
261
283
  def jsonifier(self) -> list:
262
284
  """
263
285
  Reads the log file and returns a list of JSON objects representing each log entry.
@@ -278,4 +300,12 @@ class ContribLog(logging.Logger):
278
300
  return log_entries
279
301
 
280
302
 
281
- logging.setLoggerClass(ContribLog)
303
+ def default_logger():
304
+ return LogMachine('default_logger', debug_level=0, verbose=False, central={
305
+ 'url': 'https://logmachine.bufferpunk.com',
306
+ 'room': f'{get_login()}_logs',
307
+ 'headers': {}
308
+ })
309
+
310
+
311
+ logging.setLoggerClass(LogMachine)
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: logmachine
3
+ Version: 2.2.0
4
+ Summary: Collaborative, beautiful logging system for distributed developers
5
+ Author-email: Mugabo Gusenga <mugabo@bufferpunk.com>
6
+ Project-URL: Homepage, https://logmachine.bufferpunk.com
7
+ Project-URL: Documentation, https://github.com/logmachine/python
8
+ Project-URL: Source, https://github.com/logmachine/python
9
+ Project-URL: Tracker, https://github.com/logmachine/python/issues
10
+ Keywords: logging,devtools,collaborative,open source,cli,json,ansi
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.7
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # 🧠 LogMachine
20
+
21
+ > Collaborative, beautiful logging system for distributed developers
22
+
23
+ **logmachine** helps teams log smarter. It’s a fully pluggable logging system that supports colored output, JSON parsing, structured log forwarding via **HTTP or Socket.IO**, and log centralization — all from a standard Python logging interface.
24
+
25
+ ---
26
+
27
+ ## 🚀 Features
28
+
29
+ - 🔥 **Color-coded terminal logs** (DEBUG, INFO, WARNING, ERROR, SUCCESS)
30
+ - 📤 **Log forwarding** to a central HTTP or Socket.IO server
31
+ - 🪵 **Custom log levels** (add your own with `.new_level(...)`)
32
+ - 👥 **User identity tracking** for team-based logs
33
+ - 🧩 **Pluggable backends**: send logs to a central server or local files
34
+ - 📦 **Simple JSON output** for web dashboards or collectors
35
+ - 🧽 Strips ANSI escape codes from logs for clean parsing
36
+ - 🧠 Automatically resolves usernames and saves them in `~/.cl_username`
37
+
38
+ ---
39
+
40
+ ## ⚙️ Installation
41
+
42
+ ```bash
43
+ pip install logmachine
44
+ ```
45
+
46
+ ---
47
+
48
+ ## 🧰 Usage
49
+
50
+ ### Basic Setup
51
+
52
+ ```python
53
+ from logmachine import LogMachine
54
+
55
+ # Create a simple logger without central logging
56
+ # Providing a non-empty string initializes the logger with that name, else the root logger is used to collect every single log in the python process.
57
+ logger = LogMachine("myapp", debug_level=1)
58
+
59
+ logger.info("Hello, world!")
60
+ logger.error("An error occurred!")
61
+ logger.success("Operation completed successfully!")
62
+ logger.debug("Debugging information here.")
63
+ logger.warning("This is a warning message.")
64
+ ```
65
+
66
+ ### With Central Logging (HTTP or Socket.IO)
67
+
68
+ You can use the default logger with central logging pointing to "https://logmachine.bufferpunk.com"
69
+
70
+ ```python
71
+ from logmachine import default_logger
72
+ logger = default_logger()
73
+ logger.info("This log is sent to the LogMachine default central server!")
74
+ ```
75
+
76
+ This is the default central logging server for logmachine, and you can create your own room there for free.
77
+ To use your own central logging server, provide the configuration as shown below:
78
+
79
+ ```python
80
+ logger_config = {
81
+ "url": "https://logmachine.bufferpunk.com", # Base server URL
82
+ "room": "team_alpha", # Your organization or room
83
+ "endpoint": "/api/logs", # Optional, defaults to /api/logs
84
+ "headers": {"Authorization": "Bearer token"},
85
+ "socketio": True, # Set False to use HTTP
86
+ "socketio_path": "/api/socket.io/" # Optional
87
+ }
88
+ logger = LogMachine("with_central", debug_level=0, central=logger_config, socketio=True)
89
+ logger.success("Central logging is working!")
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 🎨 Log Format
95
+
96
+ Every log includes:
97
+
98
+ * ✅ Username (resolved automatically or via server)
99
+ * 📁 Module directory
100
+ * ⏱️ Timestamp
101
+ * 📦 Level (INFO, ERROR, etc.)
102
+ * 📝 Message
103
+
104
+ Sample (terminal):
105
+
106
+ ```
107
+ (username @ myapp) 🤌 CL Timing: [ 2025-08-04T11:23:52 ]
108
+ [ INFO ] Server started on port 8000
109
+ 🏁
110
+ ```
111
+
112
+ ---
113
+
114
+ ## 🛠️ Advanced
115
+
116
+ ### Add Your Own Log Level
117
+
118
+ ```python
119
+ logger.new_level("CRITICAL_HACK", 60)
120
+ logger.new_level("CRITICAL_HACK", 60, color="\033[38;5;13m") # Optional color... does your girlfriend love pink? Maybe you should be in a relationship with your terminal.
121
+ logger.critical_hack("Zero day found!")
122
+ ```
123
+
124
+ ---
125
+
126
+ ## 📤 Parse & Export
127
+
128
+ ### Convert Logs to JSON
129
+
130
+ This is useful for sending logs to web dashboards or log collectors that expect JSON.
131
+ It reads the your log files, parses the log entries, and outputs them as JSON objects.
132
+
133
+ ```python
134
+ json_logs = log.jsonifier()
135
+ for entry in json_logs:
136
+ print(entry)
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 📡 Central Server Compatibility
142
+
143
+ To use Socket.IO, your central server must support these events:
144
+
145
+ * `log`: Receives log payloads: `{ room: string, data: object }`
146
+ * `GET /api/get_username?base=localname`: Returns `{ "username": "..." }`
147
+
148
+ ---
149
+
150
+ ## 🤖 Environment Variables
151
+
152
+ * `CL_USERNAME`: Manually override detected username
153
+ * Automatically stored in `~/.cl_username` for persistent identity
154
+
155
+ ---
156
+
157
+ ## 🔐 Security
158
+
159
+ * HTTP headers (e.g. `Authorization`) can be injected
160
+ * Central log transmission is fully customizable
161
+
162
+ ---
163
+
164
+ ## 🔧 Configuration Summary
165
+
166
+ | Param | Type | Description |
167
+ | --------------- | ------ | -------------------------------------------------- |
168
+ | `url` | `str` | Central server base URL |
169
+ | `room` | `str` | Logical group or org name |
170
+ | `endpoint` | `str` | HTTP endpoint for POST logs (default: `/api/logs`) |
171
+ | `headers` | `dict` | Extra headers to send (e.g. auth token) |
172
+ | `socketio` | `bool` | Whether to use Socket.IO instead of HTTP |
173
+ | `socketio_path` | `str` | Path to socket.io on the server |
174
+
175
+ ---
176
+
177
+ ## 📄 License
178
+
179
+ MIT License
180
+
181
+ ---
182
+
183
+ ## 🙋‍♂️ Author
184
+
185
+ Mugabo Gusenga
186
+ [logmachine.bufferpunk.com](https://logmachine.bufferpunk.com)
187
+ [GitHub](https://github.com/Scion-Kin/logmachine)
188
+
189
+ ---
190
+
191
+ ## ❤️ Contribute
192
+
193
+ PRs and issues are welcome!
194
+ This tool is built for devs who want **beautiful logs with distributed brains**.
195
+ Let’s make debugging fun again.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "logmachine"
3
- version = "2.0.0"
3
+ version = "2.2.0"
4
4
  description = "Collaborative, beautiful logging system for distributed developers"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.7"
@@ -16,11 +16,11 @@ classifiers = [
16
16
  ]
17
17
 
18
18
  [project.urls]
19
- Homepage = "https://github.com/Scion-Kin/logmachine"
20
- Documentation = "https://github.com/Scion-Kin/logmachine#readme"
21
- Source = "https://github.com/Scion-Kin/logmachine"
22
- Tracker = "https://github.com/Scion-Kin/logmachine/issues"
19
+ Homepage = "https://logmachine.bufferpunk.com"
20
+ Documentation = "https://github.com/logmachine/python"
21
+ Source = "https://github.com/logmachine/python"
22
+ Tracker = "https://github.com/logmachine/python/issues"
23
23
 
24
24
  [build-system]
25
- requires = ["setuptools>=61.0"]
25
+ requires = ["setuptools>=61.0", "websocket-client", "python-socketio", "requests"]
26
26
  build-backend = "setuptools.build_meta"
logmachine-2.0.0/PKG-INFO DELETED
@@ -1,17 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: logmachine
3
- Version: 2.0.0
4
- Summary: Collaborative, beautiful logging system for distributed developers
5
- Author-email: Mugabo Gusenga <mugabo@bufferpunk.com>
6
- Project-URL: Homepage, https://github.com/Scion-Kin/logmachine
7
- Project-URL: Documentation, https://github.com/Scion-Kin/logmachine#readme
8
- Project-URL: Source, https://github.com/Scion-Kin/logmachine
9
- Project-URL: Tracker, https://github.com/Scion-Kin/logmachine/issues
10
- Keywords: logging,devtools,collaborative,open source,cli,json,ansi
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Operating System :: OS Independent
14
- Requires-Python: >=3.7
15
- Description-Content-Type: text/markdown
16
- License-File: LICENSE
17
- Dynamic: license-file
File without changes
@@ -1 +0,0 @@
1
- from .main import ContribLog
@@ -1,17 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: logmachine
3
- Version: 2.0.0
4
- Summary: Collaborative, beautiful logging system for distributed developers
5
- Author-email: Mugabo Gusenga <mugabo@bufferpunk.com>
6
- Project-URL: Homepage, https://github.com/Scion-Kin/logmachine
7
- Project-URL: Documentation, https://github.com/Scion-Kin/logmachine#readme
8
- Project-URL: Source, https://github.com/Scion-Kin/logmachine
9
- Project-URL: Tracker, https://github.com/Scion-Kin/logmachine/issues
10
- Keywords: logging,devtools,collaborative,open source,cli,json,ansi
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Operating System :: OS Independent
14
- Requires-Python: >=3.7
15
- Description-Content-Type: text/markdown
16
- License-File: LICENSE
17
- Dynamic: license-file
File without changes