canvas 0.63.0__py3-none-any.whl → 0.89.0__py3-none-any.whl
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.
- {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/METADATA +4 -1
- {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/RECORD +184 -98
- {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/WHEEL +1 -1
- canvas_cli/apps/emit/event_fixtures/UNKNOWN.ndjson +1 -0
- canvas_cli/apps/logs/logs.py +386 -22
- canvas_cli/main.py +3 -1
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/tests/test_models.py +46 -4
- canvas_cli/utils/context/context.py +13 -13
- canvas_cli/utils/validators/manifest_schema.py +26 -1
- canvas_generated/messages/effects_pb2.py +5 -5
- canvas_generated/messages/effects_pb2.pyi +108 -2
- canvas_generated/messages/events_pb2.py +6 -6
- canvas_generated/messages/events_pb2.pyi +282 -2
- canvas_sdk/clients/__init__.py +1 -0
- canvas_sdk/clients/llms/__init__.py +17 -0
- canvas_sdk/clients/llms/libraries/__init__.py +11 -0
- canvas_sdk/clients/llms/libraries/llm_anthropic.py +87 -0
- canvas_sdk/clients/llms/libraries/llm_api.py +143 -0
- canvas_sdk/clients/llms/libraries/llm_google.py +92 -0
- canvas_sdk/clients/llms/libraries/llm_openai.py +98 -0
- canvas_sdk/clients/llms/structures/__init__.py +9 -0
- canvas_sdk/clients/llms/structures/llm_response.py +33 -0
- canvas_sdk/clients/llms/structures/llm_tokens.py +53 -0
- canvas_sdk/clients/llms/structures/llm_turn.py +47 -0
- canvas_sdk/clients/llms/structures/settings/__init__.py +13 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings.py +27 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings_anthropic.py +43 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings_gemini.py +40 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings_gpt4.py +40 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings_gpt5.py +48 -0
- canvas_sdk/clients/third_party.py +3 -0
- canvas_sdk/commands/__init__.py +12 -0
- canvas_sdk/commands/base.py +33 -2
- canvas_sdk/commands/commands/adjust_prescription.py +4 -0
- canvas_sdk/commands/commands/custom_command.py +86 -0
- canvas_sdk/commands/commands/family_history.py +17 -1
- canvas_sdk/commands/commands/immunization_statement.py +42 -2
- canvas_sdk/commands/commands/medication_statement.py +16 -1
- canvas_sdk/commands/commands/past_surgical_history.py +16 -1
- canvas_sdk/commands/commands/perform.py +18 -1
- canvas_sdk/commands/commands/prescribe.py +8 -9
- canvas_sdk/commands/commands/refill.py +5 -5
- canvas_sdk/commands/commands/resolve_condition.py +5 -5
- canvas_sdk/commands/commands/review/__init__.py +3 -0
- canvas_sdk/commands/commands/review/base.py +72 -0
- canvas_sdk/commands/commands/review/imaging.py +13 -0
- canvas_sdk/commands/commands/review/lab.py +13 -0
- canvas_sdk/commands/commands/review/referral.py +13 -0
- canvas_sdk/commands/commands/review/uncategorized_document.py +13 -0
- canvas_sdk/commands/validation.py +43 -0
- canvas_sdk/effects/batch_originate.py +22 -0
- canvas_sdk/effects/calendar/__init__.py +13 -3
- canvas_sdk/effects/calendar/{create_calendar.py → calendar.py} +19 -5
- canvas_sdk/effects/calendar/event.py +172 -0
- canvas_sdk/effects/claim_label.py +93 -0
- canvas_sdk/effects/claim_line_item.py +47 -0
- canvas_sdk/effects/claim_queue.py +49 -0
- canvas_sdk/effects/fax/__init__.py +3 -0
- canvas_sdk/effects/fax/base.py +77 -0
- canvas_sdk/effects/fax/note.py +42 -0
- canvas_sdk/effects/metadata.py +15 -1
- canvas_sdk/effects/note/__init__.py +8 -1
- canvas_sdk/effects/note/appointment.py +135 -7
- canvas_sdk/effects/note/base.py +17 -0
- canvas_sdk/effects/note/message.py +22 -14
- canvas_sdk/effects/note/note.py +150 -1
- canvas_sdk/effects/observation/__init__.py +11 -0
- canvas_sdk/effects/observation/base.py +206 -0
- canvas_sdk/effects/patient/__init__.py +2 -0
- canvas_sdk/effects/patient/base.py +8 -0
- canvas_sdk/effects/payment/__init__.py +11 -0
- canvas_sdk/effects/payment/base.py +355 -0
- canvas_sdk/effects/payment/post_claim_payment.py +49 -0
- canvas_sdk/effects/send_contact_verification.py +42 -0
- canvas_sdk/effects/task/__init__.py +2 -1
- canvas_sdk/effects/task/task.py +30 -0
- canvas_sdk/effects/validation/__init__.py +3 -0
- canvas_sdk/effects/validation/base.py +92 -0
- canvas_sdk/events/base.py +15 -0
- canvas_sdk/handlers/application.py +7 -7
- canvas_sdk/handlers/simple_api/api.py +1 -4
- canvas_sdk/handlers/simple_api/websocket.py +1 -4
- canvas_sdk/handlers/utils.py +14 -0
- canvas_sdk/questionnaires/utils.py +1 -0
- canvas_sdk/templates/utils.py +17 -4
- canvas_sdk/test_utils/factories/FACTORY_GUIDE.md +362 -0
- canvas_sdk/test_utils/factories/__init__.py +115 -0
- canvas_sdk/test_utils/factories/calendar.py +24 -0
- canvas_sdk/test_utils/factories/claim.py +81 -0
- canvas_sdk/test_utils/factories/claim_diagnosis_code.py +16 -0
- canvas_sdk/test_utils/factories/coverage.py +17 -0
- canvas_sdk/test_utils/factories/imaging.py +74 -0
- canvas_sdk/test_utils/factories/lab.py +192 -0
- canvas_sdk/test_utils/factories/medication_history.py +75 -0
- canvas_sdk/test_utils/factories/note.py +52 -0
- canvas_sdk/test_utils/factories/organization.py +50 -0
- canvas_sdk/test_utils/factories/practicelocation.py +88 -0
- canvas_sdk/test_utils/factories/referral.py +81 -0
- canvas_sdk/test_utils/factories/staff.py +111 -0
- canvas_sdk/test_utils/factories/task.py +66 -0
- canvas_sdk/test_utils/factories/uncategorized_clinical_document.py +48 -0
- canvas_sdk/utils/metrics.py +4 -1
- canvas_sdk/v1/data/__init__.py +66 -7
- canvas_sdk/v1/data/allergy_intolerance.py +5 -11
- canvas_sdk/v1/data/appointment.py +18 -4
- canvas_sdk/v1/data/assessment.py +2 -12
- canvas_sdk/v1/data/banner_alert.py +2 -4
- canvas_sdk/v1/data/base.py +53 -14
- canvas_sdk/v1/data/billing.py +8 -11
- canvas_sdk/v1/data/calendar.py +64 -0
- canvas_sdk/v1/data/care_team.py +4 -10
- canvas_sdk/v1/data/claim.py +172 -66
- canvas_sdk/v1/data/claim_diagnosis_code.py +19 -0
- canvas_sdk/v1/data/claim_line_item.py +2 -5
- canvas_sdk/v1/data/coding.py +19 -0
- canvas_sdk/v1/data/command.py +2 -4
- canvas_sdk/v1/data/common.py +10 -0
- canvas_sdk/v1/data/compound_medication.py +3 -4
- canvas_sdk/v1/data/condition.py +4 -9
- canvas_sdk/v1/data/coverage.py +66 -26
- canvas_sdk/v1/data/detected_issue.py +20 -20
- canvas_sdk/v1/data/device.py +2 -14
- canvas_sdk/v1/data/discount.py +2 -5
- canvas_sdk/v1/data/encounter.py +44 -0
- canvas_sdk/v1/data/facility.py +1 -0
- canvas_sdk/v1/data/goal.py +2 -14
- canvas_sdk/v1/data/imaging.py +4 -30
- canvas_sdk/v1/data/immunization.py +7 -15
- canvas_sdk/v1/data/lab.py +12 -65
- canvas_sdk/v1/data/line_item_transaction.py +2 -5
- canvas_sdk/v1/data/medication.py +3 -8
- canvas_sdk/v1/data/medication_history.py +142 -0
- canvas_sdk/v1/data/medication_statement.py +41 -0
- canvas_sdk/v1/data/message.py +4 -8
- canvas_sdk/v1/data/note.py +37 -38
- canvas_sdk/v1/data/observation.py +9 -36
- canvas_sdk/v1/data/organization.py +70 -9
- canvas_sdk/v1/data/patient.py +8 -12
- canvas_sdk/v1/data/patient_consent.py +4 -14
- canvas_sdk/v1/data/payment_collection.py +2 -5
- canvas_sdk/v1/data/posting.py +3 -9
- canvas_sdk/v1/data/practicelocation.py +66 -7
- canvas_sdk/v1/data/protocol_override.py +3 -4
- canvas_sdk/v1/data/protocol_result.py +3 -3
- canvas_sdk/v1/data/questionnaire.py +10 -26
- canvas_sdk/v1/data/reason_for_visit.py +2 -6
- canvas_sdk/v1/data/referral.py +41 -17
- canvas_sdk/v1/data/staff.py +34 -26
- canvas_sdk/v1/data/stop_medication_event.py +27 -0
- canvas_sdk/v1/data/task.py +30 -11
- canvas_sdk/v1/data/team.py +2 -4
- canvas_sdk/v1/data/uncategorized_clinical_document.py +84 -0
- canvas_sdk/v1/data/user.py +14 -0
- canvas_sdk/v1/data/utils.py +5 -0
- canvas_sdk/value_set/v2026/__init__.py +1 -0
- canvas_sdk/value_set/v2026/adverse_event.py +157 -0
- canvas_sdk/value_set/v2026/allergy.py +116 -0
- canvas_sdk/value_set/v2026/assessment.py +466 -0
- canvas_sdk/value_set/v2026/communication.py +496 -0
- canvas_sdk/value_set/v2026/condition.py +52934 -0
- canvas_sdk/value_set/v2026/device.py +315 -0
- canvas_sdk/value_set/v2026/diagnostic_study.py +5243 -0
- canvas_sdk/value_set/v2026/encounter.py +2714 -0
- canvas_sdk/value_set/v2026/immunization.py +297 -0
- canvas_sdk/value_set/v2026/individual_characteristic.py +339 -0
- canvas_sdk/value_set/v2026/intervention.py +1703 -0
- canvas_sdk/value_set/v2026/laboratory_test.py +1831 -0
- canvas_sdk/value_set/v2026/medication.py +8218 -0
- canvas_sdk/value_set/v2026/no_qdm_category_assigned.py +26493 -0
- canvas_sdk/value_set/v2026/physical_exam.py +342 -0
- canvas_sdk/value_set/v2026/procedure.py +27869 -0
- canvas_sdk/value_set/v2026/symptom.py +625 -0
- logger/logger.py +30 -31
- logger/logstash.py +282 -0
- logger/pubsub.py +26 -0
- plugin_runner/allowed-module-imports.json +940 -9
- plugin_runner/generate_allowed_imports.py +1 -0
- plugin_runner/installation.py +2 -2
- plugin_runner/plugin_runner.py +21 -24
- plugin_runner/sandbox.py +34 -0
- protobufs/canvas_generated/messages/effects.proto +65 -0
- protobufs/canvas_generated/messages/events.proto +150 -51
- settings.py +27 -11
- canvas_sdk/effects/calendar/create_event.py +0 -43
- {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/entry_points.txt +0 -0
logger/logger.py
CHANGED
|
@@ -2,26 +2,11 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from logstash_async.handler import AsynchronousLogstashHandler
|
|
6
7
|
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class PubSubLogHandler(logging.Handler):
|
|
11
|
-
"""Custom logging handler that publishes logs to a pub/sub channel."""
|
|
12
|
-
|
|
13
|
-
def __init__(self) -> None:
|
|
14
|
-
self.publisher = Publisher()
|
|
15
|
-
logging.Handler.__init__(self=self)
|
|
16
|
-
|
|
17
|
-
def emit(self, record: Any) -> None:
|
|
18
|
-
"""Publishes the log message to the pub/sub channel."""
|
|
19
|
-
message = self.format(record)
|
|
20
|
-
|
|
21
|
-
try:
|
|
22
|
-
self.publisher.publish(message)
|
|
23
|
-
except redis.ConnectionError as e:
|
|
24
|
-
print(f"PubSubLogHandler: failed to log message due to redis error: {e}")
|
|
8
|
+
from logger.logstash import LogstashFormatterECS
|
|
9
|
+
from logger.pubsub import PubSubLogHandler
|
|
25
10
|
|
|
26
11
|
|
|
27
12
|
class PluginLogger:
|
|
@@ -31,13 +16,13 @@ class PluginLogger:
|
|
|
31
16
|
self.logger = logging.getLogger("plugin_runner_logger")
|
|
32
17
|
self.logger.setLevel(logging.INFO)
|
|
33
18
|
|
|
34
|
-
log_prefix = os.getenv(
|
|
19
|
+
log_prefix = f"{os.getenv('HOSTNAME', '?')}: {os.getenv('APTIBLE_PROCESS_INDEX', '?')}"
|
|
35
20
|
|
|
36
21
|
if log_prefix != "":
|
|
37
22
|
log_prefix = f"[{log_prefix}] "
|
|
38
23
|
|
|
39
24
|
formatter = logging.Formatter(
|
|
40
|
-
f"
|
|
25
|
+
f"plugin-runner {log_prefix}%(levelname)s %(asctime)s %(message)s"
|
|
41
26
|
)
|
|
42
27
|
|
|
43
28
|
streaming_handler = logging.StreamHandler()
|
|
@@ -45,29 +30,43 @@ class PluginLogger:
|
|
|
45
30
|
|
|
46
31
|
self.logger.addHandler(streaming_handler)
|
|
47
32
|
|
|
48
|
-
if
|
|
33
|
+
if settings.REDIS_ENDPOINT:
|
|
49
34
|
pubsub_handler = PubSubLogHandler()
|
|
50
35
|
pubsub_handler.setFormatter(formatter)
|
|
51
36
|
|
|
52
37
|
self.logger.addHandler(pubsub_handler)
|
|
53
38
|
|
|
54
|
-
|
|
39
|
+
if settings.LOGSTASH_HOST:
|
|
40
|
+
logstash_handler = AsynchronousLogstashHandler(
|
|
41
|
+
host=settings.LOGSTASH_HOST,
|
|
42
|
+
port=settings.LOGSTASH_PORT,
|
|
43
|
+
database_path=None,
|
|
44
|
+
transport=settings.LOGSTASH_PROTOCOL,
|
|
45
|
+
)
|
|
46
|
+
logstash_handler.setFormatter(LogstashFormatterECS())
|
|
47
|
+
self.logger.addHandler(logstash_handler)
|
|
48
|
+
|
|
49
|
+
def debug(self, message: Any, *args: Any, **kwargs: Any) -> None:
|
|
55
50
|
"""Logs a debug message."""
|
|
56
|
-
self.logger.debug(message)
|
|
51
|
+
self.logger.debug(message, *args, **kwargs)
|
|
57
52
|
|
|
58
|
-
def info(self, message: Any) -> None:
|
|
53
|
+
def info(self, message: Any, *args: Any, **kwargs: Any) -> None:
|
|
59
54
|
"""Logs an info message."""
|
|
60
|
-
self.logger.info(message)
|
|
55
|
+
self.logger.info(message, *args, **kwargs)
|
|
61
56
|
|
|
62
|
-
def warning(self, message: Any) -> None:
|
|
57
|
+
def warning(self, message: Any, *args: Any, **kwargs: Any) -> None:
|
|
63
58
|
"""Logs a warning message."""
|
|
64
|
-
self.logger.warning(message)
|
|
59
|
+
self.logger.warning(message, *args, **kwargs)
|
|
65
60
|
|
|
66
|
-
def error(self, message: Any) -> None:
|
|
61
|
+
def error(self, message: Any, *args: Any, **kwargs: Any) -> None:
|
|
67
62
|
"""Logs an error message."""
|
|
68
|
-
self.logger.error(message)
|
|
63
|
+
self.logger.error(message, *args, **kwargs)
|
|
64
|
+
|
|
65
|
+
def exception(self, message: Any, *args: Any, **kwargs: Any) -> None:
|
|
66
|
+
"""Convenience method for logging an ERROR with exception information."""
|
|
67
|
+
self.logger.exception(message, *args, **kwargs)
|
|
69
68
|
|
|
70
|
-
def critical(self, message: Any) -> None:
|
|
69
|
+
def critical(self, message: Any, *args: Any, **kwargs: Any) -> None:
|
|
71
70
|
"""Logs a critical message."""
|
|
72
71
|
self.logger.critical(message)
|
|
73
72
|
|
logger/logstash.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
from logging import LogRecord
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from django.conf import settings
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HttpTransport:
|
|
17
|
+
"""
|
|
18
|
+
Send messages to Logstash in V1 format.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, host: str, **kwargs: Any) -> None:
|
|
22
|
+
self.url = host
|
|
23
|
+
|
|
24
|
+
self.session = requests.Session()
|
|
25
|
+
self.session.headers.update({"Content-Type": "application/json"})
|
|
26
|
+
|
|
27
|
+
def send(self, events: list[Any], **kwargs: Any) -> None:
|
|
28
|
+
"""Send events to Logstash."""
|
|
29
|
+
for event in events:
|
|
30
|
+
try:
|
|
31
|
+
self.session.post(self.url, data=event)
|
|
32
|
+
except (KeyboardInterrupt, SystemExit):
|
|
33
|
+
raise
|
|
34
|
+
except BaseException as e:
|
|
35
|
+
print("Logstash exception", e)
|
|
36
|
+
|
|
37
|
+
def close(self) -> None:
|
|
38
|
+
"""Close the transport."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
ExcInfo = tuple[type[BaseException], BaseException, TracebackType | None]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _figure_out_exc_info(v: Any) -> ExcInfo:
|
|
46
|
+
"""
|
|
47
|
+
Depending on the Python version will try to do the smartest thing possible
|
|
48
|
+
to transform *v* into an ``exc_info`` tuple.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(v, BaseException):
|
|
51
|
+
return v.__class__, v, v.__traceback__
|
|
52
|
+
|
|
53
|
+
if isinstance(v, tuple):
|
|
54
|
+
return v
|
|
55
|
+
|
|
56
|
+
if v:
|
|
57
|
+
return sys.exc_info() # type: ignore[return-value]
|
|
58
|
+
|
|
59
|
+
return v
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _json_default(obj: Any) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Coerce everything to strings. All objects representing time get output as
|
|
65
|
+
ISO8601.
|
|
66
|
+
"""
|
|
67
|
+
if isinstance(obj, datetime.datetime | datetime.date | datetime.time):
|
|
68
|
+
return obj.isoformat()
|
|
69
|
+
|
|
70
|
+
return str(obj)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LogstashFormatterV1(logging.Formatter):
|
|
74
|
+
"""
|
|
75
|
+
A custom formatter to prepare logs to be shipped out to logstash V1 format.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
unwanted_fields = [
|
|
79
|
+
"args",
|
|
80
|
+
"created",
|
|
81
|
+
"filename",
|
|
82
|
+
"funcName",
|
|
83
|
+
"levelno",
|
|
84
|
+
"lineno",
|
|
85
|
+
"module",
|
|
86
|
+
"msecs",
|
|
87
|
+
"name",
|
|
88
|
+
"pathname",
|
|
89
|
+
"process",
|
|
90
|
+
"processName",
|
|
91
|
+
"relativeCreated",
|
|
92
|
+
"request",
|
|
93
|
+
"response",
|
|
94
|
+
"threadName",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
mappings = {
|
|
98
|
+
"levelname": "log_level",
|
|
99
|
+
"name": "log_name",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def __init__(self) -> None:
|
|
103
|
+
super().__init__()
|
|
104
|
+
|
|
105
|
+
self.defaults = {
|
|
106
|
+
"app": "plugin-runner",
|
|
107
|
+
"customer": settings.CUSTOMER_IDENTIFIER,
|
|
108
|
+
"plugin_context": True,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
self.source_host = settings.HOSTNAME
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def get_location(fields: dict) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Format the log location in a way that saves some bytes.
|
|
117
|
+
"""
|
|
118
|
+
path = "?"
|
|
119
|
+
|
|
120
|
+
if "pathname" in fields:
|
|
121
|
+
path = re.sub(r"^/plugin-runner/", "", fields["pathname"])
|
|
122
|
+
path = re.sub(r"^/usr/local/lib/python3.\d\d/dist-packages/", "", path)
|
|
123
|
+
path = re.sub(r"^/venvs/plugin-runner/lib/python3.\d\d/site-packages/", "", path)
|
|
124
|
+
elif "filename" in fields:
|
|
125
|
+
path = fields["filename"]
|
|
126
|
+
|
|
127
|
+
return "{}:{}".format(path, fields.get("lineno", "?"))
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def get_method(fields: dict) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Get the method that originated the log.
|
|
133
|
+
"""
|
|
134
|
+
return "{}.{}:{}".format(
|
|
135
|
+
fields.get("name", "?"), fields.get("module", "?"), fields.get("funcName", "?")
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def format(self, record: LogRecord) -> str:
|
|
139
|
+
"""
|
|
140
|
+
Format a log record to JSON, if the message is a dict assume an empty
|
|
141
|
+
message and use the dict as additional fields.
|
|
142
|
+
"""
|
|
143
|
+
fields = record.__dict__.copy()
|
|
144
|
+
|
|
145
|
+
if "msg" in fields and isinstance(fields["msg"], dict):
|
|
146
|
+
msg = fields.pop("msg")
|
|
147
|
+
|
|
148
|
+
# if the dict has an "event" key, use that as the message
|
|
149
|
+
# to avoid conflicts with other message types that use event as an object.
|
|
150
|
+
if "event" in msg:
|
|
151
|
+
fields["message"] = msg.pop("event")
|
|
152
|
+
|
|
153
|
+
fields.update(msg)
|
|
154
|
+
elif "msg" in fields and "message" not in fields:
|
|
155
|
+
msg = record.getMessage()
|
|
156
|
+
|
|
157
|
+
del fields["msg"]
|
|
158
|
+
|
|
159
|
+
with contextlib.suppress(KeyError, IndexError, ValueError):
|
|
160
|
+
msg = msg.format(**fields)
|
|
161
|
+
|
|
162
|
+
fields["message"] = msg
|
|
163
|
+
|
|
164
|
+
if "exc_info" in fields:
|
|
165
|
+
if fields["exc_info"]:
|
|
166
|
+
exc_info = _figure_out_exc_info(fields["exc_info"])
|
|
167
|
+
fields["exception_message"] = str(exc_info[1])
|
|
168
|
+
fields["exception_type"] = f"{exc_info[0].__module__}.{exc_info[0].__qualname__}"
|
|
169
|
+
fields["stack_trace"] = traceback.format_exception(*exc_info)
|
|
170
|
+
del fields["exc_info"]
|
|
171
|
+
|
|
172
|
+
if "exc_text" in fields and not fields["exc_text"]:
|
|
173
|
+
del fields["exc_text"]
|
|
174
|
+
|
|
175
|
+
now = datetime.datetime.now(datetime.UTC)
|
|
176
|
+
|
|
177
|
+
base_log = {
|
|
178
|
+
"@timestamp": now.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
|
|
179
|
+
"@version": 1,
|
|
180
|
+
"location": self.get_location(fields),
|
|
181
|
+
"method": self.get_method(fields),
|
|
182
|
+
"source_host": self.source_host,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for field in self.unwanted_fields:
|
|
186
|
+
if field in fields:
|
|
187
|
+
del fields[field]
|
|
188
|
+
|
|
189
|
+
# Remove None fields (like `params` most of the time) and empty
|
|
190
|
+
# argument lists (like `args` most of the time)
|
|
191
|
+
fields = {key: value for key, value in fields.items() if value is not None and value != []}
|
|
192
|
+
|
|
193
|
+
# Rename fields with mapping set
|
|
194
|
+
for old_name, new_name in self.mappings.items():
|
|
195
|
+
if old_name in fields:
|
|
196
|
+
fields[new_name] = fields.pop(old_name)
|
|
197
|
+
|
|
198
|
+
base_log.update(fields)
|
|
199
|
+
|
|
200
|
+
log_record = self.defaults.copy()
|
|
201
|
+
log_record.update(base_log)
|
|
202
|
+
|
|
203
|
+
return json.dumps(log_record, default=_json_default)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class LogstashFormatterECS(logging.Formatter):
|
|
207
|
+
"""
|
|
208
|
+
A custom formatter to prepare logs to be shipped out to logstash ECS format.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(self) -> None:
|
|
212
|
+
super().__init__()
|
|
213
|
+
|
|
214
|
+
self.defaults = {
|
|
215
|
+
"service": {"name": "plugin-runner"},
|
|
216
|
+
"labels": {"customer": settings.CUSTOMER_IDENTIFIER},
|
|
217
|
+
"host": {"name": settings.HOSTNAME},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def format(self, record: LogRecord) -> str:
|
|
221
|
+
"""
|
|
222
|
+
Format a log record to JSON, if the message is a dict assume an empty
|
|
223
|
+
message and use the dict as additional fields.
|
|
224
|
+
"""
|
|
225
|
+
fields = record.__dict__.copy()
|
|
226
|
+
|
|
227
|
+
if "msg" in fields and isinstance(fields["msg"], dict):
|
|
228
|
+
msg = fields.pop("msg")
|
|
229
|
+
|
|
230
|
+
# if the dict has an "event" key, use that as the message
|
|
231
|
+
# to avoid conflicts with other message types that use event as an object.
|
|
232
|
+
if "event" in msg:
|
|
233
|
+
fields["message"] = msg.pop("event")
|
|
234
|
+
|
|
235
|
+
fields.update(msg)
|
|
236
|
+
elif "msg" in fields and "message" not in fields:
|
|
237
|
+
msg = record.getMessage()
|
|
238
|
+
|
|
239
|
+
del fields["msg"]
|
|
240
|
+
|
|
241
|
+
with contextlib.suppress(KeyError, IndexError, ValueError):
|
|
242
|
+
msg = msg.format(**fields)
|
|
243
|
+
|
|
244
|
+
fields["message"] = msg
|
|
245
|
+
|
|
246
|
+
if "exc_info" in fields:
|
|
247
|
+
if fields["exc_info"]:
|
|
248
|
+
exc_info = _figure_out_exc_info(fields["exc_info"])
|
|
249
|
+
fields["exception_message"] = str(exc_info[1])
|
|
250
|
+
fields["exception_type"] = f"{exc_info[0].__module__}.{exc_info[0].__qualname__}"
|
|
251
|
+
fields["stack_trace"] = traceback.format_exception(*exc_info)
|
|
252
|
+
del fields["exc_info"]
|
|
253
|
+
|
|
254
|
+
if "exc_text" in fields and not fields["exc_text"]:
|
|
255
|
+
del fields["exc_text"]
|
|
256
|
+
|
|
257
|
+
now = datetime.datetime.now(datetime.UTC)
|
|
258
|
+
|
|
259
|
+
log_record = {
|
|
260
|
+
**self.defaults,
|
|
261
|
+
"@timestamp": now.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
|
|
262
|
+
"message": fields.get("message", ""),
|
|
263
|
+
"log": {
|
|
264
|
+
"level": fields.get("levelname", ""),
|
|
265
|
+
},
|
|
266
|
+
**(
|
|
267
|
+
{
|
|
268
|
+
"error": {
|
|
269
|
+
"message": fields["exception_message"],
|
|
270
|
+
"type": fields["exception_type"],
|
|
271
|
+
"stack_trace": fields["stack_trace"],
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if "exception_message" in fields
|
|
275
|
+
else {}
|
|
276
|
+
),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return json.dumps(log_record, default=_json_default)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
__exports__ = ()
|
logger/pubsub.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import redis
|
|
5
|
+
|
|
6
|
+
from pubsub.pubsub import Publisher
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PubSubLogHandler(logging.Handler):
|
|
10
|
+
"""Custom logging handler that publishes logs to a pub/sub channel."""
|
|
11
|
+
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self.publisher = Publisher()
|
|
14
|
+
logging.Handler.__init__(self=self)
|
|
15
|
+
|
|
16
|
+
def emit(self, record: Any) -> None:
|
|
17
|
+
"""Publishes the log message to the pub/sub channel."""
|
|
18
|
+
message = self.format(record)
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
self.publisher.publish(message)
|
|
22
|
+
except redis.ConnectionError as e:
|
|
23
|
+
print(f"PubSubLogHandler: failed to log message due to redis error: {e}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__exports__ = ()
|