Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
13 changes: 0 additions & 13 deletions collector/Dockerfile

This file was deleted.

5 changes: 0 additions & 5 deletions collector/requirements.txt

This file was deleted.

152 changes: 0 additions & 152 deletions collector/server.py

This file was deleted.

8 changes: 1 addition & 7 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,4 @@ services:
- --development
- --port=14000
- --dont-delete-pdfs
snmp-collector:
build:
context: .
dockerfile: ./collector/Dockerfile
ports:
- 5000:5000

- --config-json-path=/app/config/config.json
5 changes: 1 addition & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ services:
- ~/.ssh/known_hosts:/app/known_hosts
- "/etc/cups/ppd/:/etc/cups/ppd"
tty: true
snmp-collector:
build:
context: .
dockerfile: ./collector/Dockerfile

# we attach the print container to an external docker
# network called "poweredge". we do this so a prometheus
# container can pull metrics from the server over HTTP
Expand Down
8 changes: 4 additions & 4 deletions printer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
# Base image from https://github.com/DrPsychick/docker-cups-airprint
# Docker images are here https://hub.docker.com/r/drpsychick/airprint-bridge/tags
FROM drpsychick/airprint-bridge:latest
FROM drpsychick/airprint-bridge:jammy

WORKDIR /app

RUN apt-get update

RUN apt install -y python3 python3-pip python3-venv jq ssh

# Create a virtual environment
# Create the virtual environment with Python
RUN python3 -m venv /opt/venv

# Set the virtual environment as the default Python environment
ENV PATH="/opt/venv/bin:$PATH"

COPY ./printer/requirements.txt /app/printer/requirements.txt
COPY ./printer/requirements.txt /app/printer/requirements.txt

RUN /opt/venv/bin/pip install -r /app/printer/requirements.txt

Expand All @@ -29,3 +28,4 @@ EXPOSE 9000
# The below command runs the bash script that sets up the connection to the
# printers and runs PrintHandler.js
ENTRYPOINT [ "./printer/what.sh" ]

106 changes: 106 additions & 0 deletions printer/collector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import time
import enum
import logging
import json
import asyncio

from pysnmp.hlapi import *

from metrics import MetricsHandler

metrics_handler = MetricsHandler.instance()


logging.basicConfig(
# in mondo we trust
format="%(asctime)s.%(msecs)03dZ %(levelname)s:%(name)s:%(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
level=logging.INFO,
)


class SnmpOid(enum.Enum):
INK_LEVEL = ("ink_level", "1.3.6.1.2.1.43.11.1.1.9.1.1")
INK_CAPACITY = ("ink_capacity", "1.3.6.1.2.1.43.11.1.1.8.1.1")
PAGE_COUNT = ("page_count", "1.3.6.1.2.1.43.10.2.1.4.1.1")
TRAY_EMPTY = ("tray_empty", "1.3.6.1.2.1.43.18.1.1.8.1.13", True)
# we observed each printer emitting a different SNMP OID for
# an empty paper tray, the below accounts for this second OID.
# the _2 at the end of this metric does imply that the printer has
# 2 trays. tray_empty_2 is an indication of the same exact issue
# as tray_empty: an empty paper tray.
TRAY_EMPTY_2 = ("tray_empty_2", "1.3.6.1.2.1.43.18.1.1.8.1.2", True)

def __init__(self, metric_name, metric_value, is_error=False):
self.metric_name = metric_name
self.metric_value = metric_value
self.is_error = is_error


def fetch_ips_from_config(config_file_path):
try:
with open(config_file_path, "r") as f:
config = json.load(f)
printer_configs = config.get("PRINTING")
if not printer_configs:
raise Exception("No printers defined in config file")

ip_list = []
for printer in printer_configs:
if isinstance(printer_configs[printer], dict):
ip = printer_configs[printer]["IP"]
logging.info(f"Adding printer {printer} with IP {ip}")
ip_list.append(ip)
return ip_list

except Exception as e:
logging.error(f"error opening config file: {e}")


def scrape_snmp(ip_list, sleep_duration_minutes=5):
while True:
for ip in ip_list:
get_snmp_data(ip)
time.sleep(sleep_duration_minutes * 60)


def get_snmp_data(ip):
for oid in SnmpOid:
with metrics_handler.snmp_request_duration.time():
errorIndication, errorStatus, errorIndex, varBinds = next(
getCmd(
SnmpEngine(),
CommunityData("public", mpModel=0),
UdpTransportTarget((ip, 161)),
ContextData(),
ObjectType(ObjectIdentity(oid.metric_value)),
)
)
if errorIndication:
logging.error(
f"Error indication from {ip} for metric {oid.metric_value}: {errorIndication}"
)
metrics_handler.device_unreachable.set(1)
continue
if errorStatus:
logging.error(
f"Error status from {ip} for metric {oid.metric_value}: {errorStatus.prettyPrint()}"
)
# SNMP OIDs related to errors often dissappear when
# the associated issue that the metric refers to is
# no longer present (i.e. an empty tray now has
# paper). To avoid leaving an error metric as 1
# which would create a false positive, set the metric
# to zero if the associated SNMP OID was not found
if oid.is_error:
metrics_handler.snmp_error.labels(name=oid.metric_name, ip=ip).set(0)
continue

metrics_handler.device_unreachable.set(0)
if not varBinds:
continue
res = varBinds[0]
if oid.is_error:
metrics_handler.snmp_error.labels(name=oid.metric_name, ip=ip).set(1)
continue
metrics_handler.snmp_metric.labels(name=oid.metric_name, ip=ip).set(res[1])
29 changes: 27 additions & 2 deletions printer/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ class Metrics(enum.Enum):
"total bytes of files pointed to by cache",
prometheus_client.Gauge,
)
SNMP_METRIC = (
"snmp_metric",
"ex: Number of pages printed",
prometheus_client.Gauge,
["name", "ip"],
)

SNMP_ERROR = (
"snmp_error",
"Error metrics",
prometheus_client.Gauge,
["name", "ip"],
)

SNMP_REQ_DURATION = (
"snmp_request_duration",
"Time it took for SNMP request",
prometheus_client.Summary,
)

DEVICE_UNREACHABLE = (
"device_unreachable",
"set to 1 when error",
prometheus_client.Gauge,
)

def __init__(self, title, description, prometheus_type, labels=()):
# we use the above default value for labels because it matches what's used
Expand All @@ -38,8 +63,8 @@ class MetricsHandler:
_instance = None

def __init__(self):
raise RuntimeError('Call MetricsHandler.instance() instead')
raise RuntimeError("Call MetricsHandler.instance() instead")

def init(self) -> None:
for metric in Metrics:
setattr(
Expand Down
2 changes: 2 additions & 0 deletions printer/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ py-grpc-prometheus==0.7.0
python-multipart==0.0.9
httpx==0.28.1
requests==2.32.3
pysnmp==4.4.12
pyasn1==0.4.8
Loading