plain.observer 0.1.0__py3-none-any.whl → 0.3.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.

@@ -0,0 +1,58 @@
1
+ # Generated by Plain 0.54.1 on 2025-07-21 16:01
2
+
3
+ from plain import models
4
+ from plain.models import migrations
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ dependencies = [
9
+ ("plainobserver", "0001_initial"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="trace",
15
+ name="share_created_at",
16
+ field=models.DateTimeField(allow_null=True, required=False),
17
+ ),
18
+ migrations.AddField(
19
+ model_name="trace",
20
+ name="share_id",
21
+ field=models.CharField(default="", max_length=32, required=False),
22
+ ),
23
+ migrations.AddField(
24
+ model_name="trace",
25
+ name="summary",
26
+ field=models.CharField(default="", max_length=255, required=False),
27
+ ),
28
+ migrations.AddIndex(
29
+ model_name="trace",
30
+ index=models.Index(
31
+ fields=["trace_id"], name="plainobserv_trace_i_075b48_idx"
32
+ ),
33
+ ),
34
+ migrations.AddIndex(
35
+ model_name="trace",
36
+ index=models.Index(
37
+ fields=["start_time"], name="plainobserv_start_t_636c80_idx"
38
+ ),
39
+ ),
40
+ migrations.AddIndex(
41
+ model_name="trace",
42
+ index=models.Index(
43
+ fields=["request_id"], name="plainobserv_request_d1d5b2_idx"
44
+ ),
45
+ ),
46
+ migrations.AddIndex(
47
+ model_name="trace",
48
+ index=models.Index(
49
+ fields=["share_id"], name="plainobserv_share_i_754f3c_idx"
50
+ ),
51
+ ),
52
+ migrations.AddIndex(
53
+ model_name="trace",
54
+ index=models.Index(
55
+ fields=["session_id"], name="plainobserv_session_350f42_idx"
56
+ ),
57
+ ),
58
+ ]
plain/observer/models.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import secrets
2
3
  from datetime import UTC, datetime
3
4
  from functools import cached_property
4
5
 
@@ -15,6 +16,8 @@ from opentelemetry.semconv.attributes import db_attributes
15
16
  from opentelemetry.trace import format_trace_id
16
17
 
17
18
  from plain import models
19
+ from plain.urls import reverse
20
+ from plain.utils import timezone
18
21
 
19
22
 
20
23
  @models.register_model
@@ -24,12 +27,17 @@ class Trace(models.Model):
24
27
  end_time = models.DateTimeField()
25
28
 
26
29
  root_span_name = models.TextField(default="", required=False)
30
+ summary = models.CharField(max_length=255, default="", required=False)
27
31
 
28
32
  # Plain fields
29
33
  request_id = models.CharField(max_length=255, default="", required=False)
30
34
  session_id = models.CharField(max_length=255, default="", required=False)
31
35
  user_id = models.CharField(max_length=255, default="", required=False)
32
36
 
37
+ # Shareable URL fields
38
+ share_id = models.CharField(max_length=32, default="", required=False)
39
+ share_created_at = models.DateTimeField(allow_null=True, required=False)
40
+
33
41
  class Meta:
34
42
  ordering = ["-start_time"]
35
43
  constraints = [
@@ -38,25 +46,43 @@ class Trace(models.Model):
38
46
  name="observer_unique_trace_id",
39
47
  )
40
48
  ]
49
+ indexes = [
50
+ models.Index(fields=["trace_id"]),
51
+ models.Index(fields=["start_time"]),
52
+ models.Index(fields=["request_id"]),
53
+ models.Index(fields=["share_id"]),
54
+ models.Index(fields=["session_id"]),
55
+ ]
41
56
 
42
57
  def __str__(self):
43
58
  return self.trace_id
44
59
 
60
+ def get_absolute_url(self):
61
+ """Return the canonical URL for this trace."""
62
+ return reverse("observer:trace_detail", trace_id=self.trace_id)
63
+
64
+ def generate_share_id(self):
65
+ """Generate a unique share ID for this trace."""
66
+ self.share_id = secrets.token_urlsafe(24)
67
+ self.share_created_at = timezone.now()
68
+ self.save(update_fields=["share_id", "share_created_at"])
69
+ return self.share_id
70
+
71
+ def remove_share_id(self):
72
+ """Remove the share ID from this trace."""
73
+ self.share_id = ""
74
+ self.share_created_at = None
75
+ self.save(update_fields=["share_id", "share_created_at"])
76
+
45
77
  def duration_ms(self):
