|
8 | 8 | from datetime import timedelta |
9 | 9 | from enum import Enum |
10 | 10 | from pathlib import Path |
| 11 | +from time import sleep |
11 | 12 | from typing import Annotated, Any, Literal, TypeVar |
12 | 13 |
|
13 | 14 | from humps import kebabize |
| 15 | +from prometheus_client import REGISTRY, start_http_server |
14 | 16 | from pydantic import BaseModel, ConfigDict, Field, GetCoreSchemaHandler |
15 | 17 | from pydantic_core import CoreSchema, core_schema |
16 | 18 | from typing_extensions import assert_never |
|
22 | 24 | OAuthClientCertificate, |
23 | 25 | OAuthClientCredentials, |
24 | 26 | ) |
| 27 | +from cognite.client.data_classes import Asset |
25 | 28 | from cognite.extractorutils.configtools._util import _load_certificate_data |
26 | 29 | from cognite.extractorutils.exceptions import InvalidConfigError |
| 30 | +from cognite.extractorutils.metrics import AbstractMetricsPusher, CognitePusher, PrometheusPusher |
27 | 31 | from cognite.extractorutils.statestore import ( |
28 | 32 | AbstractStateStore, |
29 | 33 | LocalStateStore, |
30 | 34 | NoStateStore, |
31 | 35 | RawStateStore, |
32 | 36 | ) |
33 | 37 | from cognite.extractorutils.threading import CancellationToken |
| 38 | +from cognite.extractorutils.util import EitherId |
34 | 39 |
|
35 | 40 | __all__ = [ |
36 | 41 | "AuthenticationConfig", |
|
43 | 48 | "LogFileHandlerConfig", |
44 | 49 | "LogHandlerConfig", |
45 | 50 | "LogLevel", |
| 51 | + "MetricsConfig", |
46 | 52 | "ScheduleConfig", |
47 | 53 | "TimeIntervalConfig", |
48 | 54 | ] |
@@ -438,6 +444,176 @@ class LogConsoleHandlerConfig(ConfigModel): |
438 | 444 | LogHandlerConfig = Annotated[LogFileHandlerConfig | LogConsoleHandlerConfig, Field(discriminator="type")] |
439 | 445 |
|
440 | 446 |
|
| 447 | +class EitherIdConfig(ConfigModel): |
| 448 | + """ |
| 449 | + Configuration parameter representing an ID in CDF, which can either be an external or internal ID. |
| 450 | +
|
| 451 | + An EitherId can only hold one ID type, not both. |
| 452 | + """ |
| 453 | + |
| 454 | + id: int | None = None |
| 455 | + external_id: str | None = None |
| 456 | + |
| 457 | + @property |
| 458 | + def either_id(self) -> EitherId: |
| 459 | + """ |
| 460 | + Returns an EitherId object based on the current configuration. |
| 461 | +
|
| 462 | + Raises: |
| 463 | + TypeError: If both id and external_id are None, or if both are set. |
| 464 | + """ |
| 465 | + return EitherId(id=self.id, external_id=self.external_id) |
| 466 | + |
| 467 | + |
| 468 | +class _PushGatewayConfig(ConfigModel): |
| 469 | + """ |
| 470 | + Configuration for pushing metrics to a Prometheus Push Gateway. |
| 471 | + """ |
| 472 | + |
| 473 | + host: str |
| 474 | + job_name: str |
| 475 | + username: str | None = None |
| 476 | + password: str | None = None |
| 477 | + |
| 478 | + clear_after: TimeIntervalConfig | None = None |
| 479 | + push_interval: TimeIntervalConfig = Field(default_factory=lambda: TimeIntervalConfig("30s")) |
| 480 | + |
| 481 | + |
| 482 | +class _PromServerConfig(ConfigModel): |
| 483 | + """ |
| 484 | + Configuration for pushing metrics to a Prometheus server. |
| 485 | + """ |
| 486 | + |
| 487 | + port: int = 9000 |
| 488 | + host: str = "0.0.0.0" |
| 489 | + |
| 490 | + |
| 491 | +class _CogniteMetricsConfig(ConfigModel): |
| 492 | + """ |
| 493 | + Configuration for pushing metrics to Cognite Data Fusion. |
| 494 | + """ |
| 495 | + |
| 496 | + external_id_prefix: str |
| 497 | + asset_name: str | None = None |
| 498 | + asset_external_id: str | None = None |
| 499 | + data_set: EitherIdConfig | None = None |
| 500 | + |
| 501 | + push_interval: TimeIntervalConfig = Field(default_factory=lambda: TimeIntervalConfig("30s")) |
| 502 | + |
| 503 | + |
| 504 | +class MetricsPushManager: |
| 505 | + """ |
| 506 | + Manages the pushing of metrics to various backends. |
| 507 | +
|
| 508 | + Starts and stops pushers based on a given configuration. |
| 509 | +
|
| 510 | + Args: |
| 511 | + metrics_config: Configuration for the metrics to be pushed. |
| 512 | + cdf_client: The CDF tenant to upload time series to |
| 513 | + cancellation_token: Event object to be used as a thread cancelation event |
| 514 | + """ |
| 515 | + |
| 516 | + def __init__( |
| 517 | + self, |
| 518 | + metrics_config: "MetricsConfig", |
| 519 | + cdf_client: CogniteClient, |
| 520 | + cancellation_token: CancellationToken | None = None, |
| 521 | + ) -> None: |
| 522 | + """ |
| 523 | + Initialize the MetricsPushManager. |
| 524 | + """ |
| 525 | + self.metrics_config = metrics_config |
| 526 | + self.cdf_client = cdf_client |
| 527 | + self.cancellation_token = cancellation_token |
| 528 | + self.pushers: list[AbstractMetricsPusher] = [] |
| 529 | + self.clear_on_stop: dict[AbstractMetricsPusher, int] = {} |
| 530 | + |
| 531 | + def start(self) -> None: |
| 532 | + """ |
| 533 | + Start all metric pushers. |
| 534 | + """ |
| 535 | + push_gateways = self.metrics_config.push_gateways or [] |
| 536 | + for counter, push_gateway in enumerate(push_gateways): |
| 537 | + prometheus_pusher = PrometheusPusher( |
| 538 | + job_name=push_gateway.job_name, |
| 539 | + username=push_gateway.username, |
| 540 | + password=push_gateway.password, |
| 541 | + url=push_gateway.host, |
| 542 | + push_interval=push_gateway.push_interval.seconds, |
| 543 | + thread_name=f"MetricsPusher_{counter}", |
| 544 | + cancellation_token=self.cancellation_token, |
| 545 | + ) |
| 546 | + prometheus_pusher.start() |
| 547 | + self.pushers.append(prometheus_pusher) |
| 548 | + if push_gateway.clear_after is not None: |
| 549 | + self.clear_on_stop[prometheus_pusher] = push_gateway.clear_after.seconds |
| 550 | + |
| 551 | + if self.metrics_config.cognite: |
| 552 | + asset = None |
| 553 | + if self.metrics_config.cognite.asset_name and self.metrics_config.cognite.asset_external_id: |
| 554 | + asset = Asset( |
| 555 | + name=self.metrics_config.cognite.asset_name, |
| 556 | + external_id=self.metrics_config.cognite.asset_external_id, |
| 557 | + ) |
| 558 | + cognite_pusher = CognitePusher( |
| 559 | + cdf_client=self.cdf_client, |
| 560 | + external_id_prefix=self.metrics_config.cognite.external_id_prefix, |
| 561 | + push_interval=self.metrics_config.cognite.push_interval.seconds, |
| 562 | + asset=asset, |
| 563 | + data_set=self.metrics_config.cognite.data_set.either_id |
| 564 | + if self.metrics_config.cognite.data_set |
| 565 | + else None, |
| 566 | + thread_name="CogniteMetricsPusher", |
| 567 | + cancellation_token=self.cancellation_token, |
| 568 | + ) |
| 569 | + cognite_pusher.start() |
| 570 | + self.pushers.append(cognite_pusher) |
| 571 | + |
| 572 | + if self.metrics_config.server: |
| 573 | + start_http_server(self.metrics_config.server.port, self.metrics_config.server.host, registry=REGISTRY) |
| 574 | + |
| 575 | + def stop(self) -> None: |
| 576 | + """ |
| 577 | + Stop all metric pushers. |
| 578 | + """ |
| 579 | + for pusher in self.pushers: |
| 580 | + pusher.stop() |
| 581 | + |
| 582 | + # Clear Prometheus pushers gateways if required |
| 583 | + if self.clear_on_stop: |
| 584 | + wait_time = max(self.clear_on_stop.values()) |
| 585 | + sleep(wait_time) |
| 586 | + for pusher in (p for p in self.clear_on_stop if isinstance(p, PrometheusPusher)): |
| 587 | + pusher.clear_gateway() |
| 588 | + |
| 589 | + |
| 590 | +class MetricsConfig(ConfigModel): |
| 591 | + """ |
| 592 | + Destination(s) for metrics. |
| 593 | +
|
| 594 | + Including options for one or several Prometheus push gateways, and pushing as CDF Time Series. |
| 595 | + """ |
| 596 | + |
| 597 | + push_gateways: list[_PushGatewayConfig] | None = None |
| 598 | + cognite: _CogniteMetricsConfig | None = None |
| 599 | + server: _PromServerConfig | None = None |
| 600 | + |
| 601 | + def create_manager( |
| 602 | + self, cdf_client: CogniteClient, cancellation_token: CancellationToken | None = None |
| 603 | + ) -> MetricsPushManager: |
| 604 | + """ |
| 605 | + Create a MetricsPushManager based on the current configuration. |
| 606 | +
|
| 607 | + Args: |
| 608 | + cdf_client: An instance of CogniteClient to interact with CDF. |
| 609 | + cancellation_token: Optional token to signal cancellation of metric pushing. |
| 610 | +
|
| 611 | + Returns: |
| 612 | + MetricsPushManager: An instance of MetricsPushManager configured with the provided parameters. |
| 613 | + """ |
| 614 | + return MetricsPushManager(self, cdf_client, cancellation_token) |
| 615 | + |
| 616 | + |
441 | 617 | # Mypy BS |
442 | 618 | def _log_handler_default() -> list[LogHandlerConfig]: |
443 | 619 | return [LogConsoleHandlerConfig(type="console", level=LogLevel.INFO)] |
|
0 commit comments