Skip to content
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

Closed
wants to merge 4 commits into from

Conversation

mweinelt
Copy link
Contributor

@mweinelt mweinelt commented Dec 24, 2020

Breaking change

None

Proposed change

Implement a sensor that queries Prometheus using PromQL expressions.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Example entry for configuration.yaml:

# Example configuration.yaml
sensor:
  - platform: prometheus
    url: http://localhost:9090  # this is also the default
    queries:
      - name: entropy
        expr: node_entropy_available_bits{instance="localhost:9100"}
      - name: free disk space
        expr: node_filesystem_free_bytes{instance="localhost:9100", mountpoint="/"}
        unit_of_measurement: Bytes

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request: TODO

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • The code has been formatted using Black (black --fast homeassistant tests)
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • Untested files have been added to .coveragerc.

The integration reached or maintains the following Integration Quality Scale:

  • No score or internal
  • 🥈 Silver
  • 🥇 Gold
  • 🏆 Platinum

To help with the load of incoming pull requests:

CC @knyar

def __init__(self, url, session):
"""Initialize the Prometheus API wrapper."""
self._session = session
self._url = f"{url}/api/v1/query"
Copy link
Contributor Author

@mweinelt mweinelt Dec 24, 2020

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied.

homeassistant/components/prometheus/sensor.py Show resolved Hide resolved
DEFAULT_URL = "http://localhost:9090"
CONF_QUERIES = "queries"
CONF_EXPR = "expr"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
Copy link
Contributor

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?

Copy link
Contributor Author

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})
Copy link
Contributor

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?

Copy link
Contributor Author

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])
Copy link
Contributor

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).

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. /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).
  2. 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.

Copy link
Contributor Author

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.

homeassistant/components/prometheus/sensor.py Show resolved Hide resolved
}
)

PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
Copy link
Contributor

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.

Copy link
Contributor Author

@mweinelt mweinelt Dec 24, 2020

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.

Copy link
Member

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"
Copy link
Contributor

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?

@knyar
Copy link
Contributor

knyar commented Dec 26, 2020

Thanks! I don't have any other comments.
Hopefully one of the core maintainers will be able to review this soon and eventually get it merged.

async_add_entities(sensors, update_before_add=True)


class Prometheus:
Copy link
Member

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.

https://developers.home-assistant.io/docs/creating_component_code_review#4-communication-with-devicesservices

@MartinHjelmare
Copy link
Member

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!

@github-actions github-actions bot locked and limited conversation to collaborators Feb 24, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants