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

Fix exporter; bring up-to-date; add flake8 #3

Merged
merged 6 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
# This is a basic workflow to help you get started with Actions

name: prometheus_fzj_weather_exporter tests

# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [ main ]
branches:
- main
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy3']
python-version: ['3.11', '3.12']

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: git config
run: |
git config --global user.email "you@example.com"
git config --global user.name "Your Name"

git config --global user.email "weather@exporter.com"
git config --global user.name "Weather Exporter"
- name: Install dependencies
run: |
pip install flake8

pip install . flake8 pyre-check
- name: flake8 linting
run: |
flake8 $(find . -type f -name "*.py")
flake8
- name: pyre type checking
run: |
pyre
6 changes: 6 additions & 0 deletions .pyre_configuration
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"site_package_search_strategy": "pep561",
"source_directories": [
"."
]
}
44 changes: 18 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,39 @@
# FZJ-Weather Prometheus Exporter
The FZJ-Weather Prometheus Exporter (short: exporter) is an exporter compatible
with the Prometheus database. It consists of two parts:
The FZJ-Weather Prometheus Exporter (short: exporter) provides weather data in
the format specified for Prometheus Exporters. It consists of two parts:
1. `fzj_weather.py`: a python script using BeautifulSoup4 and requests to
parse and return meteorological data from a weather station inside the
Forschungszentrum Jülich (short: FZJ). It does so by parsing the website
providing the information.
2. exporter (`main.py`): uses said script to receive, parse and provide
the data to the Prometheus database. Once started, it runs indefinitely until interrupt.
the data to the Prometheus database. Once started, it runs indefinitely
TobiasKadelka marked this conversation as resolved.
Show resolved Hide resolved
until interrupted.

`main.py` references the weather script and other needed scripts. It therefore
marks the entry point of the exporter.

## Install
## Installation and Usage
1. Install the package from the corresponding GitHub Repository:
`pip install git+https://github.com/psyinfra/prometheus_fzj_weather_exporter.git`
2. Start the exporter:
`prometheus_fzj_weather_exporter`
3. (from another terminal) `curl 127.0.0.1:9184`

## Test
To test the exporter, you can host the script on your own machine:
1. `prometheus_fzj_weather_exporter`
2. (from another terminal) `curl 127.0.0.1:9184`
Or

1. Clone the project:
`git clone git@github.com:psyinfra/prometheus-fzj-weather-exporter.git`
2. Install dependencies from inside the repository:
`pip install -e .`
3. Start the exporter:
`python prometheus_fzj_weather_exporter/main.py --web.listen-address 127.0.0.1:9184`
4. (from another terminal) `curl 127.0.0.1:9184`

The output of `curl 127.0.0.1:9184` has this structure (similar for the other data
i.e. air pressure, humidity, wind power, wind direction):

Running `curl 127.0.0.1:9184` should give you an output of similar structure
like this:
```
# HELP fzj_weather_air_temperature temperature in celsius
# TYPE fzj_weather_air_temperature gauge
fzj_weather_air_temperature 14.0
```
(The output should be similar for other data points, i.e. humidity)

## Usage
aqw marked this conversation as resolved.
Show resolved Hide resolved
```
usage: prometheus_fzj_weather_exporter [-h] [-i] [-w LISTEN_ADDRESS]

Set up the Prometheus exporter (connection ports)

options:
-h, --help show this help message and exit
-i, --insecure skip SSL validation of the weather website
-w LISTEN_ADDRESS, --web.listen-address LISTEN_ADDRESS
address and port to expose metrics and web interface. Default: `:9184`
To listen on all interfaces, omit the IP. `:<port>`
To listen on a specific IP: `<address>:<port>`
```
15 changes: 9 additions & 6 deletions prometheus_fzj_weather_exporter/exporter_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,25 @@
from prometheus_client import Summary
from prometheus_client.core import GaugeMetricFamily

from . import fzj_weather_crawler
from prometheus_fzj_weather_exporter import fzj_weather_crawler

