Prometheus support

This commit is contained in:
Piero Toffanin 2022-12-26 16:10:43 -05:00
parent 753a33e7c5
commit 9f243fd6be
7 changed files with 120 additions and 7 deletions

View file

@ -196,7 +196,9 @@ docker-compose -f docker-compose.cuda.yml up -d --build
| --suggestions | Allow user suggestions | `false` | LT_SUGGESTIONS |
| --disable-files-translation | Disable files translation | `false` | LT_DISABLE_FILES_TRANSLATION |
| --disable-web-ui | Disable web ui | `false` | LT_DISABLE_WEB_UI |
| --update-models | Update language models at startup | `false` | LT_UPDATE_MODELS |
| --update-models | Update language models at startup | `false` | LT_UPDATE_MODELS |
| --metrics | Enable the /metrics endpoint for exporting [Prometheus](https://prometheus.io/) usage metrics | `false` | LT_METRICS |
| --metrics-auth-token | Protect the /metrics endpoint by allowing only clients that have a valid Authorization Bearer token | `No auth` | LT_METRICS_AUTH_TOKEN |
Note that each argument has an equivalent environment variable that can be used instead. The env. variables overwrite the default values but have lower priority than the command arguments and are particularly useful if used with Docker. The environment variable names are the upper-snake-case of the equivalent command argument's name with a `LT` prefix.
@ -267,6 +269,47 @@ ltmanage keys remove <api-key>
ltmanage keys
```
## Prometheus Metrics
LibreTranslate has Prometheus [exporter](https://prometheus.io/docs/instrumenting/exporters/) capabilities when you pass the `--metrics` argument at startup (disabled by default). When metrics are enabled, a `/metrics` endpoint is mounted on the instance:
http://localhost:5000/metrics
```
# HELP request_inprogress Multiprocess metric
# TYPE request_inprogress gauge
request_inprogress{api_key="",endpoint="/translate",request_ip="127.0.0.1"} 0.0
# HELP request_seconds Multiprocess metric
# TYPE request_seconds summary
request_seconds_count{api_key="",endpoint="/translate",request_ip="127.0.0.1",status="200"} 0.0
request_seconds_sum{api_key="",endpoint="/translate",request_ip="127.0.0.1",status="200"} 0.0
```
You can then configure `prometheus.yml` to read the metrics:
```
scrape_configs:
- job_name: "libretranslate"
# Needed only if you use --metrics-auth-token
#authorization:
#credentials: "mytoken"
static_configs:
- targets: ["localhost:5000"]
```
To secure the `/metrics` endpoint you can also use `--metrics-auth-token mytoken`.
If you use Gunicorn, make sure to create a directory for storing multiprocess data metrics and set `PROMETHEUS_MULTIPROC_DIR`:
```
mkdir -p /tmp/prometheus_data
rm /tmp/prometheus_data/*
export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_data
gunicorn -c gunicorn_conf.py --bind 0.0.0.0:5000 'wsgi:app(metrics=True)'
```
## Language Bindings
You can use the LibreTranslate API using the following bindings:

View file

@ -1 +1 @@
1.3.4
1.3.5

View file

@ -4,15 +4,17 @@ import tempfile
import uuid
from functools import wraps
from html import unescape
from timeit import default_timer
import argostranslatefiles
from argostranslatefiles import get_supported_formats
from flask import (Flask, abort, jsonify, render_template, request, send_file,
url_for)
url_for, Response)
from flask_swagger import swagger
from flask_swagger_ui import get_swaggerui_blueprint
from translatehtml import translate_html
from werkzeug.utils import secure_filename
from werkzeug.exceptions import HTTPException
from app import flood, remove_translated_files, security
from app.language import detect_languages, improve_translation_formatting
@ -174,6 +176,28 @@ def create_app(args):
if args.req_flood_threshold > 0:
flood.setup(args.req_flood_threshold)
measure_request = None
gauge_request = None
if args.metrics:
from prometheus_client import CONTENT_TYPE_LATEST, Summary, Gauge, CollectorRegistry, multiprocess, generate_latest
@app.route("/metrics")
def prometheus_metrics():
if args.metrics_auth_token:
authorization = request.headers.get('Authorization')
if authorization != "Bearer " + args.metrics_auth_token:
abort(401, description="Unauthorized")
registry = CollectorRegistry()
multiprocess.MultiProcessCollector(registry)
return Response(generate_latest(registry), mimetype=CONTENT_TYPE_LATEST)
measure_request = Summary('request_seconds', 'Time spent on request', ['endpoint', 'status', 'request_ip', 'api_key'])
measure_request.labels('/translate', 200, '127.0.0.1', '')
gauge_request = Gauge('request_inprogress', 'Active requests', ['endpoint', 'request_ip', 'api_key'], multiprocess_mode='livesum')
gauge_request.labels('/translate', '127.0.0.1', '')
def access_check(f):
@wraps(f)
def func(*a, **kw):
@ -203,11 +227,30 @@ def create_app(args):
403,
description=description,
)
return f(*a, **kw)
return func
if args.metrics:
@wraps(func)
def measure_func(*a, **kw):
start_t = default_timer()
status = 200
ip = get_remote_address()
ak = get_req_api_key() or ''
g = gauge_request.labels(request.path, ip, ak)
try:
g.inc()
return func(*a, **kw)
except HTTPException as e:
status = e.code
raise e
finally:
duration = max(default_timer() - start_t, 0)
measure_request.labels(request.path, status, ip, ak).observe(duration)
g.dec()
return measure_func
else:
return func
@app.errorhandler(400)
def invalid_api(e):
return jsonify({"error": str(e.description)}), 400

View file

@ -160,6 +160,16 @@ _default_options_objects = [
'name': 'UPDATE_MODELS',
'default_value': False,
'value_type': 'bool'
},
{
'name': 'METRICS',
'default_value': False,
'value_type': 'bool'
},
{
'name': 'METRICS_AUTH_TOKEN',
'default_value': '',
'value_type': 'str'
},
]

View file

@ -147,6 +147,18 @@ def get_args():
parser.add_argument(
"--update-models", default=DEFARGS['UPDATE_MODELS'], action="store_true", help="Update language models at startup"
)
parser.add_argument(
"--metrics",
default=DEFARGS['METRICS'],
action="store_true",
help="Enable the /metrics endpoint for exporting Prometheus usage metrics",
)
parser.add_argument(
"--metrics-auth-token",
default=DEFARGS['METRICS_AUTH_TOKEN'],
type=str,
help="Protect the /metrics endpoint by allowing only clients that have a valid Authorization Bearer token (%(default)s)",
)
return parser.parse_args()

4
gunicorn_conf.py Normal file
View file

@ -0,0 +1,4 @@
from prometheus_client import multiprocess
def child_exit(server, worker):
multiprocess.mark_process_dead(worker.pid)

View file

@ -15,3 +15,4 @@ itsdangerous==2.1.2
Werkzeug==2.2.2
requests==2.28.1
redis==4.3.4
prometheus-client==0.15.0