Skip to content

Commit f317a0f

Browse files
committed
PYCBC-1763: Add checks for span recording for OTel Integration
Changes ------- * Add is_recording property to spans (defaults to true for backward compatibility) * Use OTel is_recording when using clients OTelWrapperSpan * Short circuit WrapperSpan logic when span is not recording Change-Id: I32d9047ac708c41da893fe50126a8aefbddb728a Reviewed-on: https://review.couchbase.org/c/couchbase-python-client/+/243595 Tested-by: Build Bot <build@couchbase.com> Reviewed-by: Dimitris Christodoulou <dimitris.christodoulou@couchbase.com>
1 parent 1305503 commit f317a0f

4 files changed

Lines changed: 60 additions & 9 deletions

File tree

couchbase/logic/observability/handler.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -736,13 +736,25 @@ def __init__(self,
736736
_is_multi_op
737737
and getattr(self._request_span, '_supports_multi_op_fast_dispatch', False)
738738
)
739-
self._has_multiple_encoding_spans = (self._op_name.is_multi_op() or self._op_name is OpName.MutateIn)
740-
self._encoding_spans_ended = False
741-
self._encoding_spans: Optional[Union[WrappedEncodingSpan, List[WrappedEncodingSpan]]] = None
742-
self._set_span_attrs(**options)
743-
self._cluster_name: Optional[str] = None
744-
self._cluster_uuid: Optional[str] = None
745-
self._end_time_watermark = time.time_ns()
739+
# When OTel sampler drops a span, is_recording is False and we skip
740+
# all expensive attribute setting, encoding spans, and dispatch span building.
741+
self._is_recording = getattr(self._request_span, 'is_recording', True)
742+
if self._is_recording:
743+
self._has_multiple_encoding_spans = (_is_multi_op or self._op_name is OpName.MutateIn)
744+
self._encoding_spans_ended = False
745+
self._encoding_spans: Optional[Union[WrappedEncodingSpan, List[WrappedEncodingSpan]]] = None
746+
self._set_span_attrs(**options)
747+
self._cluster_name: Optional[str] = None
748+
self._cluster_uuid: Optional[str] = None
749+
self._end_time_watermark = time.time_ns()
750+
else:
751+
# T10: Minimal init — skip all attribute work for non-recording spans
752+
self._has_multiple_encoding_spans = False
753+
self._encoding_spans_ended = True
754+
self._encoding_spans = None
755+
self._cluster_name = None
756+
self._cluster_uuid = None
757+
self._end_time_watermark = self._start_time
746758

747759
@property
748760
def cluster_name(self) -> Optional[str]:
@@ -752,6 +764,10 @@ def cluster_name(self) -> Optional[str]:
752764
def cluster_uuid(self) -> Optional[str]:
753765
return self._cluster_uuid
754766

767+
@property
768+
def is_recording(self) -> bool:
769+
return self._is_recording
770+
755771
@property
756772
def request_span(self) -> SpanProtocol:
757773
return self._request_span
@@ -763,7 +779,7 @@ def add_kv_durability_attribute(self, durability: DurabilityLevel) -> None:
763779
def maybe_add_encoding_span(self, encoding_fn: Callable[..., Tuple[bytes, int]]) -> Tuple[bytes, int]:
764780
# legacy operations did not create an encoding span; not support now
765781
# we only expect certain ops to have multiple encoding spans
766-
if self._wrapped_tracer.is_legacy or not self._has_multiple_encoding_spans:
782+
if not self._is_recording or self._wrapped_tracer.is_legacy or not self._has_multiple_encoding_spans:
767783
return encoding_fn()
768784

769785
if not self._encoding_spans:
@@ -786,7 +802,7 @@ def maybe_add_encoding_span(self, encoding_fn: Callable[..., Tuple[bytes, int]])
786802
def maybe_create_encoding_span(self, encoding_fn: Callable[..., Tuple[bytes, int]]) -> Tuple[bytes, int]:
787803
# legacy operations did not create an encoding span; not support now
788804
# if the op is expected to have multiple encoding spans, maybe_add_encoding_span() should be used instead
789-
if self._wrapped_tracer.is_legacy or self._has_multiple_encoding_spans:
805+
if not self._is_recording or self._wrapped_tracer.is_legacy or self._has_multiple_encoding_spans:
790806
return encoding_fn()
791807

792808
encoding_span = self._wrapped_tracer.tracer.request_span(_ATTR_ENCODING_SPAN_NAME,
@@ -807,6 +823,10 @@ def maybe_update_end_time_watermark(self, end_time: int) -> None:
807823
self._end_time_watermark = self._end_time_watermark if self._end_time_watermark > end_time else end_time
808824

809825
def process_core_span(self, core_span: CppWrapperSdkSpan) -> None:
826+
# Skip all core span processing if the span is not recording
827+
if not self._is_recording:
828+
return
829+
810830
# Guard cluster_[name|uuid] propagation — for multi-op batches, process_core_span
811831
# is called once per sub-op on the same WrappedSpan. cluster_name/uuid are
812832
# identical for every sub-op, so only set them on the first call.
@@ -842,6 +862,10 @@ def record_multi_op_dispatch_span(self, duration: int, attributes: Dict[str, Any
842862
self._request_span.set_attribute(attr_name, attr_val)
843863

844864
def set_attribute(self, key: str, value: SpanAttributeValue) -> None:
865+
# Skip setting any attribute if the span is not recording
866+
if not self._is_recording:
867+
return
868+
845869
if key == _ATTR_CLUSTER_NAME:
846870
self._cluster_name = value
847871
elif key == _ATTR_CLUSTER_UUID:
@@ -862,6 +886,12 @@ def set_status(self, status: SpanStatusCode) -> None:
862886
self._request_span.set_status(status)
863887

864888
def end(self, end_time: int) -> None:
889+
# For non-recording spans, still call end() for OTel context cleanup
890+
# but skip all the heavy processing (encoding spans, watermark, etc.)
891+
if not self._is_recording:
892+
self._request_span.end(end_time)
893+
return
894+
865895
self._end_encoding_spans()
866896
if self._wrapped_tracer.is_legacy:
867897
self._request_span.finish()

couchbase/logic/observability/no_op.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ def __init__(self,
5454
start_time: Optional[int] = None) -> None:
5555
pass
5656

57+
@property
58+
def is_recording(self) -> bool:
59+
return False
60+
5761
@property
5862
def name(self) -> str:
5963
return ''

couchbase/observability/otel_tracing.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ def __init__(self, otel_span: otel_trace.Span, name: str):
5757
def name(self) -> str:
5858
return self._name
5959

60+
@property
61+
def is_recording(self) -> bool:
62+
return self._otel_span.is_recording()
63+
6064
def set_attribute(self, key: str, value: SpanAttributeValue) -> None:
6165
self._otel_span.set_attribute(key, value)
6266

couchbase/observability/tracing.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ class RequestSpan(ABC):
5252
set the final status, and mark the completion of an operation.
5353
"""
5454

55+
@property
56+
def is_recording(self) -> bool:
57+
"""Whether this span is recording trace data.
58+
59+
Returns True by default for backward compatibility with existing
60+
RequestSpan implementations. Subclasses may override to return
61+
False when the span is not being sampled (e.g., OTel NonRecordingSpan).
62+
63+
When False, the SDK will skip expensive span attribute setting,
64+
encoding span creation, and dispatch span tree building.
65+
"""
66+
return True
67+
5568
@property
5669
@abstractmethod
5770
def name(self) -> str:

0 commit comments

Comments
 (0)