secploy 0.2.6__tar.gz → 0.2.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secploy
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Event tracking and monitoring SDK for Python applications
5
5
  Home-page: https://github.com/agastronics/secploy-python-sdk
6
6
  Author: Agastronics
@@ -23,7 +23,7 @@ def cli():
23
23
 
24
24
  parser = argparse.ArgumentParser(description=__description__)
25
25
  parser.add_argument('--version', action='version', version=f'Secploy SDK v{__version__}')
26
- parser.add_argument('--test-config', help='Test a configuration file', metavar='CONFIG_FILE')
26
+ parser.add_argument('--test-config', help='Test a configuration file: To verify if it is well configured', metavar='CONFIG_FILE')
27
27
 
28
28
  args = parser.parse_args()
29
29
 
@@ -104,6 +104,8 @@ class SecployClient:
104
104
 
105
105
  # Initialize log capturer
106
106
  self._log_capturer = SecployLogCapturer(self, levels=log_levels)
107
+
108
+ self.start()
107
109
 
108
110
  def capture_logs(self, loggers: Union[str, List[str], None] = None):
109
111
  """
@@ -4,7 +4,10 @@ Secploy Log Capture Module
4
4
  This module provides functionality for capturing and forwarding logs to Secploy's ingest endpoint.
5
5
  """
6
6
 
7
+ import asyncio
8
+ import json
7
9
  import logging
10
+ import sys
8
11
  import threading
9
12
  import traceback
10
13
  from typing import Any, Dict, Optional, List, Union
@@ -17,7 +20,7 @@ from .schemas import LogEntry, Context, Tags
17
20
 
18
21
  class SecployLogCapturer:
19
22
  """
20
- Manages log capture and forwarding to Secploy ingest endpoint.
23
+ Manages log capture and exception forwarding to Secploy ingest endpoint.
21
24
  """
22
25
 
23
26
  def __init__(self, client, levels: Optional[List[Union[str, int]]] = None):
@@ -30,58 +33,105 @@ class SecployLogCapturer:
30
33
  self.client = client
31
34
  self.levels = levels
32
35
  self._handler = self._create_handler()
36
+ self._installed = False
33
37
 
34
38
  def _create_handler(self) -> logging.Handler:
35
39
  """Create the custom logging handler."""
36
40
  return SecployLogHandler(self.client, levels=self.levels)
37
41
 
38
42
  def start_capture(self, loggers: Union[str, List[str], None] = None):
39
- """
40
- Start capturing logs from specified loggers.
41
-
42
- Args:
43
- loggers: Logger name(s) to capture. Can be:
44
- - None to capture the root logger
45
- - A string for a single logger
46
- - A list of logger names
47
- """
43
+ """Start capturing logs and exceptions."""
44
+ if self._installed:
45
+ return
46
+ self._installed = True
48
47
 
49
48
  if isinstance(loggers, str):
50
49
  loggers = [loggers]
51
50
  elif loggers is None:
52
- loggers = [''] # Root logger
53
-
51
+ loggers = ['']
52
+
54
53
  for logger_name in loggers:
55
54
  logger = logging.getLogger(logger_name)
56
55
  logger.addHandler(self._handler)
56
+
57
+ sys.excepthook = self._capture_uncaught_exceptions
58
+
59
+ if hasattr(threading, "excepthook"):
60
+ threading.excepthook = self._thread_exception_handler
61
+
62
+ try:
63
+ loop = asyncio.get_event_loop()
64
+ loop.set_exception_handler(self._async_exception_handler)
65
+ except RuntimeError:
66
+ pass
57
67
 
58
68
  def stop_capture(self, loggers: Union[str, List[str], None] = None):
59
- """
60
- Stop capturing logs from specified loggers.
61
-
62
- Args:
63
- loggers: Logger name(s) to stop capturing. Same format as start_capture()
64
- """
65
69
  if isinstance(loggers, str):
66
70
  loggers = [loggers]
67
71
  elif loggers is None:
