Docker now supported
This commit is contained in:
parent
fd49806b9d
commit
d3eaaf3b9f
4 changed files with 115 additions and 53 deletions
|
@ -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"]
|
58
README.md
58
README.md
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
prometheus_client==0.16.0
|
||||
utmp==21.10.0
|
Loading…
Reference in a new issue