Docker now supported

This commit is contained in:
Florian Rupp 2023-04-10 16:46:44 +02:00
parent fd49806b9d
commit d3eaaf3b9f
4 changed files with 115 additions and 53 deletions

View file

@ -7,8 +7,8 @@ COPY requirements.txt ./
RUN pip3 install -r requirements.txt
# Set this to the port you chose in the prometheus-ssh-exporter.py file
# Set this to the port you want to expose
EXPOSE 9999
CMD ["python", "./prometheus-ssh-exporter.py"]
# Set the -p option to the port you exposed above, defaults to 9999
CMD ["python", "-u", "./prometheus-ssh-exporter.py","-p", "9999"]

View file

@ -3,4 +3,60 @@ A Prometheus exporter for monitoring SSH connections
This is a personal project I wrote because I couldn't find any prometheus exporters that I could monitor my SSH connections with.
Note: The docker file is only for development. This program is not meant to be used in a docker container.
## Installation
The recommended way is to use docker. The image is available on docker hub **flor0/prometheus-ssh-exporter**
### Docker
The container can be run using the docker command
`docker run -d -p <external port>:9999 -v /run/utmp:/run/utmp flor0/prometheus-ssh-exporter`
Simply change the \<external port\> to whatever port you want the server to listen to. The default listening port is 9999.
### Docker compose
Here is an example docker-compose file.
Change the \<external port\> to what you want the server to listen to.
```
version: "3"
services:
prometheus-ssh-exporter:
container_name: prometheus-ssh-exporter
image: flor0/prometheus-ssh-exporter
ports:
- <external port>:9999
volumes:
- /run/utmp:/run/utmp
restart: unless-stopped
```
### As a python script
Alternatively, you can simply run the prometheus-ssh-exporter.py file with python3.
The command line arguments are explained if you use `python3 ./prometheus-ssh-exporter.py -h`
Make sure you set the right external port using the -p or --port argument.
## Configuring Prometheus
To have prometheus collect our new metrics, we need to add our server to the prometheus.yml file.
To do that open the /etc/prometheus/prometheus.yml file in an editor and add the lines
```
- job_name: ssh
static_configs:
- targets: ['localhost:<external port>']
```
where you replace again the \<external port\> with the same port you used in the previous steps. Make sure it's indented correctly!
## Usage
You can go to your prometheus dashboard in the web browser and query ssh_num_sessions.
If everything is set up correctly you should get the metrics.
### Grafana
I have published a grafana dashboard that can be found at https://snapshots.raintank.io/dashboard/snapshot/bZSxJ2Ig9EkPbl0Ozt5lS9SYADtSlvIY

View file

