Skip to content

Commit 0e533ce

Browse files
authored
[Core] Enable metrics delivery to Port (#1686)
# Description What - removed comments from the end points of the metrics and fixed url's to the metrics endpoints Why - feature is ready to use How - deleted the comments ## Type of change Please leave one option from the following and delete the rest: - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] New Integration (non-breaking change which adds a new integration) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Non-breaking change (fix of existing functionality that will not change current behavior) - [ ] Documentation (added/updated documentation) <h4> All tests should be run against the port production environment(using a testing org). </h4> ### Core testing checklist - [ ] Integration able to create all default resources from scratch - [ ] Resync finishes successfully - [ ] Resync able to create entities - [ ] Resync able to update entities - [ ] Resync able to detect and delete entities - [ ] Scheduled resync able to abort existing resync and start a new one - [ ] Tested with at least 2 integrations from scratch - [ ] Tested with Kafka and Polling event listeners - [ ] Tested deletion of entities that don't pass the selector
1 parent cc62664 commit 0e533ce

File tree

9 files changed

+266
-86
lines changed

9 files changed

+266
-86
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
<!-- towncrier release notes start -->
9+
## 0.23.4 (2025-05-29)
10+
11+
### Improvements
12+
- Fixed metrics urls and added reconciliation kind to report
913

1014
## 0.23.3 (2025-05-28)
1115

@@ -25,7 +29,6 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2529
## 0.23.1 (2025-05-27)
2630

2731
### Bug Fixes
28-
2932
- Event loop is blocked by waiting for a process.
3033

3134
## 0.23.0 (2025-05-27)

port_ocean/clients/port/mixins/integrations.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,9 @@ async def post_integration_sync_metrics(
214214
logger.debug("starting POST metrics request", metrics=metrics)
215215
metrics_attributes = await self.get_metrics_attributes()
216216
headers = await self.auth.headers()
217+
url = metrics_attributes["ingestUrl"] + "/syncMetrics"
217218
response = await self.client.post(
218-
metrics_attributes["ingestUrl"],
219+
url,
219220
headers=headers,
220221
json={
221222
"syncKindsMetrics": metrics,
@@ -229,7 +230,7 @@ async def put_integration_sync_metrics(self, kind_metrics: dict[str, Any]) -> No
229230
metrics_attributes = await self.get_metrics_attributes()
230231
url = (
231232
metrics_attributes["ingestUrl"]
232-
+ f"/resync/{kind_metrics['eventId']}/kind/{kind_metrics['kindIdentifier']}"
233+
+ f"/syncMetrics/resync/{kind_metrics['eventId']}/kind/{kind_metrics['kindIdentifier']}"
233234
)
234235
headers = await self.auth.headers()
235236
response = await self.client.put(

port_ocean/context/metric_resource.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from contextlib import asynccontextmanager
2+
from dataclasses import dataclass
3+
from typing import AsyncIterator, TYPE_CHECKING
4+
5+
from loguru import logger
6+
from werkzeug.local import LocalStack, LocalProxy
7+
8+
from port_ocean.exceptions.context import (
9+
ResourceContextNotFoundError,
10+
)
11+
12+
if TYPE_CHECKING:
13+
pass
14+
15+
16+
@dataclass
17+
class MetricResourceContext:
18+
"""
19+
The metric resource context is a context manager that allows you to access the current metric resource if there is one.
20+
This is useful for getting the metric resource kind
21+
"""
22+
23+
metric_resource_kind: str
24+
index: int
25+
26+
@property
27+
def kind(self) -> str:
28+
return self.metric_resource_kind
29+
30+
31+
_resource_context_stack: LocalStack[MetricResourceContext] = LocalStack()
32+
33+
34+
def _get_metric_resource_context() -> MetricResourceContext:
35+
"""
36+
Get the context from the current thread.
37+
"""
38+
top_resource_context = _resource_context_stack.top
39+
if top_resource_context is None:
40+
raise ResourceContextNotFoundError(
41+
"You must be inside an metric resource context in order to use it"
42+
)
43+
44+
return top_resource_context
45+
46+
47+
metric_resource: MetricResourceContext = LocalProxy(lambda: _get_metric_resource_context()) # type: ignore
48+
49+
50+
@asynccontextmanager
51+
async def metric_resource_context(
52+
metric_resource_kind: str, index: int = 0
53+
) -> AsyncIterator[MetricResourceContext]:
54+
_resource_context_stack.push(
55+
MetricResourceContext(metric_resource_kind=metric_resource_kind, index=index)
56+
)
57+
58+
with logger.contextualize(
59+
metric_resource_kind=metric_resource.metric_resource_kind
60+
):
61+
yield metric_resource
62+
63+
_resource_context_stack.pop()

port_ocean/core/handlers/resync_state_updater/updater.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,6 @@ async def update_after_resync(
9494
await ocean.metrics.send_metrics_to_webhook(
9595
kind=ocean.metrics.current_resource_kind()
9696
)
97-
# await ocean.metrics.report_sync_metrics(
98-
# kinds=[ocean.metrics.current_resource_kind()]
99-
# ) # TODO: uncomment this when end points are ready
97+
await ocean.metrics.report_sync_metrics(
98+
kinds=[ocean.metrics.current_resource_kind()]
99+
)

port_ocean/core/integrations/mixins/sync_raw.py

Lines changed: 106 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from loguru import logger
1010
from port_ocean.clients.port.types import UserAgentType
1111
from port_ocean.context.event import TriggerType, event_context, EventType, event
12+
from port_ocean.context.metric_resource import metric_resource_context
1213
from port_ocean.context.ocean import ocean
1314
from port_ocean.context.resource import resource_context
1415
from port_ocean.context import resource
@@ -33,8 +34,8 @@
3334
)
3435
from port_ocean.core.utils.utils import resolve_entities_diff, zip_and_sum, gather_and_split_errors_from_results
3536
from port_ocean.exceptions.core import OceanAbortException
36-
from port_ocean.helpers.metric.metric import SyncState, MetricType, MetricPhase
37-
from port_ocean.helpers.metric.utils import TimeMetric
37+
from port_ocean.helpers.metric.metric import MetricResourceKind, SyncState, MetricType, MetricPhase
38+
from port_ocean.helpers.metric.utils import TimeMetric, TimeMetricWithResourceKind
3839
from port_ocean.utils.ipc import FileIPC
3940

4041
SEND_RAW_DATA_EXAMPLES_AMOUNT = 5
@@ -249,9 +250,16 @@ async def _register_resource_raw(
249250
labels=[ocean.metrics.current_resource_kind(), MetricPhase.LOAD, MetricPhase.LoadResult.SKIPPED],
250251
value=len(objects_diff[0].entity_selector_diff.passed) - len(changed_entities)
251252
)
252-
await self.entities_state_applier.upsert(
253+
upserted_entities = await self.entities_state_applier.upsert(
253254
changed_entities, user_agent_type
254255
)
256+
257+
ocean.metrics.set_metric(
258+
name=MetricType.OBJECT_COUNT_NAME,
259+
labels=[ocean.metrics.current_resource_kind(), MetricPhase.LOAD, MetricPhase.LoadResult.LOADED],
260+
value=len(upserted_entities)
261+
)
262+
255263
else:
256264
logger.info("Entities in batch didn't changed since last sync, skipping", total_entities=len(objects_diff[0].entity_selector_diff.passed))
257265
ocean.metrics.inc_metric(
@@ -265,6 +273,11 @@ async def _register_resource_raw(
265273
modified_objects = await self.entities_state_applier.upsert(
266274
objects_diff[0].entity_selector_diff.passed, user_agent_type
267275
)
276+
ocean.metrics.set_metric(
277+
name=MetricType.OBJECT_COUNT_NAME,
278+
labels=[ocean.metrics.current_resource_kind(), MetricPhase.LOAD, MetricPhase.LoadResult.LOADED],
279+
value=len(upserted_entities)
280+
)
268281
else:
269282
modified_objects = await self.entities_state_applier.upsert(
270283
objects_diff[0].entity_selector_diff.passed, user_agent_type
@@ -633,27 +646,101 @@ async def _process_resource(self,resource: ResourceConfig, index: int, user_agen
633646
async with resource_context(resource,index):
634647
resource_kind_id = f"{resource.kind}-{index}"
635648
ocean.metrics.sync_state = SyncState.SYNCING
649+
636650
task = asyncio.create_task(
637651
self._register_in_batches(resource, user_agent_type)
638652
)
639653
event.on_abort(lambda: task.cancel())
640654
kind_results: tuple[list[Entity], list[Exception]] = await task
641-
ocean.metrics.set_metric(
642-
name=MetricType.OBJECT_COUNT_NAME,
643-
labels=[ocean.metrics.current_resource_kind(), MetricPhase.LOAD, MetricPhase.LoadResult.LOADED],
644-
value=len(kind_results[0])
645-
)
646655

647656
if ocean.metrics.sync_state != SyncState.FAILED:
648657
ocean.metrics.sync_state = SyncState.COMPLETED
649658

650659
await ocean.metrics.send_metrics_to_webhook(
651660
kind=resource_kind_id
652661
)
653-
# await ocean.metrics.report_kind_sync_metrics(kind=resource_kind_id) # TODO: uncomment this when end points are ready
662+
await ocean.metrics.report_kind_sync_metrics(kind=resource_kind_id, blueprint=resource.port.entity.mappings.blueprint)
654663

655664
return kind_results
656665

666+
@TimeMetricWithResourceKind(MetricPhase.RESYNC)
667+
async def resync_reconciliation(
668+
self,
669+
creation_results: list[tuple[list[Entity], list[Exception]]],
670+
did_fetched_current_state: bool,
671+
user_agent_type: UserAgentType,
672+
app_config: Any,
673+
silent: bool = True,
674+
) -> None:
675+
"""Handle the reconciliation phase of the resync process.
676+
677+
This method handles:
678+
1. Sorting and upserting failed entities
679+
2. Checking if current state was fetched
680+
3. Calculating resync diff
681+
4. Handling errors
682+
5. Deleting entities that are no longer needed
683+
6. Executing resync complete hooks
684+
685+
Args:
686+
creation_results (list[tuple[list[Entity], list[Exception]]]): Results from entity creation
687+
did_fetched_current_state (bool): Whether the current state was successfully fetched
688+
user_agent_type (UserAgentType): The type of user agent
689+
app_config (Any): The application configuration
690+
silent (bool): Whether to raise exceptions or handle them silently
691+
692+
"""
693+
await self.sort_and_upsert_failed_entities(user_agent_type)
694+
695+
if not did_fetched_current_state:
696+
logger.warning(
697+
"Due to an error before the resync, the previous state of entities at Port is unknown."
698+
" Skipping delete phase due to unknown initial state."
699+
)
700+
return False
701+
702+
logger.info("Starting resync diff calculation")
703+
generated_entities, errors = zip_and_sum(creation_results) or [
704+
[],
705+
[],
706+
]
707+
708+
if errors:
709+
message = f"Resync failed with {len(errors)} errors, skipping delete phase due to incomplete state"
710+
error_group = ExceptionGroup(
711+
message,
712+
errors,
713+
)
714+
if not silent:
715+
raise error_group
716+
717+
logger.error(message, exc_info=error_group)
718+
return False
719+
720+
logger.info(
721+
f"Running resync diff calculation, number of entities created during sync: {len(generated_entities)}"
722+
)
723+
entities_at_port = await ocean.port_client.search_entities(
724+
user_agent_type
725+
)
726+
727+
await self.entities_state_applier.delete_diff(
728+
{"before": entities_at_port, "after": generated_entities},
729+
user_agent_type, app_config.get_entity_deletion_threshold()
730+
)
731+
732+
logger.info("Resync finished successfully")
733+
734+
# Execute resync_complete hooks
735+
if "resync_complete" in self.event_strategy:
736+
logger.info("Executing resync_complete hooks")
737+
738+
for resync_complete_fn in self.event_strategy["resync_complete"]:
739+
await resync_complete_fn()
740+
741+
logger.info("Finished executing resync_complete hooks")
742+
743+
657744
@TimeMetric(MetricPhase.RESYNC)
658745
async def sync_raw_all(
659746
self,
@@ -689,8 +776,9 @@ async def sync_raw_all(
689776
logger.info(f"Resync will use the following mappings: {app_config.dict()}")
690777

691778
kinds = [f"{resource.kind}-{index}" for index, resource in enumerate(app_config.resources)]
779+
blueprints = [resource.port.entity.mappings.blueprint for resource in app_config.resources]
692780
ocean.metrics.initialize_metrics(kinds)
693-
# await ocean.metrics.report_sync_metrics(kinds=kinds) # TODO: uncomment this when end points are ready
781+
await ocean.metrics.report_sync_metrics(kinds=kinds, blueprints=blueprints)
694782

695783
# Clear cache
696784
await ocean.app.cache_provider.clear()
@@ -716,65 +804,20 @@ async def sync_raw_all(
716804
multiprocessing.set_start_method('fork', True)
717805
try:
718806
for index,resource in enumerate(app_config.resources):
719-
720807
logger.info(f"Starting processing resource {resource.kind} with index {index}")
721-
722808
creation_results.append(await self.process_resource(resource,index,user_agent_type))
723-
724-
await self.sort_and_upsert_failed_entities(user_agent_type)
725-
726809
except asyncio.CancelledError as e:
727810
logger.warning("Resync aborted successfully, skipping delete phase. This leads to an incomplete state")
728811
raise
729812
else:
730-
if not did_fetched_current_state:
731-
logger.warning(
732-
"Due to an error before the resync, the previous state of entities at Port is unknown."
733-
" Skipping delete phase due to unknown initial state."
734-
)
735-
return
736-
737-
logger.info("Starting resync diff calculation")
738-
generated_entities, errors = zip_and_sum(creation_results) or [
739-
[],
740-
[],
741-
]
742-
743-
if errors:
744-
message = f"Resync failed with {len(errors)} errors, skipping delete phase due to incomplete state"
745-
error_group = ExceptionGroup(
746-
message,
747-
errors,
748-
)
749-
if not silent:
750-
raise error_group
751-
752-
logger.error(message, exc_info=error_group)
753-
return False
754-
else:
755-
logger.info(
756-
f"Running resync diff calculation, number of entities created during sync: {len(generated_entities)}"
757-
)
758-
entities_at_port = await ocean.port_client.search_entities(
759-
user_agent_type
760-
)
761-
await self.entities_state_applier.delete_diff(
762-
{"before": entities_at_port, "after": generated_entities},
763-
user_agent_type, app_config.get_entity_deletion_threshold()
764-
)
765-
766-
logger.info("Resync finished successfully")
767-
768-
# Execute resync_complete hooks
769-
if "resync_complete" in self.event_strategy:
770-
logger.info("Executing resync_complete hooks")
771-
772-
for resync_complete_fn in self.event_strategy["resync_complete"]:
773-
await resync_complete_fn()
774-
775-
logger.info("Finished executing resync_complete hooks")
776-
777-
return True
813+
await self.resync_reconciliation(
814+
creation_results,
815+
did_fetched_current_state,
816+
user_agent_type,
817+
app_config,
818+
silent
819+
)
820+
await ocean.metrics.report_sync_metrics(kinds=[MetricResourceKind.RECONCILIATION])
778821
finally:
779822
await ocean.app.cache_provider.clear()
780823
if ocean.app.process_execution_mode == ProcessExecutionMode.multi_process:

0 commit comments

Comments
 (0)