Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3f7dbe9
Reduce parallel test execution from 8 to 4
BryanFauble Aug 13, 2026
cf88454
[SYNPY-1892] Slice 1: extract shared OTel resource-attribute seam
BryanFauble Aug 14, 2026
aa99dcf
[SYNPY-1892] Slice 2: instrument send_job_and_wait_async
BryanFauble Aug 14, 2026
f2b52ac
[SYNPY-1892] Slice 3: instrument upload_file_handle
BryanFauble Aug 14, 2026
97e012e
[SYNPY-1892] Slice 4: per-worker OTel identity, truthiness fix, dead …
BryanFauble Aug 14, 2026
7be3e5d
[SYNPY-1892] Slice 5: sweep stale -n 8 references, fix OTel run-proto…
BryanFauble Aug 14, 2026
3c3855f
[SYNPY-1892] Docs: reflect OTel metrics and per-worker identity in te…
BryanFauble Aug 14, 2026
6b5f1c8
[SYNPY-1892] Slice 1: measurement-trust fixes - unique span key, asyn…
BryanFauble Aug 17, 2026
a5f9b8a
[SYNPY-1892] Slice 2: upload external/internal split attribute
BryanFauble Aug 17, 2026
267e6f0
[SYNPY-1892] Slice 3: CONTRIBUTING.md tells the truth about OTEL_DEBU…
BryanFauble Aug 17, 2026
ea1b5ff
[SYNPY-1892] Slice 4: SigNoz query script, .github/scripts/measure_te…
BryanFauble Aug 17, 2026
b08e8d2
[SYNPY-1892] Slice 4 fix: correct cumulative-counter aggregation and …
BryanFauble Aug 17, 2026
aea76cb
[SYNPY-1892] Slice 6: merge duplicated-setup tests in test_permission…
BryanFauble Aug 18, 2026
03baf85
[SYNPY-1892] Slice 6: merge duplicated-setup tests in test_project_as…
BryanFauble Aug 18, 2026
318f80e
[SYNPY-1892] Slice 6: eliminate real uploads in test_permissions_asyn…
BryanFauble Aug 18, 2026
5f417ef
[SYNPY-1892] Slice 6: broaden feedback-0017 upload elimination to rem…
BryanFauble Aug 18, 2026
f94a482
[SYNPY-1892] Slice 6: eliminate real uploads in test_synapseutils_syn…
BryanFauble Aug 18, 2026
7c065c2
[SYNPY-1892] Revert TestSyncFromSynapse upload elimination — breaks l…
BryanFauble Aug 18, 2026
053e604
[SYNPY-1892] Slice 8: merge duplicated-setup project fixture in test_…
BryanFauble Aug 18, 2026
2982cc8
[SYNPY-1892] Slice 8: merge duplicated-setup evaluation/file fixtures…
BryanFauble Aug 18, 2026
82518ff
[SYNPY-1892] Slice 8: merge duplicated-setup fixtures in test_submiss…
BryanFauble Aug 18, 2026
d355dad
[SYNPY-1892] Slice 8: merge duplicated-setup fixtures in test_submiss…
BryanFauble Aug 18, 2026
4951145
[SYNPY-1892] Bound test_agent_async.py, 85% of integration-suite wall…
BryanFauble Aug 19, 2026
0bae61d
[SYNPY-1892] Revert TestFolderManifestCSV annotations/provenance test…
BryanFauble Aug 19, 2026
80663ec
[SYNPY-1892] Remove order-dependence in TestProjectCopySync shared fi…
BryanFauble Aug 19, 2026
a03f7af
[SYNPY-1892] Document external_url upload-avoidance limit in CONTRIBU…
BryanFauble Aug 19, 2026
f23afe1
[SYNPY-1892] Merge TestTableSnapshot and TestDeleteRows fixtures in t…
BryanFauble Aug 19, 2026
68bfbbc
[SYNPY-1892] Own class wiki fixtures with Folders instead of Projects
BryanFauble Aug 19, 2026
4d3726a
[SYNPY-1892] Use a Folder instead of a Project for TestFormData test_…
BryanFauble Aug 19, 2026
8f3558f
[SYNPY-1892] Share folder_with_view/grid fixtures across TestCuration…
BryanFauble Aug 19, 2026
0d291e7
[SYNPY-1892] Share record_set_with_validation_fixture/create_test_sch…
BryanFauble Aug 19, 2026
efa75be
[SYNPY-1892] Reuse a single VirtualTable via SQL updates instead of 4…
BryanFauble Aug 19, 2026
befe687
[SYNPY-1892] Reuse shared project_model instead of per-fixture Projec…
BryanFauble Aug 19, 2026
1fb4318
[SYNPY-1892] Share read-only datasets across TestDatasetCollection te…
BryanFauble Aug 19, 2026
180adf8
[SYNPY-1892] Share read-only folder+files across 4 TestEntityView tes…
BryanFauble Aug 19, 2026
6da241c
[SYNPY-1892] Share read-only evaluations across TestColumnAndScopeMod…
BryanFauble Aug 19, 2026
2671604
[SYNPY-1892] Add duration_sec to per-test load table from root-span d…
BryanFauble Aug 19, 2026
c755b8a
Drop files
BryanFauble Aug 20, 2026
0c21b4d
Drop file
BryanFauble Aug 20, 2026
6bbb0db
Drop key
BryanFauble Aug 20, 2026
cdf4215
Skip agent prompt integration tests
BryanFauble Aug 20, 2026
a176761
Enable agent test
BryanFauble Aug 21, 2026
89be414
Address PR review feedback
BryanFauble Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ jobs:
pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml \
--html=integration-test-report.html --self-contained-html \
--junit-xml=test-results.xml \
-n 8 --dist loadscope \
-n 4 --dist loadscope \
$(cat failed_tests.txt | tr '\n' ' ')
else
echo "::notice::First attempt or no previous failures - running full integration test suite"
Expand All @@ -220,7 +220,7 @@ jobs:
pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml \
--html=integration-test-report.html --self-contained-html \
--junit-xml=test-results.xml \
tests/integration -n 8 $IGNORE_FLAGS --dist loadscope
tests/integration -n 4 $IGNORE_FLAGS --dist loadscope
fi