68
- loggers = [''] # Root logger
69
-
72
+ loggers = ['']
73
+
70
74
  for logger_name in loggers:
71
75
  logger = logging.getLogger(logger_name)
72
76
  if self._handler in logger.handlers:
73
77
  logger.removeHandler(self._handler)
74
78
 
75
79
  def stop_all(self):
76
- """Stop all log capturing."""
77
- # Find all loggers that have our handler
80
+ """Stop all log capturing and exception hooks."""
78
81
  for logger in [logging.getLogger(name) for name in logging.root.manager.loggerDict]:
79
82
  if self._handler in logger.handlers:
80
83
  logger.removeHandler(self._handler)
81
- # Check root logger too
82
84
  if self._handler in logging.root.handlers:
83
85
  logging.root.removeHandler(self._handler)
84
86
 
87
+ sys.excepthook = sys.__excepthook__
88
+ if hasattr(threading, "excepthook"):
89
+ threading.excepthook = threading.__excepthook__
90
+ try:
91
+ loop = asyncio.get_event_loop()
92
+ loop.set_exception_handler(None)
93
+ except RuntimeError:
94
+ pass
95
+
96
+ self._installed = False
97
+
98
+ def _capture_uncaught_exceptions(self, exc_type, exc_value, exc_tb):
99
+ self._send_exception(exc_type, exc_value, exc_tb, source="sys")
100
+
101
+ def _thread_exception_handler(self, args):
102
+ self._send_exception(args.exc_type, args.exc_value, args.exc_traceback, source="thread")
103
+
104
+ def _async_exception_handler(self, loop, context):
105
+ exc = context.get("exception")
106
+ if exc:
107
+ self._send_exception(type(exc), exc, exc.__traceback__, source="asyncio")
108
+ else:
109
+ self._send_exception(RuntimeError, RuntimeError(context["message"]), None, source="asyncio")
110
+
111
+ def _send_exception(self, exc_type, exc_value, exc_tb, source="system"):
112
+ stacktrace = traceback.format_exception(exc_type, exc_value, exc_tb)
113
+
114
+ log_entry = LogEntry(
115
+ timestamp=datetime.now().timestamp(),
116
+ type="error",
117
+ message=str(exc_value),
118
+ context=Context(
119
+ user_id="unknown",
120
+ session_id="global",
121
+ http_method="NONE",
122
+ http_url="",
123
+ http_status=500,
124
+ stacktrace=stacktrace,
125
+ tags=Tags(
126
+ environment=self.client.environment,
127
+ service=f"exception.{source}",
128
+ region="unknown"
129
+ )
130
+ )
131
+ )
132
+ payload = log_entry.model_dump(mode="json")
133
+ self.client._event_queue.put(("log", payload))
134
+
85
135
 
86
136
  class SecployLogHandler(logging.Handler):
