Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tracing and rework cli so that the controller command does not start a FastAPI app #669

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
// Make sure SELinux does not disable with access to host filesystems like tmp
"--security-opt=label=disable"
],
// Remove this before pushing, only to allow local testing with editable library
"mounts": [],
keithralphs marked this conversation as resolved.
Show resolved Hide resolved
// Mount the parent as /workspaces so we can pip install peers as editable
"workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind",
// After the container is created, install the python project in editable form
Expand Down
2 changes: 1 addition & 1 deletion .github/pages/make_switcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def get_versions(ref: str, add: str | None) -> list[str]:
return versions


def write_json(path: Path, repository: str, versions: str):
def write_json(path: Path, repository: str, versions: list[str]):
org, repo_name = repository.split("/")
struct = [
{"version": version, "url": f"https://{org}.github.io/{repo_name}/{version}/"}
Expand Down
20 changes: 15 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,35 @@
"request": "launch",
"justMyCode": false,
"module": "blueapi",
"args": [
"serve"
]
"env": {
"OTLP_EXPORT_ENABLED": "false"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does exporting default to true? Would I also have to export OTLP_EXPORT_ENABLED=false if I wanted to run blueapi from the command line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, (assuming I understand the question), the default is false in the helm chart, dockerfile and here so you can just run BlueAPI and it wilkl not try to connect to Jaeger

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if the env var is not set (like if running blueapi outside of a dev container), then it's treated like True?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it?

Copy link
Contributor Author

@keithralphs keithralphs Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline, the code that interprest the Env Var (in core/init.py) is

OTLP_EXPORT_ENABLED = environ.get("OTLP_EXPORT_ENABLED") == "true"

and so will only enable export to Jaeger if the Env Var both exists and has been set to the string 'true'. This should however mean that there is no need to set this Env Var to 'false' as anything but 'true', including non-existance will result in the export being off, which is the desired default state. So I will remove the lines that explicitly do this.

},
"args": "-c ${input:config_path} serve"
},
{
"name": "Blueapi Controller",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"module": "blueapi",
"args": "controller ${input:args}"
"env": {
"OTLP_EXPORT_ENABLED": "false"
},
"args": "-c ${input:config_path} controller ${input:args}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should: Reconcile with #668 by @Relm-Arrowny

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @callumforrester meant #663

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add Ray's arg name change into mine so that they are consistent.

Copy link
Contributor Author

@keithralphs keithralphs Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@callumforrester do you think we should have an extra launcher for when the config file is specified or just make it the default that one is requested - you would have to press return to specify non but I guess that's fine?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quite happy with the extra launcher, I don't need to specify a config file 80% of the time, so having to press enter for no reason 80% of the time is actually mildly annoying. I realise it's only mildly but the "just one little extra step isn't too bad" mentality is a slippery slope that eventually leads to GDA.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated so there is a custom config option or both the serve and controller commands

},
],
"inputs": [
{
"id": "config_path",
"type": "promptString",
"description": "Path to configuration YAML file",
"default": ""
},
{
"id": "args",
"type": "promptString",
"description": "Arguments to pass to controller",
"default": ""
}
]
}
}
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN python -m venv /venv
ENV PATH=/venv/bin:$PATH

ENV OTLP_EXPORT_ENABLED=false
# enable opentelemetry support
ENV OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
# Change this to point to Jaeger server before merging e.g. https://daq-services-jaeger
ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
Comment on lines +18 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have said Jaeger server?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it won't be available till next year

# Ensure that all Http headers are captured
ENV OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*"
ENV OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*"
Comment on lines +15 to +22
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should: As discussed, we'd prefer not to set config in the Dockerfile and instead use the helm chart

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does that work for the dev container though as it uses the dockerfile to initialise? The config is set in the helm chart too but that doesn't help in VSCode?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the devcontainer you should be editing .devcontainer/devcontainer.json, which configures the base image into a devcontainer, I think you need to add something that looks a bit like this:

    "containerEnv": {
        "OTLP_EXPORT_ENABLED": false,
        etc
        etc
    },

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted above, not setting the variable at all will result in Export being off, which is the desired default state, however the other vars are needed to configure tracing correctly, so that if export is enabled it can function. I guess therefore these should be moved into the devcontainer.json as you say, but where can I set them for a plain valnilla blueapi being run from the command line with no Dev Container or hekm chart involved?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think at that point they should be passed in at container startup time

docker run -d --rm -e OLTP_FOO=true -e OLTP_BAR=false blueapi:latest

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should, in general, update the docs to reflect this PR. I don't mind if you do that here or make a separate issue