46
78
  return (self.end_time - self.start_time).total_seconds() * 1000
47
79
 
48
- def get_trace_summary(self, spans=None):
80
+ def get_trace_summary(self, spans):
49
81
  """Get a concise summary string for toolbar display.
50
82
 
51
83
  Args:
52
84
  spans: Optional list of span objects. If not provided, will query from database.
53
85
  """
54
- # Get spans from database if not provided
55
- if spans is None:
56
- spans = list(self.spans.all())
57
-
58
- if not spans:
59
- return ""
60
86
 
61
87
  # Count database queries and track duplicates
62
88
  query_counts = {}
@@ -132,9 +158,6 @@ class Trace(models.Model):
132
158
  else None
133
159
  )
134
160
 
135
- # Create trace instance
136
- # Note: end_time might be None if there are active spans
137
- # This is OK since this trace is only used for summaries, not persistence
138
161
  return cls(
139
162
  trace_id=trace_id,
140
163
  start_time=start_time,
@@ -146,9 +169,27 @@ class Trace(models.Model):
146
169
  root_span_name=root_span.name if root_span else "",
147
170
  )
148
171
 
149
- def get_annotated_spans(self):
150
- """Return spans with annotations and nesting information."""
151
- spans = list(self.spans.all().order_by("start_time"))
172
+ def as_dict(self):
173
+ spans = [span.span_data for span in self.spans.all().order_by("start_time")]
174
+
175
+ return {
176
+ "trace_id": self.trace_id,
177
+ "start_time": self.start_time.isoformat(),
178
+ "end_time": self.end_time.isoformat(),
179
+ "duration_ms": self.duration_ms(),
180
+ "summary": self.summary,
181
+ "root_span_name": self.root_span_name,
182
+ "request_id": self.request_id,
183
+ "user_id": self.user_id,
184
+ "session_id": self.session_id,
185
+ "spans": spans,
186
+ }
187
+
188
+
189
+ class SpanQuerySet(models.QuerySet):
190
+ def annotate_spans(self):
191
+ """Annotate spans with nesting levels and duplicate query warnings."""
192
+ spans = list(self.order_by("start_time"))
152
193
 
153
194
  # Build span dictionary for parent lookups
154
195
  span_dict = {span.span_id: span for span in spans}
@@ -191,20 +232,6 @@ class Trace(models.Model):
191
232
 
192
233
  return spans
193
234
 
194
- def as_dict(self):
195
- spans = [span.span_data for span in self.spans.all().order_by("start_time")]
196
-
197
- return {
198
- "trace_id": self.trace_id,
199
- "start_time": self.start_time.isoformat(),
200
- "end_time": self.end_time.isoformat(),
201
- "duration_ms": self.duration_ms(),
202
- "request_id": self.request_id,
203
- "user_id": self.user_id,
204
- "session_id": self.session_id,
205
- "spans": spans,
206
- }
207
-
208
235
 
209
236
  @models.register_model
210
237
  class Span(models.Model):
@@ -220,6 +247,8 @@ class Span(models.Model):
220
247
  status = models.CharField(max_length=50, default="", required=False)
221
248
  span_data = models.JSONField(default=dict, required=False)
222
249
 
250
+ objects = SpanQuerySet.as_manager()
251
+
223
252
  class Meta:
224
253
  ordering = ["-start_time"]
225
254
  constraints = [
plain/observer/otel.py CHANGED
@@ -45,6 +45,9 @@ def get_current_trace_summary() -> str | None:
45
45
  return None
46
46
 
47
47
  trace_id = f"0x{format_trace_id(current_span.get_span_context().trace_id)}"
48
+
49
+ # Technically we're still in the trace... so the duration and stuff could shift slightly
50
+ # (though we should be at the end of the template, hopefully)
48
51
  return processor.get_trace_summary(trace_id)
49
52
 
50
53
 
@@ -266,6 +269,10 @@ class ObserverSpanProcessor(SpanProcessor):
266
269
  len(trace_info["span_models"]),
267
270
  trace_id,
268
271
  )
272
+ # The trace is done now, so we can get a more accurate summary
273
+ trace_info["trace"].summary = trace_info["trace"].get_trace_summary(
274
+ trace_info["span_models"]
275
+ )
269
276
  self._export_trace(trace_info["trace"], trace_info["span_models"])
270
277
 
271
278
  # Clean up trace