Files
SolarManager/mysql/opentelemetry/sdk/metrics/_internal/__init__.py
T
adminandClaude Opus 5 79843aa2ae SolarManager unter Versionsverwaltung
Erster Stand der Hintergrundprozesse, die auf der Synology unter
/volume1/homes/wagner/SolarManager laufen: der Manager selbst, die Sammler
je Geraet, die MQTT-Bruecke, der Wecker und - neu hinzugezogen - der
AutoAction-Runner, der als Hintergrundprozess hierher gehoert und nicht ins
Web-Verzeichnis.

Zugangsdaten stehen nicht mehr im Quelltext, sondern in config.ini, die
nicht mit eingecheckt wird. Vorlage ist config.ini.example, gelesen wird sie
von konfig.py. Betroffen waren solarManager.py (Datenbank und Wattpilot),
zeit.py, gatherWaterData.py, wecker.py und skoda_testdaten.py, das sich das
Passwort bisher aus dem Quelltext eines anderen Moduls herausgesucht hat.

Die Kia-Anbindung ist mit dem Fahrzeug entfallen: kiaTest.py,
gatherCarData.py und hyundai_kia_connect_api sind nicht mehr dabei, ebenso
gatherInverterData.py, auf das nur noch eine auskommentierte Zeile zeigte.

Die mitgelieferten Bibliotheken bleiben im Repository - die NAS hat kein
pip, sie muessen neben den Skripten liegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:46:59 +02:00

468 lines
17 KiB
Python

# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from atexit import register, unregister
from logging import getLogger
from threading import Lock
from time import time_ns
from typing import Optional, Sequence
# This kind of import is needed to avoid Sphinx errors.
import mysql.opentelemetry.sdk.metrics
from mysql.opentelemetry.metrics import (
Counter as APICounter,
Histogram as APIHistogram,
Meter as APIMeter,
MeterProvider as APIMeterProvider,
NoOpMeter,
ObservableCounter as APIObservableCounter,
ObservableGauge as APIObservableGauge,
ObservableUpDownCounter as APIObservableUpDownCounter,
UpDownCounter as APIUpDownCounter,
)
from mysql.opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError
from mysql.opentelemetry.sdk.metrics._internal.instrument import (
_Counter,
_Histogram,
_ObservableCounter,
_ObservableGauge,
_ObservableUpDownCounter,
_UpDownCounter,
)
from mysql.opentelemetry.sdk.metrics._internal.measurement_consumer import (
MeasurementConsumer,
SynchronousMeasurementConsumer,
)
from mysql.opentelemetry.sdk.metrics._internal.sdk_configuration import SdkConfiguration
from mysql.opentelemetry.sdk.resources import Resource
from mysql.opentelemetry.sdk.util.instrumentation import InstrumentationScope
from mysql.opentelemetry.util._once import Once
_logger = getLogger(__name__)
class Meter(APIMeter):
"""See `mysql.opentelemetry.metrics.Meter`."""
def __init__(
self,
instrumentation_scope: InstrumentationScope,
measurement_consumer: MeasurementConsumer,
):
super().__init__(
name=instrumentation_scope.name,
version=instrumentation_scope.version,
schema_url=instrumentation_scope.schema_url,
)
self._instrumentation_scope = instrumentation_scope
self._measurement_consumer = measurement_consumer
self._instrument_id_instrument = {}
self._instrument_id_instrument_lock = Lock()
def create_counter(self, name, unit="", description="") -> APICounter:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _Counter, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APICounter.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _Counter(
name,
self._instrumentation_scope,
self._measurement_consumer,
unit,
description,
)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_up_down_counter(self, name, unit="", description="") -> APIUpDownCounter:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _UpDownCounter, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIUpDownCounter.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _UpDownCounter(
name,
self._instrumentation_scope,
self._measurement_consumer,
unit,
description,
)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_observable_counter(
self, name, callbacks=None, unit="", description=""
) -> APIObservableCounter:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _ObservableCounter, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIObservableCounter.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _ObservableCounter(
name,
self._instrumentation_scope,
self._measurement_consumer,
callbacks,
unit,
description,
)
self._measurement_consumer.register_asynchronous_instrument(instrument)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_histogram(self, name, unit="", description="") -> APIHistogram:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _Histogram, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIHistogram.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _Histogram(
name,
self._instrumentation_scope,
self._measurement_consumer,
unit,
description,
)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_observable_gauge(
self, name, callbacks=None, unit="", description=""
) -> APIObservableGauge:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _ObservableGauge, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIObservableGauge.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _ObservableGauge(
name,
self._instrumentation_scope,
self._measurement_consumer,
callbacks,
unit,
description,
)
self._measurement_consumer.register_asynchronous_instrument(instrument)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_observable_up_down_counter(
self, name, callbacks=None, unit="", description=""
) -> APIObservableUpDownCounter:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(
name, _ObservableUpDownCounter, unit, description
)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIObservableUpDownCounter.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _ObservableUpDownCounter(
name,
self._instrumentation_scope,
self._measurement_consumer,
callbacks,
unit,
description,
)
self._measurement_consumer.register_asynchronous_instrument(instrument)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
class MeterProvider(APIMeterProvider):
r"""See `mysql.opentelemetry.metrics.MeterProvider`.
Args:
metric_readers: Register metric readers to collect metrics from the SDK
on demand. Each :class:`mysql.opentelemetry.sdk.metrics.export.MetricReader` is
completely independent and will collect separate streams of
metrics. TODO: reference ``PeriodicExportingMetricReader`` usage with push
exporters here.
resource: The resource representing what the metrics emitted from the SDK pertain to.
shutdown_on_exit: If true, registers an `atexit` handler to call
`MeterProvider.shutdown`
views: The views to configure the metric output the SDK
By default, instruments which do not match any :class:`mysql.opentelemetry.sdk.metrics.view.View` (or if no :class:`mysql.opentelemetry.sdk.metrics.view.View`\ s
are provided) will report metrics with the default aggregation for the
instrument's kind. To disable instruments by default, configure a match-all
:class:`mysql.opentelemetry.sdk.metrics.view.View` with `DropAggregation` and then create :class:`mysql.opentelemetry.sdk.metrics.view.View`\ s to re-enable
individual instruments:
.. code-block:: python
:caption: Disable default views
MeterProvider(
views=[
View(instrument_name="*", aggregation=DropAggregation()),
View(instrument_name="mycounter"),
],
# ...
)
"""
_all_metric_readers_lock = Lock()
_all_metric_readers = set()
def __init__(
self,
metric_readers: Sequence[
"mysql.opentelemetry.sdk.metrics.export.MetricReader"
] = (),
resource: Resource = Resource.create({}),
shutdown_on_exit: bool = True,
views: Sequence["mysql.opentelemetry.sdk.metrics.view.View"] = (),
):
self._lock = Lock()
self._meter_lock = Lock()
self._atexit_handler = None
self._sdk_config = SdkConfiguration(
resource=resource,
metric_readers=metric_readers,
views=views,
)
self._measurement_consumer = SynchronousMeasurementConsumer(
sdk_config=self._sdk_config
)
if shutdown_on_exit:
self._atexit_handler = register(self.shutdown)
self._meters = {}
self._shutdown_once = Once()
self._shutdown = False
for metric_reader in self._sdk_config.metric_readers:
with self._all_metric_readers_lock:
if metric_reader in self._all_metric_readers:
raise Exception(
f"MetricReader {metric_reader} has been registered "
"already in other MeterProvider instance"
)
self._all_metric_readers.add(metric_reader)
metric_reader._set_collect_callback(self._measurement_consumer.collect)
def force_flush(self, timeout_millis: float = 10_000) -> bool:
deadline_ns = time_ns() + timeout_millis * 10**6
metric_reader_error = {}
for metric_reader in self._sdk_config.metric_readers:
current_ts = time_ns()
try:
if current_ts >= deadline_ns:
raise MetricsTimeoutError("Timed out while flushing metric readers")
metric_reader.force_flush(
timeout_millis=(deadline_ns - current_ts) / 10**6
)
# pylint: disable=broad-except
except Exception as error:
metric_reader_error[metric_reader] = error
if metric_reader_error:
metric_reader_error_string = "\n".join(
[
f"{metric_reader.__class__.__name__}: {repr(error)}"
for metric_reader, error in metric_reader_error.items()
]
)
raise Exception(
"MeterProvider.force_flush failed because the following "
"metric readers failed during collect:\n"
f"{metric_reader_error_string}"
)
return True
def shutdown(self, timeout_millis: float = 30_000):
deadline_ns = time_ns() + timeout_millis * 10**6
def _shutdown():
self._shutdown = True
did_shutdown = self._shutdown_once.do_once(_shutdown)
if not did_shutdown:
_logger.warning("shutdown can only be called once")
return
metric_reader_error = {}
for metric_reader in self._sdk_config.metric_readers:
current_ts = time_ns()
try:
if current_ts >= deadline_ns:
raise Exception("Didn't get to execute, deadline already exceeded")
metric_reader.shutdown(
timeout_millis=(deadline_ns - current_ts) / 10**6
)
# pylint: disable=broad-except
except Exception as error:
metric_reader_error[metric_reader] = error
if self._atexit_handler is not None:
unregister(self._atexit_handler)
self._atexit_handler = None
if metric_reader_error:
metric_reader_error_string = "\n".join(
[
f"{metric_reader.__class__.__name__}: {repr(error)}"
for metric_reader, error in metric_reader_error.items()
]
)
raise Exception(
(
"MeterProvider.shutdown failed because the following "
"metric readers failed during shutdown:\n"
f"{metric_reader_error_string}"
)
)
def get_meter(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> Meter:
if self._shutdown:
_logger.warning("A shutdown `MeterProvider` can not provide a `Meter`")
return NoOpMeter(name, version=version, schema_url=schema_url)
if not name:
_logger.warning("Meter name cannot be None or empty.")
return NoOpMeter(name, version=version, schema_url=schema_url)
info = InstrumentationScope(name, version, schema_url)
with self._meter_lock:
if not self._meters.get(info):
# FIXME #2558 pass SDKConfig object to meter so that the meter
# has access to views.
self._meters[info] = Meter(
info,
self._measurement_consumer,
)
return self._meters[info]