# Execute the CLI tests in a non-dist way because they were causing some test instability when being run concurrently
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ jobs:
export EXTERNAL_S3_BUCKET_AWS_ACCESS_KEY_ID="${{secrets.EXTERNAL_S3_BUCKET_AWS_ACCESS_KEY_ID}}"
export EXTERNAL_S3_BUCKET_AWS_SECRET_ACCESS_KEY="${{secrets.EXTERNAL_S3_BUCKET_AWS_SECRET_ACCESS_KEY}}"

pytest -sv --reruns 3 tests/integration -n 8 --ignore=tests/integration/synapseclient/test_command_line_client.py --dist loadscope
pytest -sv --reruns 3 tests/integration -n 4 --ignore=tests/integration/synapseclient/test_command_line_client.py --dist loadscope
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pip install -e ".[boto3,pandas,pysftp,tests,curator,dev]"
pytest -sv tests/unit

# Integration tests (requires Synapse credentials, runs in parallel)
pytest -sv --reruns 3 tests/integration -n 8 --dist loadscope
pytest -sv --reruns 3 tests/integration -n 4 --dist loadscope

# Pre-commit checks (ruff, black, isort, bandit)
pre-commit run --all-files
Expand Down Expand Up @@ -122,7 +122,7 @@ For type annotations referencing pandas types, use `DATA_FRAME_TYPE` and `SERIES
- `asyncio_mode = auto` in pytest.ini — no need for `@pytest.mark.asyncio`
- `asyncio_default_fixture_loop_scope = session` — all async tests share one event loop
- Unit test client fixture: session-scoped, `skip_checks=True`, `cache_client=False`
- Integration tests use `--reruns 3` for flaky retries and `-n 8 --dist loadscope` for parallelism
- Integration tests use `--reruns 3` for flaky retries and `-n 4 --dist loadscope` for parallelism
- Integration fixtures create per-worker Synapse projects; use `schedule_for_cleanup()` for teardown
- Auth env vars: `SYNAPSE_AUTH_TOKEN` (bearer token), `SYNAPSE_PROFILE` (config file profile, default: `"default"`), `SYNAPSE_TOKEN_AWS_SSM_PARAMETER_NAME` (AWS SSM path)
- CI runs integration tests only on Python 3.10 and 3.14 (oldest + newest) to limit Synapse server load
Expand Down
50 changes: 45 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ pytest -sv tests/unit

# Integration tests (requires Synapse credentials, ~30-60 minutes)
# Uses pytest-xdist for parallel execution with fixture-aware distribution
pytest -sv tests/integration -n 8 --dist loadscope
pytest -sv tests/integration -n 4 --dist loadscope

# Integration tests excluding CLI tests (which must run serially)
pytest -sv tests/integration -n 8 --dist loadscope \
pytest -sv tests/integration -n 4 --dist loadscope \
--ignore=tests/integration/synapseclient/test_command_line_client.py
```

