Skip to content

Commit 0e03383

Browse files
authored
Support OTLP in LightningStore (#313)
1 parent 0d72122 commit 0e03383

File tree

30 files changed

+2349
-365
lines changed

30 files changed

+2349
-365
lines changed

.github/workflows/examples-apo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
run-name: >-
1515
${{ github.event_name == 'repository_dispatch'
1616
&& format(
17-
'PR #{0} - Label {1} - {2}',
17+
'APO - PR #{0} - {1} - {2}',
1818
github.event.client_payload.pull_number,
1919
github.event.client_payload.ci_label,
2020
github.event.client_payload.correlation_id

.github/workflows/examples-calc-x.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
run-name: >-
1515
${{ github.event_name == 'repository_dispatch'
1616
&& format(
17-
'PR #{0} - Label {1} - {2}',
17+
'Calc-X - PR #{0} - {1} - {2}',
1818
github.event.client_payload.pull_number,
1919
github.event.client_payload.ci_label,
2020
github.event.client_payload.correlation_id

.github/workflows/examples-compat.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
run-name: >-
1515
${{ github.event_name == 'repository_dispatch'
1616
&& format(
17-
'PR #{0} - Label {1} - {2}',
17+
'Backward Compatibility - PR #{0} - {1} - {2}',
1818
github.event.client_payload.pull_number,
1919
github.event.client_payload.ci_label,
2020
github.event.client_payload.correlation_id

.github/workflows/examples-spider.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
run-name: >-
1515
${{ github.event_name == 'repository_dispatch'
1616
&& format(
17-
'PR #{0} - Label {1} - {2}',
17+
'Spider - PR #{0} - {1} - {2}',
1818
github.event.client_payload.pull_number,
1919
github.event.client_payload.ci_label,
2020
github.event.client_payload.correlation_id

.github/workflows/examples-unsloth.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
run-name: >-
1515
${{ github.event_name == 'repository_dispatch'
1616
&& format(
17-
'PR #{0} - Label {1} - {2}',
17+
'Unsloth - PR #{0} - {1} - {2}',
1818
github.event.client_payload.pull_number,
1919
github.event.client_payload.ci_label,
2020
github.event.client_payload.correlation_id

.github/workflows/tests-full.yml

Lines changed: 176 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
run-name: >-
1515
${{ github.event_name == 'repository_dispatch'
1616
&& format(
17-
'PR #{0} - Label {1} - {2}',
17+
'GPU Test - PR #{0} - {1} - {2}',
1818
github.event.client_payload.pull_number,
1919
github.event.client_payload.ci_label,
2020
github.event.client_payload.correlation_id
@@ -69,7 +69,7 @@ jobs:
6969
- name: Upload dependencies artifact
7070
uses: actions/upload-artifact@v4
7171
with:
72-
name: dependencies-${{ matrix.python-version }}-${{ matrix.setup-script }}
72+
name: dependencies-tests-full-${{ matrix.python-version }}-${{ matrix.setup-script }}
7373
path: requirements-freeze.txt
7474
compression-level: 0
7575

@@ -95,3 +95,177 @@ jobs:
9595
PYTEST_ADDOPTS: "--color=yes"
9696
OPENAI_BASE_URL: http://localhost:12306/
9797
OPENAI_API_KEY: dummy
98+
99+
minimal-examples:
100+
if: >
101+
github.event_name != 'repository_dispatch' ||
102+
github.event.action == 'ci-gpu' ||
103+
github.event.action == 'ci-all'
104+
name: Minimal Examples with Python ${{ matrix.python-version }} (${{ matrix.setup-script }})
105+
106+
runs-on: [self-hosted, 1ES.Pool=agl-runner-gpu]
107+
timeout-minutes: 30
108+
strategy:
109+
matrix:
110+
include:
111+
- python-version: '3.10'
112+
setup-script: 'legacy'
113+
- python-version: '3.12'
114+
setup-script: 'stable'
115+
- python-version: '3.13'
116+
setup-script: 'latest'
117+
fail-fast: false
118+
steps:
119+
- name: Check GPU status
120+
run: nvidia-smi
121+
- uses: actions/checkout@v4
122+
with:
123+
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.pr_ref || (github.event.pull_request.number && format('refs/pull/{0}/merge', github.event.pull_request.number)) || github.ref }}
124+
- uses: astral-sh/setup-uv@v7
125+
with:
126+
enable-cache: true
127+
python-version: ${{ matrix.python-version }}
128+
- name: Upgrade dependencies (latest)
129+
run: uv lock --upgrade
130+
if: matrix.setup-script == 'latest'
131+
- name: Sync dependencies (latest)
132+
run: uv sync --frozen --no-default-groups --extra apo --group dev --group agents --group torch-gpu-stable
133+
if: matrix.setup-script == 'latest'
134+
- name: Sync dependencies (stable & legacy)
135+
run: uv sync --frozen --no-default-groups --extra apo --group dev --group agents --group torch-gpu-${{ matrix.setup-script }}
136+
if: matrix.setup-script != 'latest'
137+
- name: Freeze dependencies
138+
run: |
139+
set -ex
140+
uv pip freeze | tee requirements-freeze.txt
141+
echo "UV_LOCKED=1" >> $GITHUB_ENV
142+
echo "UV_NO_SYNC=1" >> $GITHUB_ENV
143+
- name: Upload dependencies artifact
144+
uses: actions/upload-artifact@v4
145+
with:
146+
name: dependencies-minimal-examples-${{ matrix.python-version }}-${{ matrix.setup-script }}
147+
path: requirements-freeze.txt
148+
compression-level: 0
149+
150+
- name: Launch LiteLLM Proxy
151+
run: |
152+
./scripts/litellm_run.sh
153+
env:
154+
AZURE_API_BASE: ${{ secrets.AZURE_GROUP_SUBSCRIPTION_API_BASE }}
155+
AZURE_API_KEY: ${{ secrets.AZURE_GROUP_SUBSCRIPTION_API_KEY }}
156+
157+
- name: Write Traces via Otel Tracer
158+
run: |
159+
set -euo pipefail
160+
source .venv/bin/activate
161+
cd examples/minimal
162+
python write_traces.py otel
163+
164+
- name: Write Traces via AgentOps Tracer
165+
env:
166+
OPENAI_BASE_URL: http://localhost:12306/
167+
OPENAI_API_KEY: dummy
168+
run: |
169+
set -euo pipefail
170+
source .venv/bin/activate
171+
cd examples/minimal
172+
python write_traces.py agentops
173+
174+
- name: Write Traces via Otel Tracer with Client
175+
run: |
176+
set -euo pipefail
177+
source .venv/bin/activate
178+
cd examples/minimal
179+
agl store --port 45993 --log-level DEBUG &
180+
sleep 5
181+
python write_traces.py otel --use-client
182+
pkill -f agl && echo "SIGTERM sent to agl" || echo "No agl process found"
183+
while pgrep -f agl; do
184+
echo "Waiting for agl to finish..."
185+
sleep 5
186+
done
187+
188+
- name: Write Traces via AgentOps Tracer with Client
189+
env:
190+
OPENAI_BASE_URL: http://localhost:12306/
191+
OPENAI_API_KEY: dummy
192+
run: |
193+
set -euo pipefail
194+
source .venv/bin/activate
195+
cd examples/minimal
196+
agl store --port 45993 --log-level DEBUG &
197+
sleep 5
198+
python write_traces.py agentops --use-client
199+
pkill -f agl && echo "SIGTERM sent to agl" || echo "No agl process found"
200+
while pgrep -f agl; do
201+
echo "Waiting for agl to finish..."
202+
sleep 5
203+
done
204+
205+
- name: vLLM Server
206+
run: |
207+
set -euo pipefail
208+
source .venv/bin/activate
209+
cd examples/minimal
210+
python vllm_server.py Qwen/Qwen2.5-0.5B-Instruct
211+
212+
- name: LLM Proxy (OpenAI backend)
213+
env:
214+
OPENAI_API_BASE: http://localhost:12306/
215+
OPENAI_API_KEY: dummy
216+
run: |
217+
set -euo pipefail
218+
source .venv/bin/activate
219+
cd examples/minimal
220+
221+
python llm_proxy.py openai gpt-4.1-mini &
222+
223+
LLM_PROXY_READY=0
224+
for attempt in $(seq 1 30); do
225+
if curl -sSf http://localhost:43886/health > /dev/null 2>&1; then
226+
LLM_PROXY_READY=1
227+
break
228+
fi
229+
sleep 2
230+
done
231+
if [[ "$LLM_PROXY_READY" != "1" ]]; then
232+
echo "LLM proxy failed to become healthy" >&2
233+
exit 1
234+
fi
235+
236+
python llm_proxy.py test gpt-4.1-mini
237+
238+
pkill -f llm_proxy.py && echo "SIGTERM sent to llm_proxy.py" || echo "No llm_proxy.py process found"
239+
while pgrep -f llm_proxy.py; do
240+
echo "Waiting for llm_proxy.py to finish..."
241+
sleep 5
242+
done
243+
244+
- name: LLM Proxy (vLLM backend)
245+
if: matrix.setup-script != 'legacy' # Skip if return_token_ids is not supported
246+
run: |
247+
set -euo pipefail
248+
source .venv/bin/activate
249+
cd examples/minimal
250+
python llm_proxy.py vllm Qwen/Qwen2.5-0.5B-Instruct &
251+
252+
LLM_PROXY_READY=0
253+
for attempt in $(seq 1 30); do
254+
if curl -sSf http://localhost:43886/health > /dev/null 2>&1; then
255+
LLM_PROXY_READY=1
256+
break
257+
fi
258+
sleep 2
259+
done
260+
if [[ "$LLM_PROXY_READY" != "1" ]]; then
261+
echo "LLM proxy failed to become healthy" >&2
262+
exit 1
263+
fi
264+
265+
python llm_proxy.py test Qwen/Qwen2.5-0.5B-Instruct
266+
267+
pkill -f llm_proxy.py && echo "SIGTERM sent to llm_proxy.py" || echo "No llm_proxy.py process found"
268+
while pgrep -f llm_proxy.py; do
269+
echo "Waiting for llm_proxy.py to finish..."
270+
sleep 5
271+
done

agentlightning/cli/store.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@ def main(argv: Iterable[str] | None = None) -> int:
2525
action="append",
2626
help="Allowed CORS origin. Repeat for multiple origins. Use '*' to allow all origins.",
2727
)
28+
parser.add_argument(
29+
"--log-level",
30+
default="INFO",
31+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
32+
help="Configure the logging level for the store.",
33+
)
2834
args = parser.parse_args(list(argv) if argv is not None else None)
2935

30-
setup_logging()
36+
setup_logging(args.log_level)
3137

3238
store = InMemoryLightningStore()
3339
server = LightningStoreServer(

agentlightning/instrumentation/agentops.py

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
1414
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
1515
from opentelemetry.sdk.metrics.export import MetricExportResult
16-
from opentelemetry.sdk.trace.export import SpanExportResult
16+
17+
from agentlightning.utils.otlp import LightningStoreOTLPExporter
1718

1819
logger = logging.getLogger(__name__)
1920

@@ -32,38 +33,39 @@ def enable_agentops_service(enabled: bool = True) -> None:
3233
"""
3334
Enable or disable communication with the AgentOps service.
3435
35-
False (default): AgentOps exporters and clients will run in local mode
36-
and will not attempt to communicate with the remote AgentOps service.
37-
True: all exporters and clients will operate in normal mode and send data
38-
to the AgentOps service as expected.
36+
By default, AgentOps exporters and clients will run in local mode
37+
and will NOT attempt to communicate with the remote AgentOps service.
38+
39+
Args:
40+
enabled: If True, enable all AgentOps exporters and clients.
41+
All exporters and clients will operate in normal mode and send data
42+
to the [AgentOps service](https://www.agentops.ai).
3943
"""
4044
global _agentops_service_enabled
4145
_agentops_service_enabled = enabled
42-
logger.info(f"Switch set to {enabled} for exporters and clients.")
46+
logger.info(f"AgentOps service enabled is set to {enabled}.")
4347

4448

4549
def _patch_exporters():
4650
import agentops.client.api
4751
import agentops.sdk.core
48-
import opentelemetry.exporter.otlp.proto.http.metric_exporter
49-
import opentelemetry.exporter.otlp.proto.http.trace_exporter
5052

5153
agentops.sdk.core.AuthenticatedOTLPExporter = BypassableAuthenticatedOTLPExporter # type: ignore
52-
opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter = BypassableOTLPMetricExporter
53-
opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter = BypassableOTLPSpanExporter
54+
agentops.sdk.core.OTLPMetricExporter = BypassableOTLPMetricExporter
55+
if hasattr(agentops.sdk.core, "OTLPSpanExporter"):
56+
agentops.sdk.core.OTLPSpanExporter = BypassableOTLPSpanExporter # type: ignore
5457
agentops.client.api.V3Client = BypassableV3Client
5558
agentops.client.api.V4Client = BypassableV4Client
5659

5760

5861
def _unpatch_exporters():
5962
import agentops.client.api
6063
import agentops.sdk.core
61-
import opentelemetry.exporter.otlp.proto.http.metric_exporter
62-
import opentelemetry.exporter.otlp.proto.http.trace_exporter
6364

6465
agentops.sdk.core.AuthenticatedOTLPExporter = AuthenticatedOTLPExporter # type: ignore
65-
opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter = OTLPMetricExporter
66-
opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter = OTLPSpanExporter
66+
agentops.sdk.core.OTLPMetricExporter = OTLPMetricExporter
67+
if hasattr(agentops.sdk.core, "OTLPSpanExporter"):
68+
agentops.sdk.core.OTLPSpanExporter = OTLPSpanExporter # type: ignore
6769
agentops.client.api.V3Client = V3Client
6870
agentops.client.api.V4Client = V4Client
6971

@@ -243,18 +245,15 @@ def uninstrument_agentops():
243245
pass
244246

245247

246-
class BypassableAuthenticatedOTLPExporter(AuthenticatedOTLPExporter):
248+
class BypassableAuthenticatedOTLPExporter(LightningStoreOTLPExporter, AuthenticatedOTLPExporter):
247249
"""
248250
AuthenticatedOTLPExporter with switchable service control.
251+
249252
When `_agentops_service_enabled` is False, skip export and return success.
250253
"""
251254

252-
def export(self, *args: Any, **kwargs: Any) -> SpanExportResult:
253-
if _agentops_service_enabled:
254-
return super().export(*args, **kwargs)
255-
else:
256-
logger.debug("SwitchableAuthenticatedOTLPExporter is switched off, skipping export.")
257-
return SpanExportResult.SUCCESS
255+
def should_bypass(self) -> bool:
256+
return not _agentops_service_enabled
258257

259258

260259
class BypassableOTLPMetricExporter(OTLPMetricExporter):
@@ -271,18 +270,16 @@ def export(self, *args: Any, **kwargs: Any) -> MetricExportResult:
271270
return MetricExportResult.SUCCESS
272271

273272

274-
class BypassableOTLPSpanExporter(OTLPSpanExporter):
273+
class BypassableOTLPSpanExporter(LightningStoreOTLPExporter):
275274
"""
276275
OTLPSpanExporter with switchable service control.
277276
When `_agentops_service_enabled` is False, skip export and return success.
277+
278+
This is used instead of BypassableAuthenticatedOTLPExporter on legacy AgentOps versions.
278279
"""
279280

280-
def export(self, *args: Any, **kwargs: Any) -> SpanExportResult:
281-
if _agentops_service_enabled:
282-
return super().export(*args, **kwargs)
283-
else:
284-
logger.debug("SwitchableOTLPSpanExporter is switched off, skipping export.")
285-
return SpanExportResult.SUCCESS
281+
def should_bypass(self) -> bool:
282+
return not _agentops_service_enabled
286283

287284

288285
class BypassableV3Client(V3Client):

0 commit comments

Comments
 (0)