Skip to content

Commit 8be1413

Browse files
refactor: streamline project and logstream resolution in GalileoOTLPE… (#493)
1 parent 7360299 commit 8be1413

File tree

2 files changed

+31
-47
lines changed

2 files changed

+31
-47
lines changed

src/galileo/otel.py

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import contextvars
22
import json
33
import logging
4-
import os
54
import typing
6-
import uuid
75
from _contextvars import ContextVar
86
from collections.abc import Generator
97
from contextlib import contextmanager
@@ -14,6 +12,7 @@
1412

1513
from galileo.config import GalileoPythonConfig
1614
from galileo.decorator import _experiment_id_context, _log_stream_context, _project_context, _session_id_context
15+
from galileo.utils.env_helpers import _get_log_stream_or_default, _get_project_or_default
1716
from galileo.utils.retrievers import document_adapter
1817
from galileo_core.schemas.logging.span import RetrieverSpan, ToolSpan, WorkflowSpan
1918
from galileo_core.schemas.logging.span import Span as GalileoSpan
@@ -128,29 +127,12 @@ def __init__(self, project: Optional[str] = None, logstream: Optional[str] = Non
128127
if not api_key:
129128
raise ValueError("API key is required.")
130129

131-
if project is not None:
132-
_project_context.set(project)
133-
if logstream is not None:
134-
_log_stream_context.set(logstream)
135-
136-
# Resolve project and logstream from parameters, context, or environment variables
137-
# Check context first, then environment, then generate/use defaults
138-
ctx_project = _project_context.get(None)
139-
ctx_logstream = _log_stream_context.get(None)
140-
141-
if ctx_project is not None:
142-
self.project = ctx_project
143-
elif "GALILEO_PROJECT" in os.environ:
144-
self.project = os.environ["GALILEO_PROJECT"]
145-
else:
146-
self.project = f"project_{uuid.uuid4()}"
147-
148-
if ctx_logstream is not None:
149-
self.logstream = ctx_logstream
150-
elif "GALILEO_LOG_STREAM" in os.environ:
151-
self.logstream = os.environ["GALILEO_LOG_STREAM"]
152-
else:
153-
self.logstream = "default"
130+
# Resolve project and logstream: param first, then context var, then env var with default fallback
131+
ctx_project = project if project is not None else _project_context.get(None)
132+
ctx_logstream = logstream if logstream is not None else _log_stream_context.get(None)
133+
134+
self.project = _get_project_or_default(ctx_project)
135+
self.logstream = _get_log_stream_or_default(ctx_logstream)
154136

155137
exporter_headers = {"Galileo-API-Key": api_key, "project": self.project, "logstream": self.logstream}
156138

@@ -240,13 +222,13 @@ def __init__(
240222
"OpenTelemetry packages are not installed. "
241223
"Install optional OpenTelemetry dependencies with: pip install galileo[otel]"
242224
)
243-
if project is not None:
244-
_project_context.set(project)
245-
if logstream is not None:
246-
_log_stream_context.set(logstream)
247225

248-
self._project = _project_context.get()
249-
self._logstream = _log_stream_context.get()
226+
# Resolve project and logstream: param first, then context var, then env var with default fallback
227+
ctx_project = project if project is not None else _project_context.get(None)
228+
ctx_logstream = logstream if logstream is not None else _log_stream_context.get(None)
229+
230+
self._project = _get_project_or_default(ctx_project)
231+
self._logstream = _get_log_stream_or_default(ctx_logstream)
250232

251233
# Create the exporter using the config-based approach
252234
self._exporter = GalileoOTLPExporter(**kwargs)
@@ -259,8 +241,9 @@ def __init__(
259241
def on_start(self, span: Span, parent_context: Optional[context.Context] = None) -> None:
260242
"""Handle span start events by delegating to the underlying processor."""
261243
# Set Galileo context attributes on the span
262-
project = _project_context.get(self._project)
263-
log_stream = _log_stream_context.get(self._logstream)
244+
# Use context var if set and not None, otherwise fall back to instance defaults
245+
project = _project_context.get(None) or self._project
246+
log_stream = _log_stream_context.get(None) or self._logstream
264247
experiment_id = _experiment_id_context.get(None)
265248
session_id = _session_id_context.get(None)
266249

tests/test_otel.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import json
22
import os
33
import re
4-
import uuid
54
from unittest.mock import Mock, patch
65

76
import pytest
@@ -80,22 +79,19 @@ def test_init_and_parameter_priority(self, mock_otlp_init, mock_config, clear_en
8079
def test_init_with_env_variables(self, mock_otlp_init, mock_config, clear_env_vars):
8180
"""Test initialization using environment variables."""
8281
with patch.dict(os.environ, {"GALILEO_PROJECT": "env-project", "GALILEO_LOG_STREAM": "env-logstream"}):
83-
with patch("galileo.otel.uuid.uuid4") as mock_uuid:
84-
exporter = GalileoOTLPExporter()
85-
mock_uuid.assert_not_called() # No UUID generation when env var provided
86-
assert exporter.project == "env-project"
87-
assert exporter.logstream == "env-logstream"
82+
exporter = GalileoOTLPExporter()
83+
assert exporter.project == "env-project"
84+
assert exporter.logstream == "env-logstream"
8885

8986
@pytest.mark.skipif(not OTEL_AVAILABLE, reason="OpenTelemetry not available")
90-
@patch("galileo.otel.uuid.uuid4")
9187
@patch("galileo.otel.OTLPSpanExporter.__init__", return_value=None)
92-
def test_init_auto_generate_project(self, mock_otlp_init, mock_uuid, mock_config, clear_env_vars):
93-
"""Test automatic project generation when no project is provided."""
94-
mock_uuid.return_value = uuid.UUID("12345678-1234-5678-9012-123456789012")
88+
def test_init_uses_default_project(self, mock_otlp_init, mock_config, clear_env_vars):
89+
"""Test default project name is used when no project is provided."""
9590
GalileoOTLPExporter()
9691

9792
call_kwargs = mock_otlp_init.call_args[1]
98-
assert call_kwargs["headers"]["project"] == "project_12345678-1234-5678-9012-123456789012"
93+
assert call_kwargs["headers"]["project"] == "default"
94+
assert call_kwargs["headers"]["logstream"] == "default"
9995

10096
@pytest.mark.skipif(not OTEL_AVAILABLE, reason="OpenTelemetry not available")
10197
@patch("galileo.otel.OTLPSpanExporter.__init__", return_value=None)
@@ -391,7 +387,8 @@ def test_processor_on_start_sets_span_attributes(self, mock_processor_deps, rese
391387
assert ("galileo.project.name", "test-project") in actual_calls
392388
assert ("galileo.session.id", "test-session") in actual_calls
393389

394-
# Test with only project set (None values should be skipped)
390+
# Test that processor always sets project and logstream (using env var fallbacks)
391+
# When context is None, it falls back to env vars (set in conftest.py)
395392
_log_stream_context.set(None)
396393
_experiment_id_context.set(None)
397394
_session_id_context.set(None)
@@ -400,8 +397,12 @@ def test_processor_on_start_sets_span_attributes(self, mock_processor_deps, rese
400397
mock_span2 = Mock()
401398
processor2.on_start(mock_span2, None)
402399

403-
assert mock_span2.set_attribute.call_count == 1
404-
mock_span2.set_attribute.assert_called_with("galileo.project.name", "test-project")
400+
# Project and logstream are always set (env var fallbacks when context is None)
401+
assert mock_span2.set_attribute.call_count == 2
402+
actual_calls = {(args[0], args[1]) for args, _ in mock_span2.set_attribute.call_args_list}
403+
assert ("galileo.project.name", "test-project") in actual_calls
404+
# Falls back to GALILEO_LOG_STREAM env var set in conftest.py
405+
assert ("galileo.logstream.name", "test-log-stream") in actual_calls
405406

406407
@pytest.mark.skipif(not OTEL_AVAILABLE, reason="OpenTelemetry not available")
407408
@patch("galileo.otel.OTLPSpanExporter.export")

0 commit comments

Comments
 (0)