feat: Initial implementation of a PostgreSQL connection exporter
This commit is contained in:
commit
a8eb80aad2
7 changed files with 262 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.venv
|
||||
config.yaml
|
||||
__pycache__
|
||||
*.pyc
|
||||
venv
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2024 Kumi Mitterer <postgres-connection-exporter@kumi.email>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
32
README.md
Normal file
32
README.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# PostgreSQL Connection Exporter for Prometheus
|
||||
|
||||
This is a simple server that exports PostgreSQL connection metrics in a format that can be scraped by Prometheus.
|
||||
|
||||
It outputs the following metrics:
|
||||
|
||||
- The number of connections per database
|
||||
- The number of connections per user
|
||||
- The number of connections per client address
|
||||
- The number of connections per state
|
||||
|
||||
## Installation
|
||||
|
||||
You can install the exporter from PyPI. Within a virtual environment, run:
|
||||
|
||||
```bash
|
||||
pip install postgres-connection-exporter
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The exporter is configured using a `config.yaml`. You can find an example in [config.dist.yaml](config.dist.yaml).
|
||||
|
||||
After you have created your `config.yaml`, you can start the exporter with:
|
||||
|
||||
```bash
|
||||
postgres-connection-exporter
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
8
config.dist.yaml
Normal file
8
config.dist.yaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
hosts: # List of database hosts
|
||||
# Each host must have a user, password, host, and optionally port
|
||||
- user: "user1"
|
||||
password: "password1"
|
||||
host: "host1"
|
||||
port: "5432"
|
||||
exporter:
|
||||
port: 8989 # Port on which the exporter will listen
|
32
pyproject.toml
Normal file
32
pyproject.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "postgres-connection-exporter"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
{ name="Kumi Mitterer", email="postgres-connection-exporter@kumi.email" },
|
||||
]
|
||||
description = "A Prometheus exporter for PostgreSQL connection metrics"
|
||||
readme = "README.md"
|
||||
license = { file="LICENSE" }
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent"
|
||||
]
|
||||
dependencies = [
|
||||
"prometheus-client",
|
||||
"psycopg2-binary",
|
||||
"pyyaml",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
postgres-connection-exporter = "postgres_connection_exporter.__main__:main"
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://git.private.coffee/kumi/postgres-connection-exporter"
|
||||
"Bug Tracker" = "https://git.private.coffee/kumi/postgres-connection-exporter/issues"
|
||||
"Source Code" = "https://git.private.coffee/kumi/postgres-connection-exporter"
|
0
src/postgres_connection_exporter/__init__.py
Normal file
0
src/postgres_connection_exporter/__init__.py
Normal file
166
src/postgres_connection_exporter/__main__.py
Normal file
166
src/postgres_connection_exporter/__main__.py
Normal file
|
@ -0,0 +1,166 @@
|
|||
import yaml
|
||||
from prometheus_client import start_http_server, Gauge
|
||||
import psycopg2
|
||||
import time
|
||||
|
||||
CONNECTIONS_PER_DB = Gauge(
|
||||
"db_connections_per_database",
|
||||
"Number of current connections per database",
|
||||
["database", "host"],
|
||||
)
|
||||
CONNECTIONS_PER_USER = Gauge(
|
||||
"db_connections_per_user",
|
||||
"Number of current connections per user",
|
||||
["user", "host"],
|
||||
)
|
||||
CONNECTIONS_BY_STATE = Gauge(
|
||||
"db_connections_by_state",
|
||||
"Number of current connections by state",
|
||||
["state", "host"],
|
||||
)
|
||||
CONNECTIONS_PER_SOURCE = Gauge(
|
||||
"db_connections_per_source",
|
||||
"Number of current connections per source",
|
||||
["source", "host"],
|
||||
)
|
||||
|
||||
|
||||
def load_config(config_file="config.yaml"):
|
||||
with open(config_file, "r") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def get_all_databases(db_config):
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
dbname="postgres",
|
||||
user=db_config["user"],
|
||||
password=db_config["password"],
|
||||
host=db_config["host"],
|
||||
port=db_config.get("port", 5432),
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT datname FROM pg_database WHERE datistemplate = false;")
|
||||
databases = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return [db[0] for db in databases]
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error retrieving list of databases: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_db_connections(db_config):
|
||||
try:
|
||||
# Connect to the PostgreSQL database
|
||||
conn = psycopg2.connect(
|
||||
dbname="postgres",
|
||||
user=db_config["user"],
|
||||
password=db_config["password"],
|
||||
host=db_config["host"],
|
||||
port=db_config["port"],
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Query to get the number of connections per database
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT datname, COUNT(*)
|
||||
FROM pg_stat_activity
|
||||
GROUP BY datname;
|
||||
"""
|
||||
)
|
||||
db_connections = cursor.fetchall()
|
||||
for db, count in db_connections:
|
||||
CONNECTIONS_PER_DB.labels(database=db, host=db_config["host"]).set(count)
|
||||
|
||||
# Query to get the number of connections per user
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT usename, COUNT(*)
|
||||
FROM pg_stat_activity
|
||||
GROUP BY usename;
|
||||
"""
|
||||
)
|
||||
user_connections = cursor.fetchall()
|
||||
for user, count in user_connections:
|
||||
CONNECTIONS_PER_USER.labels(user=user, host=db_config["host"]).set(count)
|
||||
|
||||
# Query to get the number of connections by state
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT state, COUNT(*)
|
||||
FROM pg_stat_activity
|
||||
GROUP BY state;
|
||||
"""
|
||||
)
|
||||
state_connections = cursor.fetchall()
|
||||
for state, count in state_connections:
|
||||
CONNECTIONS_BY_STATE.labels(state=state, host=db_config["host"]).set(count)
|
||||
|
||||
# Query to get the number of connections per source
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT client_addr, COUNT(*)
|
||||
FROM pg_stat_activity
|
||||
GROUP BY client_addr;
|
||||
"""
|
||||
)
|
||||
source_connections = cursor.fetchall()
|
||||
for source, count in source_connections:
|
||||
CONNECTIONS_PER_SOURCE.labels(source=source, host=db_config["host"]).set(count)
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Error retrieving data from {db_config['host']}: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
config = load_config()
|
||||
|
||||
if not ("hosts" in config and config["hosts"]):
|
||||
print("No database hosts specified in the configuration file.")
|
||||
exit(1)
|
||||
|
||||
databases_to_query = []
|
||||
|
||||
for host in config["hosts"]:
|
||||
if not all(key in host for key in ["user", "password", "host", "port"]):
|
||||
print("Database configuration is missing required fields.")
|
||||
exit(1)
|
||||
|
||||
db_config = {
|
||||
"user": host["user"],
|
||||
"password": host["password"],
|
||||
"host": host["host"],
|
||||
"port": host["port"],
|
||||
}
|
||||
|
||||
databases_to_query.append(db_config)
|
||||
|
||||
if not databases_to_query:
|
||||
print("No databases to query.")
|
||||
exit(1)
|
||||
|
||||
exporter_port = (
|
||||
config["exporter"]["port"]
|
||||
if "exporter" in config and "port" in config["exporter"]
|
||||
else 8989
|
||||
)
|
||||
|
||||
start_http_server(exporter_port)
|
||||
print(f"Prometheus exporter started on port {exporter_port}")
|
||||
|
||||
while True:
|
||||
for db in databases_to_query:
|
||||
get_db_connections(db)
|
||||
time.sleep(15)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in a new issue