netra-sdk 0.1.44__py3-none-any.whl → 0.1.45__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 netra-sdk might be problematic. Click here for more details.

@@ -0,0 +1,3 @@
1
+ from netra.exporters.filtering_span_exporter import FilteringSpanExporter
2
+
3
+ __all__ = ["FilteringSpanExporter"]
@@ -0,0 +1,211 @@
1
+ import logging
2
+ from typing import Any, Dict, List, Sequence
3
+
4
+ from opentelemetry.sdk.trace import ReadableSpan
5
+ from opentelemetry.sdk.trace.export import (
6
+ SpanExporter,
7
+ SpanExportResult,
8
+ )
9
+
10
+ from netra.processors.local_filtering_span_processor import (
11
+ BLOCKED_LOCAL_PARENT_MAP,
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class FilteringSpanExporter(SpanExporter): # type: ignore[misc]
18
+ """
19
+ SpanExporter wrapper that filters out spans by name.
20
+
21
+ Matching rules:
22
+ - Exact match: pattern "Foo" blocks span.name == "Foo".
23
+ - Prefix match: pattern ending with '*' (e.g., "CloudSpanner.*") blocks spans whose
24
+ names start with the prefix before '*', e.g., "CloudSpanner.", "CloudSpanner.Query".
25
+ - Suffix match: pattern starting with '*' (e.g., "*.Query") blocks spans whose
26
+ names end with the suffix after '*', e.g., "DB.Query", "Search.Query".
27
+ """
28
+
29
+ def __init__(self, exporter: SpanExporter, patterns: Sequence[str]) -> None:
30
+ self._exporter = exporter
31
+ # Normalize once for efficient checks
32
+ exact: List[str] = []
33
+ prefixes: List[str] = []
34
+ suffixes: List[str] = []
35
+ for p in patterns:
36
+ if not p:
37
+ continue
38
+ if p.endswith("*") and not p.startswith("*"):
39
+ prefixes.append(p[:-1])
40
+ elif p.startswith("*") and not p.endswith("*"):
41
+ suffixes.append(p[1:])
42
+ else:
43
+ exact.append(p)
44
+ self._exact = set(exact)
45
+ self._prefixes = prefixes
46
+ self._suffixes = suffixes
47
+
48
+ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
49
+ filtered: List[ReadableSpan] = []
50
+ blocked_parent_map: Dict[Any, Any] = {}
51
+ for span in spans:
52
+ name = getattr(span, "name", None)
53
+ if name is None:
54
+ filtered.append(span)
55
+ continue
56
+
57
+ # Global blocking (configured patterns)
58
+ globally_blocked = self._is_blocked(name)
59
+
60
+ # Local per-span blocking via attribute set by LocalFilteringSpanProcessor
61
+ locally_blocked = False
62
+ try:
63
+ local_patterns = self._get_local_patterns(span)
64
+ if local_patterns:
65
+ locally_blocked = self._matches_any_pattern(name, local_patterns)
66
+ # Fallback: if processor explicitly marked the span as locally blocked
67
+ if not locally_blocked and self._has_local_block_flag(span):
68
+ locally_blocked = True
69
+ except Exception:
70
+ locally_blocked = False
71
+
72
+ if not (globally_blocked or locally_blocked):
73
+ filtered.append(span)
74
+ continue
75
+
76
+ # Collect mapping for reparenting children of the blocked span
77
+ span_context = getattr(span, "context", None)
78
+ span_id = getattr(span_context, "span_id", None) if span_context else None
79
+ if span_id is not None:
80
+ blocked_parent_map[span_id] = getattr(span, "parent", None)
81
+
82
+ # Merge with registry of locally blocked spans captured by processor to handle
83
+ # cases where children export before their blocked parent (SimpleSpanProcessor)
84
+ merged_map: Dict[Any, Any] = {}
85
+ try:
86
+ if BLOCKED_LOCAL_PARENT_MAP:
87
+ merged_map.update(BLOCKED_LOCAL_PARENT_MAP)
88
+ except Exception:
89
+ pass
90
+ merged_map.update(blocked_parent_map)
91
+
92
+ if merged_map:
93
+ self._reparent_blocked_children(filtered, merged_map)
94
+ if not filtered:
95
+ return SpanExportResult.SUCCESS
96
+ return self._exporter.export(filtered)
97
+
98
+ def _is_blocked(self, name: str) -> bool:
99
+ if name in self._exact:
100
+ return True
101
+ for pref in self._prefixes:
102
+ if name.startswith(pref):
103
+ return True
104
+ for suf in self._suffixes:
105
+ if name.endswith(suf):
106
+ return True
107
+ return False
108
+
109
+ def _get_local_patterns(self, span: ReadableSpan) -> List[str]:
110
+ """Fetch local-block patterns from span attributes set by LocalFilteringSpanProcessor."""
111
+ try:
112
+ attrs = getattr(span, "attributes", None)
113
+ if not attrs:
114
+ return []
115
+ value = None
116
+ # Prefer Mapping.get if available
117
+ try:
118
+ if hasattr(attrs, "get"):
119
+ value = attrs.get("netra.local_blocked_spans")
120
+ else:
121
+ value = attrs["netra.local_blocked_spans"]
122
+ except Exception:
123
+ value = None
124
+ if isinstance(value, (list, tuple)) and all(isinstance(v, str) for v in value):
125
+ return [v for v in value if v]
126
+ except Exception:
127
+ logger.debug("Failed reading local blocked patterns from span", exc_info=True)
128
+ return []
129
+
130
+ def _matches_any_pattern(self, name: str, patterns: Sequence[str]) -> bool:
131
+ for p in patterns:
132
+ if not p:
133
+ continue
134
+ if p.endswith("*") and not p.startswith("*"):
135
+ if name.startswith(p[:-1]):
136
+ return True
137
+ elif p.startswith("*") and not p.endswith("*"):
138
+ if name.endswith(p[1:]):
139
+ return True
140
+ else:
141
+ if name == p:
142
+ return True
143
+ return False
144
+
145
+ def _has_local_block_flag(self, span: ReadableSpan) -> bool:
146
+ try:
147
+ attrs = getattr(span, "attributes", None)
148
+ if not attrs:
149
+ return False
150
+ try:
151
+ if hasattr(attrs, "get"):
152
+ value = attrs.get("netra.local_blocked")
153
+ else:
154
+ value = attrs["netra.local_blocked"]
155
+ except Exception:
156
+ value = None
157
+ return bool(value) is True
158
+ except Exception:
159
+ return False
160
+
161
+ def _reparent_blocked_children(
162
+ self,
163
+ spans: Sequence[ReadableSpan],
164
+ blocked_parent_map: Dict[Any, Any],
165
+ ) -> None:
166
+ if not blocked_parent_map:
167
+ return
168
+
169
+ for span in spans:
170
+ parent_context = getattr(span, "parent", None)
171
+ if parent_context is None:
172
+ continue
173
+
174
+ updated_parent = parent_context
175
+ visited: set[Any] = set()
176
+ changed = False
177
+
178
+ while updated_parent is not None:
179
+ parent_span_id = getattr(updated_parent, "span_id", None)
180
+ if parent_span_id not in blocked_parent_map or parent_span_id in visited:
181
+ break
182
+ visited.add(parent_span_id)
183
+ updated_parent = blocked_parent_map[parent_span_id]
184
+ changed = True
185
+
186
+ if changed:
187
+ self._set_span_parent(span, updated_parent)
188
+
189
+ def _set_span_parent(self, span: ReadableSpan, parent: Any) -> None:
190
+ if hasattr(span, "_parent"):
191
+ try:
192
+ span._parent = parent
193
+ return
194
+ except Exception:
195
+ pass
196
+ try:
197
+ setattr(span, "parent", parent)
198
+ except Exception:
199
+ logger.debug("Failed to reparent span %s", getattr(span, "name", "<unknown>"), exc_info=True)
200
+
201
+ def shutdown(self) -> None:
202
+ try:
203
+ self._exporter.shutdown()
204
+ except Exception:
205
+ pass
206
+
207
+ def force_flush(self, timeout_millis: int = 30000) -> Any:
208
+ try:
209
+ return self._exporter.force_flush(timeout_millis)
210
+ except Exception:
211
+ return True
@@ -1,5 +1,11 @@
1
1
  from netra.processors.instrumentation_span_processor import InstrumentationSpanProcessor
2
+ from netra.processors.local_filtering_span_processor import LocalFilteringSpanProcessor
2
3
  from netra.processors.scrubbing_span_processor import ScrubbingSpanProcessor
3
4
  from netra.processors.session_span_processor import SessionSpanProcessor
4
5
 
5
- __all__ = ["SessionSpanProcessor", "InstrumentationSpanProcessor", "ScrubbingSpanProcessor"]
6
+ __all__ = [
7
+ "SessionSpanProcessor",
8
+ "InstrumentationSpanProcessor",
9
+ "ScrubbingSpanProcessor",
10
+ "LocalFilteringSpanProcessor",
11
+ ]
@@ -0,0 +1,155 @@
1
+ import json
2
+ import logging
3
+ from contextlib import contextmanager
4
+ from typing import List, Optional, Sequence
5
+
6
+ from opentelemetry import baggage
7
+ from opentelemetry import context as otel_context
8
+ from opentelemetry import trace
9
+ from opentelemetry.sdk.trace import SpanProcessor
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Baggage key to carry local blocked span patterns in the active context
14
+ _LOCAL_BLOCKED_SPANS_BAGGAGE_KEY = "netra.local_blocked_spans"
15
+ # Attribute key to copy resolved local blocked patterns onto each span
16
+ _LOCAL_BLOCKED_SPANS_ATTR_KEY = "netra.local_blocked_spans"
17
+
18
+ # Registry of locally blocked spans: span_id -> parent_context
19
+ # This lets exporters reparent children reliably even when children export before parents
20
+ BLOCKED_LOCAL_PARENT_MAP: dict[object, object] = {}
21
+
22
+
23
+ class LocalFilteringSpanProcessor(SpanProcessor): # type: ignore[misc]
24
+ """Propagates local blocked span patterns from baggage to span attributes.
25
+
26
+ - On span start, reads patterns from baggage key `_LOCAL_BLOCKED_SPANS_BAGGAGE_KEY` on the provided
27
+ `parent_context` and, if present, sets them on the span attribute `_LOCAL_BLOCKED_SPANS_ATTR_KEY`.
28
+ - This enables exporters (e.g., `FilteringSpanExporter`) to decide per-span blocking based on the
29
+ active context in which the span was created.
30
+ """
31
+
32
+ def on_start(self, span: trace.Span, parent_context: Optional[otel_context.Context] = None) -> None:
33
+ try:
34
+ # Use provided parent_context if available, otherwise fall back to current context
35
+ ctx_to_read = parent_context if parent_context is not None else otel_context.get_current()
36
+ raw = baggage.get_baggage(_LOCAL_BLOCKED_SPANS_BAGGAGE_KEY, context=ctx_to_read)
37
+ if not raw:
38
+ return
39
+ patterns: Optional[List[str]] = _decode_patterns(raw)
40
+ if patterns:
41
+ try:
42
+ span.set_attribute(_LOCAL_BLOCKED_SPANS_ATTR_KEY, patterns)
43
+ except Exception:
44
+ # Best-effort: never break span start
45
+ logger.debug("Failed setting local blocked patterns on span", exc_info=True)
46
+ # If this span matches the local patterns, record it as locally blocked for reparenting
47
+ try:
48
+ name = getattr(span, "name", None)
49
+ if isinstance(name, str) and name and _matches_any_pattern(name, patterns):
50
+ ctx = getattr(span, "context", None)
51
+ span_id = getattr(ctx, "span_id", None) if ctx else None
52
+ # Determine the parent SpanContext of this blocked span from the context
53
+ parent_span = trace.get_current_span(ctx_to_read)
54
+ parent_span_context = (
55
+ parent_span.get_span_context() if hasattr(parent_span, "get_span_context") else None
56
+ )
57
+ if span_id is not None and parent_span_context is not None:
58
+ BLOCKED_LOCAL_PARENT_MAP[span_id] = parent_span_context
59
+ # Mark on the span for visibility/debugging
60
+ try:
61
+ span.set_attribute("netra.local_blocked", True)
62
+ except Exception:
63
+ pass
64
+ except Exception:
65
+ logger.debug("Failed to precompute locally blocked mapping on start", exc_info=True)
66
+ except Exception:
67
+ # Never break tracing pipeline
68
+ logger.debug("LocalFilteringSpanProcessor.on_start failed", exc_info=True)
69
+
70
+ # No-ops required by interface
71
+ def on_end(self, span: trace.Span) -> None: # noqa: D401
72
+ # Cleanup registry entry to avoid leaks
73
+ try:
74
+ ctx = getattr(span, "context", None)
75
+ span_id = getattr(ctx, "span_id", None) if ctx else None
76
+ if span_id is not None:
77
+ BLOCKED_LOCAL_PARENT_MAP.pop(span_id, None)
78
+ except Exception:
79
+ pass
80
+ return
81
+
82
+ def shutdown(self) -> None: # noqa: D401
83
+ return
84
+
85
+ def force_flush(self, timeout_millis: int = 30000) -> None: # noqa: D401
86
+ return
87
+
88
+
89
+ def _decode_patterns(raw: str) -> Optional[List[str]]:
90
+ """Decode patterns stored in baggage.
91
+
92
+ We store JSON-encoded list[str] in baggage (must be a string). This safely handles complex
93
+ characters and is robust to commas in patterns.
94
+ """
95
+ try:
96
+ parsed = json.loads(raw)
97
+ if isinstance(parsed, list) and all(isinstance(p, str) for p in parsed):
98
+ return [p for p in parsed if p]
99
+ except Exception:
100
+ logger.debug("Failed to decode local blocked patterns from baggage", exc_info=True)
101
+ return None
102
+
103
+
104
+ def _matches_any_pattern(name: str, patterns: Sequence[str]) -> bool:
105
+ """Return True if name matches any pattern (exact, prefix*, *suffix)."""
106
+ try:
107
+ for p in patterns:
108
+ if not p:
109
+ continue
110
+ if p.endswith("*") and not p.startswith("*"):
111
+ if name.startswith(p[:-1]):
112
+ return True
113
+ elif p.startswith("*") and not p.endswith("*"):
114
+ if name.endswith(p[1:]):
115
+ return True
116
+ else:
117
+ if name == p:
118
+ return True
119
+ except Exception:
120
+ # Be conservative: on error, treat as no match
121
+ return False
122
+ return False
123
+
124
+
125
+ @contextmanager
126
+ def block_spans_local(patterns: Sequence[str]): # type: ignore[no-untyped-def]
127
+ """Context manager to locally block spans by name patterns.
128
+
129
+ Usage:
130
+ with block_spans_local(["openai.*", "*.internal"]):
131
+ # Spans created in this context will inherit these local rules
132
+ ...
133
+
134
+ Patterns follow the same rules as global blocking in the exporter:
135
+ - Exact match: "Foo"
136
+ - Prefix: "CloudSpanner.*"
137
+ - Suffix: "*.Query"
138
+ """
139
+ # Normalize incoming sequence to a compact list of non-empty strings
140
+ normalized: List[str] = [p for p in patterns if isinstance(p, str) and p]
141
+ # Encode as JSON string for baggage
142
+ payload = json.dumps(normalized)
143
+
144
+ # Attach to current context
145
+ token = otel_context.attach(
146
+ baggage.set_baggage(_LOCAL_BLOCKED_SPANS_BAGGAGE_KEY, payload, context=otel_context.get_current())
147
+ )
148
+ try:
149
+ yield
150
+ finally:
151
+ try:
152
+ otel_context.detach(token)
153
+ except Exception:
154
+ # If context changed unexpectedly, avoid crashing user code
155
+ logger.debug("Failed to detach local blocking context token", exc_info=True)
netra/span_wrapper.py CHANGED
@@ -4,6 +4,8 @@ import time
4
4
  from datetime import datetime
5
5
  from typing import Any, Dict, List, Literal, Optional
6
6
 
7
+ from opentelemetry import baggage
8
+ from opentelemetry import context as otel_context
7
9
  from opentelemetry import trace
8
10
  from opentelemetry.trace import SpanKind, Status, StatusCode
9
11
  from pydantic import BaseModel
@@ -15,6 +17,9 @@ from netra.session_manager import SessionManager
15
17
  logging.basicConfig(level=logging.INFO)
16
18
  logger = logging.getLogger(__name__)
17
19
 
20
+ # Baggage key for local-only blocked spans patterns
21
+ _LOCAL_BLOCKED_SPANS_BAGGAGE_KEY = "netra.local_blocked_spans"
22
+
18
23
 
19
24
  class ActionModel(BaseModel): # type: ignore[misc]
20
25
  start_time: str = str((datetime.now().timestamp() * 1_000_000_000))
@@ -72,11 +77,29 @@ class SpanWrapper:
72
77
  self.span: Optional[trace.Span] = None
73
78
  # Internal context manager to manage current-span scope safely
74
79
  self._span_cm: Optional[Any] = None
80
+ # Token for locally attached baggage (if any)
81
+ self._local_block_token: Optional[object] = None
75
82
 
76
83
  def __enter__(self) -> "SpanWrapper":
77
84
  """Start the span wrapper, begin time tracking, and create OpenTelemetry span."""
78
85
  self.start_time = time.time()
79
86
 
87
+ # If user provided local blocked patterns in attributes, attach them as baggage
88
+ try:
89
+ patterns = None
90
+ # Accept either explicit key or short key for convenience
91
+ if isinstance(self.attributes.get("netra.local_blocked_spans"), list):
92
+ patterns = [p for p in self.attributes.get("netra.local_blocked_spans", []) if isinstance(p, str) and p]
93
+ elif isinstance(self.attributes.get("blocked_spans"), list):
94
+ patterns = [p for p in self.attributes.get("blocked_spans", []) if isinstance(p, str) and p]
95
+ if patterns:
96
+ payload = json.dumps(patterns)
97
+ self._local_block_token = otel_context.attach(
98
+ baggage.set_baggage(_LOCAL_BLOCKED_SPANS_BAGGAGE_KEY, payload, context=otel_context.get_current())
99
+ )
100
+ except Exception:
101
+ logger.debug("Failed to attach local blocked spans baggage on span start", exc_info=True)
102
+
80
103
  # Create OpenTelemetry span and make it current using OTel's context manager
81
104
  # Store the context manager so we can close it in __exit__
82
105
  self._span_cm = self.tracer.start_as_current_span(
@@ -139,6 +162,15 @@ class SpanWrapper:
139
162
  finally:
140
163
  self._span_cm = None
141
164
 
165
+ # Detach local blocking baggage if we attached it
166
+ if self._local_block_token is not None:
167
+ try:
168
+ otel_context.detach(self._local_block_token)
169
+ except Exception:
170
+ logger.debug("Failed to detach local blocked spans baggage token", exc_info=True)
171
+ finally:
172
+ self._local_block_token = None
173
+
142
174
  # Don't suppress exceptions
143
175
  return False
144
176
 
netra/tracer.py CHANGED
@@ -5,139 +5,24 @@ including exporter setup and span processor configuration.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Any, Dict, List, Sequence
8
+ from typing import Any, Dict
9
9
 
10
10
  from opentelemetry import trace
11
11
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
12
12
  from opentelemetry.sdk.resources import DEPLOYMENT_ENVIRONMENT, SERVICE_NAME, Resource
13
- from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
13
+ from opentelemetry.sdk.trace import TracerProvider
14
14
  from opentelemetry.sdk.trace.export import (
15
15
  BatchSpanProcessor,
16
16
  ConsoleSpanExporter,
17
17
  SimpleSpanProcessor,
18
- SpanExporter,
19
- SpanExportResult,
20
18
  )
21
19
 
22
20
  from netra.config import Config
21
+ from netra.exporters import FilteringSpanExporter
23
22
 
24
23
  logger = logging.getLogger(__name__)
25
24
 
26
25
 
27
- class FilteringSpanExporter(SpanExporter): # type: ignore[misc]
28
- """
29
- SpanExporter wrapper that filters out spans by name.
30
-
31
- Matching rules:
32
- - Exact match: pattern "Foo" blocks span.name == "Foo".
33
- - Prefix match: pattern ending with '*' (e.g., "CloudSpanner.*") blocks spans whose
34
- names start with the prefix before '*', e.g., "CloudSpanner.", "CloudSpanner.Query".
35
- - Suffix match: pattern starting with '*' (e.g., "*.Query") blocks spans whose
36
- names end with the suffix after '*', e.g., "DB.Query", "Search.Query".
37
- """
38
-
39
- def __init__(self, exporter: SpanExporter, patterns: Sequence[str]) -> None:
40
- self._exporter = exporter
41
- # Normalize once for efficient checks
42
- exact: List[str] = []
43
- prefixes: List[str] = []
44
- suffixes: List[str] = []
45
- for p in patterns:
46
- if not p:
47
- continue
48
- if p.endswith("*") and not p.startswith("*"):
49
- prefixes.append(p[:-1])
50
- elif p.startswith("*") and not p.endswith("*"):
51
- suffixes.append(p[1:])
52
- else:
53
- exact.append(p)
54
- self._exact = set(exact)
55
- self._prefixes = prefixes
56
- self._suffixes = suffixes
57
-
58
- def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
59
- filtered: List[ReadableSpan] = []
60
- blocked_parent_map: Dict[Any, Any] = {}
61
- for span in spans:
62
- name = getattr(span, "name", None)
63
- if name is None or not self._is_blocked(name):
64
- filtered.append(span)
65
- continue
66
-
67
- span_context = getattr(span, "context", None)
68
- span_id = getattr(span_context, "span_id", None) if span_context else None
69
- if span_id is not None:
70
- blocked_parent_map[span_id] = getattr(span, "parent", None)
71
- if blocked_parent_map:
72
- self._reparent_blocked_children(filtered, blocked_parent_map)
73
- if not filtered:
74
- return SpanExportResult.SUCCESS
75
- return self._exporter.export(filtered)
76
-
77
- def _is_blocked(self, name: str) -> bool:
78
- if name in self._exact:
79
- return True
80
- for pref in self._prefixes:
81
- if name.startswith(pref):
82
- return True
83
- for suf in self._suffixes:
84
- if name.endswith(suf):
85
- return True
86
- return False
87
-
88
- def _reparent_blocked_children(
89
- self,
90
- spans: Sequence[ReadableSpan],
91
- blocked_parent_map: Dict[Any, Any],
92
- ) -> None:
93
- if not blocked_parent_map:
94
- return
95
-
96
- for span in spans:
97
- parent_context = getattr(span, "parent", None)
98
- if parent_context is None:
99
- continue
100
-
101
- updated_parent = parent_context
102
- visited: set[Any] = set()
103
- changed = False
104
-
105
- while updated_parent is not None:
106
- parent_span_id = getattr(updated_parent, "span_id", None)
107
- if parent_span_id not in blocked_parent_map or parent_span_id in visited:
108
- break
109
- visited.add(parent_span_id)
110
- updated_parent = blocked_parent_map[parent_span_id]
111
- changed = True
112
-
113
- if changed:
114
- self._set_span_parent(span, updated_parent)
115
-
116
- def _set_span_parent(self, span: ReadableSpan, parent: Any) -> None:
117
- if hasattr(span, "_parent"):
118
- try:
119
- span._parent = parent
120
- return
121
- except Exception:
122
- pass
123
- try:
124
- setattr(span, "parent", parent)
125
- except Exception:
126
- logger.debug("Failed to reparent span %s", getattr(span, "name", "<unknown>"), exc_info=True)
127
-
128
- def shutdown(self) -> None:
129
- try:
130
- self._exporter.shutdown()
131
- except Exception:
132
- pass
133
-
134
- def force_flush(self, timeout_millis: int = 30000) -> Any:
135
- try:
136
- return self._exporter.force_flush(timeout_millis)
137
- except Exception:
138
- return True
139
-
140
-
141
26
  class Tracer:
142
27
  """
143
28
  Configures Netra's OpenTelemetry tracer with OTLP exporter (or Console exporter as fallback)
@@ -181,17 +66,26 @@ class Tracer:
181
66
  endpoint=self._format_endpoint(self.cfg.otlp_endpoint),
182
67
  headers=self.cfg.headers,
183
68
  )
184
- # Wrap exporter with filtering if blocked span patterns are provided
69
+ # Wrap exporter with filtering to support both global and local (baggage-based) rules
185
70
  try:
186
- patterns = getattr(self.cfg, "blocked_spans", None)
71
+ patterns = getattr(self.cfg, "blocked_spans", None) or []
72
+ exporter = FilteringSpanExporter(exporter, patterns)
187
73
  if patterns:
188
- exporter = FilteringSpanExporter(exporter, patterns)
189
- logger.info("Enabled FilteringSpanExporter with %d pattern(s)", len(patterns))
74
+ logger.info("Enabled FilteringSpanExporter with %d global pattern(s)", len(patterns))
75
+ else:
76
+ logger.info("Enabled FilteringSpanExporter with local-only rules")
190
77
  except Exception as e:
191
78
  logger.warning("Failed to enable FilteringSpanExporter: %s", e)
192
79
  # Add span processors: first instrumentation wrapper, then session processor
193
- from netra.processors import InstrumentationSpanProcessor, ScrubbingSpanProcessor, SessionSpanProcessor
80
+ from netra.processors import (
81
+ InstrumentationSpanProcessor,
82
+ LocalFilteringSpanProcessor,
83
+ ScrubbingSpanProcessor,
84
+ SessionSpanProcessor,
85
+ )
194
86
 
87
+ # Apply local filtering propagation first so later processors and spans see attributes
88
+ provider.add_span_processor(LocalFilteringSpanProcessor())
195
89
  provider.add_span_processor(InstrumentationSpanProcessor())
196
90
  provider.add_span_processor(SessionSpanProcessor())
197
91
 
netra/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.44"
1
+ __version__ = "0.1.45"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netra-sdk
3
- Version: 0.1.44
3
+ Version: 0.1.45
4
4
  Summary: A Python SDK for AI application observability that provides OpenTelemetry-based monitoring, tracing, and PII protection for LLM and vector database applications. Enables easy instrumentation, session tracking, and privacy-focused data collection for AI systems in production environments.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENCE
@@ -8,6 +8,8 @@ netra/decorators.py,sha256=qZFHrwdj10FsTFqggo3XjdGB12aMxsrrDMMmslDqZ-0,17424
8
8
  netra/exceptions/__init__.py,sha256=uDgcBxmC4WhdS7HRYQk_TtJyxH1s1o6wZmcsnSHLAcM,174
9
9
  netra/exceptions/injection.py,sha256=ke4eUXRYUFJkMZgdSyPPkPt5PdxToTI6xLEBI0hTWUQ,1332
10
10
  netra/exceptions/pii.py,sha256=MT4p_x-zH3VtYudTSxw1Z9qQZADJDspq64WrYqSWlZc,2438
11
+ netra/exporters/__init__.py,sha256=8KFjTVKbTzoH2X0Yu1Cll6_CvCguD7dX9dHjFJqrM4M,111
12
+ netra/exporters/filtering_span_exporter.py,sha256=rWTGL8ja9v9hVhm9nuJCZGmQ6zw59F0Oh1hahhFJwU8,7649
11
13
  netra/input_scanner.py,sha256=At6N9gNY8cR0O6S8x3K6swWBV3P1a_9O-XBNM_pcKz4,5348
12
14
  netra/instrumentation/__init__.py,sha256=HdG3n5TxPRUNlOxsqjlvwDmBcnm3UtYx1OecLhnLeQM,41578
13
15
  netra/instrumentation/aiohttp/__init__.py,sha256=M1kuF0R3gKY5rlbhEC1AR13UWHelmfokluL2yFysKWc,14398
@@ -40,17 +42,18 @@ netra/instrumentation/pydantic_ai/wrappers.py,sha256=6cfIRvELBS4d9G9TttNYcHGueNI
40
42
  netra/instrumentation/weaviate/__init__.py,sha256=EOlpWxobOLHYKqo_kMct_7nu26x1hr8qkeG5_h99wtg,4330
41
43
  netra/instrumentation/weaviate/version.py,sha256=PiCZHjonujPbnIn0KmD3Yl68hrjPRG_oKe5vJF3mmG8,24
42
44
  netra/pii.py,sha256=Rn4SjgTJW_aw9LcbjLuMqF3fKd9b1ndlYt1CaK51Ge0,33125
43
- netra/processors/__init__.py,sha256=TLVBKk4Bli7MOyHTy_F-4NSm0thzIcJcZAVVNoq6gK8,333
45
+ netra/processors/__init__.py,sha256=56RO7xMoBx2aoVuESyQXll65o7IoO98nSHZ5KpR5iQk,471
44
46
  netra/processors/instrumentation_span_processor.py,sha256=VzurzwtGleFltxzKD_gjVkUQiRC6SGlb0oG4Nlpu85A,4365
47
+ netra/processors/local_filtering_span_processor.py,sha256=Vk_UP--ZxWwdrbJA7a95iRo853QfPCME2C5ChgjiHl8,6664
45
48
  netra/processors/scrubbing_span_processor.py,sha256=dJ86Ncmjvmrhm_uAdGTwcGvRpZbVVWqD9AOFwEMWHZY,6701
46
49
  netra/processors/session_span_processor.py,sha256=qcsBl-LnILWefsftI8NQhXDGb94OWPc8LvzhVA0JS_c,2432
47
50
  netra/scanner.py,sha256=kyDpeZiscCPb6pjuhS-sfsVj-dviBFRepdUWh0sLoEY,11554
48
51
  netra/session_manager.py,sha256=VzmSAiP63ODCuOWv-irsxyU2LvHoqjOBUuXtyxboBU0,13740
49
- netra/span_wrapper.py,sha256=IygQX78xQRlL_Z1MfKfUbv0okihx92qNClnRlYFtRNc,8004
50
- netra/tracer.py,sha256=3s_ZAHgpfeegcnA43KVEinEPVyo3kGi_0bvSrQcKljk,8348
52
+ netra/span_wrapper.py,sha256=3xzZs44s85KiB81WXjAJmfQcH_aPMxCuQ_1xjDVGtwE,9682
53
+ netra/tracer.py,sha256=IPVYMc4x74XZJeKvCfV6b0X9mUHGvsssUzLnRvFPV6M,4629
51
54
  netra/utils.py,sha256=FblSzI8qMTfEbusakGBKE9CNELW0GEBHl09mPPxgI-w,2521
52
- netra/version.py,sha256=AoJtnEXXv6E20uj57ChQUsGoLfKG8mvSQpdz97tcyis,23
53
- netra_sdk-0.1.44.dist-info/METADATA,sha256=f7QrKDQQtQyaaAzIzGFF44KOpHpNW00wrZ3KIPt0zvA,28208
54
- netra_sdk-0.1.44.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
55
- netra_sdk-0.1.44.dist-info/licenses/LICENCE,sha256=8B_UoZ-BAl0AqiHAHUETCgd3I2B9yYJ1WEQtVb_qFMA,11359
56
- netra_sdk-0.1.44.dist-info/RECORD,,
55
+ netra/version.py,sha256=G1Bw5SEeRoSlHVxmFKf6PIpK38SHTYTFhSFGm9d8YQM,23
56
+ netra_sdk-0.1.45.dist-info/METADATA,sha256=UbtkH3HwIO59lD3lo1TACHzHWMFWTNXquAaRputrOV4,28208
57
+ netra_sdk-0.1.45.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
58
+ netra_sdk-0.1.45.dist-info/licenses/LICENCE,sha256=8B_UoZ-BAl0AqiHAHUETCgd3I2B9yYJ1WEQtVb_qFMA,11359
59
+ netra_sdk-0.1.45.dist-info/RECORD,,