port-ocean 0.23.2__py3-none-any.whl → 0.23.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of port-ocean might be problematic. Click here for more details.

@@ -5,11 +5,15 @@ FROM ${BASE_BUILDER_PYTHON_IMAGE} AS base
5
5
 
6
6
  ARG BUILD_CONTEXT
7
7
  ARG BUILDPLATFORM
8
+ ARG PROMETHEUS_MULTIPROC_DIR=/tmp/ocean/prometheus/metrics
8
9
 
9
10
  ENV LIBRDKAFKA_VERSION=2.8.2 \
10
11
  PYTHONUNBUFFERED=1 \
11
12
  POETRY_VIRTUALENVS_IN_PROJECT=1 \
12
- PIP_ROOT_USER_ACTION=ignore
13
+ PIP_ROOT_USER_ACTION=ignore \
14
+ PROMETHEUS_MULTIPROC_DIR=${PROMETHEUS_MULTIPROC_DIR}
15
+
16
+ RUN mkdir -p ${PROMETHEUS_MULTIPROC_DIR}
13
17
 
14
18
  WORKDIR /app
15
19
 
@@ -25,6 +25,11 @@ RUN apt-get update \
25
25
  && apt-get clean
26
26
 
27
27
  ARG BUILD_CONTEXT
28
+ ARG PROMETHEUS_MULTIPROC_DIR=/tmp/ocean/prometheus/metrics
29
+
30
+ ENV PROMETHEUS_MULTIPROC_DIR=${PROMETHEUS_MULTIPROC_DIR}
31
+
32
+ RUN mkdir -p ${PROMETHEUS_MULTIPROC_DIR}
28
33
 
29
34
  WORKDIR /app
30
35
 
@@ -1,6 +1,4 @@
1
1
  #!/bin/bash
2
- mkdir -p /tmp/prometheus_multiproc_dir
3
- export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_multiproc_dir
4
2
  if [ -z "$BUILD_CONTEXT" ]; then
5
3
  echo "BUILD_CONTEXT is not set"
6
4
  exit 1
@@ -214,8 +214,9 @@ class IntegrationClientMixin:
214
214
  logger.debug("starting POST metrics request", metrics=metrics)
215
215
  metrics_attributes = await self.get_metrics_attributes()
216
216
  headers = await self.auth.headers()
