feat: add observability_pipelines resource type - #725
michael-richey wants to merge 21 commits into
Conversation
Add support for syncing Datadog Observability Pipelines via the v2 /api/v2/obs-pipelines/pipelines API: - New ObservabilityPipelines model (BaseResource subclass) with paginated LIST, GET-by-id import, POST create, PUT update, and DELETE. - Match by id (resource_mapping_key='id'), mirroring logs_metrics. The server-assigned id is excluded from create/update payloads via excluded_attributes=['id']; update_resource re-injects the destination id before the PUT. - Register the model in models/__init__.py so init_resources discovers it. - Add 'observability_pipelines' to _ID_FILE_IMPORT_SUPPORTED_TYPES so import --id-file works (default import_resource(_id=...) does a real GET and get_resources_by_ids classifies 404/429/5xx without aborting). - Unit tests cover config contract, registration, all CRUD methods, prep_resource id-stripping, no-op hooks, and id-file allowlist membership.
- Add observability_pipelines row to the supported-resources table in README.md (alphabetical, between notebooks and powerpacks). - Add integration test stub (skipped — OP pipelines hold downstream routing config, so destructive cleanup against a real org is unsafe for CI).
michael-richey
left a comment
There was a problem hiding this comment.
Thanks for pushing this — the model/test coverage is solid overall, but I found one blocking issue before we merge.
Blocking: pagination config likely mismatches OP list response shape
ObservabilityPipelines.get_resources() uses client.paginated_request(client.get) with the default PaginationConfig.
The default remaining_func in custom_client.py expects resp["meta"]["page"]["total_count"]. For Observability Pipelines, the v2 API/SDK schema exposes list metadata as meta.totalCount (not meta.page.total_count).
That means this code can work for orgs with <100 pipelines (single page), but once a page is full it may hit remaining_func and raise on the missing meta.page key, aborting import/sync for this type.
Can we switch this model to an explicit PaginationConfig for the OP response shape and add a unit test that exercises the multi-page path (not just the paginated wrapper call contract)?
The default remaining_func in custom_client.py reads resp["meta"]["page"]["total_count"], but the Observability Pipelines v2 list endpoint returns total count as meta.totalCount (camelCase). For orgs with <100 pipelines the single-page early break hides this, but a full first page would raise KeyError on the missing meta.page key, aborting import/sync for this resource type. Add a custom PaginationConfig with a remaining_func that reads meta.totalCount (with a safe fallback to 0 if meta is absent, so pagination stops gracefully). Update get_resources to pass this config explicitly. Add unit tests for the custom remaining_func (multi-page arithmetic) and for verifying get_resources passes the custom config to paginated_request.
|
Good catch — you're right that the default Fixed in 171dc9c:
All 1485 unit tests pass; |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The state-load allowlist contract is incomplete, and all API-backed integration coverage is disabled.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds Datadog Observability Pipelines as a syncable resource using the v2 API.
Changes:
- Implements list, import, create, update, and delete operations.
- Registers and documents the resource with
--id-filesupport. - Adds unit tests and a skipped integration-test stub.
| File | Description |
|---|---|
datadog_sync/model/observability_pipelines.py |
Implements the resource model and pagination. |
datadog_sync/models/__init__.py |
Registers the model. |
datadog_sync/utils/configuration.py |
Adds import ID-file support. |
README.md |
Lists the supported resource. |
tests/unit/test_observability_pipelines.py |
Tests model behavior. |
tests/unit/test_observability_pipelines_id_file.py |
Tests ID-file support. |
tests/integration/resources/test_observability_pipelines.py |
Adds a skipped integration-test stub. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Adding observability_pipelines to _ID_FILE_IMPORT_SUPPORTED_TYPES also adds it to the union _ID_FILE_SUPPORTED_TYPES, which _parse_id_file consults for both import and sync --minimize-reads --id-file. The established contract (documented in the dashboards precedent at tests/unit/test_dashboards_id_file.py:43-59) requires every ID-derivable type accepted on the state-load path to be explicitly listed in _ID_FILE_STATE_LOAD_SUPPORTED_TYPES, so the ID-derivability is audited rather than accepted incidentally via the union. The OP pipeline state key is the pipeline id (storage layout: resources/source/observability_pipelines.<id>.json), which is ID-derivable, so it qualifies. Add it to the state-load set and assert membership in a new unit test.
|
Note on the Retriggering again would occupy the queue-mode |
|
Update on |
Integration Test ResultsResource type:
| create | | update | | skip | | delete | | fuzz | ✅ pass | |
3 similar comments
Integration Test ResultsResource type:
| create | | update | | skip | | delete | | fuzz | ✅ pass | |
Integration Test ResultsResource type:
| create | | update | | skip | | delete | | fuzz | ✅ pass | |
Integration Test ResultsResource type:
| create | | update | | skip | | delete | | fuzz | ✅ pass | |
Integration Test Findings — observability_pipelinesRan the generic integration test framework against PR #725. Fixtures created successfully, but import fails due to a pagination bug. ✅ Fixtures created (3 pipelines)❌ Import fails — pagination page_size too largeRoot cause: The pagination_config = PaginationConfig(
page_size=100,
...
)But the Observability Pipelines API only accepts Suggested fix: Change pagination_config = PaginationConfig(
page_size=50,
...
)Test results
What works
What needs fixing
|
The Observability Pipelines API rejects page[size] > 50 with a 400 Bad Request. Change pagination_config.page_size from 100 to 50 so import succeeds. Update unit tests to match the new page_size.
Integration Test ResultsResource type:
| create | ❌ fail | | update | | skip | | delete | | fuzz | ❌ fail | |
|
The The current branch has |
Integration Test ResultsResource type:
| create | ❌ fail | | update | | skip | | delete | | fuzz | ❌ fail | AI Agent DiagnosisNo error logs or test output for Could you paste the actual error output from the failed test? I need the stderr/stdout from the |
Integration Test ResultsResource type:
| create | ❌ fail | | update | | skip | | delete | | fuzz | ❌ fail | AI Agent DiagnosisAnalysisRoot cause: The Suggested fix: Add # In the observability_pipelines model file (e.g., models/observability_pipelines.py)
class ObservabilityPipelines(DatadogResource):
resource_type = "observability_pipelines"
resource_id_attr = "id"
excluded_attributes = [
# ... existing exclusions ...
"processors", # API rejects if both 'processors' and 'processor_groups' are present
]Additionally, verify that |
Integration Test ResultsResource type:
| create | ❌ fail | | update | | skip | | delete | | fuzz | ❌ fail | AI Agent DiagnosisRoot CauseThe Suggested FixIn the sync-cli model file for # In the model class definition
excluded_attributes = ["processors"]Or if the field needs to be transformed rather than dropped, handle it in @classmethod
def prep_resource(cls, resource):
resource = super().prep_resource(resource)
# API only accepts processor_groups, not processors
resource.get("attributes", {}).pop("processors", None)
return resourceThis ensures the destination payload only includes |
…e payload The OP API rejects create/update payloads containing both 'processors' and 'processor_groups'. The source API returns 'processors' as a read-only field, so it must be stripped via excluded_attributes before sending to the destination. Add 'attributes.processors' to excluded_attributes and a unit test verifying the strip.
Integration Test ResultsResource type:
| create | ❌ fail | | update | | skip | | delete | | fuzz | ❌ fail | AI Agent DiagnosisAnalysisRoot cause: The Suggested fix: Add # In the observability_pipelines model file
excluded_attributes = [
# ... existing exclusions ...
"processors",
]Or, if def prep_resource(self, resource):
resource = super().prep_resource(resource)
if "attributes" in resource and "processors" in resource["attributes"]:
del resource["attributes"]["processors"]
return resource |
Re-test:
|
…ributes.config.processors
The 'processors' field is nested inside 'config', not directly in
'attributes'. The previous exclusion 'attributes.processors' did not
match the actual path 'attributes.config.processors', so the field was
not stripped and the API still rejected the payload with 400 Bad
Request ('cannot specify both processors and processor_groups').
Fix: change excluded_attributes from 'attributes.processors' to
'attributes.config.processors'. Update unit tests to match.


