├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── config ├── grafana-datasources.yml ├── grafana.ini └── tempo.yaml ├── docker-compose.yml ├── media ├── fastapi-tracing-demo-transp.png ├── fastapi-tracing-demo.png └── grafana-screen.png ├── poetry.lock ├── pyproject.toml └── python_tracing_demo ├── main.py └── pokeapi_client.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.egg-info 3 | .venv 4 | __pycache__/ 5 | 6 | 7 | .pytest_cache 8 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim 2 | 3 | ENV PATH="/root/.local/bin:$PATH" 4 | 5 | RUN apt-get update && apt-get -y install curl 6 | 7 | RUN curl -sSL https://install.python-poetry.org | python && \ 8 | ln -s $HOME/.poetry/bin/poetry /usr/bin/poetry && \ 9 | poetry --version && \ 10 | poetry config virtualenvs.create false 11 | 12 | WORKDIR /app 13 | COPY poetry.lock pyproject.toml ./ 14 | 15 | RUN poetry install --only main --no-interaction --no-root 16 | 17 | COPY python_tracing_demo/ ./ 18 | 19 | ENV PORT=8080 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | up: 2 | docker-compose up --build 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI Tracing Demo 2 | Demo to show how to instrument a [FastAPI](https://fastapi.tiangolo.com/) python project 3 | adding tracing with [OpnTelemetry](https://opentelemetry.io/) and send/show traces in [Grafana Tempo](https://grafana.com/oss/tempo/) 4 | 5 | ![Alt text](media/fastapi-tracing-demo.png "Graph") 6 | 7 | ### Requirements 8 | - docker 9 | - docker-compose 10 | - loki docker plugin 11 | 12 | You can install the Loki Docker plugin using: 13 | `docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions` 14 | 15 | The `main` branch contains the basic app, before instrumenting with OpenTelemetry but with Grafana, Tempo and Loki available. 16 | 17 | The `instrumented` branch is the instrumented app, the outcome of going through the demo steps below. 18 | 19 | ### Run 20 | 21 | To bring up the services and test the app, run `make up` 22 | The Grafana dashboard will be available at http://localhost:5000 23 | 24 | You can access the application swagger at http://localhost:8080/docs 25 | You can make a call from there to the `pokemon` api or using an http client: 26 | 27 | ```bash 28 | http http://localhost:8080/pokemon/ditto 29 | 30 | HTTP/1.1 200 OK 31 | content-length: 62 32 | content-type: application/json 33 | date: Tue, 01 Feb 2022 10:41:14 GMT 34 | server: uvicorn 35 | 36 | { 37 | "generation": { 38 | "name": "generation-i" 39 | }, 40 | "id": 132, 41 | "name": "ditto" 42 | } 43 | ``` 44 | 45 | Now, open the grafana dashboard in the `explore` panel, select `Loki` as source and query `{compose_project="python-tracing-demo"}` in the `Log Browser` input field. 46 | There you'll find the application logs and, if you click on the log of the call we just made, you can see the **TraceID** with the **Tempo** button that opens the panel with tracing info 47 | 48 | ![Alt text](media/grafana-screen.png "Grafana") 49 | 50 | ## Demo steps 51 | ### Install the SDK and API libraries 52 | > ⚠️ This project uses poetry as dependency manager. Change the commands to match yours 53 | ```shell 54 | poetry add opentelemetry-api 55 | poetry add opentelemetry-sdk 56 | ``` 57 | 58 | ### Install the instrumentation libraries 59 | For this demo we are gonna use FastAPI and Logging instrumentation 60 | 61 | ```shell 62 | poetry add opentelemetry-instrumentation-fastapi 63 | poetry add opentelemetry-instrumentation-logging 64 | ``` 65 | 66 | ### Export traces to tempo 67 | Install the [OTLP exporter](https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp) 68 | that allows us to export tracing data to an OTLP collector or, in our case, directly to Tempo 69 | This library currently install both the HTTP and gRPC collector exporters. In our example we are going to use the `gRPC` one. 70 | ```shell 71 | poetry add opentelemetry-exporter-otlp 72 | ``` 73 | 74 | In our `main.py` let's import the needed packages 75 | ```python 76 | from opentelemetry import trace 77 | from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter 78 | from opentelemetry.sdk.trace import TracerProvider 79 | from opentelemetry.sdk.trace.export import BatchSpanProcessor 80 | from opentelemetry.sdk.resources import Resource 81 | 82 | from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor 83 | from opentelemetry.instrumentation.logging import LoggingInstrumentor 84 | ``` 85 | And now let's set it up to export traces to Grafana Tempo: 86 | 87 | ```python 88 | resource = Resource(attributes={"service.name": "fastapi-tracing-demo"}) # set the service name to show in traces 89 | 90 | # set the tracer provider 91 | tracer = TracerProvider(resource=resource) 92 | trace.set_tracer_provider(tracer) 93 | 94 | # Use the OTLPSpanExporter to send traces to Tempo 95 | tracer.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://tempo:4317"))) 96 | 97 | LoggingInstrumentor().instrument() 98 | FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer) 99 | ``` 100 | 101 | To enable trace contect injection into logs, we should set the `OTEL_PYTHON_LOG_CORRELATION` to true. 102 | So let's add it to our docker-compose file in the `tracing-demo` demo service: 103 | 104 | ```yaml 105 | tracing-demo: 106 | build: . 107 | ports: 108 | - "8080:8080" 109 | environment: 110 | - OTEL_PYTHON_LOG_CORRELATION=true 111 | logging: 112 | driver: loki 113 | options: 114 | loki-url: http://localhost:3100/loki/api/v1/push 115 | ``` 116 | 117 | To enable `Loki` to recognize `trace_id` in logs, we have to add this config to the `grafana-datasources.yml` in the `loki` datasource section: 118 | ```yaml 119 | jsonData: 120 | derivedFields: 121 | - datasourceUid: tempo 122 | matcherRegex: trace_id=(\w+) 123 | name: TraceID 124 | url: "$${__value.raw}" 125 | ``` 126 | 127 | ### Resources 128 | - [OTLP exporter](https://opentelemetry-python.readthedocs.io/en/latest/exporter/otlp/otlp.html) 129 | - [Logging instrumentation docs](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/logging/logging.html) 130 | - [FastAPI instrumentation docs](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/fastapi/fastapi.html) -------------------------------------------------------------------------------- /config/grafana-datasources.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Tempo 5 | type: tempo 6 | access: proxy 7 | url: http://tempo:8000 8 | version: 1 9 | editable: false 10 | uid: tempo 11 | - name: Loki 12 | type: loki 13 | access: proxy 14 | url: http://loki:3100 15 | version: 1 16 | editable: false 17 | isDefault: true 18 | -------------------------------------------------------------------------------- /config/grafana.ini: -------------------------------------------------------------------------------- 1 | [feature_toggles] 2 | enable = tempoSearch -------------------------------------------------------------------------------- /config/tempo.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 8000 5 | 6 | storage: 7 | trace: 8 | backend: local 9 | block: 10 | encoding: zstd 11 | wal: 12 | path: /tmp/tempo/wal 13 | encoding: none 14 | local: 15 | path: /tmp/tempo/blocks 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | grafana: 3 | image: grafana/grafana:8.2.0 4 | ports: 5 | - 5000:3000 6 | volumes: 7 | - ./config/grafana.ini:/etc/grafana/grafana.ini 8 | - ./config/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yaml 9 | environment: 10 | - GF_AUTH_ANONYMOUS_ENABLED=true 11 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 12 | - GF_AUTH_DISABLE_LOGIN_FORM=true 13 | depends_on: 14 | - tempo 15 | loki: 16 | image: grafana/loki:2.3.0 17 | ports: 18 | - 3100:3100 19 | command: -config.file=/etc/loki/local-config.yaml 20 | tempo: 21 | image: grafana/tempo:1.2.0 22 | command: [ "-search.enabled=true", "-config.file=/etc/tempo.yaml" ] 23 | volumes: 24 | - ./config/tempo.yaml:/etc/tempo.yaml 25 | ports: 26 | - 8000:8000 # tempo 27 | tracing-demo: 28 | build: . 29 | ports: 30 | - "8080:8080" 31 | logging: 32 | driver: loki 33 | options: 34 | loki-url: http://localhost:3100/loki/api/v1/push 35 | -------------------------------------------------------------------------------- /media/fastapi-tracing-demo-transp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwarebloat/python-tracing-demo/57d26b311677f4745b84a6e469675e6f68a98b9e/media/fastapi-tracing-demo-transp.png -------------------------------------------------------------------------------- /media/fastapi-tracing-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwarebloat/python-tracing-demo/57d26b311677f4745b84a6e469675e6f68a98b9e/media/fastapi-tracing-demo.png -------------------------------------------------------------------------------- /media/grafana-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwarebloat/python-tracing-demo/57d26b311677f4745b84a6e469675e6f68a98b9e/media/grafana-screen.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "anyio" 3 | version = "3.3.4" 4 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6.2" 8 | 9 | [package.dependencies] 10 | idna = ">=2.8" 11 | sniffio = ">=1.1" 12 | 13 | [package.extras] 14 | doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] 15 | test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] 16 | trio = ["trio (>=0.16)"] 17 | 18 | [[package]] 19 | name = "atomicwrites" 20 | version = "1.4.0" 21 | description = "Atomic file writes." 22 | category = "dev" 23 | optional = false 24 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 25 | 26 | [[package]] 27 | name = "attrs" 28 | version = "21.2.0" 29 | description = "Classes Without Boilerplate" 30 | category = "dev" 31 | optional = false 32 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 33 | 34 | [package.extras] 35 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 36 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 37 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 38 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 39 | 40 | [[package]] 41 | name = "certifi" 42 | version = "2021.10.8" 43 | description = "Python package for providing Mozilla's CA Bundle." 44 | category = "main" 45 | optional = false 46 | python-versions = "*" 47 | 48 | [[package]] 49 | name = "colorama" 50 | version = "0.4.4" 51 | description = "Cross-platform colored terminal text." 52 | category = "dev" 53 | optional = false 54 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 55 | 56 | [[package]] 57 | name = "fastapi" 58 | version = "0.70.0" 59 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 60 | category = "main" 61 | optional = false 62 | python-versions = ">=3.6.1" 63 | 64 | [package.dependencies] 65 | pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" 66 | starlette = "0.16.0" 67 | 68 | [package.extras] 69 | all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] 70 | dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] 71 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] 72 | test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] 73 | 74 | [[package]] 75 | name = "h11" 76 | version = "0.12.0" 77 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 78 | category = "main" 79 | optional = false 80 | python-versions = ">=3.6" 81 | 82 | [[package]] 83 | name = "httpcore" 84 | version = "0.13.7" 85 | description = "A minimal low-level HTTP client." 86 | category = "main" 87 | optional = false 88 | python-versions = ">=3.6" 89 | 90 | [package.dependencies] 91 | anyio = ">=3.0.0,<4.0.0" 92 | h11 = ">=0.11,<0.13" 93 | sniffio = ">=1.0.0,<2.0.0" 94 | 95 | [package.extras] 96 | http2 = ["h2 (>=3,<5)"] 97 | 98 | [[package]] 99 | name = "httpx" 100 | version = "0.18.2" 101 | description = "The next generation HTTP client." 102 | category = "main" 103 | optional = false 104 | python-versions = ">=3.6" 105 | 106 | [package.dependencies] 107 | certifi = "*" 108 | httpcore = ">=0.13.3,<0.14.0" 109 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} 110 | sniffio = "*" 111 | 112 | [package.extras] 113 | brotli = ["brotlicffi (>=1.0.0,<2.0.0)"] 114 | http2 = ["h2 (>=3.0.0,<4.0.0)"] 115 | 116 | [[package]] 117 | name = "idna" 118 | version = "3.3" 119 | description = "Internationalized Domain Names in Applications (IDNA)" 120 | category = "main" 121 | optional = false 122 | python-versions = ">=3.5" 123 | 124 | [[package]] 125 | name = "more-itertools" 126 | version = "8.11.0" 127 | description = "More routines for operating on iterables, beyond itertools" 128 | category = "dev" 129 | optional = false 130 | python-versions = ">=3.5" 131 | 132 | [[package]] 133 | name = "packaging" 134 | version = "21.2" 135 | description = "Core utilities for Python packages" 136 | category = "dev" 137 | optional = false 138 | python-versions = ">=3.6" 139 | 140 | [package.dependencies] 141 | pyparsing = ">=2.0.2,<3" 142 | 143 | [[package]] 144 | name = "pluggy" 145 | version = "0.13.1" 146 | description = "plugin and hook calling mechanisms for python" 147 | category = "dev" 148 | optional = false 149 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 150 | 151 | [package.extras] 152 | dev = ["pre-commit", "tox"] 153 | 154 | [[package]] 155 | name = "py" 156 | version = "1.11.0" 157 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 158 | category = "dev" 159 | optional = false 160 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 161 | 162 | [[package]] 163 | name = "pydantic" 164 | version = "1.8.2" 165 | description = "Data validation and settings management using python 3.6 type hinting" 166 | category = "main" 167 | optional = false 168 | python-versions = ">=3.6.1" 169 | 170 | [package.dependencies] 171 | typing-extensions = ">=3.7.4.3" 172 | 173 | [package.extras] 174 | dotenv = ["python-dotenv (>=0.10.4)"] 175 | email = ["email-validator (>=1.0.3)"] 176 | 177 | [[package]] 178 | name = "pyparsing" 179 | version = "2.4.7" 180 | description = "Python parsing module" 181 | category = "dev" 182 | optional = false 183 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 184 | 185 | [[package]] 186 | name = "pytest" 187 | version = "5.4.3" 188 | description = "pytest: simple powerful testing with Python" 189 | category = "dev" 190 | optional = false 191 | python-versions = ">=3.5" 192 | 193 | [package.dependencies] 194 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 195 | attrs = ">=17.4.0" 196 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 197 | more-itertools = ">=4.0.0" 198 | packaging = "*" 199 | pluggy = ">=0.12,<1.0" 200 | py = ">=1.5.0" 201 | wcwidth = "*" 202 | 203 | [package.extras] 204 | checkqa-mypy = ["mypy (==v0.761)"] 205 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 206 | 207 | [[package]] 208 | name = "rfc3986" 209 | version = "1.5.0" 210 | description = "Validating URI References per RFC 3986" 211 | category = "main" 212 | optional = false 213 | python-versions = "*" 214 | 215 | [package.dependencies] 216 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} 217 | 218 | [package.extras] 219 | idna2008 = ["idna"] 220 | 221 | [[package]] 222 | name = "sniffio" 223 | version = "1.2.0" 224 | description = "Sniff out which async library your code is running under" 225 | category = "main" 226 | optional = false 227 | python-versions = ">=3.5" 228 | 229 | [[package]] 230 | name = "starlette" 231 | version = "0.16.0" 232 | description = "The little ASGI library that shines." 233 | category = "main" 234 | optional = false 235 | python-versions = ">=3.6" 236 | 237 | [package.dependencies] 238 | anyio = ">=3.0.0,<4" 239 | 240 | [package.extras] 241 | full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "graphene"] 242 | 243 | [[package]] 244 | name = "typing-extensions" 245 | version = "3.10.0.2" 246 | description = "Backported and Experimental Type Hints for Python 3.5+" 247 | category = "main" 248 | optional = false 249 | python-versions = "*" 250 | 251 | [[package]] 252 | name = "wcwidth" 253 | version = "0.2.5" 254 | description = "Measures the displayed width of unicode strings in a terminal" 255 | category = "dev" 256 | optional = false 257 | python-versions = "*" 258 | 259 | [metadata] 260 | lock-version = "1.1" 261 | python-versions = "^3.9" 262 | content-hash = "2366c12744e0ee8efc125b5b210adf916ddeed6661fe7bdc0e62a608162cb692" 263 | 264 | [metadata.files] 265 | anyio = [ 266 | {file = "anyio-3.3.4-py3-none-any.whl", hash = "sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66"}, 267 | {file = "anyio-3.3.4.tar.gz", hash = "sha256:67da67b5b21f96b9d3d65daa6ea99f5d5282cb09f50eb4456f8fb51dffefc3ff"}, 268 | ] 269 | atomicwrites = [ 270 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 271 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 272 | ] 273 | attrs = [ 274 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 275 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 276 | ] 277 | certifi = [ 278 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 279 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 280 | ] 281 | colorama = [ 282 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 283 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 284 | ] 285 | fastapi = [ 286 | {file = "fastapi-0.70.0-py3-none-any.whl", hash = "sha256:a36d5f2fad931aa3575c07a3472c784e81f3e664e3bb5c8b9c88d0ec1104f59c"}, 287 | {file = "fastapi-0.70.0.tar.gz", hash = "sha256:66da43cfe5185ea1df99552acffd201f1832c6b364e0f4136c0a99f933466ced"}, 288 | ] 289 | h11 = [ 290 | {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, 291 | {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, 292 | ] 293 | httpcore = [ 294 | {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, 295 | {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, 296 | ] 297 | httpx = [ 298 | {file = "httpx-0.18.2-py3-none-any.whl", hash = "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c"}, 299 | {file = "httpx-0.18.2.tar.gz", hash = "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"}, 300 | ] 301 | idna = [ 302 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 303 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 304 | ] 305 | more-itertools = [ 306 | {file = "more-itertools-8.11.0.tar.gz", hash = "sha256:0a2fd25d343c08d7e7212071820e7e7ea2f41d8fb45d6bc8a00cd6ce3b7aab88"}, 307 | {file = "more_itertools-8.11.0-py3-none-any.whl", hash = "sha256:88afff98d83d08fe5e4049b81e2b54c06ebb6b3871a600040865c7a592061cbb"}, 308 | ] 309 | packaging = [ 310 | {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, 311 | {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, 312 | ] 313 | pluggy = [ 314 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 315 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 316 | ] 317 | py = [ 318 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 319 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 320 | ] 321 | pydantic = [ 322 | {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, 323 | {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, 324 | {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, 325 | {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, 326 | {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, 327 | {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, 328 | {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, 329 | {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, 330 | {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, 331 | {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, 332 | {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, 333 | {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, 334 | {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, 335 | {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, 336 | {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, 337 | {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, 338 | {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, 339 | {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, 340 | {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, 341 | {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, 342 | {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, 343 | {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, 344 | ] 345 | pyparsing = [ 346 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 347 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 348 | ] 349 | pytest = [ 350 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 351 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 352 | ] 353 | rfc3986 = [ 354 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 355 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 356 | ] 357 | sniffio = [ 358 | {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, 359 | {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, 360 | ] 361 | starlette = [ 362 | {file = "starlette-0.16.0-py3-none-any.whl", hash = "sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f"}, 363 | {file = "starlette-0.16.0.tar.gz", hash = "sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870"}, 364 | ] 365 | typing-extensions = [ 366 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 367 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 368 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 369 | ] 370 | wcwidth = [ 371 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 372 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 373 | ] 374 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "python-tracing-demo" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["softwarebloat"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | fastapi = "^0.70.0" 10 | httpx = "0.18.2" 11 | 12 | [tool.poetry.dev-dependencies] 13 | pytest = "^5.2" 14 | 15 | [build-system] 16 | requires = ["poetry-core>=1.0.0"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /python_tracing_demo/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from pokeapi_client import ( 4 | PokeapiClient, 5 | PokemonResponse 6 | ) 7 | 8 | app = FastAPI() 9 | 10 | @app.get("/pokemon/{pokemon_name}") 11 | async def get_pokemon_info(pokemon_name: str): 12 | return await PokeapiClient.retrieve_pokemon_info(pokemon_name) 13 | -------------------------------------------------------------------------------- /python_tracing_demo/pokeapi_client.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import logging 3 | 4 | from pydantic import BaseModel 5 | 6 | 7 | class Generation(BaseModel): 8 | name: str 9 | 10 | class PokemonResponse(BaseModel): 11 | id: int 12 | name: str 13 | generation: Generation 14 | 15 | class PokeapiClient(): 16 | async def retrieve_pokemon_info(pokemon_name: str): 17 | try: 18 | async with httpx.AsyncClient() as client: 19 | logging.info(f"Retrieving info for {pokemon_name}") 20 | result = await client.get(f"https://pokeapi.co/api/v2/pokemon-species/{pokemon_name}/") 21 | result.raise_for_status() 22 | except httpx.HTTPError as err: 23 | logging.error('http error while retrieving pokemon species') 24 | return str(err) 25 | logging.info(f"HTTP RESULT -> {result}") 26 | return PokemonResponse(**dict(result.json())) --------------------------------------------------------------------------------