@ -1,26 +1,27 @@
import prometheus_client
import subprocess
import time
import argparse
import utmp
# These defaults can be overwritten by command line arguments
SERVER_HOST = '0.0.0.0'
# Set this port to whatever you want this service to bind to
SERVER_PORT = 9999
FETCH_INTERVAL = 5
class Session:
"""
This class is used to create a Session object containing info on an SSH session
"""
""" This class is used to create a Session object containing info on an SSH session, mainly for readability
Only the fields name, tty, from_, login are actually used for now """
def __init__(self, name, tty, from_, login, idle, jcpu, pcpu, what):
self.name = name
self.tty = tty
self.from_ = from_
self.login = login
self.idle = idle
self.jcpu = jcpu
self.pcpu = pcpu
self.what = what
self.name = name # Username that is logged in
self.tty = tty # Which tty is used
self.from_ = from_ # remote IP address
self.login = login # time of login
self.idle = idle # unused
self.jcpu = jcpu # unused
self.pcpu = pcpu # unused
self.what = what # unused
def __str__(self):
return "%s %s" % (self.name, self.from_)
@ -32,6 +33,7 @@ class Session:
return self.login == other.login and self.tty == other.tty and self.from_ == other.from_
def to_dict(self):
# maybe this will be used later
return {
'name': self.name,
'tty': self.tty,
@ -50,13 +52,13 @@ class Session:
def contains_user_list(user, other_user_list):
for other_user in other_user_list:
if are_equal(user, other_user):
#print("Found equals: %s and %s" % (user, other_user))
return True
#print("Not found: %s in %s" % (user, other_user_list))
return False
def are_equal(user_list, other_user_list):
""" Two SSh sessions are equal if their name, tty, remote IP and login time are equal
The other fields change over time hence they are not used for comparison """
assert len(user_list) == len(other_user_list)
for i in range(4):
if user_list[i] != other_user_list[i]:
@ -64,47 +66,48 @@ def are_equal(user_list, other_user_list):
return True
"""
Returns a list of Session objects
The string generated by the "w -h" command looks like follows:
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
<username> pts/0 192.168.0.X 14:57 1:54m 0.12s 0.12s -bash
"""
def get_w_data():
# Retrieve command output and discard the header
w_data = subprocess.check_output(['w', '-h']).decode('utf-8')
w_data = w_data.split('\n')[:-1]
def get_utmp_data():
"""
Returns a list of User Objects
The function uses the utmp library. The utmp file contains information about currently logged in users
"""
users = []
# Generate a list of Session objects from the output of the command
for user_string in w_data:
user_string_cleaned = user_string.split()
if len(user_string_cleaned) == 8:
users.append(Session(*user_string_cleaned))
with open('/var/run/utmp', 'rb') as f:
buffer = f.read()
for record in utmp.read(buffer):
if record.type == utmp.UTmpRecordType.user_process:
users.append(Session(record.user, record.line, record.host, record.sec, 0, 0, 0, 0))
return users
def parse_arguments():
global FETCH_INTERVAL, SERVER_PORT, SERVER_HOST
parser = argparse.ArgumentParser(
prog='python prometheus-ssh-exporter.py',
description='Prometheus exporter for info about SSH sessions')
parser.add_argument('-H', '--host', type=str, default='0.0.0.0', help='Hostname to bind to')
parser.add_argument('-p', '--port', type=int, default=9999, help='Port for the server to listen to')
parser.add_argument('-i', '--interval', type=int, default=15, help='Interval in seconds to fetch SSH sessions data')
prog='python prometheus-ssh-exporter.py',
description='Prometheus exporter for info about SSH sessions')
parser.add_argument('-H', '--host', type=str,
default='0.0.0.0', help='Hostname to bind to')
parser.add_argument('-p', '--port', type=int, default=9999,
help='Port for the server to listen to')
parser.add_argument('-i', '--interval', type=int, default=15,
help='Interval in seconds to fetch SSH sessions data')
args = parser.parse_args()
FETCH_INTERVAL = args.interval
SERVER_PORT = args.port
SERVER_HOST = args.host
"""
This program exports the number of SSH sessions as a metric "ssh_num_sessions" for prometheus.
It applies a label to each increment or decrement of that number, containing the remote IP address.
That way we can filter by the remote IP in Grafana, getting the number of SSH sessions by IP address,
or sum them up to get the total number of sessions.
"""
if __name__ == '__main__':
"""
This program exports the number of SSH sessions as a metric "ssh_num_sessions" for prometheus.
It applies a label to each increment or decrement of that number, containing the remote IP address.
That way we can filter by the remote IP in Grafana, getting the number of SSH sessions by IP address,
or sum them up to get the total number of sessions.
"""
parse_arguments()
@ -112,8 +115,10 @@ if __name__ == '__main__':
prometheus_client.start_http_server(SERVER_PORT)
print("Started metrics server bound to {}:{}".format(SERVER_HOST, SERVER_PORT))
num_sessions = []
gauge_num_sessions = prometheus_client.Gauge('ssh_num_sessions', 'Number of SSH sessions', ['remote_ip'])
data = get_w_data()
gauge_num_sessions = prometheus_client.Gauge(
'ssh_num_sessions', 'Number of SSH sessions', ['remote_ip'])
# data = get_w_data()
data = get_utmp_data()
list_data = [user.to_list() for user in data]
# Initial metrics
@ -127,7 +132,8 @@ if __name__ == '__main__':
while True:
list_old_data = list_data
data = get_w_data()
# data = get_w_data()
data = get_utmp_data()
list_data = [user.to_list() for user in data]
num_sessions = len(data)
@ -135,13 +141,12 @@ if __name__ == '__main__':
# Looking for newly found SSH sessions
if not contains_user_list(list_data[i], list_old_data):
print("Session connected: %s" % list_data[i])
gauge_num_sessions.labels(remote_ip=list_data[i][2]).dec()
gauge_num_sessions.labels(remote_ip=list_data[i][2]).inc()
for i in range(len(list_old_data)):
# Looking for SSH sessions that no longer exist
if not contains_user_list(list_old_data[i], list_data):
print("Session disconnected: %s" % list_old_data[i])
gauge_num_sessions.labels(remote_ip=list_old_data[i][2]).inc()
gauge_num_sessions.labels(remote_ip=list_old_data[i][2]).dec()
time.sleep(FETCH_INTERVAL)

View file

@ -1 +1,2 @@
prometheus_client==0.16.0
utmp==21.10.0