-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add prometheus sensor component #44508
Conversation
def __init__(self, url, session): | ||
"""Initialize the Prometheus API wrapper.""" | ||
self._session = session | ||
self._url = f"{url}/api/v1/query" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really happy about using string formatting to build a URL, but urllib.parse.urljoin
won't work for https://monitoring.example.com/prometheus
, as it would override /prometheus
with /api/v1/query
instead of appending to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't urljoin(f"{url}/", "api/v1/query")
work for you here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
>>> urljoin("http://monitoring.example.com/prometheus", "/api/v1/query")
'http://monitoring.example.com/api/v1/query'
I strips the /prometheus
base path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I am suggesting to append a slash to url
and add api/v1/query
as a relative path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
>>> urljoin("http://monitoring.example.com/prometheus/", "/api/v1/query")
'http://monitoring.example.com/api/v1/query'
Like this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
>>> url = "http://monitoring.example.com/prometheus"
>>> urljoin(f"{url}/", "api/v1/query")
'http://monitoring.example.com/prometheus/api/v1/query'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied.
864485d
to
47de698
Compare
DEFAULT_URL = "http://localhost:9090" | ||
CONF_QUERIES = "queries" | ||
CONF_EXPR = "expr" | ||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be configurable as well, or is it hard to implement because it's passed to a decorator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I'm not sure if a decorator can access a member object.
|
||
async def query(self, expr: str) -> Union[str, float]: | ||
"""Query expression response.""" | ||
response = await self._session.get(self._url, params={"query": expr}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you happy with the default request timeout (5 minutes) or should it be overriden or configurable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't looked into it. Should probably be somewhat related to the MIN_TIME_BETWEEN_UPDATES
.
_LOGGER.error("Invalid query response: %s", error) | ||
return STATE_UNKNOWN | ||
|
||
return float(result[0]["value"][1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would advise against ignoring all results other than the first one. This could surprise users, and also I don't think Prometheus really guarantees to return results in the same order.
Two possible solutions here: either return an error if the response contains multiple results (pretty simple) or use all of them (for example, by setting a separate state attribute for each result).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is two unrelated things to consider:
-
I think from a series I think I can only consider the last value in a sensor entity, or can I backfill it?
-
Expressions need to map to exactly one series or else its value is ambiguous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/api/v1/query
is an instant query API that I believe will return the latest point for each metric. If you wanted to get a time series, you would need to use a range query API (/api/v1/query_range
), but I agree that it does not seem like that can be useful for a Home Assistant sensor (history in HA seems to be provided by the separate history integration that does not seem to have a backfill API).- Yes, that's exactly the issue that I wanted to point out here. I would suggest to return an error unless
result
contains exactly one element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging an error and returning STATE_PROBLEM
if zero or multiple metrics are returned.
} | ||
) | ||
|
||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what's idiomatic here, but I believe most integrations create one entity per each defined sensor
, while in this case a single sensor definition will might result in several sensor entities created. Flattening the configuration structure might make it slightly more straightforward (even if a bit more verbose).
What I mean is something like this:
sensor:
- platform: prometheus
name: entropy
query: min(node_entropy_available_bits{instance="localhost:9100"})
- platform: prometheus
name: free disk space
url: http://localhost:9090
query: min(node_filesystem_free_bytes{instance="localhost:9100", mountpoint="/"})
unit_of_measurement: Bytes
Anyway, hopefully one of the Home Assistant core mainteners will weigh in and provide guidance here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then you'd have to pass the url
over and over again. The way I did it is basically how the influxdb sensor does it, so I think this is fine.
I guess you can still create multiple prometheus platforms for different prometheus instances though.
I think keeping it this way offers better usability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We no longer allow new integrations to use a platform config section or old integrations to do changes to the platform config section. See https://github.com/home-assistant/architecture/blob/master/adr/0007-integration-config-yaml-structure.md.
Please refactor this integration to have the config schema in the base integration module and the config yaml section under the top level integration domain key.
def __init__(self, url, session): | ||
"""Initialize the Prometheus API wrapper.""" | ||
self._session = session | ||
self._url = f"{url}/api/v1/query" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't urljoin(f"{url}/", "api/v1/query")
work for you here?
Thanks! I don't have any other comments. |
async_add_entities(sensors, update_before_add=True) | ||
|
||
|
||
class Prometheus: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please extract the service specific interface code and data parsing to a standalone library published on PyPI.
There hasn't been any movement for some time here. I'll close now. We're trying to decrease our open PR buffer. Please open a new PR when ready to finish. Thanks! |
Breaking change
None
Proposed change
Implement a sensor that queries Prometheus using PromQL expressions.
Type of change
Example entry for
configuration.yaml
:Additional information
Checklist
black --fast homeassistant tests
)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest
.requirements_all.txt
.Updated by running
python3 -m script.gen_requirements_all
..coveragerc
.The integration reached or maintains the following Integration Quality Scale:
To help with the load of incoming pull requests:
CC @knyar