Skip to content

Commit

Permalink
Add real world example
Browse files Browse the repository at this point in the history
  • Loading branch information
draincoder committed Mar 4, 2024
1 parent a728fa3 commit 47b9843
Show file tree
Hide file tree
Showing 21 changed files with 1,432 additions and 6 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ repos:
- id: ensure-dunder-all
exclude: "tests*|examples*"
args: ["--use-tuple"]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.2.2
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: ["--fix"]
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## ASGI Monitor
[![license](https://img.shields.io/github/license/draincoder/asgi-monitor)](https://github.com/draincoder/asgi-monitor/blob/master/LICENSE)
[![test](https://github.com/draincoder/asgi-monitor/actions/workflows/ci.yaml/badge.svg)](https://github.com/draincoder/asgi-monitor/actions/workflows/ci.yaml)
[![PyPI version](https://badge.fury.io/py/asgi-monitor.svg)](https://pypi.python.org/pypi/asgi-monitor)
[![Supported versions](https://img.shields.io/pypi/pyversions/asgi-monitor.svg)](https://pypi.python.org/pypi/asgi-monitor)
[![downloads](https://img.shields.io/pypi/dm/asgi-monitor.svg)](https://pypistats.org/packages/asgi-monitor)

A library for easy and fast configuration of logging, tracing and monitoring of ASGI applications.
2 changes: 1 addition & 1 deletion examples/gunicorn_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def run() -> None:
}
configure_logging(level=level, json_format=True)

from examples.asgi_app import get_app
from asgi_app import get_app

app = get_app()

Expand Down
31 changes: 31 additions & 0 deletions examples/real_world/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
FROM python:3.10-slim-buster as python-base

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
PYSETUP_PATH="/opt/pysetup" \
VENV_PATH="/opt/pysetup/.venv"

RUN python3 -m venv $VENV_PATH
ENV PATH="$VENV_PATH/bin:$PATH"

FROM python-base as builder-base
RUN apt-get update && apt-get install -y gcc git

WORKDIR $PYSETUP_PATH
COPY requirements.txt .

RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir setuptools wheel \
&& pip install --no-cache-dir -r requirements.txt

FROM python-base as production
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
RUN apt-get update && apt-get install -y curl

WORKDIR /app
COPY ./app /app/app

ENTRYPOINT ["python", "-Om", "app.main"]
16 changes: 16 additions & 0 deletions examples/real_world/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## Start example in Docker

```shell
docker compose -f examples/real_world/docker-compose.yaml --profile api --profile grafana up --build -d
```

## Stop example in Docker

```shell
docker compose -f examples/real_world/docker-compose.yaml --profile api --profile grafana down
```

## Interfaces

1. Grafana - http://127.0.0.1:3000/
2. Swagger - http://127.0.0.1:8080/docs/
File renamed without changes.
52 changes: 52 additions & 0 deletions examples/real_world/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import logging

import uvicorn
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from asgi_monitor.integrations.fastapi import TracingConfig, setup_metrics, setup_tracing
from asgi_monitor.logging import configure_logging
from asgi_monitor.logging.uvicorn import build_uvicorn_log_config

from app.routes import setup_routes

logger = logging.getLogger(__name__)

APP_NAME = "asgi-monitor"
HOST = "0.0.0.0"
PORT = 8080
GRPC_ENDPOINT = "http://asgi-monitor.tempo:4317"


def create_app() -> FastAPI:
configure_logging(level=logging.INFO, json_format=True)

resource = Resource.create(
attributes={
"service.name": APP_NAME,
"compose_service": APP_NAME,
},
)
tracer = TracerProvider(resource=resource)
trace.set_tracer_provider(tracer)
tracer.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=GRPC_ENDPOINT)))
config = TracingConfig(tracer_provider=tracer)

app = FastAPI(debug=True)
setup_metrics(app, app_name=APP_NAME, include_trace_exemplar=True, include_metrics_endpoint=True)
setup_tracing(app=app, config=config)
setup_routes(app=app)

return app


if __name__ == "__main__":
log_config = build_uvicorn_log_config(
level=logging.INFO,
json_format=True,
include_trace=True,
)
uvicorn.run(create_app(), host=HOST, port=PORT, log_config=log_config)
13 changes: 13 additions & 0 deletions examples/real_world/app/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from fastapi import FastAPI

from .error import error_router
from .healthcheck import healthcheck_router
from .slow import slow_router

__all__ = ("setup_routes",)


def setup_routes(app: FastAPI) -> None:
app.include_router(healthcheck_router)
app.include_router(error_router)
app.include_router(slow_router)
23 changes: 23 additions & 0 deletions examples/real_world/app/routes/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import logging

from fastapi import APIRouter, status
from fastapi.exceptions import HTTPException

error_router = APIRouter(
prefix="/error",
tags=["Error"],
include_in_schema=True,
)
logger = logging.getLogger(__name__)


@error_router.get("/500", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
async def get_500_error() -> None:
logger.error("Internal Server Error Occurred", extra={"status_code": 500})
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal Server Error")


@error_router.get("/404", status_code=status.HTTP_404_NOT_FOUND)
async def get_404() -> None:
logger.error("Not Found", extra={"status_code": 404})
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found")
12 changes: 12 additions & 0 deletions examples/real_world/app/routes/healthcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from fastapi import APIRouter, status

healthcheck_router = APIRouter(
prefix="/healthcheck",
tags=["Healthcheck"],
include_in_schema=True,
)


@healthcheck_router.get("/", status_code=status.HTTP_200_OK)
async def get_status() -> dict:
return {"message": "ok", "status": "success"}
25 changes: 25 additions & 0 deletions examples/real_world/app/routes/slow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import asyncio
import logging

from fastapi import APIRouter, status
from opentelemetry import trace

slow_router = APIRouter(
prefix="/slow",
tags=["Slow"],
include_in_schema=True,
)
logger = logging.getLogger(__name__)


@slow_router.get("/1000ms", status_code=status.HTTP_200_OK)
async def get_1000ms() -> dict:
with trace.get_tracer("asgi-monitor").start_as_current_span("sleep 0.1"):
await asyncio.sleep(0.1)
with trace.get_tracer("asgi-monitor").start_as_current_span("sleep 0.2"):
await asyncio.sleep(0.2)
with trace.get_tracer("asgi-monitor").start_as_current_span("sleep 0.3"):
await asyncio.sleep(0.3)
with trace.get_tracer("asgi-monitor").start_as_current_span("sleep 0.4"):
await asyncio.sleep(0.4)
return {"message": "ok", "status": "success"}
128 changes: 128 additions & 0 deletions examples/real_world/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
version: "3.9"

services:
api:
profiles: [ "api" ]
container_name: asgi-monitor.api
hostname: asgi-monitor.api
build:
context: .
restart: unless-stopped
expose:
- "8080"
ports:
- "8080:8080"
depends_on:
tempo:
condition: service_started
networks:
- asgi-monitor.grafana.network
- asgi-monitor.api.network
command: [ "python", "-Om", "app.main" ]
healthcheck:
test: [ "CMD-SHELL", "curl -fsSL http://localhost:8080/healthcheck" ]
interval: 30s
timeout: 60s
retries: 5
start_period: 10s

grafana:
profiles: [ "grafana" ]
image: grafana/grafana:latest
container_name: asgi-monitor.grafana
hostname: asgi-monitor.grafana
restart: unless-stopped
expose:
- "3000"
ports:
- "127.0.0.1:3000:3000"
networks:
- asgi-monitor.grafana.network
volumes:
- asgi-monitor.grafana.data:/var/lib/grafana:rw
- ./grafana/provisioning:/etc/grafana/provisioning:rw
- ./grafana/dashboards:/etc/grafana/dashboards
environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_DATABASE_WAL=true
- VIRTUAL_HOST=crypto_trader.grafana
- NETWORK_ACCESS=internal
- VIRTUAL_PORT=3000

loki:
profiles: [ "grafana" ]
image: grafana/loki:2.7.3
container_name: asgi-monitor.loki
hostname: asgi-monitor.loki
expose:
- "3100"
volumes:
- ./loki/config.yaml:/etc/loki/config.yaml:ro
- asgi-monitor.loki.data:/tmp/:rw
command: -config.file=/etc/loki/config.yaml
restart: unless-stopped
networks:
- asgi-monitor.grafana.network

vector:
profiles: [ "grafana" ]
image: timberio/vector:0.27.0-alpine
container_name: asgi-monitor.vector
hostname: asgi-monitor.vector
restart: unless-stopped
expose:
- "8383"
networks:
- asgi-monitor.grafana.network
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./vector/vector.toml:/etc/vector/vector.toml:ro
logging:
driver: "json-file"
options:
max-size: "10m"

tempo:
profiles: [ "grafana" ]
image: grafana/tempo:2.0.1
container_name: asgi-monitor.tempo
hostname: asgi-monitor.tempo
command: [ "--target=all", "--storage.trace.backend=local", "--storage.trace.local.path=/var/tempo", "--auth.enabled=false" ]
restart: unless-stopped
ports:
- "14250:14250"
networks:
- asgi-monitor.grafana.network
depends_on:
- loki

prometheus:
profiles: [ "grafana" ]
image: prom/prometheus:latest
container_name: asgi-monitor.prometheus
hostname: asgi-monitor.prometheus
restart: unless-stopped
networks:
- asgi-monitor.grafana.network
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- asgi-monitor.prometheus.data:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --enable-feature=exemplar-storage
ports:
- "9090:9090"
expose:
- 9090

volumes:
asgi-monitor.grafana.data: {}
asgi-monitor.loki.data: {}
asgi-monitor.prometheus.data: {}


networks:
asgi-monitor.api.network: {}
asgi-monitor.grafana.network: {}
Loading

0 comments on commit 47b9843

Please sign in to comment.