87
137
  """
@@ -94,45 +144,34 @@ class SecployLogHandler(logging.Handler):
94
144
  self._local = threading.local()
95
145
  self._event_handler = EventHandler(self.client._event_queue)
96
146
 
97
- if levels:
98
- self.levels = {
99
- lvl if isinstance(lvl, int) else logging._nameToLevel[lvl.upper()]
100
- for lvl in levels
101
- }
102
- else:
103
- self.levels = None # None = capture all levels
147
+ # if levels:
148
+ # self.levels = {
149
+ # lvl if isinstance(lvl, int) else logging._nameToLevel[lvl.upper()]
150
+ # for lvl in levels
151
+ # }
152
+ # else:
153
+ # self.levels = None # None = capture all levels
104
154
 
105
- def filter(self, record: logging.LogRecord) -> bool:
106
- """Return True if this record should be handled."""
107
- if self.levels is None:
108
- return True
109
- return record.levelno in self.levels
155
+ # def filter(self, record: logging.LogRecord) -> bool:
156
+ # """Return True if this record should be handled."""
157
+ # if self.levels is None:
158
+ # return True
159
+ # return record.levelno in self.levels
110
160
 
111
161
  def get_thread_id(self) -> str:
112
162
  """Get the current thread identifier."""
113
163
  return str(threading.get_ident())
114
164
 
115
- def format_exc_info(self, exc_info) -> Optional[Dict[str, Any]]:
116
- """Format exception information if available."""
117
- if not exc_info:
118
- return None
119
-
120
- exc_type, exc_value, exc_tb = exc_info
121
- return {
122
- 'type': exc_type.__name__ if exc_type else None,
123
- 'message': str(exc_value) if exc_value else None,
124
- 'stacktrace': traceback.format_tb(exc_tb) if exc_tb else None
125
- }
126
-
127
165
  def emit(self, record: logging.LogRecord):
128
166
  """Send the log record to Secploy ingest."""
167
+ print(f'\n\nLEVEL NAME: {record.levelname} - LEVEL NO: {record.levelno}\n\n')
129
168
  try:
130
169
  # Get stacktrace if exception exists
131
170
  stacktrace = []
132
171
  if record.exc_info:
133
- exc_info = self.format_exc_info(record.exc_info)
134
- if exc_info['stacktrace']:
135
- stacktrace = exc_info['stacktrace']
172
+ stacktrace = traceback.format_exception(*record.exc_info)
173
+ elif record.stack_info:
174
+ stacktrace = [record.stack_info]
136
175
 
137
176
  # Build tags with log metadata
138
177
  tags = Tags(
@@ -144,23 +183,22 @@ class SecployLogHandler(logging.Handler):
144
183
  # Build context
145
184
  context = Context(
146
185
  user_id=getattr(record, 'user_id', 'unknown'),
147
- session_id=self.get_thread_id(), # Using thread_id as session_id for logging
186
+ session_id=self.get_thread_id(),
148
187
  http_method=getattr(record, 'http_method', 'NONE'),
149
- http_url=getattr(record, 'path', ''),
150
188
  http_status=getattr(record, 'status_code', 0),
151
- stacktrace=stacktrace,
152
- tags=tags
189
+ tags=tags,
190
+ http_url=getattr(record, "http_url", getattr(record, "path", "")),
191
+ stacktrace=stacktrace
153
192
  )
154
193
 
155
194
  # Create LogEntry using our schema
156
195
  log_entry = LogEntry(
157
196
  timestamp=datetime.fromtimestamp(record.created).timestamp(),
158
- type=record.levelname.lower(), # convert DEBUG -> debug, ERROR -> error etc
197
+ type=record.levelname.lower(),
159
198
  message=self.format(record),
160
199
  context=context
161
200
  )
162
201
  payload = log_entry.model_dump(mode="json")
163
- # Validate and send the log entry
164
202
  self._event_handler.send_event('log', payload)
165
203
 
166
204
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  from typing import TypedDict, Optional, Union, List
2
2
  from datetime import datetime
3
- from pydantic import BaseModel, Extra
3
+ from pydantic import BaseModel, ConfigDict
4
4
  from .enums import LogLevel
5
5
 
6
6
 
@@ -28,8 +28,7 @@ class Tags(BaseModel):
28
28
  service: str
29
29
  region: str
30
30
 
31
- class Config:
32
- extra = Extra.allow # Allow additional fields in tags
31
+ model_config = ConfigDict(extra="allow")
33
32
 
34
33
 
35
34
  class Context(BaseModel):
@@ -40,6 +39,7 @@ class Context(BaseModel):
40
39
  http_status: int
41
40
  stacktrace: List[str]
42
41
  tags: Tags
42
+
43
43
 
44
44
 
45
45
  class LogEntry(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secploy
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Event tracking and monitoring SDK for Python applications
5
5
  Home-page: https://github.com/agastronics/secploy-python-sdk
6
6
  Author: Agastronics
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="secploy",
5
- version="0.2.6",
5
+ version="0.2.7",
6
6
  packages=find_packages(),
7
7
  install_requires=[
8
8
  "requests>=2.25.0",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes