mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
prometheus metrics endpoint
This commit is contained in:
96
server/api_server/api_server_start.py
Executable file
96
server/api_server/api_server_start.py
Executable file
@@ -0,0 +1,96 @@
|
||||
import threading
|
||||
from flask import Flask, request, jsonify, Response
|
||||
from flask_cors import CORS
|
||||
from .graphql_schema import devicesSchema
|
||||
from .prometheus_metrics import getMetricStats
|
||||
from graphene import Schema
|
||||
import sys
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH = "/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
from logger import mylog
|
||||
from helper import get_setting_value, timeNowTZ
|
||||
from app_state import updateState
|
||||
from messaging.in_app import write_notification
|
||||
|
||||
# Flask application
|
||||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/metrics": {"origins": "*"}}, supports_credentials=True, allow_headers=["Authorization"])
|
||||
|
||||
# --------------------------
|
||||
# GraphQL Endpoints
|
||||
# --------------------------
|
||||
|
||||
# Endpoint used when accessed via browser
|
||||
@app.route("/graphql", methods=["GET"])
|
||||
def graphql_debug():
|
||||
# Handles GET requests
|
||||
return "NetAlertX GraphQL server running."
|
||||
|
||||
# Endpoint for GraphQL queries
|
||||
@app.route("/graphql", methods=["POST"])
|
||||
def graphql_endpoint():
|
||||
# Check for API token in headers
|
||||
if not is_authorized():
|
||||
msg = '[graphql_server] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
|
||||
mylog('verbose', [msg])
|
||||
return jsonify({"error": msg}), 401
|
||||
|
||||
# Retrieve and log request data
|
||||
data = request.get_json()
|
||||
mylog('verbose', [f'[graphql_server] data: {data}'])
|
||||
|
||||
# Execute the GraphQL query
|
||||
result = devicesSchema.execute(data.get("query"), variables=data.get("variables"))
|
||||
|
||||
# Return the result as JSON
|
||||
return jsonify(result.data)
|
||||
|
||||
# --------------------------
|
||||
# Prometheus /metrics Endpoint
|
||||
# --------------------------
|
||||
|
||||
@app.route("/metrics")
|
||||
def metrics():
|
||||
|
||||
# Check for API token in headers
|
||||
if not is_authorized():
|
||||
msg = '[metrics] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
|
||||
mylog('verbose', [msg])
|
||||
return jsonify({"error": msg}), 401
|
||||
|
||||
|
||||
# Return Prometheus metrics as plain text
|
||||
return Response(getMetricStats(), mimetype="text/plain")
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Background Server Start
|
||||
# --------------------------
|
||||
def is_authorized():
|
||||
token = request.headers.get("Authorization")
|
||||
return token == f"Bearer {get_setting_value('API_TOKEN')}"
|
||||
|
||||
|
||||
def start_server(graphql_port, app_state):
|
||||
"""Start the GraphQL server in a background thread."""
|
||||
|
||||
if app_state.graphQLServerStarted == 0:
|
||||
|
||||
mylog('verbose', [f'[graphql_server] Starting on port: {graphql_port}'])
|
||||
|
||||
# Start Flask app in a separate thread
|
||||
thread = threading.Thread(
|
||||
target=lambda: app.run(
|
||||
host="0.0.0.0",
|
||||
port=graphql_port,
|
||||
debug=True,
|
||||
use_reloader=False
|
||||
)
|
||||
)
|
||||
thread.start()
|
||||
|
||||
# Update the state to indicate the server has started
|
||||
app_state = updateState("Process: Idle", None, None, None, 1)
|
||||
270
server/api_server/graphql_schema.py
Executable file
270
server/api_server/graphql_schema.py
Executable file
@@ -0,0 +1,270 @@
|
||||
import graphene
|
||||
from graphene import ObjectType, String, Int, Boolean, List, Field, InputObjectType
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH="/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
from logger import mylog
|
||||
from const import apiPath
|
||||
from helper import is_random_mac, get_number_of_children, format_ip_long, get_setting_value
|
||||
|
||||
# Define a base URL with the user's home directory
|
||||
folder = apiPath
|
||||
|
||||
# --- DEVICES ---
|
||||
# Pagination and Sorting Input Types
|
||||
class SortOptionsInput(InputObjectType):
|
||||
field = String()
|
||||
order = String()
|
||||
|
||||
class FilterOptionsInput(InputObjectType):
|
||||
filterColumn = String()
|
||||
filterValue = String()
|
||||
|
||||
|
||||
class PageQueryOptionsInput(InputObjectType):
|
||||
page = Int()
|
||||
limit = Int()
|
||||
sort = List(SortOptionsInput)
|
||||
search = String()
|
||||
status = String()
|
||||
filters = List(FilterOptionsInput)
|
||||
|
||||
|
||||
# Device ObjectType
|
||||
class Device(ObjectType):
|
||||
rowid = Int()
|
||||
devMac = String()
|
||||
devName = String()
|
||||
devOwner = String()
|
||||
devType = String()
|
||||
devVendor = String()
|
||||
devFavorite = Int()
|
||||
devGroup = String()
|
||||
devComments = String()
|
||||
devFirstConnection = String()
|
||||
devLastConnection = String()
|
||||
devLastIP = String()
|
||||
devStaticIP = Int()
|
||||
devScan = Int()
|
||||
devLogEvents = Int()
|
||||
devAlertEvents = Int()
|
||||
devAlertDown = Int()
|
||||
devSkipRepeated = Int()
|
||||
devLastNotification = String()
|
||||
devPresentLastScan = Int()
|
||||
devIsNew = Int()
|
||||
devLocation = String()
|
||||
devIsArchived = Int()
|
||||
devParentMAC = String()
|
||||
devParentPort = String()
|
||||
devIcon = String()
|
||||
devGUID = String()
|
||||
devSite = String()
|
||||
devSSID = String()
|
||||
devSyncHubNode = String()
|
||||
devSourcePlugin = String()
|
||||
devCustomProps = String()
|
||||
devStatus = String()
|
||||
devIsRandomMac = Int()
|
||||
devParentChildrenCount = Int()
|
||||
devIpLong = Int()
|
||||
devFilterStatus = String()
|
||||
devFQDN = String()
|
||||
devParentRelType = String()
|
||||
devReqNicsOnline = Int()
|
||||
|
||||
|
||||
class DeviceResult(ObjectType):
|
||||
devices = List(Device)
|
||||
count = Int()
|
||||
|
||||
|
||||
# --- SETTINGS ---
|
||||
|
||||
# Setting ObjectType
|
||||
class Setting(ObjectType):
|
||||
setKey = String()
|
||||
setName = String()
|
||||
setDescription = String()
|
||||
setType = String()
|
||||
setOptions = String()
|
||||
setGroup = String()
|
||||
setValue = String()
|
||||
setEvents = String()
|
||||
setOverriddenByEnv = Boolean()
|
||||
|
||||
|
||||
class SettingResult(ObjectType):
|
||||
settings = List(Setting)
|
||||
count = Int()
|
||||
|
||||
# Define Query Type with Pagination Support
|
||||
class Query(ObjectType):
|
||||
|
||||
# --- DEVICES ---
|
||||
devices = Field(DeviceResult, options=PageQueryOptionsInput())
|
||||
|
||||
def resolve_devices(self, info, options=None):
|
||||
# mylog('none', f'[graphql_schema] resolve_devices: {self}')
|
||||
try:
|
||||
with open(folder + 'table_devices.json', 'r') as f:
|
||||
devices_data = json.load(f)["data"]
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
mylog('none', f'[graphql_schema] Error loading devices data: {e}')
|
||||
return DeviceResult(devices=[], count=0)
|
||||
|
||||
|
||||
# Add dynamic fields to each device
|
||||
for device in devices_data:
|
||||
device["devIsRandomMac"] = 1 if is_random_mac(device["devMac"]) else 0
|
||||
device["devParentChildrenCount"] = get_number_of_children(device["devMac"], devices_data)
|
||||
device["devIpLong"] = format_ip_long(device.get("devLastIP", ""))
|
||||
|
||||
mylog('verbose', f'[graphql_schema] devices_data: {devices_data}')
|
||||
|
||||
|
||||
# Apply sorting if options are provided
|
||||
if options:
|
||||
|
||||
# Define status-specific filtering
|
||||
if options.status:
|
||||
status = options.status
|
||||
mylog('verbose', f'[graphql_schema] Applying status filter: {status}')
|
||||
|
||||
# Include devices matching criteria in UI_MY_DEVICES
|
||||
allowed_statuses = get_setting_value("UI_MY_DEVICES")
|
||||
hidden_relationships = get_setting_value("UI_hide_rel_types")
|
||||
network_dev_types = get_setting_value("NETWORK_DEVICE_TYPES")
|
||||
|
||||
mylog('verbose', f'[graphql_schema] allowed_statuses: {allowed_statuses}')
|
||||
mylog('verbose', f'[graphql_schema] hidden_relationships: {hidden_relationships}')
|
||||
mylog('verbose', f'[graphql_schema] network_dev_types: {network_dev_types}')
|
||||
|
||||
# Filtering based on the "status"
|
||||
if status == "my_devices":
|
||||
|
||||
devices_data = [
|
||||
device for device in devices_data
|
||||
if ( device.get("devParentRelType") not in hidden_relationships)
|
||||
]
|
||||
|
||||
devices_data = [
|
||||
device for device in devices_data
|
||||
if (
|
||||
(device["devPresentLastScan"] == 1 and 'online' in allowed_statuses) or
|
||||
(device["devIsNew"] == 1 and 'new' in allowed_statuses) or
|
||||
(device["devPresentLastScan"] == 0 and device["devAlertDown"] and 'down' in allowed_statuses) or
|
||||
(device["devPresentLastScan"] == 0 and 'offline' in allowed_statuses) and device["devIsArchived"] == 0 or
|
||||
(device["devIsArchived"] == 1 and 'archived' in allowed_statuses)
|
||||
)
|
||||
]
|
||||
elif status == "connected":
|
||||
devices_data = [device for device in devices_data if device["devPresentLastScan"] == 1]
|
||||
elif status == "favorites":
|
||||
devices_data = [device for device in devices_data if device["devFavorite"] == 1]
|
||||
elif status == "new":
|
||||
devices_data = [device for device in devices_data if device["devIsNew"] == 1]
|
||||
elif status == "down":
|
||||
devices_data = [
|
||||
device for device in devices_data
|
||||
if device["devPresentLastScan"] == 0 and device["devAlertDown"]
|
||||
]
|
||||
elif status == "archived":
|
||||
devices_data = [device for device in devices_data if device["devIsArchived"] == 1]
|
||||
elif status == "offline":
|
||||
devices_data = [device for device in devices_data if device["devPresentLastScan"] == 0]
|
||||
elif status == "network_devices":
|
||||
devices_data = [device for device in devices_data if device["devType"] in network_dev_types]
|
||||
elif status == "all_devices":
|
||||
devices_data = devices_data # keep all
|
||||
|
||||
# additional filters
|
||||
if options.filters:
|
||||
for filter in options.filters:
|
||||
if filter.filterColumn and filter.filterValue:
|
||||
devices_data = [
|
||||
device for device in devices_data
|
||||
if str(device.get(filter.filterColumn, "")).lower() == str(filter.filterValue).lower()
|
||||
]
|
||||
|
||||
# Search data if a search term is provided
|
||||
if options.search:
|
||||
# Define static list of searchable fields
|
||||
searchable_fields = [
|
||||
"devName", "devMac", "devOwner", "devType", "devVendor", "devLastIP",
|
||||
"devGroup", "devComments", "devLocation", "devStatus", "devSSID",
|
||||
"devSite", "devSourcePlugin", "devSyncHubNode", "devFQDN", "devParentRelType"
|
||||
]
|
||||
|
||||
search_term = options.search.lower()
|
||||
|
||||
devices_data = [
|
||||
device for device in devices_data
|
||||
if any(
|
||||
search_term in str(device.get(field, "")).lower()
|
||||
for field in searchable_fields # Search only predefined fields
|
||||
)
|
||||
]
|
||||
|
||||
# sorting
|
||||
if options.sort:
|
||||
for sort_option in options.sort:
|
||||
devices_data = sorted(
|
||||
devices_data,
|
||||
key=lambda x: mixed_type_sort_key(
|
||||
x.get(sort_option.field).lower() if isinstance(x.get(sort_option.field), str) else x.get(sort_option.field)
|
||||
),
|
||||
reverse=(sort_option.order.lower() == "desc")
|
||||
)
|
||||
|
||||
# capture total count after all the filtering and searching
|
||||
total_count = len(devices_data)
|
||||
|
||||
# Then apply pagination
|
||||
if options.page and options.limit:
|
||||
start = (options.page - 1) * options.limit
|
||||
end = start + options.limit
|
||||
devices_data = devices_data[start:end]
|
||||
|
||||
# Convert dict objects to Device instances to enable field resolution
|
||||
devices = [Device(**device) for device in devices_data]
|
||||
|
||||
return DeviceResult(devices=devices, count=total_count)
|
||||
|
||||
# --- SETTINGS ---
|
||||
settings = Field(SettingResult)
|
||||
|
||||
def resolve_settings(root, info):
|
||||
|
||||
try:
|
||||
with open(folder + 'table_settings.json', 'r') as f:
|
||||
settings_data = json.load(f)["data"]
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
mylog('none', f'[graphql_schema] Error loading settings data: {e}')
|
||||
return SettingResult(settings=[], count=0)
|
||||
|
||||
|
||||
mylog('verbose', f'[graphql_schema] settings_data: {settings_data}')
|
||||
|
||||
# Convert to Setting objects
|
||||
settings = [Setting(**setting) for setting in settings_data]
|
||||
|
||||
return SettingResult(settings=settings, count=len(settings))
|
||||
|
||||
|
||||
|
||||
# helps sorting inconsistent dataset mixed integers and strings
|
||||
def mixed_type_sort_key(value):
|
||||
if value is None or value == "":
|
||||
return (2, '') # Place None or empty strings last
|
||||
try:
|
||||
return (0, int(value)) # Integers get priority
|
||||
except (ValueError, TypeError):
|
||||
return (1, str(value)) # Strings come next
|
||||
|
||||
# Schema Definition
|
||||
devicesSchema = graphene.Schema(query=Query)
|
||||
76
server/api_server/prometheus_metrics.py
Executable file
76
server/api_server/prometheus_metrics.py
Executable file
@@ -0,0 +1,76 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH = "/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
from logger import mylog
|
||||
from const import apiPath
|
||||
from helper import is_random_mac, get_number_of_children, format_ip_long, get_setting_value
|
||||
|
||||
def escape_label_value(val):
|
||||
"""
|
||||
Escape special characters for Prometheus labels.
|
||||
"""
|
||||
return str(val).replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
|
||||
|
||||
# Define a base URL with the user's home directory
|
||||
folder = apiPath
|
||||
|
||||
def getMetricStats():
|
||||
output = []
|
||||
|
||||
# 1. Dashboard totals
|
||||
try:
|
||||
with open(folder + 'table_devices_tiles.json', 'r') as f:
|
||||
tiles_data = json.load(f)["data"]
|
||||
|
||||
if isinstance(tiles_data, list) and tiles_data:
|
||||
totals = tiles_data[0]
|
||||
output.append(f'netalertx_connected_devices {totals.get("connected", 0)}')
|
||||
output.append(f'netalertx_offline_devices {totals.get("offline", 0)}')
|
||||
output.append(f'netalertx_down_devices {totals.get("down", 0)}')
|
||||
output.append(f'netalertx_new_devices {totals.get("new", 0)}')
|
||||
output.append(f'netalertx_archived_devices {totals.get("archived", 0)}')
|
||||
output.append(f'netalertx_favorite_devices {totals.get("favorites", 0)}')
|
||||
output.append(f'netalertx_my_devices {totals.get("my_devices", 0)}')
|
||||
else:
|
||||
output.append("# Unexpected format in table_devices_tiles.json")
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
mylog('none', f'[metrics] Error loading tiles data: {e}')
|
||||
output.append(f"# Error loading tiles data: {e}")
|
||||
except Exception as e:
|
||||
output.append(f"# General error loading dashboard totals: {e}")
|
||||
|
||||
# 2. Device-level metrics
|
||||
try:
|
||||
with open(folder + 'table_devices.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
devices = data.get("data", [])
|
||||
|
||||
for row in devices:
|
||||
name = escape_label_value(row.get("devName", "unknown"))
|
||||
mac = escape_label_value(row.get("devMac", "unknown"))
|
||||
ip = escape_label_value(row.get("devLastIP", "unknown"))
|
||||
vendor = escape_label_value(row.get("devVendor", "unknown"))
|
||||
first_conn = escape_label_value(row.get("devFirstConnection", "unknown"))
|
||||
last_conn = escape_label_value(row.get("devLastConnection", "unknown"))
|
||||
dev_type = escape_label_value(row.get("devType", "unknown"))
|
||||
raw_status = row.get("devStatus", "Unknown")
|
||||
dev_status = raw_status.replace("-", "").capitalize()
|
||||
|
||||
output.append(
|
||||
f'netalertx_device_status{{device="{name}", mac="{mac}", ip="{ip}", vendor="{vendor}", '
|
||||
f'first_connection="{first_conn}", last_connection="{last_conn}", dev_type="{dev_type}", '
|
||||
f'device_status="{dev_status}"}} 1'
|
||||
)
|
||||
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
mylog('none', f'[metrics] Error loading devices data: {e}')
|
||||
output.append(f"# Error loading devices data: {e}")
|
||||
except Exception as e:
|
||||
output.append(f"# General error processing device metrics: {e}")
|
||||
|
||||
return "\n".join(output) + "\n"
|
||||
Reference in New Issue
Block a user