Skip to content

Added feature: exposing prometheus metrics - #153

Open
ahpooch wants to merge 1 commit into
Py-KMS-Organization:mainfrom
ahpooch:feature/prometheus-metrics
Open

Added feature: exposing prometheus metrics#153
ahpooch wants to merge 1 commit into
Py-KMS-Organization:mainfrom
ahpooch:feature/prometheus-metrics

Conversation

@ahpooch

@ahpooch ahpooch commented Jul 3, 2026

Copy link
Copy Markdown

This PR provides a solution to #152
It is a tested and working as expected code though written with AI support.
Based on the changes provided in this PR, I was able to build custom Docker image that exposes Prometheus metrics as proposed. After that I managed to create Grafana Dashboard for monitoring py-kms container metrics, and it looks solid (after 26 iterations of building image while working on this solution using AI and my humble programming skills).

The last image build from PR code is available here if somebody wants to test it:
https://hub.docker.com/repository/docker/ahpooch/py-kms/tags/prometheus-metrics26/sha256-f0b8449bdab21fb6491b5e5f3d0b209d96f9e09ba1b7bcb9004476b64193e259

Or you could build the Docker image yourself from the code of this PR after reading the changes it proproses to project.

@simonmicro
simonmicro requested review from simonmicro and a lite review from Copilot August 7, 2026 12:47
@simonmicro simonmicro self-assigned this Aug 7, 2026
@simonmicro simonmicro added the enhancement New feature or request label Aug 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Prometheus-format metrics export to the py-kms python3 Docker image by integrating prometheus_client into the existing Flask WebUI and recording activation/database statistics.

Changes:

  • Add /metrics endpoint to the Flask WebUI and initialize service-level metrics.
  • Introduce a new pykms_Metrics module defining Prometheus counters/gauges/histograms and DB-derived metrics.
  • Wire activation request recording into the KMS request handling flow; add Gunicorn multiprocess support config and Docker startup/requirements updates.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
py-kms/pykms_WebUI.py Adds Prometheus initialization and the /metrics HTTP endpoint in the WebUI Flask app.
py-kms/pykms_Metrics.py New module implementing metric definitions, collection output, and DB/windowed helpers.
py-kms/pykms_Base.py Records per-activation metrics from the KMS request handling logic.
py-kms/gunicorn_config.py Adds Gunicorn hooks for Prometheus multiprocess lifecycle handling/cleanup.
docker/start.py Sets PROMETHEUS_MULTIPROC_DIR and starts Gunicorn using the new config.
docker/docker-py3-kms/requirements.txt Adds prometheus-client dependency for the python3 image.
docs/Metrics.md Documents configuration and available Prometheus metrics.
examples/grafana-dashboard.json Provides an example Grafana dashboard for the exported metrics.
Suppressed comments (4)

py-kms/pykms_Base.py:297

  • Same issue in the exception path: reading _metrics_* from the shared srv_config can race with other threads. Use getattr(messagehandler, ...) so failures are attributed to the correct request.
		app_name = srv_config.get('_metrics_app_name', 'unknown')
		sku_name = srv_config.get('_metrics_sku_name', 'unknown')

py-kms/pykms_Base.py:282

  • These values are read back from srv_config, which is shared across threads; this can pick up another request's _metrics_* values. If you store the names on messagehandler (per-request instance), read them via getattr(messagehandler, ...) here to avoid cross-thread contamination.
		# Get product and SKU from parsed data (set by serverLogic)
		app_name = srv_config.get('_metrics_app_name', 'unknown')
		sku_name = srv_config.get('_metrics_sku_name', 'unknown')
		

py-kms/pykms_Metrics.py:360

  • This INFO log runs for every activation request, which can flood logs under normal traffic. It should be DEBUG-level (or removed) to avoid operational noise and I/O overhead.
    loggersrv.info(f"Recording activation metric: status={status}, product={product}, kms_version={kms_version}, sku={sku}, duration={duration}")

py-kms/pykms_Metrics.py:395

  • This INFO log runs after every activation request and will be very noisy in practice. Prefer DEBUG-level logging here to keep normal logs usable.
        loggersrv.info(f"Successfully recorded activation metric")

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread py-kms/pykms_Base.py
Comment on lines +220 to +222
# Store app and SKU names for metrics (temporary storage)
self.srv_config['_metrics_app_name'] = appName
self.srv_config['_metrics_sku_name'] = skuName

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, as long Python had not gotten its global locking problem fixed, I do not see this one as bad - but should be redone anyways as it does feel a bit wrong

Comment thread py-kms/pykms_Base.py
from pykms_Filetimes import filetime_to_dt
from pykms_Sql import sql_update, sql_update_epid
from pykms_Format import justify, byterize, enco, deco, pretty_printer
from pykms_Metrics import record_activation_request, get_product_type, get_kms_version, get_sku_label

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 - damn, I was careful with the other imports and just overlooked this one as well

Comment thread py-kms/pykms_Base.py
Comment on lines 255 to +261
def generateKmsResponseData(data, srv_config):
version = kmsBase.GenericRequestHeader(data)['versionMajor']
currentDate = time.strftime("%a %b %d %H:%M:%S %Y")
version = kmsBase.GenericRequestHeader(data)['versionMajor']
currentDate = time.strftime("%a %b %d %H:%M:%S %Y")

if version == 4:
loggersrv.info("Received V%d request on %s." % (version, currentDate))
messagehandler = pykms_RequestV4.kmsRequestV4(data, srv_config)
elif version == 5:
loggersrv.info("Received V%d request on %s." % (version, currentDate))
messagehandler = pykms_RequestV5.kmsRequestV5(data, srv_config)
elif version == 6:
loggersrv.info("Received V%d request on %s." % (version, currentDate))
messagehandler = pykms_RequestV6.kmsRequestV6(data, srv_config)
else:
loggersrv.info("Unhandled KMS version V%d." % version)
messagehandler = pykms_RequestUnknown.kmsRequestUnknown(data, srv_config)

return messagehandler.executeRequestLogic()
if version == 4:
loggersrv.info("Received V%d request on %s." % (version, currentDate))
messagehandler = pykms_RequestV4.kmsRequestV4(data, srv_config)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Comment thread py-kms/pykms_WebUI.py
Comment on lines +169 to +173
try:
clients = sql_get_all(dbPath)
pykms_Metrics.update_database_metrics(dbPath, clients)
except Exception as e:
pass # Ignore errors when updating database metrics
Comment thread py-kms/pykms_WebUI.py
Comment on lines +180 to +181
except Exception as e:
return f'Error generating metrics: {e}', 500

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 Hehe, exactly my point.

Comment thread py-kms/pykms_Metrics.py
Comment on lines +385 to +389
# Add event to shared shelve storage (for time-based metrics)
try:
with shelve.open(_EVENTS_DB_PATH) as db:
events = db.get('events', [])
events.append((time.time(), status, product))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm...

LLM#1: Shared storage for activation events using shelve (cross-process safe)
LLM#2: shelve/dbm files are not safe for concurrent writes across threads/processes

Fight!

No, for real this should be addressed. I have not used shelve before on my own, but I also suspected as such and got silenced by the comment I read. There are some mechanisms in Linux to grant exclusive file-access (which is the default under Microslop Windows), but one would need to explicitly enable those here first - which does also not feel right due to OS-specific calls...

Comment thread py-kms/pykms_Metrics.py
count_300s = counts_300s.get(key, 0)

if count_30s > 0 or count_60s > 0 or count_300s > 0:
loggersrv.info(f"Setting metrics for {status}/{product}: 30s={count_30s}, 60s={count_60s}, 300s={count_300s}")

@simonmicro simonmicro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooofff... this is A LOT of code... after reading through it once I see much information duplication and repetition, which also blows this MR up and will impact future maintainability hard

please do not take my lazy comments personal, at some point the motivation just got a bit low as most of the questionable decisions were not taken by you, but some LLM - so I also likely overlooked a lot of stuff

I would welcome a rework of this with more attention to detail and reasoning of changes before they are just done 😉

thank you in advance

🐱

Comment thread docker/start.py
os.makedirs(prometheus_multiproc_dir, exist_ok=True)
# Set environment variables for this process and all child processes
os.environ['PROMETHEUS_MULTIPROC_DIR'] = prometheus_multiproc_dir
os.environ['prometheus_multiproc_dir'] = prometheus_multiproc_dir

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In accordance to https://prometheus.github.io/client_python/multiprocess/ the variable name is uppercase only. No need for lowercase in addition.

