diff --git a/.gitignore b/.gitignore index 91c9bd0..2c07b91 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ config.yaml __pycache__ *.pyc -venv \ No newline at end of file +venv/ +dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..eda59e9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +## v0.0.2 + +### Added + +- Add `--create-config` flag to create a default configuration file +- Add `--config` flag to specify a configuration file +- Add `--host` and `--port` flags to specify the address to listen on +- Make listening address configurable in the `config.yaml` file + +## v0.0.1 + +### Added + +- Initial release diff --git a/README.md b/README.md index 78b0e8e..5d6e740 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,27 @@ You can install the exporter from PyPI. Within a virtual environment, run: pip install postgres-connection-exporter ``` -## Usage +## Configuration -The exporter is configured using a `config.yaml`. You can find an example in [config.dist.yaml](config.dist.yaml). +The exporter is configured using a `config.yaml`. You can create a default configuration file in the current working directory with: + +```bash +postgres-connection-exporter --create-config +``` + +Now, edit the `config.yaml` file to match your PostgreSQL connection settings. Here is an example configuration: + +```yaml +hosts: + host: localhost + port: 5432 + user: postgres + password: postgres +``` + +The user must have the `pg_monitor` role to access the `pg_stat_activity` view. + +## Usage After you have created your `config.yaml`, you can start the exporter with: @@ -27,6 +45,18 @@ After you have created your `config.yaml`, you can start the exporter with: postgres-connection-exporter ``` +By default, the exporter listens on `localhost:8989`. You can change the address in the `config.yaml` file, or using the `--host` and `--port` flags: + +```bash +postgres-connection-exporter --host 0.0.0.0 --port 9898 +``` + +You can also specify a different configuration file with the `--config` flag: + +```bash +postgres-connection-exporter --config /path/to/config.yaml +``` + ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/config.dist.yaml b/config.dist.yaml deleted file mode 100644 index e166c40..0000000 --- a/config.dist.yaml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 27a97a3..ebf60f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "postgres-connection-exporter" -version = "0.0.1" +version = "0.0.2" authors = [ { name="Kumi Mitterer", email="postgres-connection-exporter@kumi.email" }, ] diff --git a/src/postgres_connection_exporter/__main__.py b/src/postgres_connection_exporter/__main__.py index f83b53f..557104e 100644 --- a/src/postgres_connection_exporter/__main__.py +++ b/src/postgres_connection_exporter/__main__.py @@ -2,6 +2,10 @@ import yaml from prometheus_client import start_http_server, Gauge import psycopg2 import time +import argparse +import pathlib +import shutil +import sys CONNECTIONS_PER_DB = Gauge( "db_connections_per_database", @@ -56,6 +60,8 @@ def get_all_databases(db_config): def get_db_connections(db_config): try: + host_identifier = db_config.get("name") or db_config["host"] + # Connect to the PostgreSQL database conn = psycopg2.connect( dbname="postgres", @@ -76,7 +82,7 @@ def get_db_connections(db_config): ) db_connections = cursor.fetchall() for db, count in db_connections: - CONNECTIONS_PER_DB.labels(database=db, host=db_config["host"]).set(count) + CONNECTIONS_PER_DB.labels(database=db, host=host_identifier).set(count) # Query to get the number of connections per user cursor.execute( @@ -88,7 +94,7 @@ def get_db_connections(db_config): ) user_connections = cursor.fetchall() for user, count in user_connections: - CONNECTIONS_PER_USER.labels(user=user, host=db_config["host"]).set(count) + CONNECTIONS_PER_USER.labels(user=user, host=host_identifier).set(count) # Query to get the number of connections by state cursor.execute( @@ -100,7 +106,7 @@ def get_db_connections(db_config): ) state_connections = cursor.fetchall() for state, count in state_connections: - CONNECTIONS_BY_STATE.labels(state=state, host=db_config["host"]).set(count) + CONNECTIONS_BY_STATE.labels(state=state, host=host_identifier).set(count) # Query to get the number of connections per source cursor.execute( @@ -112,7 +118,9 @@ def get_db_connections(db_config): ) source_connections = cursor.fetchall() for source, count in source_connections: - CONNECTIONS_PER_SOURCE.labels(source=source, host=db_config["host"]).set(count) + CONNECTIONS_PER_SOURCE.labels(source=source, host=host_identifier).set( + count + ) cursor.close() conn.close() @@ -121,11 +129,45 @@ def get_db_connections(db_config): def main(): - config = load_config() + parser = argparse.ArgumentParser(description="PostgreSQL connection exporter") + parser.add_argument( + "--config", "-c", help="Path to the configuration file", default="config.yaml" + ) + parser.add_argument( + "--create", "-C", help="Create a new configuration file", action="store_true" + ) + parser.add_argument( + "--port", + "-p", + help="Port for the exporter to listen on (default: 8989, or the port specified in the configuration file)", + type=int, + ) + parser.add_argument( + "--host", + help="Host for the exporter to listen on (default: localhost, or the host specified in the configuration file)", + ) + args = parser.parse_args() + + if args.create: + config_file = pathlib.Path(args.config) + if config_file.exists(): + print("Configuration file already exists.") + sys.exit(1) + + template = pathlib.Path(__file__).parent / "config.dist.yaml" + try: + shutil.copy(template, config_file) + print(f"Configuration file created at {config_file}") + sys.exit(0) + except Exception as e: + print(f"Error creating configuration file: {e}") + sys.exit(1) + + config = load_config(args.config) if not ("hosts" in config and config["hosts"]): print("No database hosts specified in the configuration file.") - exit(1) + sys.exit(1) databases_to_query = [] @@ -135,6 +177,7 @@ def main(): exit(1) db_config = { + "name": host.get("name"), "user": host["user"], "password": host["password"], "host": host["host"], @@ -148,13 +191,27 @@ def main(): exit(1) exporter_port = ( - config["exporter"]["port"] - if "exporter" in config and "port" in config["exporter"] - else 8989 + args.port + if args.port + else ( + 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}") + exporter_host = ( + args.host + if args.host + else ( + config["exporter"]["host"] + if "exporter" in config and "host" in config["exporter"] + else "localhost" + ) + ) + + start_http_server(exporter_port, exporter_host) + print(f"Prometheus exporter started on {exporter_host}:{exporter_port}") while True: for db in databases_to_query: diff --git a/src/postgres_connection_exporter/config.dist.yaml b/src/postgres_connection_exporter/config.dist.yaml new file mode 100644 index 0000000..631295f --- /dev/null +++ b/src/postgres_connection_exporter/config.dist.yaml @@ -0,0 +1,16 @@ +hosts: # List of database hosts + # Each host must have a user, password, host, and optionally port + # A name can be provided to identify the host in the metrics instead of the host address + - name: "myserver" + user: "user1" + password: "password1" + host: "host1" + port: "5432" + - user: "user2" + password: "password2" + host: "host2" + port: "5432" + # As no name is provided, the host address "host2" will be used to identify the host in the metrics +exporter: + host: "localhost" # Network address on which the exporter will listen + port: 8989 # Port on which the exporter will listen