Summary
Adds support for syncing Datadog Observability Pipelines as a new resource type (
observability_pipelines), using the v2 API at/api/v2/obs-pipelines/pipelines.Changes
New model
datadog_sync/model/observability_pipelines.py—ObservabilityPipelines(BaseResource)with:GET /api/v2/obs-pipelines/pipelines(usesclient.paginated_request, matching the defaultPaginationConfigofpage[size]/page[number]withresponse_list_accessor="data").GET .../pipelines/{id}for the per-ID path; pass-through for the list path. Unwraps the{"data": ...}envelope.POST .../pipelineswith{"data": resource}. If the sourceidalready exists in the destination map, delegates toupdate_resource(id-keyed dedup, mirroringlogs_metrics).PUT .../pipelines/{destination_id}with{"data": resource}. Re-injects the destinationidinto the resource before the PUT (PUT, not PATCH — matches the OP API and the precedent oflogs_indexes/dashboards).DELETE .../pipelines/{destination_id}.resource_mapping_key="id",excluded_attributes=["id"](strips the server-assignedidfrom create payloads and from diff comparisons viaprep_resource).resource_connections— an OP pipeline config (sources/destinations/processors) references external systems via embedded connection settings, not sync-cli-managed Datadog resource IDs.Registration —
datadog_sync/models/__init__.pyimportsObservabilityPipelinessoinit_resourcesdiscovers it viamodels.__dict__.--id-filesupport — addedobservability_pipelinesto_ID_FILE_IMPORT_SUPPORTED_TYPES. The defaultimport_resource(_id=...)does a real GET, and the defaultget_resources_by_idsclassifies 404/429/5xx/403 without aborting (satisfies the continue-past-errors design).README — added
observability_pipelinesto the supported-resources table.Integration test stub —
tests/integration/resources/test_observability_pipelines.py, skipped (OP pipelines hold downstream routing config; destructive cleanup against a real org is unsafe for CI).Design decisions
id(notattributes.name) — mirrorslogs_metrics. Source and destination UUIDs differ, so the first sync of a pipeline always creates it on the destination; re-sync idempotency relies on persisted state. This means a pipeline deleted on the destination between syncs would be re-created rather than updated in place.PUT .../pipelines/{pipeline_id}, not PATCH.update_resourcere-injects the destinationid(stripped byprep_resource) before the PUT.--id-filestate-load allowlist entry — the state key is the pipeline id (ID-derivable), but the state-load path scopes by--resourcesintersection, not by_ID_FILE_STATE_LOAD_SUPPORTED_TYPES. This matches thelogs_metricsprecedent (also absent from the state-load set).Testing
tests/unit/test_observability_pipelines.py— config contract, registration viainit_resources, paginatedget_resources, import (pass-through + GET-by-id), create (POST + id-keyed dedup), update (PUT with destination id), delete,prep_resourceid-stripping, no-op hooks.tests/unit/test_observability_pipelines_id_file.py— allowlist membership (import set + union) andimport_resource(_id=...)GET-path verification.Test plan