Comment thread docker/start.py
def start_kms(logger):
# Set up Prometheus multiprocess directory for both KMS Server and WebUI
# This must be done BEFORE starting any process that writes metrics
prometheus_multiproc_dir = '/tmp/prometheus_multiproc'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not like hard-coded temporary paths - especially ones, which refer to /tmp from the rootfs. Could you please revisit this one and utilize tempfile? This is handed down using the env-vars anyways already.

Comment thread docker/start.py
Comment on lines +74 to +77
# Set up Prometheus multiprocess mode (for Gunicorn workers)
prometheus_multiproc_dir = '/tmp/prometheus_multiproc'
if not os.path.exists(prometheus_multiproc_dir):
os.makedirs(prometheus_multiproc_dir, exist_ok=True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Comment thread docker/start.py
prometheus_multiproc_dir = '/tmp/prometheus_multiproc'
if not os.path.exists(prometheus_multiproc_dir):
os.makedirs(prometheus_multiproc_dir, exist_ok=True)
# Set both uppercase and lowercase variants for compatibility with different prometheus_client versions

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, no - you literally specified prometheus-client==0.21.0, so I do not see us being backward-compatible to a version >3 years old (a quick search turned up https://pypi.org/project/prometheus-client/0.17.1/)

please remove that

Comment thread docs/Metrics.md

## Overview

py-kms server (python3 Docker image only) supports exporting metrics in Prometheus format for monitoring and observability. This feature is designed for testing, learning, and lab environments to better understand KMS activation protocol behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would welcome if these docs would be more concise and less flowery worded as using fillers like "This feature is designed for testing, learning, and lab environments" would need to be backed up with clear points how your design is fulfilling these words and as you just added the prometheus-client-lib...

Just leave that part out, please?

Comment thread py-kms/pykms_Metrics.py
Comment on lines +224 to +249
# Windows patterns
if 'windows' in sku_lower:
if 'server' in sku_lower:
# Extract Windows Server version
for year in ['2025', '2022', '2019', '2016', '2012']:
if year in sku_lower:
return f"windows-server-{year}"
return "windows-server"
else:
# Extract Windows client version
if 'windows 11' in sku_lower or 'windows11' in sku_lower:
return "windows-11"
elif 'windows 10' in sku_lower or 'windows10' in sku_lower:
return "windows-10"
elif 'windows 8' in sku_lower or 'windows8' in sku_lower:
return "windows-8"
elif 'windows 7' in sku_lower or 'windows7' in sku_lower:
return "windows-7"
return "windows"

# Office patterns
elif 'office' in sku_lower:
for year in ['2024', '2021', '2019', '2016', '2013']:
if year in sku_lower:
return f"office-{year}"
return "office"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see way too many hard-coded patterns in here, which will be a nightmare if we update the kms-db again in the future

please consider using RegEx and generic patterns instead to be at least somewhat compatible

if you are unsure about this, consider adding small test functions as well to demonstrate (and document) the logic on its own

Comment thread py-kms/pykms_Metrics.py
Comment on lines +205 to +206
if version_major in [4, 5, 6]:
return f"v{version_major}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wtf? which major versions we support is not something to be decided in the metrics module?

Comment thread py-kms/pykms_Metrics.py
# Helper Functions
# ==============================================================================

def get_product_type(app_name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue as below here...

Comment thread py-kms/pykms_Metrics.py

# Shared storage for activation events using shelve (cross-process safe)
# Use PROMETHEUS_MULTIPROC_DIR if available, otherwise /tmp
_events_dir = os.environ.get('PROMETHEUS_MULTIPROC_DIR', os.environ.get('prometheus_multiproc_dir', '/tmp'))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here: please do not hardcode temporary file paths - especially when thinking about (god forbid) windows-support for people bypassing the container and directly invoking the server: just ask tempfile about that stuff

Comment thread py-kms/pykms_Metrics.py
from prometheus_client import REGISTRY

# Check if we're in multiprocess mode (Gunicorn sets this)
prometheus_multiproc_dir = os.environ.get('PROMETHEUS_MULTIPROC_DIR', os.environ.get('prometheus_multiproc_dir'))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same env-var getter over and over... you could just fetch it once on module loading, right?

@simonmicro

Copy link
Copy Markdown
Member

Lol, the whole review took over an hour... This was longer than expected :/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants