plain.observer 0.10.0__tar.gz → 0.11.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.
Potentially problematic release.
This version of plain.observer might be problematic. Click here for more details.
- {plain_observer-0.10.0 → plain_observer-0.11.0}/PKG-INFO +1 -1
- plain_observer-0.11.0/plain/observer/AGENTS.md +6 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/CHANGELOG.md +23 -0
- plain_observer-0.11.0/plain/observer/core.py +172 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/otel.py +18 -40
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/toolbar.py +1 -1
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/views.py +5 -5
- {plain_observer-0.10.0 → plain_observer-0.11.0}/pyproject.toml +1 -1
- plain_observer-0.10.0/plain/observer/core.py +0 -74
- {plain_observer-0.10.0 → plain_observer-0.11.0}/.gitignore +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/LICENSE +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/README.md +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/README.md +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/__init__.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/admin.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/cli.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/config.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/default_settings.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/logging.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/migrations/0001_initial.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/migrations/0002_trace_share_created_at_trace_share_id_trace_summary_and_more.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/migrations/0003_span_plainobserv_span_id_e7ade3_idx.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/migrations/0004_trace_app_name_trace_app_version.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/migrations/0005_log_log_plainobserv_trace_i_fcfb7d_idx_and_more.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/migrations/0006_remove_log_logger.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/migrations/__init__.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/models.py +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/partials/log.html +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/partials/span.html +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/trace.html +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/trace_detail.html +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/trace_share.html +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/traces.html +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/toolbar/observer.html +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/toolbar/observer_button.html +0 -0
- {plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/urls.py +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Plain Observer AGENTS.md
|
|
2
|
+
|
|
3
|
+
- Send a request and record traces: `plain agent request /some/path --user 1 --header "Observer: persist"`
|
|
4
|
+
- Find traces by request ID: `plain observer traces --request-id abc-123-def`
|
|
5
|
+
- Diagnose trace: `plain observer diagnose [TRACE_ID]`
|
|
6
|
+
- Output raw trace data: `plain observer trace [TRACE_ID] --json`
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# plain-observer changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0](https://github.com/dropseed/plain/releases/plain-observer@0.11.0) (2025-10-08)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Observer can now be enabled in DEBUG mode using an `Observer` HTTP header (e.g., `Observer: persist` or `Observer: summary`), which takes precedence over cookies ([cba149a](https://github.com/dropseed/plain/commit/cba149a40e))
|
|
8
|
+
- Added validation for observer mode values that raises helpful errors in DEBUG mode when invalid values are provided ([cba149a](https://github.com/dropseed/plain/commit/cba149a40e))
|
|
9
|
+
- Refactored `Observer` class to accept cookies and headers as constructor parameters, with new `from_request()` and `from_otel_context()` factory methods for improved testability ([cba149a](https://github.com/dropseed/plain/commit/cba149a40e))
|
|
10
|
+
- Added AGENTS.md file with helpful commands for AI agents working with Plain Observer ([cba149a](https://github.com/dropseed/plain/commit/cba149a40e))
|
|
11
|
+
|
|
12
|
+
### Upgrade instructions
|
|
13
|
+
|
|
14
|
+
- No changes required
|
|
15
|
+
|
|
16
|
+
## [0.10.1](https://github.com/dropseed/plain/releases/plain-observer@0.10.1) (2025-10-08)
|
|
17
|
+
|
|
18
|
+
### What's changed
|
|
19
|
+
|
|
20
|
+
- Fixed content negotiation priority in trace detail and shared views to prefer HTML over JSON by default ([00212835aa](https://github.com/dropseed/plain/commit/00212835aa))
|
|
21
|
+
|
|
22
|
+
### Upgrade instructions
|
|
23
|
+
|
|
24
|
+
- No changes required
|
|
25
|
+
|
|
3
26
|
## [0.10.0](https://github.com/dropseed/plain/releases/plain-observer@0.10.0) (2025-10-07)
|
|
4
27
|
|
|
5
28
|
### What's changed
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import MutableMapping
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
|
+
|
|
8
|
+
from opentelemetry import baggage
|
|
9
|
+
|
|
10
|
+
from plain.http import Response
|
|
11
|
+
from plain.http.cookie import unsign_cookie_value
|
|
12
|
+
from plain.runtime import settings
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from plain.http import Request
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ObserverMode(Enum):
|
|
21
|
+
"""Observer operation modes."""
|
|
22
|
+
|
|
23
|
+
SUMMARY = "summary" # Real-time monitoring only, no DB export
|
|
24
|
+
PERSIST = "persist" # Real-time monitoring + DB export
|
|
25
|
+
DISABLED = "disabled" # Observer explicitly disabled
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def validate(cls, mode: str | None, source: str = "value") -> str | None:
|
|
29
|
+
"""Validate observer mode value.
|
|
30
|
+
|
|
31
|
+
In DEBUG mode, raises ValueError for invalid values.
|
|
32
|
+
In production, logs debug message and returns None for invalid values.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
mode: The mode value to validate
|
|
36
|
+
source: Description of where the mode came from (for error messages)
|
|
37
|
+
|
|
38
|
+
Returns the mode if valid, None otherwise.
|
|
39
|
+
"""
|
|
40
|
+
if mode is None:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
valid_modes = (cls.SUMMARY.value, cls.PERSIST.value, cls.DISABLED.value)
|
|
44
|
+
|
|
45
|
+
if mode not in valid_modes:
|
|
46
|
+
if settings.DEBUG:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Invalid Observer {source}: '{mode}'. "
|
|
49
|
+
f"Valid values are: {', '.join(valid_modes)}"
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
logger.debug(
|
|
53
|
+
"Invalid observer mode %s: '%s'. Expected one of: %s",
|
|
54
|
+
source,
|
|
55
|
+
mode,
|
|
56
|
+
valid_modes,
|
|
57
|
+
)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
return mode
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Observer:
|
|
64
|
+
"""Central class for managing observer state and operations."""
|
|
65
|
+
|
|
66
|
+
COOKIE_NAME = "observer"
|
|
67
|
+
DEBUG_HEADER_NAME = "Observer"
|
|
68
|
+
SUMMARY_COOKIE_DURATION = 60 * 60 * 24 * 7 # 1 week in seconds
|
|
69
|
+
PERSIST_COOKIE_DURATION = 60 * 60 * 24 # 1 day in seconds
|
|
70
|
+
|
|
71
|
+
def __init__(self, *, cookies: dict[str, str], headers: dict[str, str]) -> None:
|
|
72
|
+
self.cookies = cookies
|
|
73
|
+
self.headers = headers
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_request(cls, request: Request) -> Observer:
|
|
77
|
+
"""Create an Observer instance from a request object."""
|
|
78
|
+
return cls(cookies=request.cookies, headers=request.headers)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_otel_context(cls, context: Any) -> Observer:
|
|
82
|
+
"""Create an Observer instance from an OpenTelemetry context.
|
|
83
|
+
|
|
84
|
+
This method extracts cookies and headers from the OTEL baggage.
|
|
85
|
+
"""
|
|
86
|
+
cookies = cast(
|
|
87
|
+
MutableMapping[str, str] | None,
|
|
88
|
+
baggage.get_baggage("http.request.cookies", context),
|
|
89
|
+
)
|
|
90
|
+
if not cookies:
|
|
91
|
+
cookies = {}
|
|
92
|
+
|
|
93
|
+
headers = cast(
|
|
94
|
+
MutableMapping[str, str] | None,
|
|
95
|
+
baggage.get_baggage("http.request.headers", context),
|
|
96
|
+
)
|
|
97
|
+
if not headers:
|
|
98
|
+
headers = {}
|
|
99
|
+
|
|
100
|
+
return cls(cookies=cookies, headers=headers)
|
|
101
|
+
|
|
102
|
+
def mode(self) -> str | None:
|
|
103
|
+
"""Get the current observer mode from header (DEBUG only) or cookie.
|
|
104
|
+
|
|
105
|
+
In DEBUG mode, the Observer header takes precedence over the cookie.
|
|
106
|
+
Returns 'summary', 'persist', 'disabled', or None.
|
|
107
|
+
|
|
108
|
+
Raises ValueError if invalid value is found (DEBUG only).
|
|
109
|
+
"""
|
|
110
|
+
# Check Observer header in DEBUG mode (takes precedence)
|
|
111
|
+
if settings.DEBUG:
|
|
112
|
+
if observer_header := self.headers.get(self.DEBUG_HEADER_NAME):
|
|
113
|
+
observer_mode = observer_header.lower()
|
|
114
|
+
return ObserverMode.validate(observer_mode, source="header value")
|
|
115
|
+
|
|
116
|
+
# Check cookie
|
|
117
|
+
observer_cookie = self.cookies.get(self.COOKIE_NAME)
|
|
118
|
+
if not observer_cookie:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
mode = unsign_cookie_value(self.COOKIE_NAME, observer_cookie, default=None)
|
|
123
|
+
return ObserverMode.validate(mode, source="cookie value")
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.debug("Failed to unsign observer cookie: %s", e)
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def is_enabled(self) -> bool:
|
|
129
|
+
"""Check if observer is enabled (either summary or persist mode)."""
|
|
130
|
+
return self.mode() in (ObserverMode.SUMMARY.value, ObserverMode.PERSIST.value)
|
|
131
|
+
|
|
132
|
+
def is_persisting(self) -> bool:
|
|
133
|
+
"""Check if full persisting (with DB export) is enabled."""
|
|
134
|
+
return self.mode() == ObserverMode.PERSIST.value
|
|
135
|
+
|
|
136
|
+
def is_summarizing(self) -> bool:
|
|
137
|
+
"""Check if summary mode is enabled."""
|
|
138
|
+
return self.mode() == ObserverMode.SUMMARY.value
|
|
139
|
+
|
|
140
|
+
def is_disabled(self) -> bool:
|
|
141
|
+
"""Check if observer is explicitly disabled."""
|
|
142
|
+
return self.mode() == ObserverMode.DISABLED.value
|
|
143
|
+
|
|
144
|
+
def enable_summary_mode(self, response: Response) -> None:
|
|
145
|
+
"""Enable summary mode (real-time monitoring, no DB export)."""
|
|
146
|
+
response.set_signed_cookie(
|
|
147
|
+
self.COOKIE_NAME,
|
|
148
|
+
ObserverMode.SUMMARY.value,
|
|
149
|
+
max_age=self.SUMMARY_COOKIE_DURATION,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def enable_persist_mode(self, response: Response) -> None:
|
|
153
|
+
"""Enable full persist mode (real-time monitoring + DB export)."""
|
|
154
|
+
response.set_signed_cookie(
|
|
155
|
+
self.COOKIE_NAME,
|
|
156
|
+
ObserverMode.PERSIST.value,
|
|
157
|
+
max_age=self.PERSIST_COOKIE_DURATION,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def disable(self, response: Response) -> None:
|
|
161
|
+
"""Disable observer by setting cookie to disabled."""
|
|
162
|
+
response.set_signed_cookie(
|
|
163
|
+
self.COOKIE_NAME,
|
|
164
|
+
ObserverMode.DISABLED.value,
|
|
165
|
+
max_age=self.PERSIST_COOKIE_DURATION,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def get_current_trace_summary(self) -> str | None:
|
|
169
|
+
"""Get performance summary string for the currently active trace."""
|
|
170
|
+
from .otel import get_current_trace_summary
|
|
171
|
+
|
|
172
|
+
return get_current_trace_summary()
|
|
@@ -8,7 +8,7 @@ from collections.abc import MutableMapping, Sequence
|
|
|
8
8
|
from typing import TYPE_CHECKING, Any, cast
|
|
9
9
|
|
|
10
10
|
import opentelemetry.context as context_api
|
|
11
|
-
from opentelemetry import
|
|
11
|
+
from opentelemetry import trace
|
|
12
12
|
from opentelemetry.context import Context
|
|
13
13
|
from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor, sampling
|
|
14
14
|
from opentelemetry.semconv.attributes import url_attributes
|
|
@@ -20,7 +20,6 @@ from opentelemetry.trace import (
|
|
|
20
20
|
format_trace_id,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
-
from plain.http.cookie import unsign_cookie_value
|
|
24
23
|
from plain.logs import app_logger
|
|
25
24
|
from plain.models.otel import suppress_db_tracing
|
|
26
25
|
from plain.runtime import settings
|
|
@@ -105,28 +104,20 @@ class ObserverSampler(sampling.Sampler):
|
|
|
105
104
|
attributes=attributes,
|
|
106
105
|
)
|
|
107
106
|
|
|
108
|
-
# If no processor decision, check cookies directly for root spans
|
|
107
|
+
# If no processor decision, check headers and cookies directly for root spans
|
|
109
108
|
decision: sampling.Decision | None = None
|
|
110
109
|
if parent_context:
|
|
111
|
-
# Check
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
ObserverMode.PERSIST.value,
|
|
123
|
-
ObserverMode.SUMMARY.value,
|
|
124
|
-
):
|
|
125
|
-
# Always use RECORD_AND_SAMPLE so ParentBased works correctly
|
|
126
|
-
# The processor will check the mode to decide whether to export
|
|
127
|
-
decision = sampling.Decision.RECORD_AND_SAMPLE
|
|
128
|
-
else:
|
|
129
|
-
decision = sampling.Decision.DROP
|
|
110
|
+
# Check Observer header (DEBUG only) and cookies
|
|
111
|
+
mode = Observer.from_otel_context(parent_context).mode()
|
|
112
|
+
|
|
113
|
+
# Set decision based on mode
|
|
114
|
+
if mode in (ObserverMode.PERSIST.value, ObserverMode.SUMMARY.value):
|
|
115
|
+
# Always use RECORD_AND_SAMPLE so ParentBased works correctly
|
|
116
|
+
# The processor will check the mode to decide whether to export
|
|
117
|
+
decision = sampling.Decision.RECORD_AND_SAMPLE
|
|
118
|
+
elif mode == ObserverMode.DISABLED.value:
|
|
119
|
+
# Explicitly disabled - never sample even with remote parent
|
|
120
|
+
decision = sampling.Decision.DROP
|
|
130
121
|
|
|
131
122
|
# If there are links, assume it is to another trace/span that we are keeping
|
|
132
123
|
if links:
|
|
@@ -441,25 +432,12 @@ class ObserverSpanProcessor(SpanProcessor):
|
|
|
441
432
|
if not (context := parent_context or context_api.get_current()):
|
|
442
433
|
return None
|
|
443
434
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
baggage.get_baggage("http.request.cookies", context),
|
|
447
|
-
)
|
|
448
|
-
if not cookies:
|
|
449
|
-
return None
|
|
450
|
-
|
|
451
|
-
observer_cookie = cookies.get(Observer.COOKIE_NAME)
|
|
452
|
-
if not observer_cookie:
|
|
453
|
-
return None
|
|
435
|
+
# Check Observer header (DEBUG only) and cookies
|
|
436
|
+
mode = Observer.from_otel_context(context).mode()
|
|
454
437
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
)
|
|
459
|
-
if mode in (ObserverMode.SUMMARY.value, ObserverMode.PERSIST.value):
|
|
460
|
-
return mode
|
|
461
|
-
except Exception as e:
|
|
462
|
-
logger.warning("Failed to unsign observer cookie: %s", e)
|
|
438
|
+
# Only return valid recording modes (summary/persist), not disabled
|
|
439
|
+
if mode in (ObserverMode.SUMMARY.value, ObserverMode.PERSIST.value):
|
|
440
|
+
return mode
|
|
463
441
|
|
|
464
442
|
return None
|
|
465
443
|
|
|
@@ -17,7 +17,7 @@ class ObserverToolbarItem(ToolbarItem):
|
|
|
17
17
|
@cached_property
|
|
18
18
|
def observer(self) -> Observer:
|
|
19
19
|
"""Get the Observer instance for this request."""
|
|
20
|
-
return Observer(self.request)
|
|
20
|
+
return Observer.from_request(self.request)
|
|
21
21
|
|
|
22
22
|
def get_template_context(self) -> dict[str, Any]:
|
|
23
23
|
context = super().get_template_context()
|
|
@@ -37,13 +37,13 @@ class ObserverTracesView(AuthViewMixin, HTMXViewMixin, ListView):
|
|
|
37
37
|
|
|
38
38
|
def get_template_context(self) -> dict[str, Any]:
|
|
39
39
|
context = super().get_template_context()
|
|
40
|
-
context["observer"] = Observer(self.request)
|
|
40
|
+
context["observer"] = Observer.from_request(self.request)
|
|
41
41
|
return context
|
|
42
42
|
|
|
43
43
|
def htmx_put_mode(self) -> Response:
|
|
44
44
|
"""Set observer mode via HTMX PUT."""
|
|
45
45
|
mode = self.request.data.get("mode")
|
|
46
|
-
observer = Observer(self.request)
|
|
46
|
+
observer = Observer.from_request(self.request)
|
|
47
47
|
|
|
48
48
|
response = Response(status_code=204)
|
|
49
49
|
response.headers["HX-Refresh"] = "true"
|
|
@@ -70,7 +70,7 @@ class ObserverTracesView(AuthViewMixin, HTMXViewMixin, ListView):
|
|
|
70
70
|
"""Handle POST requests to set observer mode."""
|
|
71
71
|
action = self.request.data.get("observe_action")
|
|
72
72
|
if action == "summary":
|
|
73
|
-
observer = Observer(self.request)
|
|
73
|
+
observer = Observer.from_request(self.request)
|
|
74
74
|
response = Response(status_code=204)
|
|
75
75
|
observer.enable_summary_mode(response)
|
|
76
76
|
return response
|
|
@@ -95,7 +95,7 @@ class ObserverTraceDetailView(AuthViewMixin, HTMXViewMixin, DetailView):
|
|
|
95
95
|
|
|
96
96
|
def get(self) -> Response | dict[str, Any]:
|
|
97
97
|
"""Return trace data as HTML, JSON, or logs based on content negotiation."""
|
|
98
|
-
preferred = self.request.get_preferred_type("
|
|
98
|
+
preferred = self.request.get_preferred_type("text/html", "application/json")
|
|
99
99
|
if (
|
|
100
100
|
preferred == "application/json"
|
|
101
101
|
or self.request.query_params.get("format") == "json"
|
|
@@ -152,7 +152,7 @@ class ObserverTraceSharedView(DetailView):
|
|
|
152
152
|
|
|
153
153
|
def get(self) -> Response:
|
|
154
154
|
"""Return trace data as HTML or JSON based on content negotiation."""
|
|
155
|
-
preferred = self.request.get_preferred_type("
|
|
155
|
+
preferred = self.request.get_preferred_type("text/html", "application/json")
|
|
156
156
|
if (
|
|
157
157
|
preferred == "application/json"
|
|
158
158
|
or self.request.query_params.get("format") == "json"
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
from plain.http import Request, Response
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ObserverMode(Enum):
|
|
9
|
-
"""Observer operation modes."""
|
|
10
|
-
|
|
11
|
-
SUMMARY = "summary" # Real-time monitoring only, no DB export
|
|
12
|
-
PERSIST = "persist" # Real-time monitoring + DB export
|
|
13
|
-
DISABLED = "disabled" # Observer explicitly disabled
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Observer:
|
|
17
|
-
"""Central class for managing observer state and operations."""
|
|
18
|
-
|
|
19
|
-
COOKIE_NAME = "observer"
|
|
20
|
-
SUMMARY_COOKIE_DURATION = 60 * 60 * 24 * 7 # 1 week in seconds
|
|
21
|
-
PERSIST_COOKIE_DURATION = 60 * 60 * 24 # 1 day in seconds
|
|
22
|
-
|
|
23
|
-
def __init__(self, request: Request) -> None:
|
|
24
|
-
self.request = request
|
|
25
|
-
|
|
26
|
-
def mode(self) -> str | None:
|
|
27
|
-
"""Get the current observer mode from signed cookie."""
|
|
28
|
-
return self.request.get_signed_cookie(self.COOKIE_NAME, default=None)
|
|
29
|
-
|
|
30
|
-
def is_enabled(self) -> bool:
|
|
31
|
-
"""Check if observer is enabled (either summary or persist mode)."""
|
|
32
|
-
return self.mode() in (ObserverMode.SUMMARY.value, ObserverMode.PERSIST.value)
|
|
33
|
-
|
|
34
|
-
def is_persisting(self) -> bool:
|
|
35
|
-
"""Check if full persisting (with DB export) is enabled."""
|
|
36
|
-
return self.mode() == ObserverMode.PERSIST.value
|
|
37
|
-
|
|
38
|
-
def is_summarizing(self) -> bool:
|
|
39
|
-
"""Check if summary mode is enabled."""
|
|
40
|
-
return self.mode() == ObserverMode.SUMMARY.value
|
|
41
|
-
|
|
42
|
-
def is_disabled(self) -> bool:
|
|
43
|
-
"""Check if observer is explicitly disabled."""
|
|
44
|
-
return self.mode() == ObserverMode.DISABLED.value
|
|
45
|
-
|
|
46
|
-
def enable_summary_mode(self, response: Response) -> None:
|
|
47
|
-
"""Enable summary mode (real-time monitoring, no DB export)."""
|
|
48
|
-
response.set_signed_cookie(
|
|
49
|
-
self.COOKIE_NAME,
|
|
50
|
-
ObserverMode.SUMMARY.value,
|
|
51
|
-
max_age=self.SUMMARY_COOKIE_DURATION,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
def enable_persist_mode(self, response: Response) -> None:
|
|
55
|
-
"""Enable full persist mode (real-time monitoring + DB export)."""
|
|
56
|
-
response.set_signed_cookie(
|
|
57
|
-
self.COOKIE_NAME,
|
|
58
|
-
ObserverMode.PERSIST.value,
|
|
59
|
-
max_age=self.PERSIST_COOKIE_DURATION,
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
def disable(self, response: Response) -> None:
|
|
63
|
-
"""Disable observer by setting cookie to disabled."""
|
|
64
|
-
response.set_signed_cookie(
|
|
65
|
-
self.COOKIE_NAME,
|
|
66
|
-
ObserverMode.DISABLED.value,
|
|
67
|
-
max_age=self.PERSIST_COOKIE_DURATION,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
def get_current_trace_summary(self) -> str | None:
|
|
71
|
-
"""Get performance summary string for the currently active trace."""
|
|
72
|
-
from .otel import get_current_trace_summary
|
|
73
|
-
|
|
74
|
-
return get_current_trace_summary()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/migrations/0006_remove_log_logger.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/partials/log.html
RENAMED
|
File without changes
|
{plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/partials/span.html
RENAMED
|
File without changes
|
{plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/trace.html
RENAMED
|
File without changes
|
{plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/trace_detail.html
RENAMED
|
File without changes
|
{plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/trace_share.html
RENAMED
|
File without changes
|
{plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/observer/traces.html
RENAMED
|
File without changes
|
{plain_observer-0.10.0 → plain_observer-0.11.0}/plain/observer/templates/toolbar/observer.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|