diff --git a/README.md b/README.md index c968f52..3e0e350 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,31 @@ * Публикация в основном канале хаба, если это ветка master. Этот документ можно обсуждать и предлагать к нему правки. + +## OpenHub — hub-new.oscript.io + +Новый хаб пакетов ([OpenHub](https://github.com/Segate-ekb/openhub)) живёт в том же `docker-compose.yml`: + +| Сервис | Что это | +| --- | --- | +| `openhub` | сам хаб, образ `segateekb/openhub`; | +| `openhub_db` | PostgreSQL хаба;| +| `lgtm` | мониторинг хаба одним контейнером (`grafana/otel-lgtm`), Grafana — grafana.oscript.io; дашборд хаба — в `monitoring/` | + +Файлы пакетов хаб хранит в общем MinIO + +### Первый запуск на работающем сервере + +1. Добавить в `.env` переменные из [`openhub.env.example`](openhub.env.example). +2. Завести DNS-записи `hub-new.oscript.io` и `grafana.oscript.io` на сервер. +3. Выпустить сертификаты и пересобрать nginx с новыми сайтами: + + ```bash + ./add-letsencrypt-domain.sh hub-new.oscript.io + ./add-letsencrypt-domain.sh grafana.oscript.io + ``` +4. Завести в MinIO бакет `openhub` и учётку хаба с ключами `OPENHUB_S3_ACCESS_KEY` / + `OPENHUB_S3_SECRET_KEY` из `.env` — руками, один раз. +5. Поднять хаб — база и мониторинг поднимутся сами: + +6. Сразу открыть и завести первого администратора. \ No newline at end of file diff --git a/add-letsencrypt-domain.sh b/add-letsencrypt-domain.sh new file mode 100755 index 0000000..76d1cd7 --- /dev/null +++ b/add-letsencrypt-domain.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Сертификат для одного нового домена, остальные не трогает: +# ./add-letsencrypt-domain.sh hub-new.oscript.io + +set -e + +domain="$1" +if [ -z "$domain" ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +if ! [ -x "$(command -v docker-compose)" ]; then + echo 'Error: docker-compose is not installed.' >&2 + exit 1 +fi + +rsa_key_size=4096 +data_path="./web/certbot" +email="ovsiankin.aa@gmail.com" +staging=0 + +if [ -d "$data_path/conf/live/$domain" ]; then + echo "Certificate for $domain already exists, nothing to do." >&2 + exit 1 +fi + +echo "### Creating dummy certificate for $domain ..." +path="/etc/letsencrypt/live/$domain" +mkdir -p "$data_path/conf/live/$domain" +docker-compose run --rm --no-deps --entrypoint "\ + openssl req -x509 -nodes -newkey rsa:2048 -days 1 \ + -keyout '$path/privkey.pem' \ + -out '$path/fullchain.pem' \ + -subj '/CN=localhost'" certbot + +echo "### Rebuilding nginx with the new site ..." +docker-compose build nginx +docker-compose up --force-recreate --no-deps -d nginx + +echo "### Deleting dummy certificate for $domain ..." +docker-compose run --rm --no-deps --entrypoint "\ + rm -Rf /etc/letsencrypt/live/$domain && \ + rm -Rf /etc/letsencrypt/archive/$domain && \ + rm -Rf /etc/letsencrypt/renewal/$domain.conf" certbot + +echo "### Requesting Let's Encrypt certificate for $domain ..." +if [ $staging != "0" ]; then staging_arg="--staging"; fi + +docker-compose run --rm --no-deps --entrypoint "\ + certbot certonly --webroot -w /var/www/certbot \ + $staging_arg \ + --email $email \ + -d $domain \ + --rsa-key-size $rsa_key_size \ + --agree-tos \ + --non-interactive" certbot + +echo "### Reloading nginx ..." +docker-compose exec nginx nginx -s reload diff --git a/docker-compose.yml b/docker-compose.yml index 11c218b..060bd3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -109,8 +109,77 @@ services: volumes: - minio_data:/data + # OpenHub — hub-new.oscript.io. Секреты — в .env, см. openhub.env.example + openhub: + image: segateekb/openhub:0.7.24 + restart: unless-stopped + environment: + - TZ=UTC + - OSHUB_INSTANCE_URL=https://hub-new.oscript.io + - OSHUB_INSTANCE_TRUSTED_PROXIES=10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + - data_ИсточникиДанных_ТипКоннектора=КоннекторPostgreSQL + - data_ИсточникиДанных_СтрокаСоединения=Host=openhub_db;Username=openhub;Password=${OPENHUB_DB_PASSWORD};Database=openhub + - OSHUB_STORAGE_BACKEND=s3 + - OSHUB_STORAGE_S3_ENDPOINT=http://minio:9000 + - OSHUB_STORAGE_S3_BUCKET=openhub + - OSHUB_STORAGE_S3_REGION=us-east-1 + - OSHUB_STORAGE_S3_FORCE__PATH__STYLE=true + - OSHUB_STORAGE_S3_ACCESS_KEY=${OPENHUB_S3_ACCESS_KEY} + - OSHUB_STORAGE_S3_SECRET_KEY=${OPENHUB_S3_SECRET_KEY} + - Kestrel__Limits__MaxRequestBodySize=134217728 + - OSHUB_PUBLISH_MAX__UPLOAD__BYTES=134217728 + - OSHUB_PUBLISH_MAX__UNPACKED__BYTES=268435456 + - OSHUB_PUBLISH_MAX__FILES=20000 + - OTEL_ENABLED=true + - OTEL_SERVICE_NAME=openhub + - OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318 + - OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf + - OTEL_ENTITY_QUERY_TEXT=true + volumes: + - openhub_data:/var/lib/openhub + depends_on: + - openhub_db + - minio + - lgtm + networks: + - default + - openhub + + openhub_db: + image: postgres:17-alpine + restart: unless-stopped + environment: + - POSTGRES_USER=openhub + - POSTGRES_DB=openhub + - POSTGRES_PASSWORD=${OPENHUB_DB_PASSWORD} + volumes: + - openhub_postgres_data:/var/lib/postgresql/data + networks: + - openhub + + # Мониторинг OpenHub одним контейнером: коллектор OTLP, Prometheus, Tempo, Loki и Grafana. + # Наружу смотрит только Grafana: grafana.oscript.io + lgtm: + image: grafana/otel-lgtm:0.33.1 + restart: unless-stopped + environment: + - GF_SERVER_ROOT_URL=https://grafana.oscript.io + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?задайте GRAFANA_ADMIN_PASSWORD в .env} + # В образе анонимный вход по умолчанию открыт с ролью Admin + - GF_AUTH_ANONYMOUS_ENABLED=false + volumes: + - lgtm_data:/data + - ./monitoring/grafana-dashboards.yaml:/otel-lgtm/grafana/conf/provisioning/dashboards/openhub.yaml:ro + - ./monitoring/openhub-dashboard.json:/otel-lgtm/openhub/openhub-dashboard.json:ro + +networks: + openhub: + volumes: web_content: jenkins_home: postgres_data: minio_data: + openhub_data: + openhub_postgres_data: + lgtm_data: diff --git a/init-letsencrypt.sh b/init-letsencrypt.sh index d5b2d91..1e4d3c6 100755 --- a/init-letsencrypt.sh +++ b/init-letsencrypt.sh @@ -5,7 +5,7 @@ if ! [ -x "$(command -v docker-compose)" ]; then exit 1 fi -domains=(hub.oscript.io oscript.io build.oscript.io s3.oscript.io) +domains=(hub.oscript.io oscript.io build.oscript.io s3.oscript.io hub-new.oscript.io grafana.oscript.io) rsa_key_size=4096 data_path="./web/certbot" ssl_conf_path="./web/nginx/ssl_conf" diff --git a/monitoring/grafana-dashboards.yaml b/monitoring/grafana-dashboards.yaml new file mode 100644 index 0000000..341d0ca --- /dev/null +++ b/monitoring/grafana-dashboards.yaml @@ -0,0 +1,13 @@ +apiVersion: 1 + +providers: + - name: OpenHub + orgId: 1 + folder: OpenHub + type: file + disableDeletion: false + allowUiUpdates: false + updateIntervalSeconds: 30 + options: + path: /otel-lgtm/openhub + foldersFromFilesStructure: false diff --git a/monitoring/openhub-dashboard.json b/monitoring/openhub-dashboard.json new file mode 100644 index 0000000..8b11faa --- /dev/null +++ b/monitoring/openhub-dashboard.json @@ -0,0 +1,845 @@ +{ + "uid": "openhub", + "title": "OpenHub — наблюдаемость", + "description": "Что хаб делает прямо сейчас: темп запросов, задержка и ошибки по маршрутам, методы контроллеров, логи и карта сервисов. Из любой панели есть переход в трассу.", + "tags": [ + "openhub" + ], + "timezone": "browser", + "editable": true, + "schemaVersion": 39, + "version": 4, + "refresh": "10s", + "time": { + "from": "now-30m", + "to": "now" + }, + "templating": { + "list": [ + { + "name": "service", + "label": "Служба", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "query": { + "qryType": 1, + "query": "label_values(traces_spanmetrics_calls_total, service)", + "refId": "СписокСлужб" + }, + "definition": "label_values(traces_spanmetrics_calls_total, service)", + "current": { + "text": "openhub", + "value": "openhub" + }, + "includeAll": false, + "multi": false, + "refresh": 2, + "sort": 1 + } + ] + }, + "panels": [ + { + "type": "row", + "id": 100, + "title": "Маршруты: темп, задержка, ошибки", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "collapsed": false, + "panels": [] + }, + { + "type": "stat", + "id": 1, + "title": "Запросов в секунду", + "description": "Суммарный темп обслуженных запросов. Считается генератором метрик Tempo по серверным спанам winow.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "unit": "reqps", + "decimals": 2, + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "graphMode": "area", + "colorMode": "value" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(traces_spanmetrics_calls_total{service=\"$service\", span_kind=\"SPAN_KIND_SERVER\"}[$__rate_interval]))", + "legendFormat": "всего", + "range": true + } + ] + }, + { + "type": "stat", + "id": 2, + "title": "Доля ошибочных запросов", + "description": "Отношение спанов со статусом Error ко всем серверным спанам. Статус Error winow ставит только на пятисотке — четырёхсотка это ответ, а не отказ.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "decimals": 2, + "min": 0, + "max": 1, + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 0.01 + }, + { + "color": "red", + "value": 0.05 + } + ] + } + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "graphMode": "area", + "colorMode": "value" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "(sum(rate(traces_spanmetrics_calls_total{service=\"$service\", span_kind=\"SPAN_KIND_SERVER\", status_code=\"STATUS_CODE_ERROR\"}[$__rate_interval])) or vector(0)) / clamp_min(sum(rate(traces_spanmetrics_calls_total{service=\"$service\", span_kind=\"SPAN_KIND_SERVER\"}[$__rate_interval])), 0.0000001)", + "legendFormat": "доля ошибок", + "range": true + } + ] + }, + { + "type": "timeseries", + "id": 3, + "title": "Задержка запросов: p50, p95, p99", + "description": "Квантили гистограммы traces_spanmetrics_latency. Точки-экземпляры на графике ведут в конкретную трассу.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 8, + "showPoints": "never" + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "p50", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(0.50, sum by (le) (rate(traces_spanmetrics_latency_bucket{service=\"$service\", span_kind=\"SPAN_KIND_SERVER\"}[$__rate_interval])))", + "legendFormat": "p50", + "range": true + }, + { + "refId": "p95", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(0.95, sum by (le) (rate(traces_spanmetrics_latency_bucket{service=\"$service\", span_kind=\"SPAN_KIND_SERVER\"}[$__rate_interval])))", + "legendFormat": "p95", + "range": true + }, + { + "refId": "p99", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(0.99, sum by (le) (rate(traces_spanmetrics_latency_bucket{service=\"$service\", span_kind=\"SPAN_KIND_SERVER\"}[$__rate_interval])))", + "legendFormat": "p99", + "range": true + } + ] + }, + { + "type": "timeseries", + "id": 4, + "title": "Темп запросов по маршрутам", + "description": "Разбивка по имени серверного спана — «метод шаблон_маршрута», а не по конкретному адресу.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 7 + }, + "fieldConfig": { + "defaults": { + "unit": "reqps", + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 12, + "stacking": { + "mode": "normal", + "group": "A" + }, + "showPoints": "never" + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by (span_name) (rate(traces_spanmetrics_calls_total{service=\"$service\", span_kind=\"SPAN_KIND_SERVER\"}[$__rate_interval]))", + "legendFormat": "{{span_name}}", + "range": true + } + ] + }, + { + "type": "table", + "id": 5, + "title": "Ответы по маршрутам и кодам", + "description": "Сколько запросов на каком маршруте чем закончилось за выбранный интервал. Ошибкой спан считается только на пятисотке, поэтому четырёхсотки видны здесь, а не в доле ошибок.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 13 + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "decimals": 0, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + } + } + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "sortBy": [ + { + "displayName": "Запросов", + "desc": true + } + ] + }, + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "renameByName": { + "span_name": "Маршрут", + "http_response_status_code": "Код ответа", + "Value": "Запросов" + } + } + } + ], + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "format": "table", + "instant": true, + "expr": "sum by (span_name, http_response_status_code) (increase(traces_spanmetrics_calls_total{service=\"$service\", span_kind=\"SPAN_KIND_SERVER\"}[$__range]))", + "range": false + } + ] + }, + { + "type": "table", + "id": 6, + "title": "Свежие трассы", + "description": "Поиск по Tempo. Клик по строке открывает трассу целиком; из спана внутри доступны переходы в логи и в метрики.", + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 13 + }, + "options": { + "showHeader": true + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "queryType": "traceql", + "query": "{ resource.service.name = \"$service\" }", + "limit": 20, + "tableType": "traces" + } + ] + }, + { + "type": "row", + "id": 200, + "title": "Методы контроллеров: метрики самого хаба", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "collapsed": false, + "panels": [] + }, + { + "type": "timeseries", + "id": 7, + "title": "Задержка методов, среднее", + "description": "Среднее время вызова точки маршрута: сумма длительностей, делённая на число вызовов. Не p95: autumn-opentelemetry 1.1.0 пишет длительность в секундах, но границы корзин гистограмме не задаёт, и SDK ставит умолчательные миллисекундные ([0, 5, 10 … 10000]) — все секундные значения падают в первую корзину, и histogram_quantile вернул бы не задержку, а границу корзины. Среднее от границ не зависит; p95 вернётся, когда библиотека передаст секундные границы (issues/ЧЕРНОВИК-AUTUMN-OTEL-секунды-при-миллисекундных-границах.md). Отбор code_namespace!=\"\" оставляет метрики точек маршрута: под ту же маску имён попали гистограммы базы данных, у которых этой метки нет. Окно rate — 5 мин фиксировано: метрики хаба экспортируются раз в 60 с, и авто-интервал Grafana на коротких диапазонах пустеет.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 21 + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never" + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "calcs": [ + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by (code_namespace, code_function_name) (rate({__name__=~\".+_duration_seconds_sum\", service_name=\"$service\", code_namespace!=\"\"}[5m])) / sum by (code_namespace, code_function_name) (rate({__name__=~\".+_duration_seconds_count\", service_name=\"$service\", code_namespace!=\"\"}[5m]))", + "legendFormat": "{{code_namespace}}.{{code_function_name}}", + "range": true + } + ] + }, + { + "type": "timeseries", + "id": 8, + "title": "Вызовы методов в секунду", + "description": "Счётчики *.counted той же точки маршрута. Метка result отделяет успех от отказа. Окно rate — 5 мин фиксировано: метрики хаба экспортируются раз в 60 с, и авто-интервал Grafana на коротких диапазонах пустеет.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 21 + }, + "fieldConfig": { + "defaults": { + "unit": "ops", + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 12, + "showPoints": "never" + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by (code_namespace, code_function_name, result) (rate({__name__=~\".+_counted_total\", service_name=\"$service\"}[5m]))", + "legendFormat": "{{code_namespace}}.{{code_function_name}} — {{result}}", + "range": true + } + ] + }, + { + "type": "row", + "id": 250, + "title": "База данных: операции и хранилища", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 29 + }, + "collapsed": false, + "panels": [] + }, + { + "type": "timeseries", + "id": 11, + "title": "Задержка обращений к СУБД, p95", + "description": "Гистограмма db.client.operation.duration наблюдателя источника данных entity (opentelemetry-instrumentation-entity). Разбивка по операции СУБД: SELECT, INSERT, COMMIT. Пусто — значит отключено otel.entity.enabled=false.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 30 + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never" + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "calcs": [ + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum by (le, db_operation_name) (rate(db_client_operation_duration_seconds_bucket{service_name=\"$service\"}[5m])))", + "legendFormat": "{{db_operation_name}}", + "range": true + } + ] + }, + { + "type": "timeseries", + "id": 12, + "title": "Вызовы хранилищ сущностей, p95", + "description": "Гистограмма entity.repository.invocation.duration напильника слоя хранилищ. Разбивка по хранилищу и методу; метка entity_repository_state отделяет отказ от успеха. Пусто — значит отключено otel.entity.repository.enabled=false.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 30 + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never" + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "calcs": [ + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum by (le, entity_repository, code_function_name) (rate(entity_repository_invocation_duration_seconds_bucket{service_name=\"$service\"}[5m])))", + "legendFormat": "{{entity_repository}}.{{code_function_name}}", + "range": true + } + ] + }, + { + "type": "timeseries", + "id": 13, + "title": "Пул соединений с БД", + "description": "Датчики db.client.connection.* наблюдателя источника данных entity: занятые и свободные соединения пула, его предел (РазмерПула) и потоки, ждущие соединения. Ожидающие выше нуля — пул мал для нагрузки; занятые у предела — то же самое, только раньше.", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 30 + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "decimals": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "предел" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "fill": "dash", + "dash": [ + 10, + 10 + ] + } + } + ] + } + ] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "calcs": [ + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "db_client_connection_count{service_name=\"$service\"}", + "legendFormat": "{{db_client_connection_state}}", + "range": true + }, + { + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "db_client_connection_max{service_name=\"$service\"}", + "legendFormat": "предел", + "range": true + }, + { + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "db_client_connection_pending_requests{service_name=\"$service\"}", + "legendFormat": "ожидают", + "range": true + } + ] + }, + { + "type": "row", + "id": 300, + "title": "Логи и карта сервисов", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "collapsed": false, + "panels": [] + }, + { + "type": "logs", + "id": 9, + "title": "Журнал хаба", + "description": "Строки, приехавшие мостом logos → OTel. У строки с trace_id есть переход в трассу — поле trace_id живёт в structured metadata.", + "datasource": { + "type": "loki", + "uid": "loki" + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 39 + }, + "options": { + "showTime": true, + "showLabels": false, + "showCommonLabels": false, + "wrapLogMessage": true, + "prettifyLogMessage": false, + "enableLogDetails": true, + "dedupStrategy": "none", + "sortOrder": "Descending" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "{service_name=~\"$service\"}", + "queryType": "range" + } + ] + }, + { + "type": "nodeGraph", + "id": 10, + "title": "Карта сервисов", + "description": "Строится из метрик service-graph, которые генератор Tempo считает по трассам.", + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 16, + "y": 39 + }, + "options": {}, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "queryType": "serviceMap" + } + ] + } + ] +} diff --git a/openhub.env.example b/openhub.env.example new file mode 100644 index 0000000..4e38103 --- /dev/null +++ b/openhub.env.example @@ -0,0 +1,11 @@ +# Добавьте в .env в корне репозитория и задайте значения на сервере. + +# Пароль базы хаба; применяется только при первом запуске. Без точки с запятой. +OPENHUB_DB_PASSWORD=change-me-strong-password + +# Учётка хаба в MinIO; её и бакет openhub заводят руками +OPENHUB_S3_ACCESS_KEY=openhub +OPENHUB_S3_SECRET_KEY=change-me-strong-secret + +# Обязателен: Grafana смотрит в интернет (grafana.oscript.io) +GRAFANA_ADMIN_PASSWORD=change-me-strong-password diff --git a/web/nginx/sites-enabled/grafana.oscript.io b/web/nginx/sites-enabled/grafana.oscript.io new file mode 100644 index 0000000..c83d356 --- /dev/null +++ b/web/nginx/sites-enabled/grafana.oscript.io @@ -0,0 +1,55 @@ +server { + listen 80; + listen [::]:80; + server_name grafana.oscript.io; + + access_log /var/log/nginx/access.log with_host; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name grafana.oscript.io; + + access_log /var/log/nginx/access.log with_host; + + add_header X-Content-Type-Options nosniff; + add_header X-Robots-Tag none; + add_header X-Download-Options noopen; + add_header X-Permitted-Cross-Domain-Policies none; + + add_header Strict-Transport-Security "max-age=31536000" always; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_redirect off; + + resolver 127.0.0.11 valid=30s; + + location / { + set $target_url http://lgtm:3000; + proxy_pass $target_url; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + } + + include /etc/nginx/ssl_conf/options-ssl-nginx.conf; + ssl_dhparam /etc/nginx/ssl_conf/ssl-dhparams.pem; + ssl_certificate /etc/letsencrypt/live/grafana.oscript.io/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/grafana.oscript.io/privkey.pem; +} diff --git a/web/nginx/sites-enabled/hub-new.oscript.io b/web/nginx/sites-enabled/hub-new.oscript.io new file mode 100644 index 0000000..a7beea1 --- /dev/null +++ b/web/nginx/sites-enabled/hub-new.oscript.io @@ -0,0 +1,58 @@ +server { + listen 80; + listen [::]:80; + server_name hub-new.oscript.io; + + access_log /var/log/nginx/access.log with_host; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name hub-new.oscript.io; + + access_log /var/log/nginx/access.log with_host; + + add_header X-Content-Type-Options nosniff; + add_header X-Download-Options noopen; + add_header X-Permitted-Cross-Domain-Policies none; + + add_header Strict-Transport-Security "max-age=31536000" always; + + client_max_body_size 128M; + + proxy_redirect off; + + resolver 127.0.0.11 valid=30s; + + location / { + gzip off; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + proxy_request_buffering off; + proxy_read_timeout 300; + proxy_send_timeout 300; + + set $target_url http://openhub:3333; + proxy_pass $target_url; + } + + include /etc/nginx/ssl_conf/options-ssl-nginx.conf; + ssl_dhparam /etc/nginx/ssl_conf/ssl-dhparams.pem; + ssl_certificate /etc/letsencrypt/live/hub-new.oscript.io/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/hub-new.oscript.io/privkey.pem; +}