├── configs ├── grafana │ ├── ds_prometheus.yml │ ├── datasource.yml │ └── dashboard.json ├── prometheus │ └── prometheus.yml ├── promtail │ └── promtail.yaml └── tempo │ └── tempo.yml ├── docker.env ├── k6lib ├── http_post.js └── http_gets.js ├── app ├── Dockerfile ├── requirements.txt └── server.py ├── Makefile ├── README.md └── docker-compose.yml /configs/grafana/ds_prometheus.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'Prometheus' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | options: 11 | path: /etc/grafana/provisioning/dashboards 12 | -------------------------------------------------------------------------------- /docker.env: -------------------------------------------------------------------------------- 1 | GF_AUTH_ANONYMOUS_ENABLED=true 2 | GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 3 | GF_AUTH_DISABLE_LOGIN_FORM=true 4 | GF_FEATURE_TOGGLES_ENABLE=traceqlEditor 5 | # https://grafana.com/grafana/plugins/?type=panel 6 | GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource,simpod-json-datasource,marcusolsson-json-datasource,knightss27-weathermap-panel 7 | -------------------------------------------------------------------------------- /k6lib/http_post.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | 3 | export default function () { 4 | const url = 'http://app:5000/users'; 5 | const payload = JSON.stringify({ 6 | username: 'k6', 7 | email: 'k6@k6.io', 8 | }); 9 | 10 | const params = { 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | }; 15 | 16 | http.post(url, payload, params); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | WORKDIR /src 3 | RUN apt update && apt install python3-dev build-essential -y && pip install -U pip && pip install --upgrade setuptools && pip install wheel 4 | COPY requirements.txt /src/requirements.txt 5 | RUN pip install -r requirements.txt 6 | COPY server.py /src/server.py 7 | #CMD ["gunicorn", "server:app", "--bind", "0.0.0.0:80", "--capture-output", "--access-logfile", "'-'", "--error-logfile", "'-'"] 8 | CMD ["gunicorn", "server:app", "--bind", "0.0.0.0:5000"] 9 | #CMD [ "python", "server.py" ] -------------------------------------------------------------------------------- /k6lib/http_gets.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { check, sleep } from 'k6'; 3 | 4 | export let options = { 5 | vus: 5, 6 | duration: '20s', 7 | }; 8 | 9 | export default function () { 10 | const res = http.get('http://app:5000/users/1'); 11 | const params = { 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | }, 15 | }; 16 | 17 | check(res, { 18 | 'is status 200': (r) => r.status === 200, 19 | 'returns k6@k6.io': (r) => r.body.includes('k6@k6.io'), 20 | }); 21 | sleep(1); 22 | } 23 | -------------------------------------------------------------------------------- /configs/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | #rule_files: 6 | # - "rules.yml" 7 | 8 | scrape_configs: 9 | - job_name: 'prometheus' 10 | static_configs: 11 | - targets: ['prometheus:9090'] 12 | 13 | - job_name: 'tempo' 14 | static_configs: 15 | - targets: [ 'tempo:3200' ] 16 | 17 | - job_name: 'promtail' 18 | static_configs: 19 | - targets: [ 'tempo:9080' ] 20 | 21 | - job_name: 'loki' 22 | static_configs: 23 | - targets: [ 'loki:3100' ] 24 | 25 | - job_name: 'flask-app' 26 | scrape_interval: 5s 27 | static_configs: 28 | - targets: ['app:5000'] -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.0.4 2 | Flask==2.0.3 3 | flask-marshmallow==0.14.0 4 | Flask-SQLAlchemy==2.5.1 5 | greenlet==1.1.2 6 | importlib-metadata==4.11.3 7 | itsdangerous==2.1.2 8 | Jinja2==3.1.1 9 | MarkupSafe==2.1.1 10 | marshmallow==3.15.0 11 | marshmallow-sqlalchemy==0.28.0 12 | packaging==21.3 13 | pyparsing==3.0.7 14 | six==1.16.0 15 | SQLAlchemy==1.4.32 16 | typing_extensions==4.1.1 17 | Werkzeug==2.0.3 18 | zipp==3.7.0 19 | gunicorn==20.1.0 20 | prometheus-client==0.13.1 21 | prometheus-flask-exporter==0.19.0 22 | opentelemetry-api #==1.0.0 23 | opentelemetry-sdk #==1.0.0 24 | opentelemetry-exporter-otlp #==1.0.0 25 | opentelemetry-instrumentation-flask #==0.19b0 26 | opentelemetry-instrumentation-requests #==0.19b0 27 | requests==2.22.0 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Thanks: https://gist.github.com/mpneuried/0594963ad38e68917ef189b4e6a269db 2 | .PHONY: help 3 | 4 | HAS_DOCKER_COMPOSE := $(shell command -v docker-compose 2> /dev/null) 5 | HAS_DOCKER_COMPOSE_V2 := $(shell command -v docker 2> /dev/null) 6 | 7 | ifeq ($(strip $(HAS_DOCKER_COMPOSE)),) 8 | ifeq ($(strip $(HAS_DOCKER_COMPOSE_V2)),) 9 | $(error No compatible command found) 10 | else 11 | DOCKER_COMPOSE_BINARY := docker compose 12 | endif 13 | else 14 | DOCKER_COMPOSE_BINARY := docker-compose 15 | endif 16 | 17 | help: ## This help. 18 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 19 | 20 | .DEFAULT_GOAL := help 21 | 22 | # DOCKER TASKS 23 | up: ## Runs the containers in detached mode 24 | @$(DOCKER_COMPOSE_BINARY) up -d --build 25 | 26 | clean: ## Stops and removes all containers 27 | @$(DOCKER_COMPOSE_BINARY) down 28 | 29 | logs: ## View the logs from the containers 30 | @$(DOCKER_COMPOSE_BINARY) logs -f 31 | 32 | open: ## Opens tabs in container 33 | open http://localhost:3000/ 34 | -------------------------------------------------------------------------------- /configs/promtail/promtail.yaml: -------------------------------------------------------------------------------- 1 | # https://grafana.com/docs/loki/latest/clients/promtail/configuration/ 2 | # https://docs.docker.com/engine/api/v1.41/#operation/ContainerList 3 | server: 4 | http_listen_port: 9080 5 | grpc_listen_port: 0 6 | 7 | positions: 8 | filename: /tmp/positions.yaml 9 | 10 | clients: 11 | - url: http://loki:3100/loki/api/v1/push 12 | 13 | scrape_configs: 14 | - job_name: docker-socket-scrape 15 | docker_sd_configs: 16 | - host: unix:///var/run/docker.sock 17 | refresh_interval: 5s 18 | filters: 19 | - name: label 20 | values: ["logging=promtail"] 21 | relabel_configs: 22 | - source_labels: ['__meta_docker_container_name'] 23 | regex: '/(.*)' 24 | target_label: 'container' 25 | - source_labels: ['__meta_docker_container_log_stream'] 26 | target_label: 'logstream' 27 | - source_labels: ['__meta_docker_container_label_logging_jobname'] 28 | target_label: 'job' 29 | pipeline_stages: 30 | - cri: {} 31 | - multiline: 32 | firstline: ^\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}:\d{2},\d{3} 33 | max_wait_time: 3s 34 | # https://grafana.com/docs/loki/latest/clients/promtail/stages/json/ 35 | - json: 36 | expressions: 37 | #message: message 38 | level: level 39 | #output: 'message' 40 | - match: 41 | selector: '{job="containerlogs", container="app"} |= "/metrics?"' 42 | action: drop 43 | drop_counter_reason: loki_metrics_endpoint 44 | -------------------------------------------------------------------------------- /configs/grafana/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | deleteDatasources: 4 | - name: Prometheus 5 | orgId: 1 6 | 7 | datasources: 8 | - name: Prometheus 9 | type: prometheus 10 | uid: prometheus 11 | access: proxy 12 | url: http://prometheus:9090 13 | isDefault: true 14 | editable: true 15 | jsonData: 16 | httpMethod: POST 17 | manageAlerts: true 18 | prometheusType: Prometheus 19 | prometheusVersion: 2.37.6 20 | exemplarTraceIdDestinations: 21 | - datasourceUid: tempo 22 | name: trace_id 23 | - name: Tempo 24 | type: tempo 25 | uid: tempo 26 | access: proxy 27 | url: http://tempo:3200 28 | basicAuth: false 29 | isDefault: false 30 | version: 1 31 | editable: true 32 | apiVersion: 1 33 | jsonData: 34 | httpMethod: GET 35 | tracesToLogs: 36 | datasourceUid: 'loki' 37 | tags: ['job', 'container', 'job', 'logstream'] 38 | mappedTags: [{ key: 'service.name', value: 'service' }] 39 | mapTagNamesEnabled: false 40 | spanStartTimeShift: '1h' 41 | spanEndTimeShift: '1h' 42 | filterByTraceID: false 43 | filterBySpanID: false 44 | tracesToMetrics: 45 | datasourceUid: 'prometheus' 46 | tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }] 47 | queries: 48 | - name: 'Sample query' 49 | query: 'sum(rate(traces_spanmetrics_latency_bucket{$__tags}[5m]))' 50 | serviceMap: 51 | datasourceUid: 'prometheus' 52 | search: 53 | hide: false 54 | nodeGraph: 55 | enabled: true 56 | lokiSearch: 57 | datasourceUid: 'loki' 58 | - name: Loki 59 | type: loki 60 | uid: loki 61 | access: proxy 62 | url: http://loki:3100 63 | version: 1 64 | editable: true 65 | isDefault: false 66 | jsonData: 67 | maxLines: 1000 68 | derivedFields: 69 | - datasourceUid: tempo 70 | matcherRegex: "trace_id=(\\w+)" 71 | name: TraceID 72 | url: '$${__value.raw}' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grafana-observability-primer 2 | Grafana Observability Primer 3 | 4 | ## stack 5 | 6 | Deploy: 7 | 8 | ```bash 9 | docker-compose up -d --build 10 | ``` 11 | 12 | ## k6 13 | 14 | Create user: 15 | 16 | ```bash 17 | docker run --rm -i --network=docknet loadimpact/k6 run --quiet - < k6lib/http_post.js 18 | ``` 19 | 20 | Run tests to perform get requests: 21 | 22 | ```bash 23 | docker run --rm -i --network=docknet loadimpact/k6 run --quiet - < k6lib/http_gets.js 24 | ``` 25 | 26 | ## usage 27 | 28 | API Usage: 29 | 30 | ``` 31 | # list all users 32 | curl -H 'Content-Type: application/json' http://localhost:5000/users 33 | ``` 34 | 35 | ``` 36 | # create user 37 | curl -XPOST -H 'Content-Type: application/json' http://localhost:5000/users -d '{"username": "ruan", "email": "ruan@localhost"}' 38 | ``` 39 | 40 | ## grafana screenshots 41 | 42 | CPU and Memory: 43 | 44 | ![image](https://user-images.githubusercontent.com/567298/160496251-76fea7a6-11fa-419c-a9fc-2f9b2c2f2604.png) 45 | 46 | Requests per Second: 47 | 48 | ![image](https://user-images.githubusercontent.com/567298/160496321-3b42bdf3-ce19-4c68-b0bd-4961c9fac24c.png) 49 | 50 | Average Response Time: 51 | 52 | ![image](https://user-images.githubusercontent.com/567298/160496357-08a0d009-265f-4cd7-ad02-635d7e1d58f1.png) 53 | 54 | Response Duration: 55 | 56 | ![image](https://user-images.githubusercontent.com/567298/160496393-8a65a499-882a-49ad-8f7d-1157fff4063a.png) 57 | 58 | View the logs from our dashboard: 59 | 60 | ![logs](https://user-images.githubusercontent.com/567298/227045215-8d39086f-b329-485a-85c4-d5b7659d545f.png) 61 | 62 | After we created a user, we can explore our logs dashboard and view the traceid: 63 | 64 | ![logs-to-traces](https://user-images.githubusercontent.com/567298/227044932-ab6f30e0-ff09-48dc-b8ec-24860732ccfb.png) 65 | 66 | ## Stargazers over time 67 | 68 | [![Stargazers over time](https://starchart.cc/ruanbekker/grafana-observability-primer.svg)](https://starchart.cc/ruanbekker/grafana-observability-primer) 69 | -------------------------------------------------------------------------------- /configs/tempo/tempo.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/grafana/tempo/blob/main/example/docker-compose/shared/tempo.yaml 2 | auth_enabled: false 3 | 4 | server: 5 | http_listen_port: 3200 6 | 7 | distributor: 8 | receivers: # this configuration will listen on all ports and protocols that tempo is capable of. 9 | jaeger: # the receives all come from the OpenTelemetry collector. more configuration information can 10 | protocols: # be found there: https://github.com/open-telemetry/opentelemetry-collector/tree/master/receiver 11 | thrift_http: # 12 | grpc: # for a production deployment you should only enable the receivers you need! 13 | thrift_binary: 14 | thrift_compact: 15 | zipkin: # :9411 zipkin 16 | otlp: 17 | protocols: 18 | http: # :4318 otlp http 19 | grpc: # :4317 otlp grpc 20 | opencensus: 21 | 22 | ingester: 23 | trace_idle_period: 10s # the length of time after a trace has not received spans to consider it complete and flush it 24 | max_block_bytes: 1_000_000 # cut the head block when it hits this size or ... 25 | max_block_duration: 5m # this much time passes 26 | 27 | compactor: 28 | compaction: 29 | compaction_window: 1h # blocks in this time window will be compacted together 30 | max_block_bytes: 100_000_000 # maximum size of compacted blocks 31 | block_retention: 1h 32 | compacted_block_retention: 10m 33 | 34 | storage: 35 | trace: 36 | backend: local # backend configuration to use 37 | block: 38 | bloom_filter_false_positive: .05 # bloom filter false positive rate. lower values create larger filters but fewer false positives 39 | v2_index_downsample_bytes: 1000 # number of bytes per index record 40 | v2_encoding: zstd # block encoding/compression. options: none, gzip, lz4-64k, lz4-256k, lz4-1M, lz4, snappy, zstd 41 | wal: 42 | path: /tmp/tempo/wal # where to store the the wal locally 43 | v2_encoding: none # wal encoding/compression. options: none, gzip, lz4-64k, lz4-256k, lz4-1M, lz4, snappy, zstd 44 | local: 45 | path: /tmp/tempo/blocks 46 | pool: 47 | max_workers: 100 # the worker pool mainly drives querying, but is also used for polling the blocklist 48 | queue_depth: 10000 49 | 50 | metrics_generator: 51 | registry: 52 | external_labels: 53 | source: tempo 54 | cluster: docker-compose 55 | storage: 56 | path: /tmp/tempo/generator/wal 57 | remote_write: 58 | - url: http://prometheus:9090/api/v1/write 59 | send_exemplars: true 60 | 61 | overrides: 62 | metrics_generator_processors: [service-graphs, span-metrics] # enables metrics generator 63 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | app: 4 | build: ./app 5 | container_name: app 6 | restart: unless-stopped 7 | labels: 8 | logging: "promtail" 9 | logging_jobname: "containerlogs" 10 | environment: 11 | - AGENT_HOSTNAME=tempo 12 | - AGENT_PORT=4317 13 | networks: 14 | - docknet 15 | depends_on: 16 | - prometheus 17 | ports: 18 | - 5000:5000 19 | 20 | tempo: 21 | image: grafana/tempo:2.0.1 22 | container_name: tempo 23 | command: ["-config.file=/etc/tempo.yaml"] 24 | ports: 25 | - "3200:3200" # tempo 26 | - "55680:55680" 27 | #- "14268:14268" # jaeger ingest 28 | #- "4317:4317" # otlp grpc 29 | #- "4318:4318" # otlp http 30 | #- "9411:9411" # zipkin 31 | volumes: 32 | - ./configs/tempo/tempo.yml:/etc/tempo.yaml 33 | #- ./data/tempo:/tmp/tempo 34 | networks: 35 | - docknet 36 | 37 | prometheus: 38 | image: prom/prometheus:v2.37.6 39 | container_name: prometheus 40 | restart: unless-stopped 41 | ports: 42 | - 9090:9090 43 | volumes: 44 | - ./configs/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 45 | command: 46 | - '--config.file=/etc/prometheus/prometheus.yml' 47 | #- '--storage.tsdb.path=/data' 48 | - '--storage.tsdb.retention.time=2d' 49 | - '--web.console.libraries=/etc/prometheus/console_libraries' 50 | - '--web.console.templates=/etc/prometheus/consoles' 51 | - '--web.enable-lifecycle' 52 | - '--web.enable-remote-write-receiver' 53 | #- '--enable-feature=exemplar-storage' 54 | networks: 55 | - docknet 56 | 57 | grafana: 58 | image: grafana/grafana:9.5.3 59 | container_name: grafana 60 | restart: unless-stopped 61 | depends_on: 62 | - prometheus 63 | - loki 64 | ports: 65 | - 3000:3000 66 | volumes: 67 | - ./configs/grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml 68 | - ./configs/grafana/ds_prometheus.yml:/etc/grafana/provisioning/dashboards/ds_prometheus.yml 69 | - ./configs/grafana/dashboard.json:/etc/grafana/provisioning/dashboards/dashboard.json 70 | env_file: 71 | - ./docker.env 72 | networks: 73 | - docknet 74 | 75 | loki: 76 | image: grafana/loki:2.8.2 77 | container_name: loki 78 | restart: unless-stopped 79 | ports: 80 | - 3100:3100 81 | command: -config.file=/etc/loki/local-config.yaml 82 | #volumes: 83 | # - ./configs/loki/config.yaml:/etc/loki/local-config.yaml 84 | networks: 85 | - docknet 86 | 87 | promtail: 88 | image: grafana/promtail:2.7.4 89 | container_name: promtail 90 | volumes: 91 | - ./configs/promtail/promtail.yaml:/etc/promtail/docker-config.yaml 92 | - /var/lib/docker/containers:/var/lib/docker/containers:ro 93 | - /var/run/docker.sock:/var/run/docker.sock 94 | command: -config.file=/etc/promtail/docker-config.yaml 95 | depends_on: 96 | - loki 97 | networks: 98 | - docknet 99 | 100 | networks: 101 | docknet: 102 | name: docknet 103 | -------------------------------------------------------------------------------- /app/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from flask import Flask, request, jsonify 4 | from flask.logging import default_handler 5 | from flask_sqlalchemy import SQLAlchemy 6 | from flask_marshmallow import Marshmallow 7 | from sqlalchemy.exc import IntegrityError 8 | from prometheus_flask_exporter import PrometheusMetrics 9 | from time import sleep 10 | from opentelemetry import trace 11 | from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter 12 | from opentelemetry.instrumentation.flask import FlaskInstrumentor 13 | from opentelemetry.sdk.trace import TracerProvider 14 | from opentelemetry.sdk.trace.export import BatchSpanProcessor 15 | from opentelemetry.sdk.resources import SERVICE_NAME, Resource 16 | from random import random 17 | from time import strftime 18 | 19 | AGENT_HOSTNAME = os.getenv("AGENT_HOSTNAME", "tempo") 20 | AGENT_PORT = int(os.getenv("AGENT_PORT", "4317")) 21 | 22 | class SpanFormatter(logging.Formatter): 23 | def format(self, record): 24 | trace_id = trace.get_current_span().get_span_context().trace_id 25 | if trace_id == 0: 26 | record.trace_id = None 27 | else: 28 | record.trace_id = "{trace:032x}".format(trace=trace_id) 29 | return super().format(record) 30 | 31 | resource = Resource(attributes={ 32 | "service.name": "service-api" 33 | }) 34 | 35 | trace.set_tracer_provider( 36 | TracerProvider(resource=resource) 37 | ) 38 | otlp_exporter = OTLPSpanExporter(endpoint=f"{AGENT_HOSTNAME}:{AGENT_PORT}", insecure=True) 39 | trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(otlp_exporter)) 40 | 41 | # trace_provider = TracerProvider( 42 | # resource=Resource.create({"service.name": "my-flask-app"}), 43 | # ) 44 | # trace_provider.add_span_processor( 45 | # SimpleExportSpanProcessor(otlp_exporter) 46 | # ) 47 | 48 | #logging.basicConfig(level=logging.INFO) 49 | #logging.info("LOGLEVEL=INFO") 50 | 51 | app = Flask(__name__) 52 | basedir = os.path.abspath(os.path.dirname(__file__)) 53 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'db.sqlite') 54 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 55 | app.config['JSON_SORT_KEYS'] = False 56 | 57 | FlaskInstrumentor().instrument_app(app) 58 | 59 | log = logging.getLogger("werkzeug") 60 | log.setLevel(logging.ERROR) 61 | app.logger.setLevel(logging.INFO) 62 | default_handler.setFormatter( 63 | SpanFormatter( 64 | 'time="%(asctime)s" service=%(name)s level=%(levelname)s %(message)s trace_id=%(trace_id)s' 65 | ) 66 | ) 67 | 68 | metrics = PrometheusMetrics(app) 69 | metrics.info("app_info", "grafana observability primer", version="1.0.0") 70 | 71 | db = SQLAlchemy(app) 72 | ma = Marshmallow(app) 73 | 74 | class User(db.Model): 75 | id = db.Column(db.Integer, primary_key=True) 76 | username = db.Column(db.String(80), unique=True) 77 | email = db.Column(db.String(120), unique=True) 78 | 79 | def __init__(self, username, email): 80 | self.username = username 81 | self.email = email 82 | 83 | class UserSchema(ma.Schema): 84 | class Meta: 85 | fields = ('id', 'username', 'email') 86 | 87 | user_schema = UserSchema() 88 | users_schema = UserSchema(many=True) 89 | 90 | @app.before_first_request 91 | def create_database(): 92 | db.create_all() 93 | return 'created' 94 | 95 | @app.route("/edge") 96 | def bar(): 97 | random_value = random() # random number in range [0.0,1.0) 98 | if random_value < 0.05: 99 | return "Edge case!", 500 100 | return "ok" 101 | 102 | @app.after_request 103 | def after_request(response): 104 | app.logger.info( 105 | 'addr="%s" method=%s scheme=%s path="%s" status=%s', 106 | request.remote_addr, 107 | request.method, 108 | request.scheme, 109 | request.full_path, 110 | response.status_code, 111 | ) 112 | return response 113 | 114 | @app.route("/users", methods=["POST"]) 115 | def add_user(): 116 | tracer = trace.get_tracer(__name__) 117 | with tracer.start_as_current_span("db-entry"): 118 | username = request.json['username'] 119 | email = request.json['email'] 120 | try: 121 | new_user = User(username, email) 122 | db.session.add(new_user) 123 | db.session.commit() 124 | result = user_schema.dump(new_user) 125 | except IntegrityError: 126 | db.session.rollback() 127 | result = {"message": "user {} already exists".format(username)} 128 | return jsonify(result) 129 | 130 | @app.route("/users", methods=["GET"]) 131 | def get_user(): 132 | all_users = User.query.all() 133 | result = users_schema.dump(all_users) 134 | return jsonify(result) 135 | 136 | @app.route("/users/", methods=["GET"]) 137 | def user_detail(id): 138 | user = User.query.get(id) 139 | return user_schema.jsonify(user) 140 | 141 | @app.route("/users/", methods=["PUT"]) 142 | def user_update(id): 143 | user = User.query.get(id) 144 | username = request.json['username'] 145 | email = request.json['email'] 146 | user.email = email 147 | user.username = username 148 | db.session.commit() 149 | return user_schema.jsonify(user) 150 | 151 | @app.route("/users/", methods=["DELETE"]) 152 | def user_delete(id): 153 | user = User.query.get(id) 154 | db.session.delete(user) 155 | db.session.commit() 156 | return user_schema.jsonify(user) 157 | 158 | if __name__ == '__main__': 159 | app.run(host='0.0.0.0', port=5000, debug=False) 160 | -------------------------------------------------------------------------------- /configs/grafana/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "datasource", 8 | "uid": "grafana" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | } 22 | ] 23 | }, 24 | "editable": true, 25 | "fiscalYearStartMonth": 0, 26 | "graphTooltip": 0, 27 | "links": [], 28 | "liveNow": false, 29 | "panels": [ 30 | { 31 | "datasource": { 32 | "type": "prometheus", 33 | "uid": "prometheus" 34 | }, 35 | "fieldConfig": { 36 | "defaults": { 37 | "color": { 38 | "mode": "palette-classic" 39 | }, 40 | "custom": { 41 | "axisCenteredZero": false, 42 | "axisColorMode": "text", 43 | "axisLabel": "", 44 | "axisPlacement": "auto", 45 | "barAlignment": 0, 46 | "drawStyle": "line", 47 | "fillOpacity": 0, 48 | "gradientMode": "none", 49 | "hideFrom": { 50 | "legend": false, 51 | "tooltip": false, 52 | "viz": false 53 | }, 54 | "lineInterpolation": "linear", 55 | "lineWidth": 1, 56 | "pointSize": 5, 57 | "scaleDistribution": { 58 | "type": "linear" 59 | }, 60 | "showPoints": "auto", 61 | "spanNulls": false, 62 | "stacking": { 63 | "group": "A", 64 | "mode": "none" 65 | }, 66 | "thresholdsStyle": { 67 | "mode": "off" 68 | } 69 | }, 70 | "decimals": 2, 71 | "mappings": [], 72 | "thresholds": { 73 | "mode": "absolute", 74 | "steps": [ 75 | { 76 | "color": "green", 77 | "value": null 78 | }, 79 | { 80 | "color": "red", 81 | "value": 80 82 | } 83 | ] 84 | }, 85 | "unit": "percent" 86 | }, 87 | "overrides": [] 88 | }, 89 | "gridPos": { 90 | "h": 7, 91 | "w": 13, 92 | "x": 0, 93 | "y": 0 94 | }, 95 | "id": 2, 96 | "options": { 97 | "legend": { 98 | "calcs": [ 99 | "mean", 100 | "lastNotNull" 101 | ], 102 | "displayMode": "table", 103 | "placement": "bottom", 104 | "showLegend": true 105 | }, 106 | "tooltip": { 107 | "mode": "single", 108 | "sort": "none" 109 | } 110 | }, 111 | "targets": [ 112 | { 113 | "datasource": { 114 | "type": "prometheus", 115 | "uid": "prometheus" 116 | }, 117 | "editorMode": "code", 118 | "exemplar": true, 119 | "expr": "rate(process_cpu_seconds_total{job=\"flask-app\"}[30s])", 120 | "interval": "", 121 | "legendFormat": "{{job}}", 122 | "range": true, 123 | "refId": "A" 124 | } 125 | ], 126 | "title": "CPU Utilisation", 127 | "type": "timeseries" 128 | }, 129 | { 130 | "datasource": { 131 | "type": "prometheus", 132 | "uid": "prometheus" 133 | }, 134 | "fieldConfig": { 135 | "defaults": { 136 | "color": { 137 | "mode": "palette-classic" 138 | }, 139 | "custom": { 140 | "axisCenteredZero": false, 141 | "axisColorMode": "text", 142 | "axisLabel": "", 143 | "axisPlacement": "auto", 144 | "barAlignment": 0, 145 | "drawStyle": "line", 146 | "fillOpacity": 0, 147 | "gradientMode": "none", 148 | "hideFrom": { 149 | "legend": false, 150 | "tooltip": false, 151 | "viz": false 152 | }, 153 | "lineInterpolation": "linear", 154 | "lineWidth": 1, 155 | "pointSize": 5, 156 | "scaleDistribution": { 157 | "type": "linear" 158 | }, 159 | "showPoints": "auto", 160 | "spanNulls": false, 161 | "stacking": { 162 | "group": "A", 163 | "mode": "none" 164 | }, 165 | "thresholdsStyle": { 166 | "mode": "off" 167 | } 168 | }, 169 | "mappings": [], 170 | "thresholds": { 171 | "mode": "absolute", 172 | "steps": [ 173 | { 174 | "color": "green", 175 | "value": null 176 | }, 177 | { 178 | "color": "red", 179 | "value": 80 180 | } 181 | ] 182 | }, 183 | "unit": "bytes" 184 | }, 185 | "overrides": [] 186 | }, 187 | "gridPos": { 188 | "h": 7, 189 | "w": 11, 190 | "x": 13, 191 | "y": 0 192 | }, 193 | "id": 4, 194 | "options": { 195 | "legend": { 196 | "calcs": [ 197 | "lastNotNull", 198 | "mean" 199 | ], 200 | "displayMode": "table", 201 | "placement": "bottom", 202 | "showLegend": true 203 | }, 204 | "tooltip": { 205 | "mode": "single", 206 | "sort": "none" 207 | } 208 | }, 209 | "targets": [ 210 | { 211 | "datasource": { 212 | "type": "prometheus", 213 | "uid": "prometheus" 214 | }, 215 | "exemplar": true, 216 | "expr": "process_resident_memory_bytes{job=\"flask-app\"}", 217 | "interval": "", 218 | "legendFormat": "{{job}}", 219 | "refId": "A" 220 | } 221 | ], 222 | "title": "Memory Usage", 223 | "type": "timeseries" 224 | }, 225 | { 226 | "datasource": { 227 | "type": "prometheus", 228 | "uid": "prometheus" 229 | }, 230 | "description": "", 231 | "fieldConfig": { 232 | "defaults": { 233 | "color": { 234 | "mode": "palette-classic" 235 | }, 236 | "custom": { 237 | "axisCenteredZero": false, 238 | "axisColorMode": "text", 239 | "axisLabel": "", 240 | "axisPlacement": "auto", 241 | "barAlignment": 0, 242 | "drawStyle": "line", 243 | "fillOpacity": 0, 244 | "gradientMode": "none", 245 | "hideFrom": { 246 | "legend": false, 247 | "tooltip": false, 248 | "viz": false 249 | }, 250 | "lineInterpolation": "linear", 251 | "lineWidth": 1, 252 | "pointSize": 5, 253 | "scaleDistribution": { 254 | "type": "linear" 255 | }, 256 | "showPoints": "auto", 257 | "spanNulls": false, 258 | "stacking": { 259 | "group": "A", 260 | "mode": "none" 261 | }, 262 | "thresholdsStyle": { 263 | "mode": "off" 264 | } 265 | }, 266 | "mappings": [], 267 | "thresholds": { 268 | "mode": "absolute", 269 | "steps": [ 270 | { 271 | "color": "green", 272 | "value": null 273 | }, 274 | { 275 | "color": "red", 276 | "value": 80 277 | } 278 | ] 279 | } 280 | }, 281 | "overrides": [] 282 | }, 283 | "gridPos": { 284 | "h": 6, 285 | "w": 24, 286 | "x": 0, 287 | "y": 7 288 | }, 289 | "id": 6, 290 | "options": { 291 | "legend": { 292 | "calcs": [ 293 | "mean" 294 | ], 295 | "displayMode": "table", 296 | "placement": "right", 297 | "showLegend": true 298 | }, 299 | "tooltip": { 300 | "mode": "single", 301 | "sort": "none" 302 | } 303 | }, 304 | "targets": [ 305 | { 306 | "datasource": { 307 | "type": "prometheus", 308 | "uid": "prometheus" 309 | }, 310 | "exemplar": true, 311 | "expr": "rate(flask_http_request_duration_seconds_count{status=\"200\"}[30s])", 312 | "interval": "", 313 | "legendFormat": "{{path}}", 314 | "refId": "A" 315 | } 316 | ], 317 | "title": "Requests per Second", 318 | "type": "timeseries" 319 | }, 320 | { 321 | "datasource": { 322 | "type": "prometheus", 323 | "uid": "prometheus" 324 | }, 325 | "fieldConfig": { 326 | "defaults": { 327 | "color": { 328 | "mode": "palette-classic" 329 | }, 330 | "custom": { 331 | "axisCenteredZero": false, 332 | "axisColorMode": "text", 333 | "axisLabel": "", 334 | "axisPlacement": "auto", 335 | "barAlignment": 0, 336 | "drawStyle": "line", 337 | "fillOpacity": 0, 338 | "gradientMode": "none", 339 | "hideFrom": { 340 | "legend": false, 341 | "tooltip": false, 342 | "viz": false 343 | }, 344 | "lineInterpolation": "linear", 345 | "lineWidth": 1, 346 | "pointSize": 5, 347 | "scaleDistribution": { 348 | "type": "linear" 349 | }, 350 | "showPoints": "auto", 351 | "spanNulls": false, 352 | "stacking": { 353 | "group": "A", 354 | "mode": "none" 355 | }, 356 | "thresholdsStyle": { 357 | "mode": "off" 358 | } 359 | }, 360 | "mappings": [], 361 | "thresholds": { 362 | "mode": "absolute", 363 | "steps": [ 364 | { 365 | "color": "green", 366 | "value": null 367 | }, 368 | { 369 | "color": "red", 370 | "value": 80 371 | } 372 | ] 373 | } 374 | }, 375 | "overrides": [] 376 | }, 377 | "gridPos": { 378 | "h": 6, 379 | "w": 24, 380 | "x": 0, 381 | "y": 13 382 | }, 383 | "id": 8, 384 | "options": { 385 | "legend": { 386 | "calcs": [ 387 | "max", 388 | "mean" 389 | ], 390 | "displayMode": "table", 391 | "placement": "right", 392 | "showLegend": true 393 | }, 394 | "tooltip": { 395 | "mode": "single", 396 | "sort": "none" 397 | } 398 | }, 399 | "targets": [ 400 | { 401 | "datasource": { 402 | "type": "prometheus", 403 | "uid": "prometheus" 404 | }, 405 | "exemplar": true, 406 | "expr": "increase(flask_http_request_total[1m])", 407 | "interval": "", 408 | "legendFormat": "HTTP {{ method }} {{ status }}", 409 | "refId": "A" 410 | } 411 | ], 412 | "title": "Total Requests per Minute", 413 | "type": "timeseries" 414 | }, 415 | { 416 | "datasource": { 417 | "type": "prometheus", 418 | "uid": "prometheus" 419 | }, 420 | "fieldConfig": { 421 | "defaults": { 422 | "color": { 423 | "mode": "palette-classic" 424 | }, 425 | "custom": { 426 | "axisCenteredZero": false, 427 | "axisColorMode": "text", 428 | "axisLabel": "", 429 | "axisPlacement": "auto", 430 | "barAlignment": 0, 431 | "drawStyle": "line", 432 | "fillOpacity": 0, 433 | "gradientMode": "none", 434 | "hideFrom": { 435 | "legend": false, 436 | "tooltip": false, 437 | "viz": false 438 | }, 439 | "lineInterpolation": "linear", 440 | "lineWidth": 1, 441 | "pointSize": 5, 442 | "scaleDistribution": { 443 | "type": "linear" 444 | }, 445 | "showPoints": "auto", 446 | "spanNulls": false, 447 | "stacking": { 448 | "group": "A", 449 | "mode": "none" 450 | }, 451 | "thresholdsStyle": { 452 | "mode": "off" 453 | } 454 | }, 455 | "mappings": [], 456 | "thresholds": { 457 | "mode": "absolute", 458 | "steps": [ 459 | { 460 | "color": "green", 461 | "value": null 462 | }, 463 | { 464 | "color": "red", 465 | "value": 80 466 | } 467 | ] 468 | }, 469 | "unit": "s" 470 | }, 471 | "overrides": [] 472 | }, 473 | "gridPos": { 474 | "h": 8, 475 | "w": 24, 476 | "x": 0, 477 | "y": 19 478 | }, 479 | "id": 10, 480 | "options": { 481 | "legend": { 482 | "calcs": [ 483 | "max", 484 | "mean" 485 | ], 486 | "displayMode": "table", 487 | "placement": "right", 488 | "showLegend": true 489 | }, 490 | "tooltip": { 491 | "mode": "single", 492 | "sort": "none" 493 | } 494 | }, 495 | "targets": [ 496 | { 497 | "datasource": { 498 | "type": "prometheus", 499 | "uid": "prometheus" 500 | }, 501 | "exemplar": true, 502 | "expr": "rate(flask_http_request_duration_seconds_sum{status=\"200\"}[30s])\n/ rate(flask_http_request_duration_seconds_count{status=\"200\"}[30s])", 503 | "interval": "", 504 | "legendFormat": "2xx {{ method }}: {{ path }}", 505 | "refId": "A" 506 | }, 507 | { 508 | "datasource": { 509 | "type": "prometheus", 510 | "uid": "prometheus" 511 | }, 512 | "exemplar": true, 513 | "expr": "rate(flask_http_request_duration_seconds_sum{status=~\"4.+\"}[30s])\n/ rate(flask_http_request_duration_seconds_count{status=\"4.+\"}[30s])", 514 | "hide": false, 515 | "interval": "", 516 | "legendFormat": "4xx {{ method }}: {{ path }}", 517 | "refId": "B" 518 | }, 519 | { 520 | "datasource": { 521 | "type": "prometheus", 522 | "uid": "prometheus" 523 | }, 524 | "exemplar": true, 525 | "expr": "rate(flask_http_request_duration_seconds_sum{status=~\"5.+\"}[30s])\n/ rate(flask_http_request_duration_seconds_count{status=\"5.+\"}[30s])", 526 | "hide": false, 527 | "interval": "", 528 | "legendFormat": "5xx {{ method }}: {{ path }}", 529 | "refId": "C" 530 | } 531 | ], 532 | "title": "Average Response Time [30s]", 533 | "type": "timeseries" 534 | }, 535 | { 536 | "datasource": { 537 | "type": "prometheus", 538 | "uid": "prometheus" 539 | }, 540 | "fieldConfig": { 541 | "defaults": { 542 | "color": { 543 | "fixedColor": "dark-red", 544 | "mode": "fixed" 545 | }, 546 | "custom": { 547 | "axisCenteredZero": false, 548 | "axisColorMode": "text", 549 | "axisLabel": "", 550 | "axisPlacement": "auto", 551 | "barAlignment": 0, 552 | "drawStyle": "line", 553 | "fillOpacity": 0, 554 | "gradientMode": "none", 555 | "hideFrom": { 556 | "legend": false, 557 | "tooltip": false, 558 | "viz": false 559 | }, 560 | "lineInterpolation": "linear", 561 | "lineWidth": 1, 562 | "pointSize": 5, 563 | "scaleDistribution": { 564 | "type": "linear" 565 | }, 566 | "showPoints": "auto", 567 | "spanNulls": false, 568 | "stacking": { 569 | "group": "A", 570 | "mode": "none" 571 | }, 572 | "thresholdsStyle": { 573 | "mode": "off" 574 | } 575 | }, 576 | "mappings": [], 577 | "thresholds": { 578 | "mode": "absolute", 579 | "steps": [ 580 | { 581 | "color": "green", 582 | "value": null 583 | }, 584 | { 585 | "color": "red", 586 | "value": 80 587 | } 588 | ] 589 | } 590 | }, 591 | "overrides": [] 592 | }, 593 | "gridPos": { 594 | "h": 7, 595 | "w": 24, 596 | "x": 0, 597 | "y": 27 598 | }, 599 | "id": 11, 600 | "options": { 601 | "legend": { 602 | "calcs": [ 603 | "max", 604 | "mean" 605 | ], 606 | "displayMode": "table", 607 | "placement": "right", 608 | "showLegend": true 609 | }, 610 | "tooltip": { 611 | "mode": "single", 612 | "sort": "none" 613 | } 614 | }, 615 | "targets": [ 616 | { 617 | "datasource": { 618 | "type": "prometheus", 619 | "uid": "prometheus" 620 | }, 621 | "editorMode": "code", 622 | "exemplar": true, 623 | "expr": "sum(rate(flask_http_request_duration_seconds_count{status!=\"200\"}[30s]))", 624 | "interval": "", 625 | "legendFormat": "HTTP {{ method }} {{ status }}", 626 | "range": true, 627 | "refId": "A" 628 | } 629 | ], 630 | "title": "Errors per Second", 631 | "type": "timeseries" 632 | }, 633 | { 634 | "datasource": { 635 | "type": "prometheus", 636 | "uid": "prometheus" 637 | }, 638 | "fieldConfig": { 639 | "defaults": { 640 | "color": { 641 | "mode": "palette-classic" 642 | }, 643 | "custom": { 644 | "axisCenteredZero": false, 645 | "axisColorMode": "text", 646 | "axisLabel": "", 647 | "axisPlacement": "auto", 648 | "barAlignment": 0, 649 | "drawStyle": "line", 650 | "fillOpacity": 0, 651 | "gradientMode": "none", 652 | "hideFrom": { 653 | "legend": false, 654 | "tooltip": false, 655 | "viz": false 656 | }, 657 | "lineInterpolation": "linear", 658 | "lineWidth": 1, 659 | "pointSize": 5, 660 | "scaleDistribution": { 661 | "type": "linear" 662 | }, 663 | "showPoints": "auto", 664 | "spanNulls": false, 665 | "stacking": { 666 | "group": "A", 667 | "mode": "none" 668 | }, 669 | "thresholdsStyle": { 670 | "mode": "off" 671 | } 672 | }, 673 | "mappings": [], 674 | "thresholds": { 675 | "mode": "absolute", 676 | "steps": [ 677 | { 678 | "color": "green", 679 | "value": null 680 | }, 681 | { 682 | "color": "red", 683 | "value": 80 684 | } 685 | ] 686 | } 687 | }, 688 | "overrides": [] 689 | }, 690 | "gridPos": { 691 | "h": 6, 692 | "w": 24, 693 | "x": 0, 694 | "y": 34 695 | }, 696 | "id": 13, 697 | "options": { 698 | "legend": { 699 | "calcs": [ 700 | "max", 701 | "mean" 702 | ], 703 | "displayMode": "table", 704 | "placement": "right", 705 | "showLegend": true 706 | }, 707 | "tooltip": { 708 | "mode": "single", 709 | "sort": "none" 710 | } 711 | }, 712 | "targets": [ 713 | { 714 | "datasource": { 715 | "type": "prometheus", 716 | "uid": "prometheus" 717 | }, 718 | "exemplar": true, 719 | "expr": "increase(flask_http_request_duration_seconds_bucket{status=\"200\",le=\"0.25\"}[30s]) / ignoring (le) increase(flask_http_request_duration_seconds_count{status=\"200\"}[30s])", 720 | "interval": "", 721 | "legendFormat": "{{ method }}: {{ path }}", 722 | "refId": "A" 723 | } 724 | ], 725 | "title": "Requests under 250ms", 726 | "type": "timeseries" 727 | }, 728 | { 729 | "datasource": { 730 | "type": "prometheus", 731 | "uid": "prometheus" 732 | }, 733 | "fieldConfig": { 734 | "defaults": { 735 | "color": { 736 | "mode": "palette-classic" 737 | }, 738 | "custom": { 739 | "axisCenteredZero": false, 740 | "axisColorMode": "text", 741 | "axisLabel": "", 742 | "axisPlacement": "auto", 743 | "barAlignment": 0, 744 | "drawStyle": "line", 745 | "fillOpacity": 0, 746 | "gradientMode": "none", 747 | "hideFrom": { 748 | "legend": false, 749 | "tooltip": false, 750 | "viz": false 751 | }, 752 | "lineInterpolation": "linear", 753 | "lineWidth": 1, 754 | "pointSize": 5, 755 | "scaleDistribution": { 756 | "type": "linear" 757 | }, 758 | "showPoints": "auto", 759 | "spanNulls": false, 760 | "stacking": { 761 | "group": "A", 762 | "mode": "none" 763 | }, 764 | "thresholdsStyle": { 765 | "mode": "off" 766 | } 767 | }, 768 | "decimals": 2, 769 | "mappings": [], 770 | "thresholds": { 771 | "mode": "absolute", 772 | "steps": [ 773 | { 774 | "color": "green", 775 | "value": null 776 | }, 777 | { 778 | "color": "red", 779 | "value": 80 780 | } 781 | ] 782 | } 783 | }, 784 | "overrides": [] 785 | }, 786 | "gridPos": { 787 | "h": 6, 788 | "w": 24, 789 | "x": 0, 790 | "y": 40 791 | }, 792 | "id": 14, 793 | "options": { 794 | "legend": { 795 | "calcs": [ 796 | "max", 797 | "mean" 798 | ], 799 | "displayMode": "table", 800 | "placement": "right", 801 | "showLegend": true 802 | }, 803 | "tooltip": { 804 | "mode": "single", 805 | "sort": "none" 806 | } 807 | }, 808 | "targets": [ 809 | { 810 | "datasource": { 811 | "type": "prometheus", 812 | "uid": "prometheus" 813 | }, 814 | "exemplar": true, 815 | "expr": "histogram_quantile(0.9, rate(flask_http_request_duration_seconds_bucket{status=\"200\"}[30s]))", 816 | "interval": "", 817 | "legendFormat": "{{ method }}: {{ path }}", 818 | "refId": "A" 819 | } 820 | ], 821 | "title": "Request duration [s] - p90", 822 | "type": "timeseries" 823 | }, 824 | { 825 | "datasource": { 826 | "type": "prometheus", 827 | "uid": "prometheus" 828 | }, 829 | "fieldConfig": { 830 | "defaults": { 831 | "color": { 832 | "mode": "palette-classic" 833 | }, 834 | "custom": { 835 | "axisCenteredZero": false, 836 | "axisColorMode": "text", 837 | "axisLabel": "", 838 | "axisPlacement": "auto", 839 | "barAlignment": 0, 840 | "drawStyle": "line", 841 | "fillOpacity": 0, 842 | "gradientMode": "none", 843 | "hideFrom": { 844 | "legend": false, 845 | "tooltip": false, 846 | "viz": false 847 | }, 848 | "lineInterpolation": "linear", 849 | "lineWidth": 1, 850 | "pointSize": 5, 851 | "scaleDistribution": { 852 | "type": "linear" 853 | }, 854 | "showPoints": "auto", 855 | "spanNulls": false, 856 | "stacking": { 857 | "group": "A", 858 | "mode": "none" 859 | }, 860 | "thresholdsStyle": { 861 | "mode": "off" 862 | } 863 | }, 864 | "decimals": 2, 865 | "mappings": [], 866 | "thresholds": { 867 | "mode": "absolute", 868 | "steps": [ 869 | { 870 | "color": "green", 871 | "value": null 872 | }, 873 | { 874 | "color": "red", 875 | "value": 80 876 | } 877 | ] 878 | } 879 | }, 880 | "overrides": [] 881 | }, 882 | "gridPos": { 883 | "h": 6, 884 | "w": 24, 885 | "x": 0, 886 | "y": 46 887 | }, 888 | "id": 15, 889 | "options": { 890 | "legend": { 891 | "calcs": [ 892 | "max", 893 | "mean" 894 | ], 895 | "displayMode": "table", 896 | "placement": "right", 897 | "showLegend": true 898 | }, 899 | "tooltip": { 900 | "mode": "single", 901 | "sort": "none" 902 | } 903 | }, 904 | "targets": [ 905 | { 906 | "datasource": { 907 | "type": "prometheus", 908 | "uid": "prometheus" 909 | }, 910 | "exemplar": true, 911 | "expr": "histogram_quantile(0.5, rate(flask_http_request_duration_seconds_bucket{status=\"200\"}[30s]))", 912 | "interval": "", 913 | "legendFormat": "{{ method }}: {{ path }}", 914 | "refId": "A" 915 | } 916 | ], 917 | "title": "Request duration [s] - p50", 918 | "type": "timeseries" 919 | }, 920 | { 921 | "datasource": { 922 | "type": "loki", 923 | "uid": "loki" 924 | }, 925 | "gridPos": { 926 | "h": 14, 927 | "w": 24, 928 | "x": 0, 929 | "y": 52 930 | }, 931 | "id": 17, 932 | "options": { 933 | "dedupStrategy": "none", 934 | "enableLogDetails": true, 935 | "prettifyLogMessage": false, 936 | "showCommonLabels": false, 937 | "showLabels": false, 938 | "showTime": false, 939 | "sortOrder": "Descending", 940 | "wrapLogMessage": false 941 | }, 942 | "targets": [ 943 | { 944 | "datasource": { 945 | "type": "loki", 946 | "uid": "loki" 947 | }, 948 | "editorMode": "code", 949 | "expr": "{container=\"app\"} |= `` | logfmt", 950 | "queryType": "range", 951 | "refId": "A" 952 | } 953 | ], 954 | "title": "Logs", 955 | "type": "logs" 956 | } 957 | ], 958 | "refresh": "", 959 | "revision": 1, 960 | "schemaVersion": 38, 961 | "style": "dark", 962 | "tags": [], 963 | "templating": { 964 | "list": [] 965 | }, 966 | "time": { 967 | "from": "now-15m", 968 | "to": "now" 969 | }, 970 | "timepicker": {}, 971 | "timezone": "", 972 | "title": "Application Metrics", 973 | "uid": "j0VZzcy7z", 974 | "version": 1, 975 | "weekStart": "" 976 | } 977 | --------------------------------------------------------------------------------