REQUEST_TIME = Summary("weather_exporter_collect_seconds",
"Time spent to collect metrics from fzj_weather.py")


class FZJWeatherExporter:
insec : bool
insecure: bool
url: str

def __init__(self, insec_bool) -> None:
self.insec = insec_bool
def __init__(self,
url: str,
insecure: bool) -> None:
self.url = url
self.insecure = insecure

@REQUEST_TIME.time()
def collect(self):

weather = fzj_weather_crawler.fzj_weather_crawler(self.insec)
weather = fzj_weather_crawler.fzj_weather_crawler(self.url, self.insecure)

g = GaugeMetricFamily(
name='fzj_weather_air_temperature_celsius',
Expand Down
39 changes: 16 additions & 23 deletions prometheus_fzj_weather_exporter/fzj_weather.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# This file is licensed under the ISC license.
# Oskar Druska 2022
# For further information look up LICENSE.txt
# For further information look up LICENSE

# This script parses weather data from an FZJ inside website

Expand All @@ -10,42 +10,35 @@
from bs4 import BeautifulSoup


# Python module to execute

def get_weather_data(insec_bool):
url = "https://www.fz-juelich.de/de/gs/ueber-uns/meteo/aktuelle-wetterdaten/wetterdaten"

r = requests.get(url, verify = not insec_bool) # if insec_bool, then Request shall ignore the SSL certificate

def get_weather_data(url: str,
insecure: bool) -> dict:
# if `insecure`, then Request shall ignore the SSL certificate
r = requests.get(url, verify=(not insecure))
if r.status_code != 200:
raise ConnectionError("Something's wrong with the Website:\n" + url + "\n" + str(r.status_code))
raise ConnectionError(f"Something's wrong with the Website:\n{url}\n{r.status_code}")

soup = BeautifulSoup(r.text, 'html.parser')

weather_dict = make_weather_dict(url, soup) # {header: data}
weather_dict = make_weather_dict(url, soup)

return weather_dict


def make_weather_dict(url, soup):
# Parses the table containing the needed information to get all table rows.
weather_tablerows = soup.table.find_all("tr")

# Creates a dictionary with headers as keys and data as values
# (i.e. Luftdruck: 1016.6 hPa).
# `.replace(u'\xa0', u' ')` replaces parsing errors with whitespaces
# `re.sub('[^0-9 , .]', '', weather_td[1].get_text(strip=True)` strips
# all non-numeric characters from the string

def make_weather_dict(url, soup) -> dict:
"""Parses the table containing weather information from the webpage into a
dictionary with headers as keys and data as values (i.e. Luftdruck: 1016.6 hPa).
"""
weather_table = soup.table.find_all("tr")
weather_data = {
"source": url,
"title": soup.title.get_text(strip=True),
"date": soup.u.get_text(strip=True)
}

for row in weather_tablerows:
weather_td = row.find_all("td") # td, table data
for row in weather_table:
weather_td = row.find_all("td") # td: table data

# `replace(u'\xa0', u' ')` replaces parsing errors with whitespaces
# `re.sub('[^0-9 , .]', ''` strips all non-numeric characters from the string
weather_data[weather_td[0].get_text(strip=True).replace(u'\xa0', u' ')] \
= re.sub('[^0-9 , .]', '', weather_td[1].get_text(strip=True))

Expand Down
21 changes: 11 additions & 10 deletions prometheus_fzj_weather_exporter/fzj_weather_crawler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,30 @@
# For further information look up LICENSE.txt

from dataclasses import dataclass
from . import fzj_weather
from prometheus_fzj_weather_exporter import fzj_weather


@dataclass
class Weather:
temperature: float # celsius
air_pressure: float # hectoPascal
humidity: int # percent
wind_power: int # beaufort
wind_power: float # beaufort
wind_direction: int # degree

def fzj_weather_crawler(insec_bool):
""" scrapes data from the FZJ weather site via the fzj_weather.py script
and returns a dataclass object containing the information """

crawled_weather_data = fzj_weather.get_weather_data(insec_bool)

weather_return = Weather(
def fzj_weather_crawler(url: str,
insecure: bool) -> Weather:
"""Scrape data from the FZJ weather site via fzj_weather.py
and return a dataclass object containing the information.
"""
crawled_weather_data = fzj_weather.get_weather_data(url, insecure)
weather = Weather(
temperature=float(crawled_weather_data['Lufttemperatur']),
air_pressure=float(crawled_weather_data['Luftdruck (92 m ü.N.H.N.)']),
humidity=int(crawled_weather_data['relative Feuchte']),
wind_power=int(crawled_weather_data['Windstärke']),
wind_power=float(crawled_weather_data['Windstärke']),
wind_direction=int(crawled_weather_data['Windrichtung'])
)

return weather_return
return weather
36 changes: 14 additions & 22 deletions prometheus_fzj_weather_exporter/main.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,51 @@
#!/usr/bin/env python3
# This file is licensed under the ISC license.
# Oskar Druska 2022
# For further information look up LICENSE.txt

# exporter entry point

# usage: > python3 main.py --web.listen-address 9184
# test: > curl 127.0.0.1:9184
# (test in a different console or start in background)
# expected output (similar to):
# > # HELP fzj_weather_air_temperature temperature in celsius
# > # TYPE fzj_weather_air_temperature gauge
# > fzj_weather_air_temperature 14.0
# (equivalent output for other data i.e. humidity)
# For further information look up LICENSE

import sys
import argparse
from argparse import RawTextHelpFormatter
import time
from prometheus_client import start_http_server, REGISTRY
from . import exporter_file
from prometheus_fzj_weather_exporter import exporter_file


def main():
args = get_parsed_args()

url = "https://www.fz-juelich.de/de/gs/ueber-uns/meteo/aktuelle-wetterdaten/wetterdaten"
try:
REGISTRY.register(exporter_file.FZJWeatherExporter(args.insecure))
REGISTRY.register(exporter_file.FZJWeatherExporter(url, args.insecure))
except ConnectionError as c:
sys.exit(c.strerror)

if args.listenaddress is None:
start_http_server(port=9184, addr='127.0.0.1')
else:
# start the http server
if args.listenaddress:
ip, port = args.listenaddress.split(":")
if ip:
start_http_server(port=int(port), addr=ip)
else: # listen on all interfaces
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the behavior that changed upstream.

IIRC, the old behavior was to listen on all interfaces if it's not specified. Now it's 127.0.0.1.

I'm not certain though, so you should check into that.

Copy link
Contributor

Choose a reason for hiding this comment

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

We talked and the changes around this will be addressed in another PR.

start_http_server(port=int(port))
else:
start_http_server(port=9184, addr='127.0.0.1')

# keep the thing going indefinitely
# keep the exporter running indefinitely
while True:
time.sleep(1)


def get_parsed_args():
parser = argparse.ArgumentParser(
description='Set up the Prometheus exporter (connection ports)')
description='Set up the Prometheus exporter (connection ports)',
formatter_class=RawTextHelpFormatter)
group = parser.add_argument_group()
group.add_argument(
'-w', '--web.listen-address',
type=str,
dest='listenaddress',
help='Address and port to expose metrics and web interface. Default: ":9184"\n'
'To listen on all interfaces, omit the IP. ":<port>"`\n'\
'To listen on a specific IP: <address>:<port>')
'To listen on all interfaces, omit the IP: ":<port>"\n'
Copy link
Contributor

Choose a reason for hiding this comment

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

IIRC, this behavior recently changed...

See psyinfra/prometheus-eaton-ups-exporter@162c390

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Dang it, I forgot about that.
I would follow the eaton-ups-exporter and change it to 0.0.0.0

'To listen on a specific IP: <address>:<port>')
group.add_argument(
'-i', '--insecure',
dest='insecure',
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
'BeautifulSoup4',
'prometheus_client'
],
python_requires=">=3.6",
python_requires=">=3.11",
entry_points={
'console_scripts': [
'prometheus_fzj_weather_exporter=prometheus_fzj_weather_exporter.main:main'
Expand Down
Loading