plain.observer 0.0.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 plain.observer might be problematic. Click here for more details.
- plain/observer/CHANGELOG.md +1 -0
- plain/observer/README.md +3 -0
- plain/observer/__init__.py +0 -0
- plain/observer/admin.py +102 -0
- plain/observer/cli.py +23 -0
- plain/observer/config.py +36 -0
- plain/observer/core.py +63 -0
- plain/observer/default_settings.py +9 -0
- plain/observer/migrations/0001_initial.py +96 -0
- plain/observer/migrations/__init__.py +0 -0
- plain/observer/models.py +355 -0
- plain/observer/otel.py +335 -0
- plain/observer/templates/admin/observer/trace_detail.html +10 -0
- plain/observer/templates/observer/_trace_detail.html +364 -0
- plain/observer/templates/observer/traces.html +288 -0
- plain/observer/templates/toolbar/observer.html +42 -0
- plain/observer/templates/toolbar/observer_button.html +45 -0
- plain/observer/urls.py +10 -0
- plain/observer/views.py +105 -0
- plain_observer-0.0.0.dist-info/METADATA +16 -0
- plain_observer-0.0.0.dist-info/RECORD +23 -0
- plain_observer-0.0.0.dist-info/WHEEL +4 -0
- plain_observer-0.0.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# plain-observer changelog
|
plain/observer/README.md
ADDED
|
File without changes
|
plain/observer/admin.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
|
|
3
|
+
from plain.admin.toolbar import ToolbarPanel, register_toolbar_panel
|
|
4
|
+
from plain.admin.views import (
|
|
5
|
+
AdminModelDetailView,
|
|
6
|
+
AdminModelListView,
|
|
7
|
+
AdminViewset,
|
|
8
|
+
register_viewset,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from .core import Observer
|
|
12
|
+
from .models import Span, Trace
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register_viewset
|
|
16
|
+
class TraceViewset(AdminViewset):
|
|
17
|
+
class ListView(AdminModelListView):
|
|
18
|
+
nav_section = "Observer"
|
|
19
|
+
model = Trace
|
|
20
|
+
fields = [
|
|
21
|
+
"trace_id",
|
|
22
|
+
"request_id",
|
|
23
|
+
"session_id",
|
|
24
|
+
"user_id",
|
|
25
|
+
"start_time",
|
|
26
|
+
]
|
|
27
|
+
allow_global_search = False
|
|
28
|
+
# Actually want a button to delete ALL! not possible yet
|
|
29
|
+
# actions = ["Delete"]
|
|
30
|
+
|
|
31
|
+
# def perform_action(self, action: str, target_pks: list):
|
|
32
|
+
# if action == "Delete":
|
|
33
|
+
# Trace.objects.filter(id__in=target_pks).delete()
|
|
34
|
+
|
|
35
|
+
class DetailView(AdminModelDetailView):
|
|
36
|
+
model = Trace
|
|
37
|
+
template_name = "admin/observer/trace_detail.html"
|
|
38
|
+
|
|
39
|
+
def get_template_context(self):
|
|
40
|
+
context = super().get_template_context()
|
|
41
|
+
trace_id = self.url_kwargs["pk"]
|
|
42
|
+
context["trace"] = Trace.objects.get(pk=trace_id)
|
|
43
|
+
context["show_delete_button"] = False
|
|
44
|
+
return context
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@register_viewset
|
|
48
|
+
class SpanViewset(AdminViewset):
|
|
49
|
+
class ListView(AdminModelListView):
|
|
50
|
+
nav_section = "Observer"
|
|
51
|
+
model = Span
|
|
52
|
+
fields = [
|
|
53
|
+
"name",
|
|
54
|
+
"kind",
|
|
55
|
+
"status",
|
|
56
|
+
"span_id",
|
|
57
|
+
"parent_id",
|
|
58
|
+
"start_time",
|
|
59
|
+
]
|
|
60
|
+
queryset_order = ["-pk"]
|
|
61
|
+
allow_global_search = False
|
|
62
|
+
displays = ["Parents only"]
|
|
63
|
+
search_fields = ["name", "span_id", "parent_id"]
|
|
64
|
+
|
|
65
|
+
def get_objects(self):
|
|
66
|
+
return (
|
|
67
|
+
super()
|
|
68
|
+
.get_objects()
|
|
69
|
+
.only(
|
|
70
|
+
"name",
|
|
71
|
+
"kind",
|
|
72
|
+
"span_id",
|
|
73
|
+
"parent_id",
|
|
74
|
+
"start_time",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def get_initial_queryset(self):
|
|
79
|
+
queryset = super().get_initial_queryset()
|
|
80
|
+
if self.display == "Parents only":
|
|
81
|
+
queryset = queryset.filter(parent_id="")
|
|
82
|
+
return queryset
|
|
83
|
+
|
|
84
|
+
class DetailView(AdminModelDetailView):
|
|
85
|
+
model = Span
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@register_toolbar_panel
|
|
89
|
+
class ObserverToolbarPanel(ToolbarPanel):
|
|
90
|
+
name = "Observer"
|
|
91
|
+
template_name = "toolbar/observer.html"
|
|
92
|
+
button_template_name = "toolbar/observer_button.html"
|
|
93
|
+
|
|
94
|
+
@cached_property
|
|
95
|
+
def observer(self):
|
|
96
|
+
"""Get the Observer instance for this request."""
|
|
97
|
+
return Observer(self.request)
|
|
98
|
+
|
|
99
|
+
def get_template_context(self):
|
|
100
|
+
context = super().get_template_context()
|
|
101
|
+
context["observer"] = self.observer
|
|
102
|
+
return context
|
plain/observer/cli.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from plain.cli import register_cli
|
|
4
|
+
from plain.observer.models import Trace
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register_cli("observer")
|
|
8
|
+
@click.group("observer")
|
|
9
|
+
def observer_cli():
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@observer_cli.command()
|
|
14
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompt.")
|
|
15
|
+
def clear(force: bool):
|
|
16
|
+
"""Clear all observer data."""
|
|
17
|
+
if not force:
|
|
18
|
+
click.confirm(
|
|
19
|
+
"Are you sure you want to clear all observer data? This cannot be undone.",
|
|
20
|
+
abort=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
print("Deleted", Trace.objects.all().delete())
|
plain/observer/config.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from opentelemetry import trace
|
|
2
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
3
|
+
|
|
4
|
+
from plain.packages import PackageConfig, register_config
|
|
5
|
+
|
|
6
|
+
from .otel import ObserverSampler, ObserverSpanProcessor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register_config
|
|
10
|
+
class Config(PackageConfig):
|
|
11
|
+
package_label = "plainobserver"
|
|
12
|
+
|
|
13
|
+
def ready(self):
|
|
14
|
+
if self.has_existing_trace_provider():
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
self.setup_observer()
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def has_existing_trace_provider() -> bool:
|
|
21
|
+
"""Check if there is an existing trace provider."""
|
|
22
|
+
current_provider = trace.get_tracer_provider()
|
|
23
|
+
return current_provider and not isinstance(
|
|
24
|
+
current_provider, trace.ProxyTracerProvider
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def setup_observer() -> None:
|
|
29
|
+
sampler = ObserverSampler()
|
|
30
|
+
provider = TracerProvider(sampler=sampler)
|
|
31
|
+
|
|
32
|
+
# Add our combined processor that handles both memory storage and export
|
|
33
|
+
observer_processor = ObserverSpanProcessor()
|
|
34
|
+
provider.add_span_processor(observer_processor)
|
|
35
|
+
|
|
36
|
+
trace.set_tracer_provider(provider)
|
plain/observer/core.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ObserverMode(Enum):
|
|
5
|
+
"""Observer operation modes."""
|
|
6
|
+
|
|
7
|
+
SUMMARY = "summary" # Real-time monitoring only, no DB export
|
|
8
|
+
PERSIST = "persist" # Real-time monitoring + DB export
|
|
9
|
+
DISABLED = "disabled" # Observer explicitly disabled
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Observer:
|
|
13
|
+
"""Central class for managing observer state and operations."""
|
|
14
|
+
|
|
15
|
+
COOKIE_NAME = "observer"
|
|
16
|
+
COOKIE_DURATION = 60 * 60 * 24 # 1 day in seconds
|
|
17
|
+
|
|
18
|
+
def __init__(self, request):
|
|
19
|
+
self.request = request
|
|
20
|
+
|
|
21
|
+
def mode(self):
|
|
22
|
+
"""Get the current observer mode from signed cookie."""
|
|
23
|
+
return self.request.get_signed_cookie(self.COOKIE_NAME, default=None)
|
|
24
|
+
|
|
25
|
+
def is_enabled(self):
|
|
26
|
+
"""Check if observer is enabled (either summary or persist mode)."""
|
|
27
|
+
return self.mode() in (ObserverMode.SUMMARY.value, ObserverMode.PERSIST.value)
|
|
28
|
+
|
|
29
|
+
def is_persisting(self):
|
|
30
|
+
"""Check if full persisting (with DB export) is enabled."""
|
|
31
|
+
return self.mode() == ObserverMode.PERSIST.value
|
|
32
|
+
|
|
33
|
+
def is_summarizing(self):
|
|
34
|
+
"""Check if summary mode is enabled."""
|
|
35
|
+
return self.mode() == ObserverMode.SUMMARY.value
|
|
36
|
+
|
|
37
|
+
def is_disabled(self):
|
|
38
|
+
"""Check if observer is explicitly disabled."""
|
|
39
|
+
return self.mode() == ObserverMode.DISABLED.value
|
|
40
|
+
|
|
41
|
+
def enable_summary_mode(self, response):
|
|
42
|
+
"""Enable summary mode (real-time monitoring, no DB export)."""
|
|
43
|
+
response.set_signed_cookie(
|
|
44
|
+
self.COOKIE_NAME, ObserverMode.SUMMARY.value, max_age=self.COOKIE_DURATION
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def enable_persist_mode(self, response):
|
|
48
|
+
"""Enable full persist mode (real-time monitoring + DB export)."""
|
|
49
|
+
response.set_signed_cookie(
|
|
50
|
+
self.COOKIE_NAME, ObserverMode.PERSIST.value, max_age=self.COOKIE_DURATION
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def disable(self, response):
|
|
54
|
+
"""Disable observer by setting cookie to disabled."""
|
|
55
|
+
response.set_signed_cookie(
|
|
56
|
+
self.COOKIE_NAME, ObserverMode.DISABLED.value, max_age=self.COOKIE_DURATION
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def get_current_trace_summary(self):
|
|
60
|
+
"""Get performance summary string for the currently active trace."""
|
|
61
|
+
from .otel import get_current_trace_summary
|
|
62
|
+
|
|
63
|
+
return get_current_trace_summary()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Generated by Plain 0.52.2 on 2025-07-17 02:27
|
|
2
|
+
|
|
3
|
+
import plain.models.deletion
|
|
4
|
+
from plain import models
|
|
5
|
+
from plain.models import migrations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name="Trace",
|
|
16
|
+
fields=[
|
|
17
|
+
("id", models.BigAutoField(auto_created=True, primary_key=True)),
|
|
18
|
+
("trace_id", models.CharField(max_length=255)),
|
|
19
|
+
("start_time", models.DateTimeField()),
|
|
20
|
+
("end_time", models.DateTimeField()),
|
|
21
|
+
("root_span_name", models.TextField(default="", required=False)),
|
|
22
|
+
(
|
|
23
|
+
"request_id",
|
|
24
|
+
models.CharField(default="", max_length=255, required=False),
|
|
25
|
+
),
|
|
26
|
+
(
|
|
27
|
+
"session_id",
|
|
28
|
+
models.CharField(default="", max_length=255, required=False),
|
|
29
|
+
),
|
|
30
|
+
(
|
|
31
|
+
"user_id",
|
|
32
|
+
models.CharField(default="", max_length=255, required=False),
|
|
33
|
+
),
|
|
34
|
+
],
|
|
35
|
+
options={
|
|
36
|
+
"ordering": ["-start_time"],
|
|
37
|
+
},
|
|
38
|
+
),
|
|
39
|
+
migrations.CreateModel(
|
|
40
|
+
name="Span",
|
|
41
|
+
fields=[
|
|
42
|
+
("id", models.BigAutoField(auto_created=True, primary_key=True)),
|
|
43
|
+
("span_id", models.CharField(max_length=255)),
|
|
44
|
+
("name", models.CharField(max_length=255)),
|
|
45
|
+
("kind", models.CharField(max_length=50)),
|
|
46
|
+
(
|
|
47
|
+
"parent_id",
|
|
48
|
+
models.CharField(default="", max_length=255, required=False),
|
|
49
|
+
),
|
|
50
|
+
("start_time", models.DateTimeField()),
|
|
51
|
+
("end_time", models.DateTimeField()),
|
|
52
|
+
("status", models.CharField(default="", max_length=50, required=False)),
|
|
53
|
+
("span_data", models.JSONField(default=dict, required=False)),
|
|
54
|
+
],
|
|
55
|
+
options={
|
|
56
|
+
"ordering": ["-start_time"],
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
migrations.AddConstraint(
|
|
60
|
+
model_name="trace",
|
|
61
|
+
constraint=models.UniqueConstraint(
|
|
62
|
+
fields=("trace_id",), name="observer_unique_trace_id"
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
migrations.AddField(
|
|
66
|
+
model_name="span",
|
|
67
|
+
name="trace",
|
|
68
|
+
field=models.ForeignKey(
|
|
69
|
+
on_delete=plain.models.deletion.CASCADE,
|
|
70
|
+
related_name="spans",
|
|
71
|
+
to="plainobserver.trace",
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
migrations.AddIndex(
|
|
75
|
+
model_name="span",
|
|
76
|
+
index=models.Index(
|
|
77
|
+
fields=["trace", "span_id"], name="plainobserv_trace_i_89a97c_idx"
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
migrations.AddIndex(
|
|
81
|
+
model_name="span",
|
|
82
|
+
index=models.Index(fields=["trace"], name="plainobserv_trace_i_84958a_idx"),
|
|
83
|
+
),
|
|
84
|
+
migrations.AddIndex(
|
|
85
|
+
model_name="span",
|
|
86
|
+
index=models.Index(
|
|
87
|
+
fields=["start_time"], name="plainobserv_start_t_cb47a3_idx"
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
migrations.AddConstraint(
|
|
91
|
+
model_name="span",
|
|
92
|
+
constraint=models.UniqueConstraint(
|
|
93
|
+
fields=("trace", "span_id"), name="observer_unique_span_id"
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
]
|
|
File without changes
|