# The build stage installs the context into the venv
FROM developer as build
COPY . /context
Expand Down
30 changes: 21 additions & 9 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ annotated-types==0.7.0
anyio==4.4.0
appdirs==1.4.4
asciitree==0.3.3
asgiref==3.8.1
asttokens==2.4.1
async-timeout==4.0.3
attrs==24.2.0
babel==2.16.0
beautifulsoup4==4.12.3
Expand All @@ -20,7 +20,6 @@ bluesky-kafka==0.10.0
bluesky-live==0.0.8
bluesky-stomp==0.1.2
boltons==24.0.0
bump-pydantic==0.8.0
cachetools==5.5.0
caproto==1.1.1
certifi==2024.8.30
Expand All @@ -42,6 +41,7 @@ databroker==1.2.5
dataclasses-json==0.6.7
decorator==5.1.1
deepmerge==2.0
Deprecated==1.2.14
distlib==0.3.8
dls-bluesky-core==0.0.4
dls-dodal==1.31.1
Expand All @@ -54,7 +54,6 @@ email_validator==2.2.0
entrypoints==0.4
epicscorelibs==7.0.7.99.0.2
event-model==1.21.0
exceptiongroup==1.2.2
executing==2.1.0
fastapi==0.114.2
fastapi-cli==0.0.5
Expand All @@ -68,7 +67,9 @@ fsspec==2024.9.0
funcy==2.0
gitdb==4.0.11
GitPython==3.1.43
googleapis-common-protos==1.65.0
graypy==2.1.0
grpcio==1.66.2
h11==0.14.0
h5py==3.11.0
HeapDict==1.0.1
Expand All @@ -81,7 +82,7 @@ identify==2.6.1
idna==3.10
imageio==2.35.1
imagesize==1.4.1
importlib_metadata==8.5.0
importlib_metadata==8.4.0
importlib_resources==6.4.5
iniconfig==2.0.0
intake==0.6.4
Expand All @@ -96,8 +97,6 @@ jsonschema-specifications==2023.12.1
jupyterlab_widgets==3.0.13
kiwisolver==1.4.7
ldap3==2.9.1
libcst==1.4.0
livereload==2.7.0
locket==1.0.0
lz4==4.3.3
markdown-it-py==3.0.0
Expand All @@ -122,7 +121,21 @@ nose2==0.15.1
nslsii==0.10.3
numcodecs==0.13.0
numpy==1.26.4
observability-utils==0.1.2
opencv-python-headless==4.10.0.84
opentelemetry-api==1.27.0
opentelemetry-distro==0.48b0
opentelemetry-exporter-otlp==1.27.0
opentelemetry-exporter-otlp-proto-common==1.27.0
opentelemetry-exporter-otlp-proto-grpc==1.27.0
opentelemetry-exporter-otlp-proto-http==1.27.0
opentelemetry-instrumentation==0.48b0
opentelemetry-instrumentation-asgi==0.48b0
opentelemetry-instrumentation-fastapi==0.48b0
opentelemetry-proto==1.27.0
opentelemetry-sdk==1.27.0
opentelemetry-semantic-conventions==0.48b0
opentelemetry-util-http==0.48b0
ophyd==1.9.0
ophyd-async==0.5.2
orjson==3.10.7
Expand All @@ -147,6 +160,7 @@ ply==3.11
pre-commit==3.8.0
prettytable==3.11.0
prompt-toolkit==3.0.36
protobuf==4.25.5
psutil==6.0.0
ptyprocess==0.7.0
pure_eval==0.2.3
Expand All @@ -173,7 +187,6 @@ python-dotenv==1.0.1
python-multipart==0.0.9
pytz==2024.2
PyYAML==6.0.2
pyyaml-include==2.1
questionary==2.0.1
redis==5.0.8
redis-json-dict==0.2.0
Expand Down Expand Up @@ -217,9 +230,7 @@ suitcase-msgpack==0.3.0
suitcase-utils==0.5.4
super-state-machine==2.0.2
tifffile==2024.8.30
tomli==2.0.1
toolz==0.12.1
tornado==6.4.1
tox==3.28.0
tox-direct==0.4
tqdm==4.66.5
Expand All @@ -244,6 +255,7 @@ websocket-client==1.8.0
websockets==13.0.1
widgetsnbextension==4.0.13
workflows==2.27
wrapt==1.16.0
xarray==2024.9.0
yarl==1.11.1
zarr==2.18.3
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ components:
default: true
title: Is Pending
type: boolean
request_id:
default: ''
title: Request Id
type: string
task:
title: Task
task_id:
Expand Down
42 changes: 33 additions & 9 deletions helm/blueapi/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,34 +76,58 @@ listener:
resources: {}

# Additional envVars to mount to the pod as a String
extraEnvVars: []
extraEnvVars: |
- name: OTLP_EXPORT_ENABLED
value: {{ .Values.tracing.otlp.export_enabled] }}
- name: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL
value: {{ .Values.tracing.otlp.protocol }}
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.tracing.otlp.host }}:{{ .Values.tracing.otlp.port }}"
- name: OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
value: {{ .Values.tracing.http.request.headers }}
- name: OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
value: {{ .Values.tracing.http.response.headers }}
Comment on lines +80 to +89
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add defaults in case these aren't set

# - name: RABBITMQ_PASSWORD
# valueFrom:
# secretKeyRef:
# name: rabbitmq-password
# key: rabbitmq-password

tracing:
otlp:
export_enabled: false
protocol: http/protobuf
host: https://daq-services-jaeger # replace with central instance
port: 4318
http:
request:
headers: ".*"
response:
headers: ".*"
Comment on lines +96 to +106
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What uses this? It's not referenced in the helm chart

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It populates the env vars above?


