diff --git a/.dockerignore b/.dockerignore index d11e52d..0a040ca 100644 --- a/.dockerignore +++ b/.dockerignore @@ -21,6 +21,7 @@ conf/ log_merging_config.json dockerComposeMPI.yml *.csv +reversim-conf # Debugpy logs, WinMerge Backups etc. *.log @@ -29,6 +30,11 @@ dockerComposeMPI.yml # Ignore statistics folder statistics/ +# Hide deployment storage +tmp/ +secrets/ + + # Maybe ignore unnecessary code # app/statistics # app/tests diff --git a/.github/workflows/deploy-image.yml b/.github/workflows/deploy-image.yml index adf752c..79672d2 100644 --- a/.github/workflows/deploy-image.yml +++ b/.github/workflows/deploy-image.yml @@ -1,18 +1,21 @@ # name: Create and publish a Docker image -# Configures this workflow to run every time a change is pushed to the branch called `main`. +# Configures this workflow to run every time a change is pushed to the branch called +# `main` or `dev`. on: push: branches: ['main', 'dev'] tags: ['*'] -# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +# Defines two custom environment variables for the workflow. These are used for the +# Container registry domain, and a name for the Docker image that this workflow builds. env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} -# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +# There is a single job in this workflow. It's configured to run on the latest available +# version of Ubuntu. jobs: build-and-push-image: runs-on: ubuntu-latest @@ -25,21 +28,28 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + submodules: true - # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + # Uses the `docker/login-action` action to log in to the Container registry registry + # using the account and password that will publish the packages. Once published, the + # packages are scoped to the account defined here. - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) + # to extract tags and labels that will be applied to the specified image. + # The `id` "meta" allows the output of this step to be referenced in a subsequent + # step. The `images` value provides the base name for the tags and labels. # It will automatically create the latest Docker tag, if a git tag is found: https://github.com/docker/metadata-action?tab=readme-ov-file#latest-tag - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -51,12 +61,17 @@ jobs: calculatedSha=$(git rev-parse --short ${{ github.sha }}) echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV - # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. - # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository. - # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + # This step uses the `docker/build-push-action` action to build the image, based on + # your repository's `Dockerfile`. If the build succeeds, it pushes the image to + # GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files + # located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) + # in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the + # output from the "meta" step. - name: Build and push Docker image id: push - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + uses: docker/build-push-action@v6 with: context: . push: true @@ -66,9 +81,12 @@ jobs: GAME_GIT_HASH=${{ github.sha }} GAME_GIT_HASH_SHORT=${{ env.COMMIT_SHORT_SHA }} - # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). + # This step generates an artifact attestation for the image, which is an unforgeable + # statement about where and how it was built. It increases supply chain security for + # people who consume the image. For more information, see [Using artifact attestations + # to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v3 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} subject-digest: ${{ steps.push.outputs.digest }} diff --git a/.gitignore b/.gitignore index 054ed4b..9a3cf08 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ __pycache__/ env/ venv/ .venv/ -tmp/ # --- Debugpy logs, WinMerge Backups etc. *.log @@ -44,3 +43,7 @@ reversim-conf # --- Generated level thumbnails doc/levels + +# --- Don't push instance relevant configuration +tmp/ +secrets/ diff --git a/.vscode/settings.json b/.vscode/settings.json index edd04cf..a863c83 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -143,6 +143,7 @@ "hreserver", "hrestudy", "htmlsafe", + "httpauth", "iframe", "imgdata", "imgstring", diff --git a/Dockerfile b/Dockerfile index 88042af..9f8c976 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ARG PROMETHEUS_MULTIPROC_DIR="/tmp/prometheus_multiproc" MAINTAINER Max Planck Institute for Security and Privacy LABEL org.opencontainers.image.authors="Max Planck Institute for Security and Privacy" # NOTE Also change the version in config.py -LABEL org.opencontainers.image.version="2.1.0" +LABEL org.opencontainers.image.version="2.1.1" LABEL org.opencontainers.image.licenses="AGPL-3.0-only" LABEL org.opencontainers.image.description="Ready to deploy Docker container to use ReverSim for research. ReverSim is an open-source environment for the browser, originally developed at the Max Planck Institute for Security and Privacy (MPI-SP) to study human aspects in hardware reverse engineering." LABEL org.opencontainers.image.source="https://github.com/emsec/ReverSim" @@ -62,6 +62,7 @@ ENV PROMETHEUS_MULTIPROC_DIR=${PROMETHEUS_MULTIPROC_DIR} # Create empty statistics folders WORKDIR /usr/var/reversim-instance/statistics/LogFiles WORKDIR /usr/var/reversim-instance/statistics/canvasPics +WORKDIR /usr/var/reversim-instance/secrets WORKDIR /usr/src/hregame # Specify mount points for the statistics folder, levels, researchInfo & disclaimer diff --git a/app/authentication.py b/app/authentication.py new file mode 100644 index 0000000..fc24ce9 --- /dev/null +++ b/app/authentication.py @@ -0,0 +1,45 @@ +import logging +import os +import secrets + +from flask_httpauth import HTTPTokenAuth # type: ignore + +from app.config import BEARER_TOKEN_BYTES +from app.model.ApiKey import ApiKey +from app.storage.database import db +from app.utilsGame import safe_join + +auth = HTTPTokenAuth(scheme='Bearer') + +USER_METRICS = 'api_metrics' + +@auth.verify_token # type: ignore +def verifyToken(token: str) -> ApiKey|None: + """Check if this token exists. If yes return the user object, otherwise return `None` + + https://flask-httpauth.readthedocs.io/en/latest/#flask_httpauth.HTTPTokenAuth.verify_token + """ + + return db.session.query(ApiKey).filter_by(token=token).first() + + +def populate_data(instance_path: str): + if ApiKey.query.count() < 1: + apiKey = ApiKey(secrets.token_urlsafe(BEARER_TOKEN_BYTES), USER_METRICS) + db.session.add(apiKey) + db.session.commit() + + defaultToken = db.session.query(ApiKey).where(ApiKey.user == USER_METRICS).first() + if defaultToken is not None: + + # Try to write the bearer secret to a file so other containers can use it + try: + folder = safe_join(instance_path, 'secrets') + os.makedirs(folder, exist_ok=True) + with open(safe_join(folder, 'bearer_api.txt'), encoding='UTF-8', mode='wt') as f: + f.write(defaultToken.token) + + except Exception as e: + # When the file can't be created print the bearer to stdout + logging.error('Could not write bearer token to file: ' + str(e)) + logging.info('Bearer token for /metrics endpoint: ' + defaultToken.token) diff --git a/app/config.py b/app/config.py index e74e25c..cfaad3a 100644 --- a/app/config.py +++ b/app/config.py @@ -16,7 +16,7 @@ # CONFIG Current Log File Version. # NOTE Also change this in the Dockerfile -LOGFILE_VERSION = "2.1.0" # Major.Milestone.Subversion +LOGFILE_VERSION = "2.1.1" # Major.Milestone.Subversion PSEUDONYM_LENGTH = 32 LEVEL_ENCODING = 'UTF-8' # was Windows-1252 @@ -24,6 +24,9 @@ STALE_LOGFILE_TIME = 48 * 60 * 60 # close logfiles after 48h MAX_ERROR_LOGS_PER_PLAYER = 25 +# The bearer token for the /metrics endpoint +BEARER_TOKEN_BYTES = 32 + # Number of seconds, after which the player is considered disconnected. A "Back Online" # message will be printed to the log, if the player connects afterwards. Also used for the # Prometheus Online Player Count metric diff --git a/app/model/ApiKey.py b/app/model/ApiKey.py new file mode 100644 index 0000000..6c8b922 --- /dev/null +++ b/app/model/ApiKey.py @@ -0,0 +1,16 @@ +from datetime import datetime, timezone +from sqlalchemy import DateTime, String +from sqlalchemy.orm import Mapped, mapped_column + +from app.storage.database import db + + +class ApiKey(db.Model): + token: Mapped[str] = mapped_column(primary_key=True) + user: Mapped[str] = mapped_column(String(64)) + created: Mapped[datetime] = mapped_column(DateTime) + + def __init__(self, token: str, user: str) -> None: + self.token = token + self.user = user + self.created = datetime.now(timezone.utc) diff --git a/app/model/LevelLoader/JsonLevelList.py b/app/model/LevelLoader/JsonLevelList.py index 56f2b5e..c790d30 100644 --- a/app/model/LevelLoader/JsonLevelList.py +++ b/app/model/LevelLoader/JsonLevelList.py @@ -54,9 +54,11 @@ def getPossibleLevels(self) -> list[Level]: current_list = MappingProxyType(self.levelList[list_name]) for entry in current_list['levels']: + # If this is a list, add all levels that are contained in that list if isinstance(entry, list): for subEntry in cast(list[dict[str, str]], entry): - levels.append(Level(type=subEntry['type'], fileName=subEntry['name'])) + levels.append(Level(type=subEntry['type'], fileName=subEntry['name'])) + # Else add the single level else: levels.append(Level(type=entry['type'], fileName=entry['name'])) @@ -152,7 +154,8 @@ def fromFile( try: conf = load_config(fileName=fileName, instanceFolder=instanceFolder) - # TODO Run checks + # TODO Run checks to catch any errors directly on launch and not later when + # someone tries to load the first level logging.info(f'Successfully loaded {len(conf)} level lists.') return conf diff --git a/app/prometheusMetrics.py b/app/prometheusMetrics.py index 6a05d72..7135cb4 100644 --- a/app/prometheusMetrics.py +++ b/app/prometheusMetrics.py @@ -1,6 +1,7 @@ import logging from threading import Thread import time +from typing import Any from flask import Flask # Prometheus Metrics @@ -14,13 +15,14 @@ class ServerMetrics: @staticmethod - def __prometheusFactory(): + def __prometheusFactory(auth_provider: Any): EXCLUDED_PATHS = ["/?res\\/.*", "/?src\\/.*", "/?doc\\/.*"] # Try to use the uWSGI exporter. This will fail, if uWSGI is not installed try: metrics = UWsgiPrometheusMetrics.for_app_factory( # type: ignore - excluded_paths=EXCLUDED_PATHS + excluded_paths=EXCLUDED_PATHS, + metrics_decorator=auth_provider ) # Use the regular Prometheus exporter @@ -28,13 +30,14 @@ def __prometheusFactory(): logging.error(e) metrics = PrometheusMetrics.for_app_factory( # type: ignore - excluded_paths=EXCLUDED_PATHS + excluded_paths=EXCLUDED_PATHS, + metrics_decorator=auth_provider ) logging.info(f'Using {type(metrics).__name__} as the Prometheus exporter') return metrics - metrics = __prometheusFactory() + metrics: PrometheusMetrics|UWsgiPrometheusMetrics|None = None # ReverSim Prometheus Metrics #met_openLogs = Gauge("reversim_logfile_count", "The number of open logfiles") # type: ignore @@ -47,8 +50,9 @@ def __prometheusFactory(): met_clientErrors: Gauge|None = None @classmethod - def createPrometheus(cls, app: Flask): + def createPrometheus(cls, app: Flask, auth_provider: Any): """Init Prometheus""" + cls.metrics = cls.__prometheusFactory(auth_provider) cls.metrics.init_app(app) # type: ignore cls.metrics.info('app_info', 'Application info', version=gameConfig.LOGFILE_VERSION) # type: ignore diff --git a/app/statistics/statsPhase.py b/app/statistics/statsPhase.py index 778acb3..d41174c 100644 --- a/app/statistics/statsPhase.py +++ b/app/statistics/statsPhase.py @@ -11,7 +11,7 @@ from app.statistics.statisticUtils import LogSyntaxError, calculateDuration, removeprefix from app.statistics.statsLevel import StatsLevel -from app.utilsGame import PhaseType +from app.utilsGame import LevelType, PhaseType class StatsPhase: @@ -196,22 +196,35 @@ def onLevelRequested(self, event: EVENT_T): nextLevelName: str = event['Filename'] expectedLevelType: str = ALL_LEVEL_TYPES[self.levels[self.levelCounter].type] - # New Special Case: The old asset folder was /res - if nextLevelName.startswith('/res/'): - logging.debug(f'Old asset folder: "{nextLevelName}"') - nextLevelName = nextLevelName.replace('res', 'assets') - # Check if the current level type matches the expected level type if nextLevelType != expectedLevelType: # Tutorials might get inserted dynamically - if nextLevelType in [ALL_LEVEL_TYPES[SLIDE_TYPE_TUTORIAL], ALL_LEVEL_TYPES[SLIDE_TYPE_SPECIAL]]: + if nextLevelType in [ALL_LEVEL_TYPES[LevelType.TUTORIAL], ALL_LEVEL_TYPES[LevelType.SPECIAL]]: nextLevelTypeLog = next(k for k, v in ALL_LEVEL_TYPES.items() if v == nextLevelType) tut = StatsLevel(nextLevelTypeLog, nextLevelName) self.levels.insert(self.levelCounter, tut) nextLevel = tut + + # We might have too many levels in the que, since the new JSON Level List + # allows to pick one of multiple levels. Try to skip ahead to find a slide + # with matching type + elif nextLevelType in ALL_LEVEL_TYPES[LevelType.INFO]: + oldLevelCounter = self.levelCounter + for level in self.levels[self.levelCounter:]: + # Break if we found a slide with matching type + if ALL_LEVEL_TYPES[level.type] == nextLevelType: + nextLevel = self.levels[self.levelCounter] + assert nextLevel == level + break + + # Increment counter and check if we reached the end of the phase + self.levelCounter += 1 + if self.levelCounter > len(self.levels)-1: + raise LogSyntaxError(f'Unable to find a slide of type {nextLevelType} until the end of the phase') + + # Unable to find a matching slide with all edge cases, raise error else: raise LogSyntaxError("Expected slide of type " + expectedLevelType + ", got " + nextLevelType + " in " + self.name + "!") - # If the level contains a circuit, the order might be randomized, so search for the level in the array if nextLevelType in [ALL_LEVEL_TYPES['level'], ALL_LEVEL_TYPES['tutorial']]: @@ -364,9 +377,6 @@ def createStatsPhase() -> Dict[str, int]: THINKALOUD_CONFIG_OPTIONS = ['concurrent', 'retrospective'] THINKALOUD_SLIDE_NAMES = ['thinkaloudCon', 'thinkaloudRet'] -SLIDE_TYPE_SPECIAL = 'special' -SLIDE_TYPE_TUTORIAL = 'tutorial' - INTRO_SLIDE_NAMES = ['covert', 'camouflage'] INTRO_SLIDE_NAMES_OLD = { 'Tutorial covert': INTRO_SLIDE_NAMES[0], diff --git a/docker-compose-build.yml b/docker-compose-build.yml index c6247b4..9d321a3 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -10,16 +10,15 @@ volumes: # docker volume create reversim_playerdata playerdata: name: reversim_playerdata - external: true - - # Holds Flask, uWSGI and the game-config and all assets that are needed by the game - gameConfig: + #external: true # If enabled in reversim_uwsgi.ini, uWSGI will log the server logs into text files # instead of stdout. This way you do not loose the logs when you restart # the Docker container #uwsgi_logs: + metric_secret: + # All containers that belong to this compose file services: # the container will be named reversim_game when launched @@ -35,7 +34,7 @@ services: # mount your volumes here volumes: - playerdata:/usr/var/reversim-instance/statistics - - gameConfig:/usr/var/reversim-instance/conf:ro + - metric_secret:/usr/var/reversim-instance/secrets #- uwsgi_logs:/var/log/uwsgi # add resource limits. You may want to tweak this for your deployment @@ -82,6 +81,8 @@ services: uid: "65534" profiles: - grafana + volumes: + - metric_secret:/etc/reversim/secrets:ro #entrypoint: ["ls", "-lan", "/etc/prometheus"] grafana: @@ -108,9 +109,10 @@ configs: evaluation_interval: 15s # Evaluate rules every 15 seconds. scrape_configs: - - job_name: 'prometheus' + - job_name: 'reversim' static_configs: - targets: ['game:8000'] + bearer_token_file: '/etc/reversim/secrets/bearer_api.txt' grafana_datasources.yml: content: | diff --git a/docker-compose.yml b/docker-compose.yml index 089f2d6..f044591 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,6 @@ services: # mount your volumes here volumes: - playerdata:/usr/var/reversim-instance/statistics - - gameConfig:/usr/var/reversim-instance/conf:ro #- uwsgi_logs:/var/log/uwsgi # add resource limits. You may want to tweak this for your deployment diff --git a/examples/grafana-dashboard-provisioned.json b/examples/grafana-dashboard-provisioned.json index c588d5c..b09ecd2 100644 --- a/examples/grafana-dashboard-provisioned.json +++ b/examples/grafana-dashboard-provisioned.json @@ -1,595 +1,716 @@ { - "__inputs": [], - "__elements": {}, - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "12.0.2" - }, - { - "type": "panel", - "id": "heatmap", - "name": "Heatmap", - "version": "" - }, - { - "type": "datasource", - "id": "prometheus", - "name": "Prometheus", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "stat", - "name": "Stat", - "version": "" - }, - { - "type": "panel", - "id": "timeseries", - "name": "Time series", - "version": "" - } - ], - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": null, - "links": [], - "panels": [ - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 8, - "panels": [], - "title": "status", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "name": "Prometheus" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 4, - "w": 6, - "x": 0, - "y": 1 - }, - "id": 9, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.0.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "name": "Prometheus" - }, - "editorMode": "code", - "expr": "round(rate(flask_http_request_duration_seconds_count{path=\"/testConnection\", status=\"200\"}[$__rate_interval]))", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "Players Connected", - "type": "stat" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 5 - }, - "id": 7, - "panels": [], - "title": "metrics", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "name": "Prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "axisSoftMin": 0, - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 57, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineStyle": { - "fill": "solid" - }, - "lineWidth": 0, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "/testConnection" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "blue", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "/action" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "yellow", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "/canvasImage" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 6 - }, - "id": 5, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.0.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "name": "Prometheus" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "sum by(path) (irate(flask_http_request_duration_seconds_count[1m]))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Request Rate (1/sec) by endpoint", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "name": "Prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "500" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "dark-red", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "307" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "blue", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "400" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "yellow", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "404" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "yellow", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "406" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 6 - }, - "id": 3, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.0.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "name": "Prometheus" - }, - "disableTextWrap": false, - "editorMode": "builder", - "expr": "sum by(status) (irate(flask_http_request_total[1m]))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Request Rate (1/sec) by status", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "name": "Prometheus" - }, - "description": "", - "fieldConfig": { - "defaults": { - "custom": { - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "scaleDistribution": { - "type": "linear" - } - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 14 - }, - "id": 4, - "options": { - "calculate": false, - "calculation": { - "xBuckets": { - "mode": "size" - }, - "yBuckets": { - "mode": "size", - "scale": { - "type": "linear" - } - } - }, - "cellGap": 1, - "color": { - "exponent": 0.5, - "fill": "dark-orange", - "min": 0, - "mode": "scheme", - "reverse": false, - "scale": "exponential", - "scheme": "Oranges", - "steps": 64 - }, - "exemplars": { - "color": "rgba(255,0,255,0.7)" - }, - "filterValues": { - "le": 1e-9 - }, - "legend": { - "show": true - }, - "rowsFrame": { - "layout": "auto" - }, - "tooltip": { - "mode": "single", - "showColorScale": false, - "yHistogram": false - }, - "yAxis": { - "axisPlacement": "left", - "min": 0, - "reverse": false - } - }, - "pluginVersion": "12.0.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "name": "Prometheus" - }, - "disableTextWrap": false, - "editorMode": "code", - "exemplar": false, - "expr": "(sum(rate(flask_http_request_duration_seconds_bucket{}[$__rate_interval])) by (le))", - "format": "heatmap", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Response Time (seconds)", - "type": "heatmap" - } - ], - "refresh": "30s", - "schemaVersion": 41, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": {}, - "timezone": "", - "title": "ReverSim", - "uid": "bf262498-02a6-4943-acf6-9f131f9cd2a0", - "version": 3, - "weekStart": "" + "__inputs": [], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "12.3.0" + }, + { + "type": "panel", + "id": "heatmap", + "name": "Heatmap", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 8, + "panels": [], + "title": "status", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "name": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "name": "Prometheus" + }, + "editorMode": "code", + "expr": "round(rate(flask_http_request_duration_seconds_count{path=\"/testConnection\", status=\"200\", instance=\"$instance\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Players Connected", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 7, + "panels": [], + "title": "metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "name": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 57, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "/testConnection" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "/action" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "/canvasImage" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "name": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "sum by(path) (irate(flask_http_request_duration_seconds_count{instance=\"$instance\"}[1m]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request Rate (1/sec) by endpoint", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "name": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "500" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "307" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "400" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "404" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "406" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "name": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(status) (irate(flask_http_request_total{instance=\"$instance\"}[1m]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request Rate (1/sec) by status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "name": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 14 + }, + "id": 4, + "options": { + "calculate": false, + "calculation": { + "xBuckets": { + "mode": "size" + }, + "yBuckets": { + "mode": "size", + "scale": { + "type": "linear" + } + } + }, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "min": 0, + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "min": 0, + "reverse": false + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "name": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "(sum(rate(flask_http_request_duration_seconds_bucket{instance=\"$instance\"}[$__rate_interval])) by (le))", + "format": "heatmap", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Response Time (seconds)", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "name": "Prometheus" + }, + "description": "console.error messages and exceptions that where reported back to the server by all browsers. See instance/statistics/crash_reporter.log to read the error messages.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "editorMode": "builder", + "expr": "increase(reversim_client_errors{instance=\"$instance\"}[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A", + "datasource": { + "type": "prometheus", + "name": "Prometheus" + } + } + ], + "title": "Client Error Messages received (1/min)", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "5s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": {}, + "definition": "label_values(instance)", + "description": "", + "name": "instance", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(instance)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ReverSim", + "uid": "bf262498-02a6-4943-acf6-9f131f9cd2a0", + "version": 4, + "weekStart": "", + "id": null } \ No newline at end of file diff --git a/examples/grafana-dashboard.json b/examples/grafana-dashboard.json index ee82e6e..db1632d 100644 --- a/examples/grafana-dashboard.json +++ b/examples/grafana-dashboard.json @@ -1,604 +1,725 @@ { - "__inputs": [ - { - "name": "DS_PROMETHEUS", - "label": "Prometheus", - "description": "", - "type": "datasource", - "pluginId": "prometheus", - "pluginName": "Prometheus" - } - ], - "__elements": {}, - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "12.0.2" - }, - { - "type": "panel", - "id": "heatmap", - "name": "Heatmap", - "version": "" - }, - { - "type": "datasource", - "id": "prometheus", - "name": "Prometheus", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "stat", - "name": "Stat", - "version": "" - }, - { - "type": "panel", - "id": "timeseries", - "name": "Time series", - "version": "" - } - ], - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": null, - "links": [], - "panels": [ - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 8, - "panels": [], - "title": "status", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 4, - "w": 6, - "x": 0, - "y": 1 - }, - "id": 9, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.0.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "round(rate(flask_http_request_duration_seconds_count{path=\"/testConnection\", status=\"200\"}[$__rate_interval]))", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "Players Connected", - "type": "stat" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 5 - }, - "id": 7, - "panels": [], - "title": "metrics", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "axisSoftMin": 0, - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 57, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineStyle": { - "fill": "solid" - }, - "lineWidth": 0, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "/testConnection" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "blue", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "/action" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "yellow", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "/canvasImage" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 6 - }, - "id": 5, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.0.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "disableTextWrap": false, - "editorMode": "builder", - "exemplar": false, - "expr": "sum by(path) (irate(flask_http_request_duration_seconds_count[1m]))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Request Rate (1/sec) by endpoint", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "500" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "dark-red", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "307" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "blue", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "400" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "yellow", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "404" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "yellow", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "406" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 6 - }, - "id": 3, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.0.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "disableTextWrap": false, - "editorMode": "builder", - "expr": "sum by(status) (irate(flask_http_request_total[1m]))", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Request Rate (1/sec) by status", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "", - "fieldConfig": { - "defaults": { - "custom": { - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "scaleDistribution": { - "type": "linear" - } - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 14 - }, - "id": 4, - "options": { - "calculate": false, - "calculation": { - "xBuckets": { - "mode": "size" - }, - "yBuckets": { - "mode": "size", - "scale": { - "type": "linear" - } - } - }, - "cellGap": 1, - "color": { - "exponent": 0.5, - "fill": "dark-orange", - "min": 0, - "mode": "scheme", - "reverse": false, - "scale": "exponential", - "scheme": "Oranges", - "steps": 64 - }, - "exemplars": { - "color": "rgba(255,0,255,0.7)" - }, - "filterValues": { - "le": 1e-9 - }, - "legend": { - "show": true - }, - "rowsFrame": { - "layout": "auto" - }, - "tooltip": { - "mode": "single", - "showColorScale": false, - "yHistogram": false - }, - "yAxis": { - "axisPlacement": "left", - "min": 0, - "reverse": false - } - }, - "pluginVersion": "12.0.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "disableTextWrap": false, - "editorMode": "code", - "exemplar": false, - "expr": "(sum(rate(flask_http_request_duration_seconds_bucket{}[$__rate_interval])) by (le))", - "format": "heatmap", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Response Time (seconds)", - "type": "heatmap" - } - ], - "refresh": "30s", - "schemaVersion": 41, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": {}, - "timezone": "", - "title": "ReverSim", - "uid": "bf262498-02a6-4943-acf6-9f131f9cd2a0", - "version": 3, - "weekStart": "" + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "12.3.0" + }, + { + "type": "panel", + "id": "heatmap", + "name": "Heatmap", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 8, + "panels": [], + "title": "status", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "round(rate(flask_http_request_duration_seconds_count{path=\"/testConnection\", status=\"200\", instance=\"$instance\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Players Connected", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 7, + "panels": [], + "title": "metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 57, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "/testConnection" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "/action" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "/canvasImage" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "sum by(path) (irate(flask_http_request_duration_seconds_count{instance=\"$instance\"}[1m]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request Rate (1/sec) by endpoint", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "500" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "307" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "400" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "404" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "406" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(status) (irate(flask_http_request_total{instance=\"$instance\"}[1m]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Request Rate (1/sec) by status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 14 + }, + "id": 4, + "options": { + "calculate": false, + "calculation": { + "xBuckets": { + "mode": "size" + }, + "yBuckets": { + "mode": "size", + "scale": { + "type": "linear" + } + } + }, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "min": 0, + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "min": 0, + "reverse": false + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "(sum(rate(flask_http_request_duration_seconds_bucket{instance=\"$instance\"}[$__rate_interval])) by (le))", + "format": "heatmap", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Response Time (seconds)", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "console.error messages and exceptions that where reported back to the server by all browsers. See instance/statistics/crash_reporter.log to read the error messages.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "editorMode": "builder", + "expr": "increase(reversim_client_errors{instance=\"$instance\"}[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + } + } + ], + "title": "Client Error Messages received (1/min)", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "5s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": {}, + "definition": "label_values(instance)", + "description": "", + "name": "instance", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(instance)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ReverSim", + "uid": "bf262498-02a6-4943-acf6-9f131f9cd2a0", + "version": 4, + "weekStart": "", + "id": null } \ No newline at end of file diff --git a/gameServer.py b/gameServer.py index ce521fe..347b0b9 100644 --- a/gameServer.py +++ b/gameServer.py @@ -11,6 +11,7 @@ import app.config as gameConfig from app.model.LevelLoader.JsonLevelList import JsonLevelList import app.router.routerGame as routerGame +from app.authentication import auth, populate_data # import all routes, belonging to this app import app.router.routerStatic as routerStatic @@ -139,13 +140,17 @@ def createApp(): # Init the Database ReverSimDatabase.createDatabase(app) + # Generate the default Bearer token for the /metrics endpoint + with app.app_context(): + populate_data(instance_path=app.instance_path) + # Init the Legacy logger and Screenshots initScreenshotWriter(app) initLegacyLogFile(app) # Init Prometheus (must be done before Flask context is created) try: - ServerMetrics.createPrometheus(app) + ServerMetrics.createPrometheus(app, auth_provider=auth.login_required) # type: ignore except Exception as e: logging.warning(f'The Prometheus metrics failed to initialize: "{e}"') logging.warning('This can be safely ignored when not in production') diff --git a/requirements.txt b/requirements.txt index 6ff7b83..0934054 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ uwsgitop # Metrics prometheus-flask-exporter +Flask-HTTPAuth # Database support SQLAlchemy # ORM diff --git a/requirementsDev.txt b/requirementsDev.txt index 3104474..fcd9280 100644 --- a/requirementsDev.txt +++ b/requirementsDev.txt @@ -5,6 +5,7 @@ # --- Game dependencies Flask prometheus-flask-exporter +Flask-HTTPAuth # --- Database support SQLAlchemy # ORM diff --git a/static/src/scenes/GameScene.js b/static/src/scenes/GameScene.js index ee4326e..625244c 100644 --- a/static/src/scenes/GameScene.js +++ b/static/src/scenes/GameScene.js @@ -721,7 +721,7 @@ ${languageDict['yourScore'][gameLanguage]} ${this.level.stats.score.toString()} let str = `${textBestClicks} ${this.level.minimalHammingDistance.toString()} -${textYourScore} ${this.level.stats.score.toString()} +${textYourScore} ${this.level.stats.score.toString()} / 100 `; if(scoreValues.switchClick != 0) str += `\n${textSwitchClicks} ${scoreValues.switchClick != 0 ? this.level.getBestScorePossible() : ''}`;