plain.observer 0.0.0__tar.gz → 0.1.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.

Files changed (26) hide show
  1. {plain_observer-0.0.0 → plain_observer-0.1.0}/PKG-INFO +1 -1
  2. plain_observer-0.1.0/plain/observer/CHANGELOG.md +15 -0
  3. plain_observer-0.1.0/plain/observer/config.py +44 -0
  4. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/otel.py +56 -2
  5. {plain_observer-0.0.0 → plain_observer-0.1.0}/pyproject.toml +1 -1
  6. plain_observer-0.0.0/plain/observer/CHANGELOG.md +0 -1
  7. plain_observer-0.0.0/plain/observer/config.py +0 -36
  8. {plain_observer-0.0.0 → plain_observer-0.1.0}/.gitignore +0 -0
  9. {plain_observer-0.0.0 → plain_observer-0.1.0}/LICENSE +0 -0
  10. {plain_observer-0.0.0 → plain_observer-0.1.0}/README.md +0 -0
  11. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/README.md +0 -0
  12. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/__init__.py +0 -0
  13. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/admin.py +0 -0
  14. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/cli.py +0 -0
  15. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/core.py +0 -0
  16. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/default_settings.py +0 -0
  17. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/migrations/0001_initial.py +0 -0
  18. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/migrations/__init__.py +0 -0
  19. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/models.py +0 -0
  20. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/templates/admin/observer/trace_detail.html +0 -0
  21. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/templates/observer/_trace_detail.html +0 -0
  22. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/templates/observer/traces.html +0 -0
  23. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/templates/toolbar/observer.html +0 -0
  24. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/templates/toolbar/observer_button.html +0 -0
  25. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/urls.py +0 -0
  26. {plain_observer-0.0.0 → plain_observer-0.1.0}/plain/observer/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.observer
3
- Version: 0.0.0
3
+ Version: 0.1.0
4
4
  Summary: On-page telemetry and observability tools for Plain.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -0,0 +1,15 @@
1
+ # plain-observer changelog
2
+
3
+ ## [0.1.0](https://github.com/dropseed/plain/releases/plain-observer@0.1.0) (2025-07-19)
4
+
5
+ ### What's changed
6
+
7
+ - Initial release of plain-observer package providing OpenTelemetry-based observability and monitoring for Plain applications ([b0224d0](https://github.com/dropseed/plain/commit/b0224d0418))
8
+ - Added real-time trace monitoring with summary and persist modes via signed cookies ([b0224d0](https://github.com/dropseed/plain/commit/b0224d0418))
9
+ - Added admin interface for viewing detailed trace information and spans ([b0224d0](https://github.com/dropseed/plain/commit/b0224d0418))
10
+ - Added toolbar integration showing performance summaries for current requests ([b0224d0](https://github.com/dropseed/plain/commit/b0224d0418))
11
+ - Observer can now combine with existing OpenTelemetry trace providers instead of replacing them ([7e55779](https://github.com/dropseed/plain/commit/7e55779548))
12
+
13
+ ### Upgrade instructions
14
+
15
+ - No changes required
@@ -0,0 +1,44 @@
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 (
7
+ ObserverCombinedSampler,
8
+ ObserverSampler,
9
+ ObserverSpanProcessor,
10
+ get_observer_span_processor,
11
+ )
12
+
13
+
14
+ @register_config
15
+ class Config(PackageConfig):
16
+ package_label = "plainobserver"
17
+
18
+ def ready(self):
19
+ sampler = ObserverSampler()
20
+ span_processor = ObserverSpanProcessor()
21
+
22
+ if provider := self.get_existing_trace_provider():
23
+ # There is already a trace provider, so combine our sampler
24
+ # and add an additional span processor for Observer
25
+ if hasattr(provider, "sampler"):
26
+ provider.sampler = ObserverCombinedSampler(provider.sampler, sampler)
27
+
28
+ if not get_observer_span_processor():
29
+ provider.add_span_processor(span_processor)
30
+ else:
31
+ # Start our own provider, new sampler, and span processor
32
+ provider = TracerProvider(sampler=sampler)
33
+ provider.add_span_processor(span_processor)
34
+ trace.set_tracer_provider(provider)
35
+
36
+ @staticmethod
37
+ def get_existing_trace_provider():
38
+ """Return the currently configured provider if set."""
39
+ current_provider = trace.get_tracer_provider()
40
+ if current_provider and not isinstance(
41
+ current_provider, trace.ProxyTracerProvider
42
+ ):
43
+ return current_provider
44
+ return None
@@ -18,7 +18,7 @@ from .core import Observer, ObserverMode
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
20
 
21
- def get_span_processor():
21
+ def get_observer_span_processor():
22
22
  """Get the span collector instance from the tracer provider."""
23
23
  if not (current_provider := trace.get_tracer_provider()):
24
24
  return None
@@ -41,7 +41,7 @@ def get_current_trace_summary() -> str | None:
41
41
  if not (current_span := trace.get_current_span()):
42
42
  return None
43
43
 
44
- if not (processor := get_span_processor()):
44
+ if not (processor := get_observer_span_processor()):
45
45
  return None
46
46
 
47
47
  trace_id = f"0x{format_trace_id(current_span.get_span_context().trace_id)}"
@@ -126,6 +126,50 @@ class ObserverSampler(sampling.Sampler):
126
126
  return "ObserverSampler"
127
127
 
128
128
 
129
+ class ObserverCombinedSampler(sampling.Sampler):
130
+ """Combine another sampler with ``ObserverSampler``."""
131
+
132
+ def __init__(self, primary: sampling.Sampler, secondary: sampling.Sampler):
133
+ self.primary = primary
134
+ self.secondary = secondary
135
+
136
+ def should_sample(
137
+ self,
138
+ parent_context,
139
+ trace_id,
140
+ name,
141
+ kind: SpanKind | None = None,
142
+ attributes=None,
143
+ links=None,
144
+ trace_state=None,
145
+ ):
146
+ result = self.primary.should_sample(
147
+ parent_context,
148
+ trace_id,
149
+ name,
150
+ kind=kind,
151
+ attributes=attributes,
152
+ links=links,
153
+ trace_state=trace_state,
154
+ )
155
+
156
+ if result.decision is sampling.Decision.DROP:
157
+ return self.secondary.should_sample(
158
+ parent_context,
159
+ trace_id,
160
+ name,
161
+ kind=kind,
162
+ attributes=attributes,
163
+ links=links,
164
+ trace_state=trace_state,
165
+ )
166
+
167
+ return result
168
+
169
+ def get_description(self) -> str:
170
+ return f"ObserverCombinedSampler({self.primary.get_description()}, {self.secondary.get_description()})"
171
+
172
+
129
173
  class ObserverSpanProcessor(SpanProcessor):
130
174
  """Collects spans in real-time for current trace performance monitoring.
131
175
 
@@ -148,6 +192,9 @@ class ObserverSpanProcessor(SpanProcessor):
148
192
  }
149
193
  )
150
194
  self._traces_lock = threading.Lock()
195
+ self._ignore_url_paths = [
196
+ re.compile(p) for p in settings.OBSERVER_IGNORE_URL_PATTERNS
197
+ ]
151
198
 
152
199
  def on_start(self, span, parent_context=None):
153
200
  """Called when a span starts."""
@@ -295,6 +342,13 @@ class ObserverSpanProcessor(SpanProcessor):
295
342
  )
296
343
 
297
344
  def _get_recording_mode(self, span, parent_context) -> str | None:
345
+ # Again check the span attributes, in case we relied on another sampler
346
+ if span.attributes:
347
+ if url_path := span.attributes.get(url_attributes.URL_PATH, ""):
348
+ for pattern in self._ignore_url_paths:
349
+ if pattern.match(url_path):
350
+ return None
351
+
298
352
  # If the span has links, then we are going to export if the linked span is also exported
299
353
  for link in span.links:
300
354
  if link.context.is_valid and link.context.span_id:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.observer"
3
- version = "0.0.0"
3
+ version = "0.1.0"
4
4
  description = "On-page telemetry and observability tools for Plain."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  license = "BSD-3-Clause"
@@ -1 +0,0 @@
1
- # plain-observer changelog
@@ -1,36 +0,0 @@
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)
File without changes
File without changes