# Config for the worker goes here, will be mounted into a config file
worker:
api:
host: 0.0.0.0 # Allow non-loopback traffic
port: 8000
env:
sources:
- kind: deviceFunctions
module: blueapi.startup.example_devices
- kind: planFunctions
module: blueapi.startup.example_plans
- kind: planFunctions
module: dls_bluesky_core.plans
- kind: planFunctions
module: dls_bluesky_core.stubs
- kind: deviceFunctions
module: blueapi.startup.example_devices
- kind: planFunctions
module: blueapi.startup.example_plans
- kind: planFunctions
module: dls_bluesky_core.plans
- kind: planFunctions
module: dls_bluesky_core.stubs
Comment on lines +115 to +122
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could: This change seems unrelated to the PR, ideally it should be raised separately

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linting thing did it not me

stomp:
auth:
username: guest
password: guest
host: rabbitmq
port: 61613
tracing_exporter:
host: ""
keithralphs marked this conversation as resolved.
Show resolved Hide resolved

initContainer:
scratch:
Expand Down
9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ dependencies = [
"fastapi>=0.112.0",
"uvicorn",
"requests",
"dls-bluesky-core", #requires ophyd-async
"dls-bluesky-core", #requires ophyd-async
"dls-dodal>=1.31.0",
"super-state-machine", # See GH issue 553
"super-state-machine", # See GH issue 553
"GitPython",
"bluesky-stomp>=0.1.2",
"opentelemetry-distro>=0.48b0",
"opentelemetry-instrumentation-fastapi>=0.48b0",
"observability-utils>=0.1.2",
]
dynamic = ["version"]
license.file = "LICENSE"
Expand Down Expand Up @@ -79,7 +82,7 @@ write_to = "src/blueapi/_version.py"

[tool.mypy]
ignore_missing_imports = true # Ignore missing stubs in imported modules
namespace_packages = false # rely only on __init__ files to determine fully qualified module names.
namespace_packages = true # necessary for tracing sdk to work with mypy
keithralphs marked this conversation as resolved.
Show resolved Hide resolved

[tool.pytest.ini_options]
# Run pytest with all our checkers, and don't spam us with massive tracebacks on error
Expand Down
34 changes: 26 additions & 8 deletions src/blueapi/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from bluesky.callbacks.best_effort import BestEffortCallback
from bluesky_stomp.messaging import MessageContext, StompClient
from bluesky_stomp.models import Broker
from observability_utils.tracing import setup_tracing
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.trace import get_tracer_provider
from pydantic import ValidationError
from requests.exceptions import ConnectionError

Expand All @@ -18,14 +21,7 @@
from blueapi.client.event_bus import AnyEvent, BlueskyStreamingError, EventBusClient
from blueapi.client.rest import BlueskyRemoteControlError
from blueapi.config import ApplicationConfig, ConfigLoader
from blueapi.core import DataEvent
from blueapi.service.main import start
from blueapi.service.openapi import (
DOCS_SCHEMA_LOCATION,
generate_schema,
print_schema_as_yaml,
write_schema_as_yaml,
)
from blueapi.core import OTLP_EXPORT_ENABLED, DataEvent
from blueapi.worker import ProgressEvent, Task, WorkerEvent

from .scratch import setup_scratch
Expand Down Expand Up @@ -70,6 +66,16 @@ def main(ctx: click.Context, config: Path | None | tuple[Path, ...]) -> None:
help="[Development only] update the schema in the documentation",
)
def schema(output: Path | None = None, update: bool = False) -> None:
"""Only import the service functions when starting the service or generating
the schema, not the controller as a new FastAPI app will be started each time.
"""
from blueapi.service.openapi import (
DOCS_SCHEMA_LOCATION,
generate_schema,
print_schema_as_yaml,
write_schema_as_yaml,
)

"""Generate the schema for the REST API"""
schema = generate_schema()

Expand All @@ -87,6 +93,17 @@ def start_application(obj: dict):
"""Run a worker that accepts plans to run"""
config: ApplicationConfig = obj["config"]

"""Only import the service functions when starting the service or generating
the schema, not the controller as a new FastAPI app will be started each time.
"""
from blueapi.service.main import app, start

"""
Set up basic automated instrumentation for the FastAPI app, creating the
observability context.
"""
setup_tracing("BlueAPI", OTLP_EXPORT_ENABLED)
FastAPIInstrumentor().instrument_app(app, tracer_provider=get_tracer_provider())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Probably) should: Rather than importing app into this module, you could just include this line in start()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure waht the result of that would be, as you would be starting the FastAPI instrumentation inside an already instrymented method, but I can try and see what happens.

start(config)


Expand All @@ -101,6 +118,7 @@ def start_application(obj: dict):
def controller(ctx: click.Context, output: str) -> None:
"""Client utility for controlling and introspecting the worker"""

setup_tracing("BlueAPICLI", OTLP_EXPORT_ENABLED)
keithralphs marked this conversation as resolved.
Show resolved Hide resolved
if ctx.invoked_subcommand is None:
print("Please invoke subcommand!")
return
Expand Down
Loading
Loading