canvas 0.34.0__py3-none-any.whl → 0.35.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.

Potentially problematic release.


This version of canvas might be problematic. Click here for more details.

Files changed (59) hide show
  1. {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/METADATA +1 -1
  2. {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/RECORD +58 -50
  3. canvas_generated/messages/effects_pb2.py +4 -4
  4. canvas_generated/messages/effects_pb2.pyi +22 -2
  5. canvas_generated/messages/events_pb2.py +2 -2
  6. canvas_generated/messages/events_pb2.pyi +30 -0
  7. canvas_sdk/base.py +56 -0
  8. canvas_sdk/commands/base.py +22 -46
  9. canvas_sdk/commands/commands/adjust_prescription.py +0 -10
  10. canvas_sdk/commands/commands/allergy.py +0 -1
  11. canvas_sdk/commands/commands/assess.py +2 -2
  12. canvas_sdk/commands/commands/change_medication.py +58 -0
  13. canvas_sdk/commands/commands/close_goal.py +0 -1
  14. canvas_sdk/commands/commands/diagnose.py +0 -1
  15. canvas_sdk/commands/commands/exam.py +0 -1
  16. canvas_sdk/commands/commands/family_history.py +0 -1
  17. canvas_sdk/commands/commands/follow_up.py +4 -2
  18. canvas_sdk/commands/commands/goal.py +8 -7
  19. canvas_sdk/commands/commands/history_present_illness.py +0 -1
  20. canvas_sdk/commands/commands/imaging_order.py +9 -8
  21. canvas_sdk/commands/commands/instruct.py +2 -2
  22. canvas_sdk/commands/commands/lab_order.py +10 -9
  23. canvas_sdk/commands/commands/medical_history.py +0 -1
  24. canvas_sdk/commands/commands/medication_statement.py +0 -1
  25. canvas_sdk/commands/commands/past_surgical_history.py +0 -1
  26. canvas_sdk/commands/commands/perform.py +3 -2
  27. canvas_sdk/commands/commands/plan.py +0 -1
  28. canvas_sdk/commands/commands/prescribe.py +0 -9
  29. canvas_sdk/commands/commands/refer.py +10 -10
  30. canvas_sdk/commands/commands/refill.py +0 -9
  31. canvas_sdk/commands/commands/remove_allergy.py +0 -1
  32. canvas_sdk/commands/commands/resolve_condition.py +3 -2
  33. canvas_sdk/commands/commands/review_of_systems.py +0 -1
  34. canvas_sdk/commands/commands/stop_medication.py +0 -1
  35. canvas_sdk/commands/commands/structured_assessment.py +0 -1
  36. canvas_sdk/commands/commands/task.py +0 -4
  37. canvas_sdk/commands/commands/update_diagnosis.py +8 -6
  38. canvas_sdk/commands/commands/update_goal.py +0 -1
  39. canvas_sdk/commands/commands/vitals.py +0 -1
  40. canvas_sdk/effects/note/__init__.py +10 -0
  41. canvas_sdk/effects/note/appointment.py +148 -0
  42. canvas_sdk/effects/note/base.py +129 -0
  43. canvas_sdk/effects/note/note.py +79 -0
  44. canvas_sdk/effects/patient/__init__.py +3 -0
  45. canvas_sdk/effects/patient/base.py +123 -0
  46. canvas_sdk/utils/http.py +7 -26
  47. canvas_sdk/utils/metrics.py +192 -0
  48. canvas_sdk/utils/plugins.py +24 -0
  49. canvas_sdk/v1/data/__init__.py +4 -0
  50. canvas_sdk/v1/data/message.py +82 -0
  51. plugin_runner/load_all_plugins.py +0 -3
  52. plugin_runner/plugin_runner.py +107 -114
  53. plugin_runner/sandbox.py +102 -8
  54. protobufs/canvas_generated/messages/effects.proto +13 -0
  55. protobufs/canvas_generated/messages/events.proto +16 -0
  56. settings.py +4 -0
  57. canvas_sdk/utils/stats.py +0 -74
  58. {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/WHEEL +0 -0
  59. {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,192 @@
1
+ import time
2
+ from collections.abc import Callable, Generator
3
+ from contextlib import contextmanager
4
+ from datetime import timedelta
5
+ from functools import wraps
6
+ from typing import Any, TypeVar, cast, overload
7
+
8
+ from django.conf import settings
9
+ from statsd.client.base import StatsClientBase
10
+ from statsd.client.udp import Pipeline
11
+ from statsd.defaults.env import statsd as default_statsd_client
12
+
13
+ LINE_PROTOCOL_TRANSLATION = str.maketrans(
14
+ {
15
+ ",": r"\,",
16
+ "=": r"\=",
17
+ " ": r"\ ",
18
+ ":": r"__",
19
+ }
20
+ )
21
+
22
+
23
+ def tags_to_line_protocol(tags: dict[str, Any]) -> str:
24
+ """Generate a tags string compatible with the InfluxDB line protocol.
25
+
26
+ See: https://docs.influxdata.com/influxdb/v1.1/write_protocols/line_protocol_tutorial/
27
+ """
28
+ return ",".join(
29
+ f"{tag_name}={str(tag_value).translate(LINE_PROTOCOL_TRANSLATION)}"
30
+ for tag_name, tag_value in tags.items()
31
+ )
32
+
33
+
34
+ def get_qualified_name(fn: Callable) -> str:
35
+ """Get the qualified name of a function."""
36
+ return f"{fn.__module__}.{fn.__qualname__}"
37
+
38
+
39
+ class StatsDClientProxy:
40
+ """Proxy for a StatsD client."""
41
+
42
+ def __init__(self) -> None:
43
+ self.client = default_statsd_client
44
+
45
+ def gauge(self, metric_name: str, value: float, tags: dict[str, str]) -> None:
46
+ """Sends a gauge metric to StatsD with properly formatted tags.
47
+
48
+ Args:
49
+ metric_name (str): The name of the metric.
50
+ value (float): The value to report.
51
+ tags (dict[str, str]): Dictionary of tags to attach to the metric.
52
+ """
53
+ if not settings.METRICS_ENABLED:
54
+ return
55
+
56
+ statsd_tags = tags_to_line_protocol(tags)
57
+ self.client.gauge(f"{metric_name},{statsd_tags}", value)
58
+
59
+ def timing(self, metric_name: str, delta: float | timedelta, tags: dict[str, str]) -> None:
60
+ """Sends a timing metric to StatsD with properly formatted tags.
61
+
62
+ Args:
63
+ metric_name (str): The name of the metric.
64
+ delta (float | timedelta): The value to report.
65
+ tags (dict[str, str]): Dictionary of tags to attach to the metric.
66
+ """
67
+ if not settings.METRICS_ENABLED:
68
+ return
69
+
70
+ statsd_tags = tags_to_line_protocol(tags)
71
+ self.client.timing(f"{metric_name},{statsd_tags}", delta)
72
+
73
+ def incr(self, metric_name: str, tags: dict[str, str], count: int = 1, rate: int = 1) -> None:
74
+ """Sends an increment metric to StatsD with properly formatted tags.
75
+
76
+ Args:
77
+ metric_name (str): The name of the metric.
78
+ count (int): The increment to report.
79
+ rate (int): The sample rate.
80
+ tags (dict[str, str]): Dictionary of tags to attach to the metric.
81
+ """
82
+ if not settings.METRICS_ENABLED:
83
+ return
84
+
85
+ statsd_tags = tags_to_line_protocol(tags)
86
+ self.client.incr(f"{metric_name},{statsd_tags}", count, rate)
87
+
88
+ def pipeline(self) -> "PipelineProxy":
89
+ """Returns a pipeline for batching StatsD metrics."""
90
+ return PipelineProxy(self.client)
91
+
92
+
93
+ class PipelineProxy(StatsDClientProxy):
94
+ """Proxy for a StatsD pipeline."""
95
+
96
+ def __init__(self, client: StatsClientBase | None) -> None:
97
+ super().__init__()
98
+ self.client = Pipeline(client or self.client)
99
+
100
+ def send(self) -> None:
101
+ """Sends the batched metrics to StatsD."""
102
+ if not settings.METRICS_ENABLED:
103
+ return
104
+
105
+ self.client.send()
106
+
107
+
108
+ statsd_client = StatsDClientProxy()
109
+
110
+
111
+ @contextmanager
112
+ def measure(
113
+ name: str,
114
+ extra_tags: dict[str, str] | None = None,
115
+ client: StatsDClientProxy | None = None,
116
+ track_plugins_usage: bool = False,
117
+ ) -> Generator[PipelineProxy, None, None]:
118
+ """A context manager for collecting metrics about a context block.
119
+
120
+ Args:
121
+ name: The name of the block being measured (added as a StatsD tag)
122
+ extra_tags: A dict of extra tags to be added to all recorded metrics.
123
+ client: An optional alternate StatsD client.
124
+ track_plugins_usage: Whether to track plugin usage (Adds plugin and handler tags if the caller was a plugin).
125
+
126
+ Yields:
127
+ A pipeline for collecting additional metrics in the same batch.
128
+ """
129
+ client = client or statsd_client
130
+
131
+ if track_plugins_usage:
132
+ from canvas_sdk.utils.plugins import is_plugin_caller
133
+
134
+ is_plugin, caller = is_plugin_caller()
135
+ if is_plugin and caller:
136
+ extra_tags = extra_tags or {}
137
+ extra_tags["plugin"] = caller.split(".")[0]
138
+ extra_tags["handler"] = caller
139
+
140
+ tags = {"name": name, **(extra_tags or {})}
141
+
142
+ pipeline = client.pipeline()
143
+ timing_start = time.perf_counter_ns()
144
+ try:
145
+ yield pipeline
146
+ except BaseException as ex:
147
+ tags = {**tags, "status": "error"}
148
+ raise ex
149
+ else:
150
+ tags = {**tags, "status": "success"}
151
+ finally:
152
+ duration_ms = (time.perf_counter_ns() - timing_start) / 1_000_000
153
+ pipeline.timing("plugins.timings", duration_ms, tags=tags)
154
+ pipeline.incr("plugins.executions", tags=tags)
155
+ pipeline.send()
156
+
157
+
158
+ F = TypeVar("F", bound=Callable)
159
+
160
+
161
+ @overload
162
+ def measured(fn: F) -> F: ...
163
+
164
+
165
+ @overload
166
+ def measured(**options: Any) -> Callable[[F], F]: ...
167
+
168
+
169
+ def measured(fn: F | None = None, **options: Any) -> Callable[[F], F] | F:
170
+ """Collect metrics about the decorated function.
171
+
172
+ Args:
173
+ fn: The decorated function.
174
+ options: Additional options for the decorator, such as `client` and `extra_tags`.
175
+
176
+ Returns:
177
+ A decorated function if called without arguments (@measured), or a
178
+ decorator if called with arguments (@measured(client=...))
179
+ """
180
+
181
+ def _decorator(fn: F) -> F:
182
+ @wraps(fn)
183
+ def _wrapped(*args: Any, **kwargs: Any) -> Any:
184
+ with measure(get_qualified_name(fn), **options):
185
+ return fn(*args, **kwargs)
186
+
187
+ return cast(F, _wrapped)
188
+
189
+ return _decorator(fn) if fn else _decorator
190
+
191
+
192
+ __exports__ = ()
@@ -1,11 +1,14 @@
1
1
  import inspect
2
2
  from collections.abc import Callable
3
3
  from pathlib import Path
4
+ from types import FrameType
4
5
  from typing import Any
5
6
 
7
+ from canvas_sdk.utils.metrics import measured
6
8
  from settings import PLUGIN_DIRECTORY
7
9
 
8
10
 
11
+ @measured
9
12
  def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
10
13
  """Decorator to restrict a function's execution to plugins only."""
11
14
 
@@ -26,4 +29,25 @@ def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
26
29
  return wrapper
27
30
 
28
31
 
32
+ @measured
33
+ def is_plugin_caller(depth: int = 10, frame: FrameType | None = None) -> tuple[bool, str | None]:
34
+ """Check if a function is called from a plugin."""
35
+ current_frame = frame or inspect.currentframe()
36
+ caller = current_frame.f_back if current_frame else None
37
+
38
+ if not caller:
39
+ return False, None
40
+
41
+ if "__is_plugin__" not in caller.f_globals:
42
+ if depth > 0:
43
+ return is_plugin_caller(frame=caller, depth=depth - 1)
44
+ else:
45
+ return False, None
46
+
47
+ module = caller.f_globals.get("__name__")
48
+ qualname = caller.f_code.co_qualname
49
+
50
+ return True, f"{module}.{qualname}"
51
+
52
+
29
53
  __exports__ = ()
@@ -21,6 +21,7 @@ from .lab import (
21
21
  LabValueCoding,
22
22
  )
23
23
  from .medication import Medication, MedicationCoding
24
+ from .message import Message, MessageAttachment, MessageTransmission
24
25
  from .note import Note, NoteType
25
26
  from .observation import (
26
27
  Observation,
@@ -89,6 +90,9 @@ __all__ = __exports__ = (
89
90
  "LabValueCoding",
90
91
  "Medication",
91
92
  "MedicationCoding",
93
+ "Message",
94
+ "MessageAttachment",
95
+ "MessageTransmission",
92
96
  "Note",
93
97
  "NoteType",
94
98
  "Observation",
@@ -0,0 +1,82 @@
1
+ from django.db import models
2
+
3
+
4
+ class TransmissionChannel(models.TextChoices):
5
+ """Transmission channel."""
6
+
7
+ MANUAL = "manual", "Manual"
8
+ TEXT_MESSAGE = "sms", "Text Message"
9
+ EMAIL = "email", "Email"
10
+ NOOP = "noop", "No-op"
11
+
12
+
13
+ class Message(models.Model):
14
+ """Message."""
15
+
16
+ class Meta:
17
+ managed = False
18
+ db_table = "canvas_sdk_data_api_message_001"
19
+
20
+ id = models.UUIDField()
21
+ dbid = models.BigIntegerField(primary_key=True)
22
+ created = models.DateTimeField()
23
+ modified = models.DateTimeField()
24
+ content = models.TextField()
25
+ sender = models.ForeignKey(
26
+ "v1.CanvasUser", on_delete=models.DO_NOTHING, related_name="sent_messages", null=True
27
+ )
28
+ recipient = models.ForeignKey(
29
+ "v1.CanvasUser", on_delete=models.DO_NOTHING, related_name="received_messages", null=True
30
+ )
31
+ note = models.ForeignKey(
32
+ "v1.Note", on_delete=models.DO_NOTHING, related_name="message", null=True
33
+ )
34
+ read = models.BooleanField()
35
+
36
+
37
+ class MessageAttachment(models.Model):
38
+ """Message attachment."""
39
+
40
+ class Meta:
41
+ managed = False
42
+ db_table = "canvas_sdk_data_api_messageattachment_001"
43
+
44
+ id = models.UUIDField()
45
+ dbid = models.BigIntegerField(primary_key=True)
46
+ file = models.TextField()
47
+ content_type = models.CharField(max_length=255)
48
+ message = models.ForeignKey(
49
+ "v1.Message", on_delete=models.DO_NOTHING, related_name="message", null=True
50
+ )
51
+
52
+
53
+ class MessageTransmission(models.Model):
54
+ """Message Transmission."""
55
+
56
+ class Meta:
57
+ managed = False
58
+ db_table = "canvas_sdk_data_api_messagetransmission_001"
59
+
60
+ id = models.UUIDField()
61
+ dbid = models.BigIntegerField(primary_key=True)
62
+ created = models.DateTimeField()
63
+ modified = models.DateTimeField()
64
+ message = models.ForeignKey(
65
+ "v1.Message", on_delete=models.DO_NOTHING, related_name="transmissions", null=True
66
+ )
67
+ delivered = models.BooleanField()
68
+ failed = models.BooleanField()
69
+
70
+ contact_point_system = models.CharField(choices=TransmissionChannel.choices)
71
+ contact_point_value = models.CharField(max_length=255)
72
+
73
+ comment = models.TextField()
74
+ delivered_by = models.ForeignKey(
75
+ "v1.Staff",
76
+ on_delete=models.DO_NOTHING,
77
+ related_name="transmissions_delivered",
78
+ null=True,
79
+ )
80
+
81
+
82
+ __exports__ = ("TransmissionChannel", "Message", "MessageAttachment", "MessageTransmission")
@@ -18,11 +18,8 @@ from canvas_generated.messages.events_pb2 import PLUGIN_CREATED
18
18
  from canvas_generated.messages.events_pb2 import Event as EventRequest
19
19
  from canvas_sdk.events.base import Event
20
20
  from canvas_sdk.protocols.base import BaseProtocol
21
- from canvas_sdk.utils import stats
22
21
  from plugin_runner.plugin_runner import LOADED_PLUGINS, load_or_reload_plugin
23
22
 
24
- stats.STATS_ENABLED = False
25
-
26
23
  ORIGINAL_PATH = sys.path.copy()
27
24
 
28
25
 
@@ -5,7 +5,6 @@ import pathlib
5
5
  import pickle
6
6
  import pkgutil
7
7
  import sys
8
- import time
9
8
  import traceback
10
9
  from collections import defaultdict
11
10
  from collections.abc import AsyncGenerator
@@ -29,7 +28,8 @@ from canvas_sdk.effects import Effect
29
28
  from canvas_sdk.effects.simple_api import Response
30
29
  from canvas_sdk.events import Event, EventRequest, EventResponse, EventType
31
30
  from canvas_sdk.protocols import ClinicalQualityMeasure
32
- from canvas_sdk.utils.stats import get_duration_ms, statsd_client
31
+ from canvas_sdk.utils import metrics
32
+ from canvas_sdk.utils.metrics import measured, statsd_client
33
33
  from logger import log
34
34
  from plugin_runner.authentication import token_for_plugin
35
35
  from plugin_runner.installation import install_plugins
@@ -177,121 +177,118 @@ class PluginRunner(PluginRunnerServicer):
177
177
  self, request: EventRequest, context: Any
178
178
  ) -> AsyncGenerator[EventResponse, None]:
179
179
  """This is invoked when an event comes in."""
180
- event_start_time = time.time()
181
180
  event = Event(request)
182
- event_type = event.type
183
- event_name = event.name
184
- relevant_plugins = EVENT_HANDLER_MAP[event_name]
185
- relevant_plugin_handlers = []
186
-
187
- log.debug(f"Processing {relevant_plugins} for {event_name}")
188
- sentry_sdk.set_tag("event-name", event_name)
189
-
190
- if relevant_plugins:
191
- await reconnect_if_needed()
192
-
193
- if event_type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]:
194
- plugin_name = event.target.id
195
- # filter only for the plugin(s) that were created/updated
196
- relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
197
- elif event_type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
198
- # The target plugin's name will be part of the home-app URL path, so other plugins that
199
- # respond to SimpleAPI request events are not relevant
200
- plugin_name = event.context["plugin_name"]
201
- relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
202
-
203
- effect_list = []
204
-
205
- for plugin_name in relevant_plugins:
206
- log.debug(f"Processing {plugin_name}")
207
- sentry_sdk.set_tag("plugin-name", plugin_name)
208
-
209
- plugin = LOADED_PLUGINS[plugin_name]
210
- handler_class = plugin["class"]
211
- base_plugin_name = plugin_name.split(":")[0]
181
+ with metrics.measure(
182
+ metrics.get_qualified_name(self.HandleEvent), extra_tags={"event": event.name}
183
+ ):
184
+ event_type = event.type
185
+ event_name = event.name
186
+ relevant_plugins = EVENT_HANDLER_MAP[event_name]
187
+ relevant_plugin_handlers = []
188
+
189
+ log.debug(f"Processing {relevant_plugins} for {event_name}")
190
+ sentry_sdk.set_tag("event-name", event_name)
191
+
192
+ if relevant_plugins:
193
+ await reconnect_if_needed()
194
+
195
+ if event_type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]:
196
+ plugin_name = event.target.id
197
+ # filter only for the plugin(s) that were created/updated
198
+ relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
199
+ elif event_type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
200
+ # The target plugin's name will be part of the home-app URL path, so other plugins that
201
+ # respond to SimpleAPI request events are not relevant
202
+ plugin_name = event.context["plugin_name"]
203
+ relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
204
+
205
+ effect_list = []
206
+
207
+ for plugin_name in relevant_plugins:
208
+ log.debug(f"Processing {plugin_name}")
209
+ sentry_sdk.set_tag("plugin-name", plugin_name)
210
+
211
+ plugin = LOADED_PLUGINS[plugin_name]
212
+ handler_class = plugin["class"]
213
+ base_plugin_name = plugin_name.split(":")[0]
214
+
215
+ secrets = plugin.get("secrets", {})
216
+
217
+ secrets.update(
218
+ {"graphql_jwt": token_for_plugin(plugin_name=plugin_name, audience="home")}
219
+ )
212
220
 
213
- secrets = plugin.get("secrets", {})
221
+ try:
222
+ handler = handler_class(event, secrets, ENVIRONMENT)
214
223
 
215
- secrets.update(
216
- {"graphql_jwt": token_for_plugin(plugin_name=plugin_name, audience="home")}
217
- )
224
+ if not handler.accept_event():
225
+ continue
226
+ relevant_plugin_handlers.append(handler_class)
218
227
 
219
- try:
220
- handler = handler_class(event, secrets, ENVIRONMENT)
221
-
222
- if not handler.accept_event():
228
+ classname = (
229
+ handler.__class__.__name__
230
+ if isinstance(handler, ClinicalQualityMeasure)
231
+ else None
232
+ )
233
+ handler_name = metrics.get_qualified_name(handler.compute)
234
+ with metrics.measure(
235
+ name=handler_name,
236
+ extra_tags={
237
+ "plugin": base_plugin_name,
238
+ "event": event_name,
239
+ },
240
+ ):
241
+ _effects = await sync_to_async(handler.compute)()
242
+ effects = [
243
+ Effect(
244
+ type=effect.type,
245
+ payload=effect.payload,
246
+ plugin_name=base_plugin_name,
247
+ classname=classname,
248
+ handler_name=handler_name,
249
+ )
250
+ for effect in _effects
251
+ ]
252
+
253
+ effects = validate_effects(effects)
254
+
255
+ apply_effects_to_context(effects, event=event)
256
+
257
+ log.info(f"{plugin_name}.compute() completed.")
258
+
259
+ except Exception as e:
260
+ log.error(f"Encountered exception in plugin {plugin_name}:")
261
+
262
+ for error_line_with_newlines in traceback.format_exception(e):
263
+ for error_line in error_line_with_newlines.split("\n"):
264
+ log.error(error_line)
265
+
266
+ sentry_sdk.capture_exception(e)
223
267
  continue
224
- relevant_plugin_handlers.append(handler_class)
225
268
 
226
- classname = (
227
- handler.__class__.__name__
228
- if isinstance(handler, ClinicalQualityMeasure)
229
- else None
230
- )
231
-
232
- compute_start_time = time.time()
233
- _effects = await sync_to_async(handler.compute)()
234
- effects = [
235
- Effect(
236
- type=effect.type,
237
- payload=effect.payload,
238
- plugin_name=base_plugin_name,
239
- classname=classname,
269
+ effect_list += effects
270
+
271
+ sentry_sdk.set_tag("plugin-name", None)
272
+
273
+ # Special handling for SimpleAPI requests: if there were no relevant handlers (as determined
274
+ # by calling ignore_event on handlers), then set the effects list to be a single 404 Not
275
+ # Found response effect. If multiple handlers were able to respond, log an error and set the
276
+ # effects list to be a single 500 Internal Server Error response effect.
277
+ if event.type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
278
+ if len(relevant_plugin_handlers) == 0:
279
+ effect_list = [Response(status_code=HTTPStatus.NOT_FOUND).apply()]
280
+ elif len(relevant_plugin_handlers) > 1:
281
+ log.error(
282
+ f"Multiple handlers responded to {EventType.Name(EventType.SIMPLE_API_REQUEST)}"
283
+ f" {event.context['path']}"
240
284
  )
241
- for effect in _effects
242
- ]
243
-
244
- effects = validate_effects(effects)
245
-
246
- apply_effects_to_context(effects, event=event)
285
+ effect_list = [Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()]
247
286
 
248
- compute_duration = get_duration_ms(compute_start_time)
287
+ # Don't log anything if a plugin handler didn't actually run.
288
+ if relevant_plugins:
289
+ log.info(f"Responded to Event {event_name}.")
249
290
 
250
- log.info(f"{plugin_name}.compute() completed ({compute_duration} ms)")
251
- statsd_client.timing(
252
- "plugins.protocol_duration_ms",
253
- delta=compute_duration,
254
- tags={"plugin": plugin_name},
255
- )
256
- except Exception as e:
257
- log.error(f"Encountered exception in plugin {plugin_name}:")
258
-
259
- for error_line_with_newlines in traceback.format_exception(e):
260
- for error_line in error_line_with_newlines.split("\n"):
261
- log.error(error_line)
262
-
263
- sentry_sdk.capture_exception(e)
264
-
265
- continue
266
-
267
- effect_list += effects
268
-
269
- sentry_sdk.set_tag("plugin-name", None)
270
-
271
- # Special handling for SimpleAPI requests: if there were no relevant handlers (as determined
272
- # by calling ignore_event on handlers), then set the effects list to be a single 404 Not
273
- # Found response effect. If multiple handlers were able to respond, log an error and set the
274
- # effects list to be a single 500 Internal Server Error response effect.
275
- if event.type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
276
- if len(relevant_plugin_handlers) == 0:
277
- effect_list = [Response(status_code=HTTPStatus.NOT_FOUND).apply()]
278
- elif len(relevant_plugin_handlers) > 1:
279
- log.error(
280
- f"Multiple handlers responded to {EventType.Name(EventType.SIMPLE_API_REQUEST)}"
281
- f" {event.context['path']}"
282
- )
283
- effect_list = [Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()]
284
-
285
- event_duration = get_duration_ms(event_start_time)
286
-
287
- # Don't log anything if a plugin handler didn't actually run.
288
- if relevant_plugins:
289
- log.info(f"Responded to Event {event_name} ({event_duration} ms)")
290
- statsd_client.timing(
291
- "plugins.event_duration_ms", delta=event_duration, tags={"event": event_name}
292
- )
293
-
294
- yield EventResponse(success=True, effects=effect_list)
291
+ yield EventResponse(success=True, effects=effect_list)
295
292
 
296
293
  async def ReloadPlugins(
297
294
  self, request: ReloadPluginsRequest, context: Any
@@ -560,6 +557,7 @@ def refresh_event_type_map() -> None:
560
557
  log.warning(f"Unknown RESPONDS_TO type: {type(responds_to)}")
561
558
 
562
559
 
560
+ @measured
563
561
  def load_plugins(specified_plugin_paths: list[str] | None = None) -> None:
564
562
  """Load the plugins."""
565
563
  # first mark each plugin as inactive since we want to remove it from
@@ -596,14 +594,9 @@ def load_plugins(specified_plugin_paths: list[str] | None = None) -> None:
596
594
 
597
595
  refresh_event_type_map()
598
596
 
599
- log_nr_event_handlers()
600
-
601
-
602
- def log_nr_event_handlers() -> None:
603
- """Log the number of event handlers for each event."""
604
597
  for key in EventType.keys(): # noqa: SIM118
605
598
  value = len(EVENT_HANDLER_MAP[key]) if key in EVENT_HANDLER_MAP else 0
606
- statsd_client.timing("plugins.event_nr_handlers", value, tags={"event": key})
599
+ statsd_client.gauge("plugins.event_handler_count", value, tags={"event": key})
607
600
 
608
601
 
609
602
  _cleanup_coroutines = []