217
+ url = metrics_attributes["ingestUrl"] + "/syncMetrics"
217
218
  response = await self.client.post(
218
- metrics_attributes["ingestUrl"],
219
+ url,
219
220
  headers=headers,
220
221
  json={
221
222
  "syncKindsMetrics": metrics,
@@ -229,7 +230,7 @@ class IntegrationClientMixin:
229
230
  metrics_attributes = await self.get_metrics_attributes()
230
231
  url = (
231
232
  metrics_attributes["ingestUrl"]
232
- + f"/resync/{kind_metrics['eventId']}/kind/{kind_metrics['kindIdentifier']}"
233
+ + f"/syncMetrics/resync/{kind_metrics['eventId']}/kind/{kind_metrics['kindIdentifier']}"
233
234
  )
234
235
  headers = await self.auth.headers()
235
236
  response = await self.client.put(
@@ -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()
@@ -94,6 +94,6 @@ class ResyncStateUpdater:
94
94
  await ocean.metrics.send_metrics_to_webhook(
95
95
  kind=ocean.metrics.current_resource_kind()
96
96
  )
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
+ )
@@ -9,6 +9,7 @@ import httpx
9
9
  from loguru import logger
10
10
  from port_ocean.clients.port.types import UserAgentType
11
11
  from port_ocean.context.event import TriggerType, event_context, EventType, event
12
+ from port_ocean.context.metric_resource import metric_resource_context
12
13
  from port_ocean.context.ocean import ocean
13
14
  from port_ocean.context.resource import resource_context
14
15
  from port_ocean.context import resource
@@ -16,6 +17,7 @@ from port_ocean.core.handlers.port_app_config.models import ResourceConfig
16
17
  from port_ocean.core.integrations.mixins import HandlerMixin, EventsMixin
17
18
  from port_ocean.core.integrations.mixins.utils import (
18
19
  ProcessWrapper,
20
+ clear_http_client_context,
19
21
  is_resource_supported,
20
22
  unsupported_kind_response,
21
23
  resync_generator_wrapper,
@@ -32,8 +34,8 @@ from port_ocean.core.ocean_types import (
32
34
  )
33
35
  from port_ocean.core.utils.utils import resolve_entities_diff, zip_and_sum, gather_and_split_errors_from_results
34
36
  from port_ocean.exceptions.core import OceanAbortException
35
- from port_ocean.helpers.metric.metric import SyncState, MetricType, MetricPhase
36
- 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
37
39
  from port_ocean.utils.ipc import FileIPC
38
40
 
39
41
  SEND_RAW_DATA_EXAMPLES_AMOUNT = 5
@@ -248,9 +250,16 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
248
250
  labels=[ocean.metrics.current_resource_kind(), MetricPhase.LOAD, MetricPhase.LoadResult.SKIPPED],
249
251
  value=len(objects_diff[0].entity_selector_diff.passed) - len(changed_entities)
250
252
  )
251
- await self.entities_state_applier.upsert(
253
+ upserted_entities = await self.entities_state_applier.upsert(
252
254
  changed_entities, user_agent_type
253
255
  )
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
+
254
263
  else:
255
264
  logger.info("Entities in batch didn't changed since last sync, skipping", total_entities=len(objects_diff[0].entity_selector_diff.passed))
256
265
  ocean.metrics.inc_metric(
@@ -264,6 +273,11 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
264
273
  modified_objects = await self.entities_state_applier.upsert(
265
274
  objects_diff[0].entity_selector_diff.passed, user_agent_type
266
275
  )
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
+ )
267
281
  else:
268
282
  modified_objects = await self.entities_state_applier.upsert(
269
283
  objects_diff[0].entity_selector_diff.passed, user_agent_type
@@ -594,6 +608,7 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
594
608
  ) -> None:
595
609
  logger.info(f"process started successfully for {resource.kind} with index {index}")
596
610
 
611
+ clear_http_client_context()
597
612
  async def process_resource_task() -> None:
598
613
  result = await self._process_resource(
599
614
  resource, index, user_agent_type
@@ -631,16 +646,12 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
631
646
  async with resource_context(resource,index):
632
647
  resource_kind_id = f"{resource.kind}-{index}"
633
648
  ocean.metrics.sync_state = SyncState.SYNCING
649
+
634
650
  task = asyncio.create_task(
635
651
  self._register_in_batches(resource, user_agent_type)
636
652
  )
637
653
  event.on_abort(lambda: task.cancel())
638
654
  kind_results: tuple[list[Entity], list[Exception]] = await task
639
- ocean.metrics.set_metric(
640
- name=MetricType.OBJECT_COUNT_NAME,
641
- labels=[ocean.metrics.current_resource_kind(), MetricPhase.LOAD, MetricPhase.LoadResult.LOADED],
642
- value=len(kind_results[0])
643
- )
644
655
 
645
656
  if ocean.metrics.sync_state != SyncState.FAILED:
646
657
  ocean.metrics.sync_state = SyncState.COMPLETED
@@ -648,10 +659,88 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
648
659
  await ocean.metrics.send_metrics_to_webhook(
649
660
  kind=resource_kind_id
650
661
  )
651
- # 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)
652
663
 
653
664
  return kind_results
654
665
 
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
+
655
744
  @TimeMetric(MetricPhase.RESYNC)
656
745
  async def sync_raw_all(
657
746
  self,
@@ -687,8 +776,9 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
687
776
  logger.info(f"Resync will use the following mappings: {app_config.dict()}")
688
777
 
689
778
  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]
690
780
  ocean.metrics.initialize_metrics(kinds)
691
- # 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)
692
782
 
693
783
  # Clear cache
694
784
  await ocean.app.cache_provider.clear()
@@ -714,64 +804,21 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
714
804
  multiprocessing.set_start_method('fork', True)
715
805
  try:
716
806
  for index,resource in enumerate(app_config.resources):
717
-
718
807
  logger.info(f"Starting processing resource {resource.kind} with index {index}")
719
-
720
808
  creation_results.append(await self.process_resource(resource,index,user_agent_type))
721
-
722
- await self.sort_and_upsert_failed_entities(user_agent_type)
723
-
724
809
  except asyncio.CancelledError as e:
725
810
  logger.warning("Resync aborted successfully, skipping delete phase. This leads to an incomplete state")
726
811
  raise
727
812
  else:
728
- if not did_fetched_current_state:
729
- logger.warning(
730
- "Due to an error before the resync, the previous state of entities at Port is unknown."
731
- " Skipping delete phase due to unknown initial state."
732
- )
733
- return
734
-
735
- logger.info("Starting resync diff calculation")
736
- generated_entities, errors = zip_and_sum(creation_results) or [
737
- [],
738
- [],
739
- ]
740
-
741
- if errors:
742
- message = f"Resync failed with {len(errors)} errors, skipping delete phase due to incomplete state"
743
- error_group = ExceptionGroup(
744
- message,
745
- errors,
746
- )
747
- if not silent:
748
- raise error_group
749
-
750
- logger.error(message, exc_info=error_group)
751
- return False
752
- else:
753
- logger.info(
754
- f"Running resync diff calculation, number of entities created during sync: {len(generated_entities)}"
755
- )
756
- entities_at_port = await ocean.port_client.search_entities(
757
- user_agent_type
758
- )
759
- await self.entities_state_applier.delete_diff(
760
- {"before": entities_at_port, "after": generated_entities},
761
- user_agent_type, app_config.get_entity_deletion_threshold()
762
- )
763
-
764
- logger.info("Resync finished successfully")
765
-
766
- # Execute resync_complete hooks
767
- if "resync_complete" in self.event_strategy:
768
- logger.info("Executing resync_complete hooks")
769
-
770
- for resync_complete_fn in self.event_strategy["resync_complete"]:
771
- await resync_complete_fn()
772
-
773
- logger.info("Finished executing resync_complete hooks")
774
-
775
- 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])
776
821
  finally:
777
822
  await ocean.app.cache_provider.clear()
823
+ if ocean.app.process_execution_mode == ProcessExecutionMode.multi_process:
824
+ ocean.metrics.cleanup_prometheus_metrics()
@@ -19,6 +19,10 @@ from port_ocean.exceptions.core import (
19
19
  KindNotImplementedException,
20
20
  )
21
21
 
22
+ from port_ocean.utils.async_http import _http_client
23
+ from port_ocean.clients.port.utils import _http_client as _port_http_client
24
+
25
+ from port_ocean.context.ocean import ocean
22
26
 
23
27
  @contextmanager
24
28
  def resync_error_handling() -> Generator[None, None, None]:
@@ -76,6 +80,7 @@ def unsupported_kind_response(
76
80
  logger.error(f"Kind {kind} is not supported in this integration")
77
81
  return [], [KindNotImplementedException(kind, available_resync_kinds)]
78
82
 
83
+
79
84
  class ProcessWrapper(multiprocessing.Process):
80
85
  def __init__(self, *args, **kwargs):
81
86
  super().__init__(*args, **kwargs)
@@ -87,4 +92,18 @@ class ProcessWrapper(multiprocessing.Process):
87
92
  logger.error(f"Process {self.pid} failed with exit code {self.exitcode}")
88
93
  else:
89
94
  logger.info(f"Process {self.pid} finished with exit code {self.exitcode}")
95
+ ocean.metrics.cleanup_prometheus_metrics(self.pid)
90
96
  return super().join()
97
+
98
+ def clear_http_client_context() -> None:
99
+ try:
100
+ while _http_client.top is not None:
101
+ _http_client.pop()
102
+ except (RuntimeError, AttributeError):
103
+ pass
104
+
105
+ try:
106
+ while _port_http_client.top is not None:
107
+ _port_http_client.pop()
108
+ except (RuntimeError, AttributeError):
109
+ pass
@@ -1,11 +1,12 @@
1
+ import os
1
2
  from typing import Any, TYPE_CHECKING, Optional, Dict, List, Tuple
2
3
  from fastapi import APIRouter
3
4
  from port_ocean.exceptions.context import ResourceContextNotFoundError
4
5
  import prometheus_client
5
6
  from httpx import AsyncClient
6
-
7
+ from fastapi.responses import PlainTextResponse
7
8
  from loguru import logger
8
- from port_ocean.context import resource
9
+ from port_ocean.context import metric_resource, resource
9
10
  from prometheus_client import Gauge
10
11
  import prometheus_client.openmetrics
11
12
  import prometheus_client.openmetrics.exposition
@@ -56,6 +57,11 @@ class SyncState:
56
57
  FAILED = "failed"
57
58
 
58
59
 
60
+ class MetricResourceKind:
61
+ RECONCILIATION = "__reconciliation__"
62
+ RESYNC = "__resync__"
63
+
64
+
59
65
  # Registry for core and custom metrics
60
66
  _metrics_registry: Dict[str, Tuple[str, str, List[str]]] = {
61
67
  MetricType.DURATION_NAME: (
@@ -193,7 +199,21 @@ class Metrics:
193
199
  """
194
200
  self.get_metric(name, labels).set(value)
195
201
 
202
+ @staticmethod
203
+ def cleanup_prometheus_metrics(pid: int | None = None) -> None:
204
+ try:
205
+ prometheus_multiproc_dir = os.environ.get("PROMETHEUS_MULTIPROC_DIR")
206
+ for file in os.listdir(prometheus_multiproc_dir):
207
+ if pid:
208
+ if file.endswith(".db") and file[0:-3].split("_")[-1] == str(pid):
209
+ os.remove(f"{prometheus_multiproc_dir}/{file}")
210
+ else:
211
+ os.remove(f"{prometheus_multiproc_dir}/{file}")
212
+ except Exception as e:
213
+ logger.error(f"Failed to cleanup prometheus metrics: {e}")
214
+
196
215
  def initialize_metrics(self, kind_blockes: list[str]) -> None:
216
+ self.cleanup_prometheus_metrics()
197
217
  for kind in kind_blockes:
198
218
  self.set_metric(MetricType.SUCCESS_NAME, [kind, MetricPhase.RESYNC], 0)
199
219
  self.set_metric(MetricType.DURATION_NAME, [kind, MetricPhase.RESYNC], 0)
@@ -237,11 +257,9 @@ class Metrics:
237
257
  )
238
258
 
239
259
  def create_mertic_router(self) -> APIRouter:
240
- if not self.enabled:
241
- return APIRouter()
242
260
  router = APIRouter()
243
261
 
244
- @router.get("/")
262
+ @router.get("/", response_class=PlainTextResponse)
245
263
  async def prom_metrics() -> str:
246
264
  return self.generate_latest()
247
265
 
@@ -250,6 +268,12 @@ class Metrics:
250
268
  def current_resource_kind(self) -> str:
251
269
  try:
252
270
  return f"{resource.resource.kind}-{resource.resource.index}"
271
+ except ResourceContextNotFoundError:
272
+ return self.current_metric_resource_kind()
273
+
274
+ def current_metric_resource_kind(self) -> str:
275
+ try:
276
+ return metric_resource.metric_resource.metric_resource_kind
253
277
  except ResourceContextNotFoundError:
254
278
  return "__runtime__"
255
279
 
@@ -259,15 +283,21 @@ class Metrics:
259
283
  ).decode()
260
284
 
261
285
  async def report_sync_metrics(
262
- self, metric_name: Optional[str] = None, kinds: Optional[list[str]] = None
286
+ self,
287
+ metric_name: Optional[str] = None,
288
+ kinds: Optional[list[str]] = None,
289
+ blueprints: Optional[list[Optional[str]]] = None,
263
290
  ) -> None:
264
291
  if kinds is None:
265
292
  return None
266
293
 
267
294
  metrics = []
268
295
 
269
- for kind in kinds:
270
- metric = self.generate_metrics(metric_name, kind)
296
+ if blueprints is None:
297
+ blueprints = [None] * len(kinds)
298
+
299
+ for kind, blueprint in zip(kinds, blueprints):
300
+ metric = self.generate_metrics(metric_name, kind, blueprint)
271
301
  metrics.extend(metric)
272
302
 
273
303
  try:
@@ -276,9 +306,12 @@ class Metrics:
276
306
  logger.error(f"Error posting metrics: {e}", metrics=metrics)
277
307
 
278
308
  async def report_kind_sync_metrics(
279
- self, metric_name: Optional[str] = None, kind: Optional[str] = None
309
+ self,
310
+ metric_name: Optional[str] = None,
311
+ kind: Optional[str] = None,
312
+ blueprint: Optional[str] = None,
280
313
  ) -> None:
281
- metrics = self.generate_metrics(metric_name, kind)
314
+ metrics = self.generate_metrics(metric_name, kind, blueprint)
282
315
  if not metrics:
283
316
  return None
284
317
 
@@ -289,7 +322,10 @@ class Metrics:
289
322
  logger.error(f"Error putting metrics: {e}", metrics=metrics)
290
323
 
291
324
  def generate_metrics(
292
- self, metric_name: Optional[str] = None, kind: Optional[str] = None
325
+ self,
326
+ metric_name: Optional[str] = None,
327
+ kind: Optional[str] = None,
328
+ blueprint: Optional[str] = None,
293
329
  ) -> list[dict[str, Any]]:
294
330
  try:
295
331
  latest_raw = self.generate_latest()
@@ -348,9 +384,10 @@ class Metrics:
348
384
  if "-" in kind_key
349
385
  else kind_key
350
386
  ),
351
- "kindIndex": 0 if kind_key == "__runtime__" else int(kind_key[-1]),
387
+ "kindIndex": int(kind_key[-1]) if kind_key[-1].isdigit() else 0,
352
388
  "eventId": self.event_id,
353
389
  "syncState": self.sync_state,
390
+ "blueprint": blueprint if blueprint else "",
354
391
  "metrics": metrics,
355
392
  }
356
393
  events.append(event)
@@ -2,8 +2,9 @@ from functools import wraps
2
2
  import time
3
3
  from typing import Any, Callable
4
4
 
5
+ from port_ocean.context.metric_resource import metric_resource_context
5
6
  from port_ocean.context.ocean import ocean
6
- from port_ocean.helpers.metric.metric import MetricType
7
+ from port_ocean.helpers.metric.metric import MetricResourceKind, MetricType
7
8
 
8
9
 
9
10
  def TimeMetric(phase: str) -> Any:
@@ -26,3 +27,26 @@ def TimeMetric(phase: str) -> Any:
26
27
  return wrapper
27
28
 
28
29
  return decorator
30
+
31
+
32
+ def TimeMetricWithResourceKind(phase: str) -> Any:
33
+ def decorator(func: Callable[..., Any]) -> Any:
34
+
35
+ @wraps(func)
36
+ async def wrapper(*args: Any, **kwargs: dict[Any, Any]) -> Any:
37
+ async with metric_resource_context(MetricResourceKind.RECONCILIATION):
38
+ start = time.monotonic()
39
+ res = await func(*args, **kwargs)
40
+ end = time.monotonic()
41
+ duration = end - start
42
+ ocean.metrics.inc_metric(
43
+ name=MetricType.DURATION_NAME,
44
+ labels=[ocean.metrics.current_resource_kind(), phase],
45
+ value=duration,
46
+ )
47
+
48
+ return res
49
+
50
+ return wrapper
51
+
52
+ return decorator
port_ocean/ocean.py CHANGED
@@ -96,7 +96,6 @@ class Ocean:
96
96
  self.resync_state_updater = ResyncStateUpdater(
97
97
  self.port_client, self.config.scheduled_resync_interval
98
98
  )
99
-
100
99
  self.app_initialized = False
101
100
 
102
101
  def _get_process_execution_mode(self) -> ProcessExecutionMode:
@@ -154,7 +154,7 @@ async def test_sync_raw_mixin_self_dependency(
154
154
 
155
155
  # Add assertions for actual metrics
156
156
  metrics = mock_ocean.metrics.generate_metrics()
157
- assert len(metrics) == 2
157
+ assert len(metrics) == 3
158
158
 
159
159
  # Verify object counts
160
160
  for metric in metrics:
@@ -187,7 +187,7 @@ async def test_sync_raw_mixin_self_dependency(
187
187
  metric["metrics"]["phase"]["load"]["object_count_type"][
188
188
  "loaded"
189
189
  ]["object_count"]
190
- == 2
190
+ == 1
191
191
  )
192
192
 
193
193
  # Verify success
@@ -196,6 +196,14 @@ async def test_sync_raw_mixin_self_dependency(
196
196
  # Verify sync state
197
197
  assert metric["syncState"] == "completed"
198
198
 
199
+ if metric["kind"] == "reconciliation":
200
+ assert (
201
+ metric["metrics"]["phase"]["load"]["object_count_type"][
202
+ "failed"
203
+ ]["object_count"]
204
+ == 1
205
+ )
206
+
199
207
 
200
208
  @pytest.mark.asyncio
201
209
  async def test_sync_raw_mixin_circular_dependency(
@@ -282,7 +290,7 @@ async def test_sync_raw_mixin_circular_dependency(
282
290
 
283
291
  # Add assertions for actual metrics
284
292
  metrics = mock_ocean.metrics.generate_metrics()
285
- assert len(metrics) == 2
293
+ assert len(metrics) == 3
286
294
 
287
295
  # Verify object counts
288
296
  for metric in metrics:
@@ -315,7 +323,7 @@ async def test_sync_raw_mixin_circular_dependency(
315
323
  metric["metrics"]["phase"]["load"]["object_count_type"][
316
324
  "loaded"
317
325
  ]["object_count"]
318
- == 2
326
+ == 0
319
327
  )
320
328
 
321
329
  # Verify success
@@ -324,6 +332,14 @@ async def test_sync_raw_mixin_circular_dependency(
324
332
  # Verify sync state
325
333
  assert metric["syncState"] == "completed"
326
334
 
335
+ if metric["kind"] == "reconciliation":
336
+ assert (
337
+ metric["metrics"]["phase"]["load"]["object_count_type"][
338
+ "loaded"
339
+ ]["object_count"]
340
+ == 2
341
+ )
342
+
327
343
 
328
344
  @pytest.mark.asyncio
329
345
  async def test_sync_raw_mixin_dependency(
@@ -422,7 +438,7 @@ async def test_sync_raw_mixin_dependency(
422
438
 
423
439
  # Add assertions for actual metrics
424
440
  metrics = mock_ocean.metrics.generate_metrics()
425
- assert len(metrics) == 2
441
+ assert len(metrics) == 3
426
442
 
427
443
  # Verify object counts
428
444
  for metric in metrics:
@@ -458,6 +474,14 @@ async def test_sync_raw_mixin_dependency(
458
474
  # Verify sync state
459
475
  assert metric["syncState"] == "completed"
460
476
 
477
+ if metric["kind"] == "reconciliation":
478
+ assert (
479
+ metric["metrics"]["phase"]["load"]["object_count_type"][
480
+ "loaded"
481
+ ]["object_count"]
482
+ == 5
483
+ )
484
+
461
485
 
462
486
  @pytest.mark.asyncio
463
487
  async def test_register_raw(
port_ocean/utils/ipc.py CHANGED
@@ -7,7 +7,7 @@ class FileIPC:
7
7
  def __init__(self, process_id: str, name: str, default_return: Any = None):
8
8
  self.process_id = process_id
9
9
  self.name = name
10
- self.dir_path = f"/tmp/p_{self.process_id}"
10
+ self.dir_path = f"/tmp/ocean/processes/p_{self.process_id}"
11
11
  self.file_path = f"{self.dir_path}/{self.name}.pkl"
12
12
  self.default_return = default_return
13
13
  os.makedirs(self.dir_path, exist_ok=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: port-ocean
3
- Version: 0.23.2
3
+ Version: 0.23.4
4
4
  Summary: Port Ocean is a CLI tool for managing your Port projects.
5
5
  Home-page: https://app.getport.io
6
6
  Keywords: ocean,port-ocean,port
@@ -1,11 +1,11 @@
1
- integrations/_infra/Dockerfile.Deb,sha256=ruQRZwWN5qrJCecgA-rZklX1zXO8OAS78SjAqMv6dLg,1519
1
+ integrations/_infra/Dockerfile.Deb,sha256=JMyxgvJxLeo3Vcys-w6m_F6fxfu6JyAcjAMPuXsNQBM,1679
2
2
  integrations/_infra/Dockerfile.alpine,sha256=7E4Sb-8supsCcseerHwTkuzjHZoYcaHIyxiBZ-wewo0,3482
3
3
  integrations/_infra/Dockerfile.base.builder,sha256=ESe1PKC6itp_AuXawbLI75k1Kruny6NTANaTinxOgVs,743
4
4
  integrations/_infra/Dockerfile.base.runner,sha256=uAcs2IsxrAAUHGXt_qULA5INr-HFguf5a5fCKiqEzbY,384
5
5
  integrations/_infra/Dockerfile.dockerignore,sha256=CM1Fxt3I2AvSvObuUZRmy5BNLSGC7ylnbpWzFgD4cso,1163
6
- integrations/_infra/Dockerfile.local,sha256=Aqj3y4U6XFS78i5Zz3IfyZkvVmAdB7eEAe6khQaxRxI,876
6
+ integrations/_infra/Dockerfile.local,sha256=4M5wyksRcGQ0WSXfDcJsHshE8uUSQw5yf6O3JK_DyK4,1035
7
7
  integrations/_infra/Makefile,sha256=YgLKvuF_Dw4IA7X98Nus6zIW_3cJ60M1QFGs3imj5c4,2430
8
- integrations/_infra/entry_local.sh,sha256=cH2Gd82qDnLKXvjoK1MNay9vdIZzTTF_hrhmvZuYZbg,648
8
+ integrations/_infra/entry_local.sh,sha256=GIuAXACcYv4DDxH6ubDidNJboqmAJYW4OanUK_VW4nQ,547
9
9
  integrations/_infra/grpcio.sh,sha256=m924poYznoRZ6Tt7Ct8Cs5AV_cmmOx598yIZ3z4DvZE,616
10
10
  integrations/_infra/init.sh,sha256=nN8lTrOhB286UfFvD6sJ9YJ-9asT9zVSddQB-RAb7Z4,99
11
11
  port_ocean/__init__.py,sha256=uMpjg5d_cXgnyCxA_LmICR8zqBmC6Fe9Ivu9hcvJ7EY,313
@@ -60,7 +60,7 @@ port_ocean/clients/port/client.py,sha256=dv0mxIOde6J-wFi1FXXZkoNPVHrZzY7RSMhNkDD
60
60
  port_ocean/clients/port/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
61
  port_ocean/clients/port/mixins/blueprints.py,sha256=aMCG4zePsMSMjMLiGrU37h5z5_ElfMzTcTvqvOI5wXY,4683
62
62
  port_ocean/clients/port/mixins/entities.py,sha256=-1Gs74z_8eviWItHIpveQhKdA7gnjbqZ3STS4jgGONs,11668
63
- port_ocean/clients/port/mixins/integrations.py,sha256=QuPZ4MC8FtJ-3FKbm0CqJ5TOxtSyaQLR_rsJ-p7-y_Q,11115
63
+ port_ocean/clients/port/mixins/integrations.py,sha256=s6paomK9bYWW-Tu3y2OIaEGSxsXCHyhapVi4JIhhO64,11162
64
64
  port_ocean/clients/port/mixins/migrations.py,sha256=vdL_A_NNUogvzujyaRLIoZEu5vmKDY2BxTjoGP94YzI,1467
65
65
  port_ocean/clients/port/mixins/organization.py,sha256=A2cP5V49KnjoAXxjmnm_XGth4ftPSU0qURNfnyUyS_Y,1041
66
66
  port_ocean/clients/port/retry_transport.py,sha256=PtIZOAZ6V-ncpVysRUsPOgt8Sf01QLnTKB5YeKBxkJk,1861
@@ -74,6 +74,7 @@ port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
74
74
  port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
75
75
  port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  port_ocean/context/event.py,sha256=z7DBNOPOL9P3s-SR8jpgwoyaQ6IL9vZxLaAxIjv1Faw,6493
77
+ port_ocean/context/metric_resource.py,sha256=_EbLRjv3r1y6E3B9mU10wuGg_oepD8zo3VueaJMeH6A,1689
77
78
  port_ocean/context/ocean.py,sha256=xWD30AbStQK44QFW9ad5pO4i5dQG7uouvjuHScKnJOM,7884
78
79
  port_ocean/context/resource.py,sha256=WQlPzplxWic0hvvMxWKu8xPfQQC0VjrsJVeA6yZytw0,1712
79
80
  port_ocean/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -108,7 +109,7 @@ port_ocean/core/handlers/queue/__init__.py,sha256=1fICM0ZLATmmj6f7cdq_eV2kmw0_jy
108
109
  port_ocean/core/handlers/queue/abstract_queue.py,sha256=q_gpaWFFZHxM3XovEbgsDn8jEOLM45iAZWVC81Paxto,620
109
110
  port_ocean/core/handlers/queue/local_queue.py,sha256=EzqsGIX43xbVAcePwTcCg5QDrXATQpy-VzWxxN_OyAM,574
110
111
  port_ocean/core/handlers/resync_state_updater/__init__.py,sha256=kG6y-JQGpPfuTHh912L_bctIDCzAK4DN-d00S7rguWU,81
111
- port_ocean/core/handlers/resync_state_updater/updater.py,sha256=KZ55dubVYmFHxUOzdUFBPGLQhQSVBiRAoyk3R-NKKRU,3819
112
+ port_ocean/core/handlers/resync_state_updater/updater.py,sha256=TRYq6QnTtPlJg6MvgZPtQdZPvkAhkvpcmWjtkxCnkg4,3764
112
113
  port_ocean/core/handlers/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
114
  port_ocean/core/handlers/webhook/abstract_webhook_processor.py,sha256=5KwZkdkDd5HdVkXPzKiqabodZKl-hOtMypkTKd8Hq3M,3891
114
115
  port_ocean/core/handlers/webhook/processor_manager.py,sha256=ipyAXoFtF84EGczyzRcZCzQG4Ippjo4eMsnVxMVz12A,12072
@@ -120,8 +121,8 @@ port_ocean/core/integrations/mixins/events.py,sha256=2L7P3Jhp8XBqddh2_o9Cn4N261n
120
121
  port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
121
122
  port_ocean/core/integrations/mixins/live_events.py,sha256=8HklZmlyffYY_LeDe8xbt3Tb08rlLkqVhFF-2NQeJP4,4126
122
123
  port_ocean/core/integrations/mixins/sync.py,sha256=Vm_898pLKBwfVewtwouDWsXoxcOLicnAy6pzyqqk6U8,4053
123
- port_ocean/core/integrations/mixins/sync_raw.py,sha256=XIVx_Y9TM8TCMzuoNowYGNtQG98n2pLTCFFHWbkIbTo,32176
124
- port_ocean/core/integrations/mixins/utils.py,sha256=0rzzFnxrFNaVLHXShfDda5zjO8WwEUBW9oPWxnDsaXQ,2878
124
+ port_ocean/core/integrations/mixins/sync_raw.py,sha256=wZrn0Bz05HmVKDgAJ5LPvP54nSUDpE8uHvjHAYqXVD4,33987
125
+ port_ocean/core/integrations/mixins/utils.py,sha256=g1XbC12dswefQ-NpcLSCqFtd_WRp2bTL98jyZ5rRbGk,3444
125
126
  port_ocean/core/models.py,sha256=MKfq69zGbFRzo0I2HRDUvSbz_pjrtcFVsD5B4Qwa3fw,2538
126
127
  port_ocean/core/ocean_types.py,sha256=4VipWFOHEh_d9LmWewQccwx1p2dtrRYW0YURVgNsAjo,1398
127
128
  port_ocean/core/utils/entity_topological_sorter.py,sha256=MDUjM6OuDy4Xj68o-7InNN0w1jqjxeDfeY8U02vySNI,3081
@@ -138,15 +139,15 @@ port_ocean/exceptions/utils.py,sha256=gjOqpi-HpY1l4WlMFsGA9yzhxDhajhoGGdDDyGbLnq
138
139
  port_ocean/exceptions/webhook_processor.py,sha256=yQYazg53Y-ohb7HfViwq1opH_ZUuUdhHSRxcUNveFpI,114
139
140
  port_ocean/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
140
141
  port_ocean/helpers/async_client.py,sha256=SRlP6o7_FCSY3UHnRlZdezppePVxxOzZ0z861vE3K40,1783
141
- port_ocean/helpers/metric/metric.py,sha256=iktHKXQNzkLYHgCLWA5wxRDvAMJrJIMoGQYFXV83mH0,12973
142
- port_ocean/helpers/metric/utils.py,sha256=Wnr-6HwVwBtYJ3so44OkhDRs8udLMSB1oduzl2-zRHo,781
142
+ port_ocean/helpers/metric/metric.py,sha256=mlXH4T_ehShZ0XjGkng3h4MDv7Myw0upwTlpAoC84tA,14395
143
+ port_ocean/helpers/metric/utils.py,sha256=1lAgrxnZLuR_wUNDyPOPzLrm32b8cDdioob2lvnPQ1A,1619
143
144
  port_ocean/helpers/retry.py,sha256=gmS4YxM6N4fboFp7GSgtOzyBJemxs46bnrz4L4rDS6Y,16136
144
145
  port_ocean/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
146
  port_ocean/log/handlers.py,sha256=ncVjgqrZRh6BhyRrA6DQG86Wsbxph1yWYuEC0cWfe-Q,3631
146
147
  port_ocean/log/logger_setup.py,sha256=0K3zVG0YYrYOWEV8-rCGks1o-bMRxgHXlqawu9w_tSw,2656
147
148
  port_ocean/log/sensetive.py,sha256=lVKiZH6b7TkrZAMmhEJRhcl67HNM94e56x12DwFgCQk,2920
148
149
  port_ocean/middlewares.py,sha256=9wYCdyzRZGK1vjEJ28FY_DkfwDNENmXp504UKPf5NaQ,2727
149
- port_ocean/ocean.py,sha256=h0d-lOf7FQdrRylEglD1MqNzHk-OdvwAorFWyiT6UBo,8825
150
+ port_ocean/ocean.py,sha256=83zgTEI5o2wfl8mq-iIC9DzPOZbWyCqNIy1BEjv9TOk,8824
150
151
  port_ocean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
151
152
  port_ocean/run.py,sha256=CmKz14bxfdOooNbQ5QqH1MwX-XLYVG4NgT4KbrzFaqI,2216
152
153
  port_ocean/sonar-project.properties,sha256=X_wLzDOkEVmpGLRMb2fg9Rb0DxWwUFSvESId8qpvrPI,73
@@ -165,7 +166,7 @@ port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcG
165
166
  port_ocean/tests/core/handlers/entities_state_applier/test_applier.py,sha256=WNg1fWZsXu0MDnz9-ahRiPb_OPofWx7E8wxBx0cyZKs,8946
166
167
  port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=8WpMn559Mf0TFWmloRpZrVgr6yWwyA0C4n2lVHCtyq4,13596
167
168
  port_ocean/tests/core/handlers/mixins/test_live_events.py,sha256=iAwVpr3n3PIkXQLw7hxd-iB_SR_vyfletVXJLOmyz28,12480
168
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=QHFigCpvYHf6sVirLBI23pMFyZCn4TcWaaFbHtUjoFA,42441
169
+ port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=iK-lfu6vk-DBScYm-nLvRzbj0ImbKUN4LfNLXOb8Z3Q,43413
169
170
  port_ocean/tests/core/handlers/port_app_config/test_api.py,sha256=eJZ6SuFBLz71y4ca3DNqKag6d6HUjNJS0aqQPwiLMTI,1999
170
171
  port_ocean/tests/core/handlers/port_app_config/test_base.py,sha256=hSh556bJM9zuELwhwnyKSfd9z06WqWXIfe-6hCl5iKI,9799
171
172
  port_ocean/tests/core/handlers/queue/test_local_queue.py,sha256=9Ly0HzZXbs6Rbl_bstsIdInC3h2bgABU3roP9S_PnJM,2582
@@ -192,15 +193,15 @@ port_ocean/utils/__init__.py,sha256=KMGnCPXZJbNwtgxtyMycapkDz8tpSyw23MSYT3iVeHs,
192
193
  port_ocean/utils/async_http.py,sha256=aDsw3gQIMwt6qLegbZtkHqD8em48tKvbITnblsrTY3g,1260
193
194
  port_ocean/utils/async_iterators.py,sha256=CPXskYWkhkZtAG-ducEwM8537t3z5usPEqXR9vcivzw,3715
194
195
  port_ocean/utils/cache.py,sha256=tRwPomG2VIxx8ZNi4QYH6Yc47d9yYV1A7Hx-L_fX4Dg,4494
195
- port_ocean/utils/ipc.py,sha256=BMVUxdftf0i7Z2Xp8KMFlttUjZhTE7VUCpY4SBBnoVY,896
196
+ port_ocean/utils/ipc.py,sha256=eTjTTvsKl6IXYeOkIjP5iyrw-8gLQ9rf15WeyxCqXog,912
196
197
  port_ocean/utils/misc.py,sha256=0q2cJ5psqxn_5u_56pT7vOVQ3shDM02iC1lzyWQ_zl0,2098
197
198
  port_ocean/utils/queue_utils.py,sha256=KWWl8YVnG-glcfIHhM6nefY-2sou_C6DVP1VynQwzB4,2762
198
199
  port_ocean/utils/repeat.py,sha256=U2OeCkHPWXmRTVoPV-VcJRlQhcYqPWI5NfmPlb1JIbc,3229
199
200
  port_ocean/utils/signal.py,sha256=mMVq-1Ab5YpNiqN4PkiyTGlV_G0wkUDMMjTZp5z3pb0,1514
200
201
  port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
201
202
  port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
202
- port_ocean-0.23.2.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
203
- port_ocean-0.23.2.dist-info/METADATA,sha256=VnbW8FFOYaCKz8w6PUz0nxZgkpzED3Ny8ui9qB5uXBY,6764
204
- port_ocean-0.23.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
205
- port_ocean-0.23.2.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
206
- port_ocean-0.23.2.dist-info/RECORD,,
203
+ port_ocean-0.23.4.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
204
+ port_ocean-0.23.4.dist-info/METADATA,sha256=6drOs6277IC86mO9ZhmdCjVUKy6QL2IqVTi_6x7vyI4,6764
205
+ port_ocean-0.23.4.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
206
+ port_ocean-0.23.4.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
207
+ port_ocean-0.23.4.dist-info/RECORD,,