Expand All @@ -218,9 +218,48 @@ fileHandleEndpoint=https://repo-dev.dev.sagebase.org/file/v1
```

#### Running OpenTelemetry in Integration Tests
`tests/integration/conftest.py` is where we defining which trace exporter to use. Set the `SYNAPSE_OTEL_INTEGRATION_TEST_EXPORTER` environment variable to `otlp` or `console` depending on your use case.
`tests/integration/conftest.py`'s `setup_otel` fixture decides whether to enable OpenTelemetry
for the run. Set `SYNAPSE_INTEGRATION_TEST_OTEL_ENABLED=true` to turn it on; only the values
`1`, `true`, `yes`, and `on` (case-insensitive) count, everything else is treated as off.

When integration tests are ran in the Github CI/CD pipeline it will upload the trace data automatically using OLTP.
```
export SYNAPSE_INTEGRATION_TEST_OTEL_ENABLED=true
export OTEL_EXPORTER_OTLP_ENDPOINT=<your collector endpoint>
export OTEL_EXPORTER_OTLP_HEADERS=<auth headers your collector requires>
```

To verify locally without a real collector, set `OTEL_DEBUG_CONSOLE=true` to print spans and
metrics to stdout **instead of** exporting them. This only produces output for a **serial** run
with `-s`:
```
pytest -s tests/integration/<some module>
```
It does not work under pytest's default output capture (the console exporters flush after
pytest has already closed the captured stream) and it does not work under `-n` (each
pytest-xdist worker's stdout goes through `execnet` and is discarded, even with `-s`) — so it
cannot be used to verify the `-n 4 --dist loadscope` recipe below. For parallel verification, use
a real collector (`OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_HEADERS`, no
`OTEL_DEBUG_CONSOLE`) and check the data landed there.

A rejected export (e.g. malformed `OTEL_EXPORTER_OTLP_HEADERS`) does not fail the run on its
own — the exporter logs the error and pytest exits 0 regardless, so a run can finish "green"
having recorded nothing. `tests/integration/conftest.py` guards against this: it captures OTLP
export-rejection log records for the session (forwarding a worker's via `workeroutput` under
`-n` so the controller sees them too) and fails the session (non-zero exit, plus a terminal
summary line naming the rejection) if any export was rejected. Trust that exit code, not the
`passed`/`failed` count, to know whether telemetry was actually accepted.

For a measurement run, set `SYNAPSE_TEST_RUN_LABEL` to a value that identifies the run (e.g. a
ticket number or date) so its data points can be grouped and compared against other runs; each
pytest-xdist worker gets its own `service.instance.id` so counts are not double-counted across
workers. Keep `--reruns 3` for measurement runs, same as any other run.

The two metric instruments to query are `synapse.async_job.submissions` /
`synapse.async_job.duration` (async-job load, with `request_type` and `outcome` attributes) and
`synapse.file_handle.uploads` / `synapse.file_handle.upload.duration` (file-handle upload load).

When integration tests are run in the GitHub CI/CD pipeline it will upload the trace and metric
data automatically using OTLP.


#### Integration testing for external collaborators
Expand Down Expand Up @@ -443,7 +482,8 @@ following set of guidelines should be followed:
- `function` scope: Use for entities that tests **mutate** (e.g., files with changed names, datasets with added/removed items, submission statuses being updated). Each test gets a fresh entity.
- All fixtures that create Synapse entities **must** call `schedule_for_cleanup()` to register them for cleanup at session end.
- **Polling and retries:** For eventual-consistency scenarios (e.g., waiting for permission propagation, schema binding, attachment preview generation), use `wait_for_condition()` from `tests/integration/helpers.py` instead of hardcoded `asyncio.sleep()` calls. This uses exponential backoff and returns as soon as the condition is met.
- **Parallel execution:** Tests run with `pytest -n 8 --dist loadscope`, which ensures all tests in a class execute on the same worker sequentially. Session-scoped fixtures are shared within each worker.
- **Avoiding real uploads:** A `File(external_url=..., synapse_store=False)` file handle creates a real FileEntity without a real upload, and is the right default when a test only needs the entity to exist (e.g. as a parent, a copy/walk target, or a structure check). **Do not** use it for a test that reads the file back — `sync_from_synapse_async`, a real download, an md5 comparison, or a manifest/annotation/provenance round-trip all require real content on disk.
- **Parallel execution:** Tests run with `pytest -n 4 --dist loadscope`, which ensures all tests in a class execute on the same worker sequentially. Session-scoped fixtures are shared within each worker.

### Repository Admins

Expand Down
58 changes: 29 additions & 29 deletions synapseclient/core/otel_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""OpenTelemetry configuration for Synapse Python Client."""

import os
import platform
import sys
from typing import Any, Dict, List, Optional

Expand Down Expand Up @@ -85,19 +86,19 @@ def force_flush(self, timeout_millis: int = 30000) -> None:
"""No-op method that does nothing when the span processor is forced to flush."""


def configure_traces(
def _build_resource_attributes(
resource_attributes: Optional[Dict[str, Any]] = None,
include_context: bool = True,
) -> TracerProvider:
) -> Dict[str, Any]:
"""
Configure OpenTelemetry tracing for the Synapse Python Client.
Build the resource attributes shared by the trace and metric providers.

Args:
resource_attributes: Additional resource attributes to include
include_context: Whether to include contextual information about the runtime environment

Returns:
The configured TracerProvider
The resource attributes to pass to `Resource.create`
"""
resource_attrs = {
SERVICE_NAME: os.environ.get("OTEL_SERVICE_NAME", DEFAULT_SERVICE_NAME),
Expand All @@ -112,7 +113,7 @@ def configure_traces(
str(v) for v in sys.version_info[:3]
)

resource_attrs["os.type"] = os.name
resource_attrs["os.type"] = platform.system().lower()

try:
from synapseclient import __version__ as client_version
Expand All @@ -124,7 +125,26 @@ def configure_traces(
if resource_attributes:
resource_attrs.update(resource_attributes)

resource = Resource.create(resource_attrs)
return resource_attrs


def configure_traces(
resource_attributes: Optional[Dict[str, Any]] = None,
include_context: bool = True,
) -> TracerProvider:
"""
Configure OpenTelemetry tracing for the Synapse Python Client.

Args:
resource_attributes: Additional resource attributes to include
include_context: Whether to include contextual information about the runtime environment

Returns:
The configured TracerProvider
"""
resource = Resource.create(
_build_resource_attributes(resource_attributes, include_context)
)

provider = TracerProvider(resource=resource)

Expand Down Expand Up @@ -165,29 +185,9 @@ def configure_metrics(
Returns:
The configured MeterProvider
"""
resource_attrs = {
SERVICE_NAME: os.environ.get("OTEL_SERVICE_NAME", DEFAULT_SERVICE_NAME),
SYNAPSE_SERVICE_VERSION: CLIENT_VERSION,
}

if include_context:
resource_attrs["python.version"] = ".".join(
str(v) for v in sys.version_info[:3]
)

resource_attrs["os.type"] = os.name

try:
from synapseclient import __version__ as client_version

resource_attrs[SYNAPSE_SERVICE_VERSION] = client_version
except ImportError:
pass

if resource_attributes:
resource_attrs.update(resource_attributes)

resource = Resource.create(resource_attrs)
resource = Resource.create(
_build_resource_attributes(resource_attributes, include_context)
)

readers = []

Expand Down
Loading
Loading