diff --git a/README.md b/README.md index 07fff883f..291c80b56 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,7 @@ rake app=ruby/rails7-delayed-job mode=collector app:up rake app=ruby/rails7-goodjob mode=collector app:up rake app=ruby/rails7-postgres mode=collector app:up rake app=ruby/rails7-sequel mode=collector app:up +rake app=ruby/rails7-sidekiq mode=collector app:up rake app=ruby/rails7-solid-cache mode=collector app:up rake app=ruby/rails7-solid-queue mode=collector app:up rake app=ruby/rails8-delayed-job mode=collector app:up diff --git a/python/django4-asgi/app/appsignal_python_opentelemetry/settings.py b/python/django4-asgi/app/appsignal_python_opentelemetry/settings.py index 0842ad00f..193158e10 100644 --- a/python/django4-asgi/app/appsignal_python_opentelemetry/settings.py +++ b/python/django4-asgi/app/appsignal_python_opentelemetry/settings.py @@ -26,7 +26,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['app', 'localhost'] +ALLOWED_HOSTS = ['app', 'downstream', 'localhost'] # Application definition diff --git a/python/django4-asgi/app/appsignal_python_opentelemetry/views.py b/python/django4-asgi/app/appsignal_python_opentelemetry/views.py index 111501bb2..a8f3e19dc 100644 --- a/python/django4-asgi/app/appsignal_python_opentelemetry/views.py +++ b/python/django4-asgi/app/appsignal_python_opentelemetry/views.py @@ -1,3 +1,4 @@ +import os import requests import json import time @@ -84,7 +85,7 @@ def error_queue_inline(request): def make_request(request): - requests.get('https://www.appsignal.com/') + requests.get(os.environ.get("DOWNSTREAM_URL", "https://www.appsignal.com/")) return HttpResponse("I did a request to appsignal.com!") diff --git a/python/django4-asgi/app/processmon.downstream.toml b/python/django4-asgi/app/processmon.downstream.toml new file mode 100644 index 000000000..ef2b749f1 --- /dev/null +++ b/python/django4-asgi/app/processmon.downstream.toml @@ -0,0 +1,14 @@ +[[paths_to_watch]] +path = "/app" + +# Downstream web server on :4002 (no celery here; that is the worker split). +[processes.django] +command = "python3" +args = [ + "-m", + "uvicorn", + "appsignal_python_opentelemetry.asgi:application", + "--host", + "0.0.0.0" +] +working_dir = "/app" diff --git a/python/django4-asgi/app/processmon.toml b/python/django4-asgi/app/processmon.toml index f60d586e9..e7d12dd83 100644 --- a/python/django4-asgi/app/processmon.toml +++ b/python/django4-asgi/app/processmon.toml @@ -11,13 +11,3 @@ args = [ "0.0.0.0" ] working_dir = "/app" - -[processes.celery] -command = "celery" -args = [ - "--app=tasks", - "worker", - "--loglevel=INFO", - "--queues=high-priority,low-priority" -] -working_dir = "/app" diff --git a/python/django4-asgi/app/processmon.worker.toml b/python/django4-asgi/app/processmon.worker.toml new file mode 100644 index 000000000..5a1ff7183 --- /dev/null +++ b/python/django4-asgi/app/processmon.worker.toml @@ -0,0 +1,15 @@ +[[paths_to_watch]] +path = "/app" + +# The worker service runs only the background worker. Work enqueued by the web +# `app` service is performed here, so it reports under the worker's own app name +# (agent mode) or `worker` service name (collector mode). +[processes.celery] +command = "celery" +args = [ + "--app=tasks", + "worker", + "--loglevel=INFO", + "--queues=high-priority,low-priority" +] +working_dir = "/app" diff --git a/python/django4-asgi/commands/run-downstream b/python/django4-asgi/commands/run-downstream new file mode 100755 index 000000000..146d97134 --- /dev/null +++ b/python/django4-asgi/commands/run-downstream @@ -0,0 +1,24 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service builds the appsignal dist into the shared /integration +# volume and runs migrations, then serves HTTP. Wait for it to be reachable: +# that means the dist is built (so we install from it without racing the app's +# `hatch build`) and the schema is migrated (so we never migrate from here). +echo "Waiting for the app service to be ready..." +until python3 -c "import socket; socket.create_connection(('app', 4001), 2).close()" >/dev/null 2>&1; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Installing dependencies" +pip3 install -r requirements.txt +pip3 install /integration/dist/* --force-reinstall + +export UVICORN_PORT=4002 + +echo "Starting downstream processes" +/commands/processmon processmon.downstream.toml diff --git a/python/django4-asgi/commands/run-worker b/python/django4-asgi/commands/run-worker new file mode 100755 index 000000000..2df2ee274 --- /dev/null +++ b/python/django4-asgi/commands/run-worker @@ -0,0 +1,23 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service builds the appsignal dist into the shared /integration +# volume and (for the Django apps) runs migrations, then serves HTTP. Wait for +# it to be reachable before installing here: that means the dist is built (so we +# install from it without racing the app's `hatch build`) and the schema is +# migrated (so this container never migrates). +echo "Waiting for the app service to be ready..." +until python3 -c "import socket; socket.create_connection(('app', 4001), 2).close()" >/dev/null 2>&1; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Installing dependencies" +pip3 install -r requirements.txt +pip3 install /integration/dist/* --force-reinstall + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/python/django4-asgi/docker-compose.agent.yml b/python/django4-asgi/docker-compose.agent.yml index 83d8ee52c..cb9bf46e7 100644 --- a/python/django4-asgi/docker-compose.agent.yml +++ b/python/django4-asgi/docker-compose.agent.yml @@ -5,3 +5,9 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/django4-asgi + downstream: + environment: + - APPSIGNAL_APP_NAME=python/django4-asgi-downstream + worker: + environment: + - APPSIGNAL_APP_NAME=python/django4-asgi-worker diff --git a/python/django4-asgi/docker-compose.collector.yml b/python/django4-asgi/docker-compose.collector.yml index db91024e3..f230289ad 100644 --- a/python/django4-asgi/docker-compose.collector.yml +++ b/python/django4-asgi/docker-compose.collector.yml @@ -6,4 +6,15 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/django4-asgi-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + downstream: + environment: + - APPSIGNAL_APP_NAME=python/django4-asgi-collector + - APPSIGNAL_SERVICE_NAME=downstream + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=python/django4-asgi-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/python/django4-asgi/docker-compose.shared.yml b/python/django4-asgi/docker-compose.shared.yml index ce3bbb9ac..3b3d9b93f 100644 --- a/python/django4-asgi/docker-compose.shared.yml +++ b/python/django4-asgi/docker-compose.shared.yml @@ -28,6 +28,36 @@ services: - ../../appsignal_key.env environment: - PORT=4001 + - DOWNSTREAM_URL=http://downstream:4002/ + volumes: + - ./app:/app + - ../integration:/integration + # Second instance of the same image (image-only; only `app` builds). Receives + # the HTTP request the `app` service makes, so the trace continues into it. + downstream: + image: python/django4-asgi + command: /commands/run-downstream + depends_on: + - postgres + - redis + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # Runs only the background worker (image-only; only `app` builds). + worker: + image: python/django4-asgi + command: /commands/run-worker + depends_on: + - postgres + - redis + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env volumes: - ./app:/app - ../integration:/integration diff --git a/python/django4-celery/app/appsignal_python_opentelemetry/settings.py b/python/django4-celery/app/appsignal_python_opentelemetry/settings.py index 0842ad00f..193158e10 100644 --- a/python/django4-celery/app/appsignal_python_opentelemetry/settings.py +++ b/python/django4-celery/app/appsignal_python_opentelemetry/settings.py @@ -26,7 +26,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['app', 'localhost'] +ALLOWED_HOSTS = ['app', 'downstream', 'localhost'] # Application definition diff --git a/python/django4-celery/app/appsignal_python_opentelemetry/views.py b/python/django4-celery/app/appsignal_python_opentelemetry/views.py index 81ff096ef..ef339e0ea 100644 --- a/python/django4-celery/app/appsignal_python_opentelemetry/views.py +++ b/python/django4-celery/app/appsignal_python_opentelemetry/views.py @@ -1,3 +1,4 @@ +import os import requests import json import time @@ -91,9 +92,13 @@ def error_queue_inline(request): return HttpResponse("I ran an error_task inline!") +# Calls a second, separately-instrumented app (the `downstream` service) so the +# injected traceparent is extracted there and the trace spans both apps. Falls +# back to the external, uninstrumented URL when DOWNSTREAM_URL is unset, so the +# setup still works as a single service. def make_request(request): - requests.get('https://www.appsignal.com/') - return HttpResponse("I did a request to appsignal.com!") + requests.get(os.environ.get("DOWNSTREAM_URL", "https://www.appsignal.com/")) + return HttpResponse("I did a request downstream!") def custom_instrumentation(request): diff --git a/python/django4-celery/app/processmon.downstream.toml b/python/django4-celery/app/processmon.downstream.toml new file mode 100644 index 000000000..8a463add9 --- /dev/null +++ b/python/django4-celery/app/processmon.downstream.toml @@ -0,0 +1,15 @@ +[[paths_to_watch]] +path = "/app" + +# The downstream service is a second instance of the same Django app on a +# different port. It receives the HTTP request the `app` service makes, so the +# trace continues into it. In collector mode it reports under the `downstream` +# service name; in agent mode under a separate `-downstream` app. +[processes.django] +command = "python3" +args = [ + "manage.py", + "runserver", + "0.0.0.0:4002" +] +working_dir = "/app" diff --git a/python/django4-celery/app/processmon.toml b/python/django4-celery/app/processmon.toml index 0f26e1af2..86fb3bc76 100644 --- a/python/django4-celery/app/processmon.toml +++ b/python/django4-celery/app/processmon.toml @@ -9,13 +9,3 @@ args = [ "0.0.0.0:4001" ] working_dir = "/app" - -[processes.celery] -command = "celery" -args = [ - "--app=tasks", - "worker", - "--loglevel=INFO", - "--queues=high-priority,low-priority" -] -working_dir = "/app" diff --git a/python/django4-celery/app/processmon.worker.toml b/python/django4-celery/app/processmon.worker.toml new file mode 100644 index 000000000..5a1ff7183 --- /dev/null +++ b/python/django4-celery/app/processmon.worker.toml @@ -0,0 +1,15 @@ +[[paths_to_watch]] +path = "/app" + +# The worker service runs only the background worker. Work enqueued by the web +# `app` service is performed here, so it reports under the worker's own app name +# (agent mode) or `worker` service name (collector mode). +[processes.celery] +command = "celery" +args = [ + "--app=tasks", + "worker", + "--loglevel=INFO", + "--queues=high-priority,low-priority" +] +working_dir = "/app" diff --git a/python/django4-celery/commands/run-downstream b/python/django4-celery/commands/run-downstream new file mode 100755 index 000000000..0583e8792 --- /dev/null +++ b/python/django4-celery/commands/run-downstream @@ -0,0 +1,23 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service owns building the appsignal dist into the shared +# /integration volume and running migrations. Wait for it to start serving +# before doing anything here. That single signal means the dist is built, so we +# can install from it without racing the app's `hatch build`, and it means the +# schema is migrated, so this container never runs migrations itself. +echo "Waiting for the app service to be ready..." +until python3 -c "import socket; socket.create_connection(('app', 4001), 2).close()" >/dev/null 2>&1; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Installing dependencies" +pip3 install -r requirements.txt +pip3 install /integration/dist/* --force-reinstall + +echo "Starting downstream processes" +/commands/processmon processmon.downstream.toml diff --git a/python/django4-celery/commands/run-worker b/python/django4-celery/commands/run-worker new file mode 100755 index 000000000..2df2ee274 --- /dev/null +++ b/python/django4-celery/commands/run-worker @@ -0,0 +1,23 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service builds the appsignal dist into the shared /integration +# volume and (for the Django apps) runs migrations, then serves HTTP. Wait for +# it to be reachable before installing here: that means the dist is built (so we +# install from it without racing the app's `hatch build`) and the schema is +# migrated (so this container never migrates). +echo "Waiting for the app service to be ready..." +until python3 -c "import socket; socket.create_connection(('app', 4001), 2).close()" >/dev/null 2>&1; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Installing dependencies" +pip3 install -r requirements.txt +pip3 install /integration/dist/* --force-reinstall + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/python/django4-celery/docker-compose.agent.yml b/python/django4-celery/docker-compose.agent.yml index 9ec6f3634..519d5cb11 100644 --- a/python/django4-celery/docker-compose.agent.yml +++ b/python/django4-celery/docker-compose.agent.yml @@ -5,3 +5,9 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/django4-celery + downstream: + environment: + - APPSIGNAL_APP_NAME=python/django4-celery-downstream + worker: + environment: + - APPSIGNAL_APP_NAME=python/django4-celery-worker diff --git a/python/django4-celery/docker-compose.collector.yml b/python/django4-celery/docker-compose.collector.yml index e30d90ef7..737099c7d 100644 --- a/python/django4-celery/docker-compose.collector.yml +++ b/python/django4-celery/docker-compose.collector.yml @@ -6,4 +6,15 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/django4-celery-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + downstream: + environment: + - APPSIGNAL_APP_NAME=python/django4-celery-collector + - APPSIGNAL_SERVICE_NAME=downstream + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=python/django4-celery-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/python/django4-celery/docker-compose.shared.yml b/python/django4-celery/docker-compose.shared.yml index 271d75c38..6cd445fd3 100644 --- a/python/django4-celery/docker-compose.shared.yml +++ b/python/django4-celery/docker-compose.shared.yml @@ -28,6 +28,41 @@ services: - ../../appsignal_key.env environment: - PORT=4001 + # The make_request view calls this second, separately-instrumented app so + # the trace propagates across services. Falls back to an external URL when + # unset (single-service runs). + - DOWNSTREAM_URL=http://downstream:4002/ + volumes: + - ./app:/app + - ../integration:/integration + # A second instance of the same image, running as a distinct AppSignal app + # (agent mode) or service (collector mode). It receives the HTTP request the + # `app` service makes, so the trace continues into it. Image-only: only `app` + # builds, so `docker compose build` does not collide on the shared image tag. + downstream: + image: python/django4-celery + command: /commands/run-downstream + depends_on: + - postgres + - redis + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # Runs only the background worker (image-only; only `app` builds). + worker: + image: python/django4-celery + command: /commands/run-worker + depends_on: + - postgres + - redis + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env volumes: - ./app:/app - ../integration:/integration diff --git a/python/django4-wsgi/app/appsignal_python_opentelemetry/settings.py b/python/django4-wsgi/app/appsignal_python_opentelemetry/settings.py index 0842ad00f..193158e10 100644 --- a/python/django4-wsgi/app/appsignal_python_opentelemetry/settings.py +++ b/python/django4-wsgi/app/appsignal_python_opentelemetry/settings.py @@ -26,7 +26,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['app', 'localhost'] +ALLOWED_HOSTS = ['app', 'downstream', 'localhost'] # Application definition diff --git a/python/django4-wsgi/app/appsignal_python_opentelemetry/views.py b/python/django4-wsgi/app/appsignal_python_opentelemetry/views.py index 111501bb2..a8f3e19dc 100644 --- a/python/django4-wsgi/app/appsignal_python_opentelemetry/views.py +++ b/python/django4-wsgi/app/appsignal_python_opentelemetry/views.py @@ -1,3 +1,4 @@ +import os import requests import json import time @@ -84,7 +85,7 @@ def error_queue_inline(request): def make_request(request): - requests.get('https://www.appsignal.com/') + requests.get(os.environ.get("DOWNSTREAM_URL", "https://www.appsignal.com/")) return HttpResponse("I did a request to appsignal.com!") diff --git a/python/django4-wsgi/app/processmon.downstream.toml b/python/django4-wsgi/app/processmon.downstream.toml new file mode 100644 index 000000000..86e5be8f4 --- /dev/null +++ b/python/django4-wsgi/app/processmon.downstream.toml @@ -0,0 +1,12 @@ +[[paths_to_watch]] +path = "/app" + +# Downstream web server on :4002 (no celery here; that is the worker split). +[processes.django] +command = "gunicorn" +args = [ + "appsignal_python_opentelemetry.wsgi", + "--bind", + "0.0.0.0:4002" +] +working_dir = "/app" diff --git a/python/django4-wsgi/app/processmon.toml b/python/django4-wsgi/app/processmon.toml index 0e096c604..aa361888a 100644 --- a/python/django4-wsgi/app/processmon.toml +++ b/python/django4-wsgi/app/processmon.toml @@ -7,13 +7,3 @@ args = [ "appsignal_python_opentelemetry.wsgi" ] working_dir = "/app" - -[processes.celery] -command = "celery" -args = [ - "--app=tasks", - "worker", - "--loglevel=INFO", - "--queues=high-priority,low-priority" -] -working_dir = "/app" diff --git a/python/django4-wsgi/app/processmon.worker.toml b/python/django4-wsgi/app/processmon.worker.toml new file mode 100644 index 000000000..5a1ff7183 --- /dev/null +++ b/python/django4-wsgi/app/processmon.worker.toml @@ -0,0 +1,15 @@ +[[paths_to_watch]] +path = "/app" + +# The worker service runs only the background worker. Work enqueued by the web +# `app` service is performed here, so it reports under the worker's own app name +# (agent mode) or `worker` service name (collector mode). +[processes.celery] +command = "celery" +args = [ + "--app=tasks", + "worker", + "--loglevel=INFO", + "--queues=high-priority,low-priority" +] +working_dir = "/app" diff --git a/python/django4-wsgi/commands/run-downstream b/python/django4-wsgi/commands/run-downstream new file mode 100755 index 000000000..707620794 --- /dev/null +++ b/python/django4-wsgi/commands/run-downstream @@ -0,0 +1,22 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service builds the appsignal dist into the shared /integration +# volume and runs migrations, then serves HTTP. Wait for it to be reachable: +# that means the dist is built (so we install from it without racing the app's +# `hatch build`) and the schema is migrated (so we never migrate from here). +echo "Waiting for the app service to be ready..." +until python3 -c "import socket; socket.create_connection(('app', 4001), 2).close()" >/dev/null 2>&1; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Installing dependencies" +pip3 install -r requirements.txt +pip3 install /integration/dist/* --force-reinstall + +echo "Starting downstream processes" +/commands/processmon processmon.downstream.toml diff --git a/python/django4-wsgi/commands/run-worker b/python/django4-wsgi/commands/run-worker new file mode 100755 index 000000000..2df2ee274 --- /dev/null +++ b/python/django4-wsgi/commands/run-worker @@ -0,0 +1,23 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service builds the appsignal dist into the shared /integration +# volume and (for the Django apps) runs migrations, then serves HTTP. Wait for +# it to be reachable before installing here: that means the dist is built (so we +# install from it without racing the app's `hatch build`) and the schema is +# migrated (so this container never migrates). +echo "Waiting for the app service to be ready..." +until python3 -c "import socket; socket.create_connection(('app', 4001), 2).close()" >/dev/null 2>&1; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Installing dependencies" +pip3 install -r requirements.txt +pip3 install /integration/dist/* --force-reinstall + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/python/django4-wsgi/docker-compose.agent.yml b/python/django4-wsgi/docker-compose.agent.yml index 483db63ba..a9825089a 100644 --- a/python/django4-wsgi/docker-compose.agent.yml +++ b/python/django4-wsgi/docker-compose.agent.yml @@ -5,3 +5,9 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/django4-wsgi + downstream: + environment: + - APPSIGNAL_APP_NAME=python/django4-wsgi-downstream + worker: + environment: + - APPSIGNAL_APP_NAME=python/django4-wsgi-worker diff --git a/python/django4-wsgi/docker-compose.collector.yml b/python/django4-wsgi/docker-compose.collector.yml index d3c6d9504..094b540c0 100644 --- a/python/django4-wsgi/docker-compose.collector.yml +++ b/python/django4-wsgi/docker-compose.collector.yml @@ -6,4 +6,15 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/django4-wsgi-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + downstream: + environment: + - APPSIGNAL_APP_NAME=python/django4-wsgi-collector + - APPSIGNAL_SERVICE_NAME=downstream + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=python/django4-wsgi-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/python/django4-wsgi/docker-compose.shared.yml b/python/django4-wsgi/docker-compose.shared.yml index abae6b943..73def3a13 100644 --- a/python/django4-wsgi/docker-compose.shared.yml +++ b/python/django4-wsgi/docker-compose.shared.yml @@ -28,6 +28,36 @@ services: - ../../appsignal_key.env environment: - PORT=4001 + - DOWNSTREAM_URL=http://downstream:4002/ + volumes: + - ./app:/app + - ../integration:/integration + # Second instance of the same image (image-only; only `app` builds). Receives + # the HTTP request the `app` service makes, so the trace continues into it. + downstream: + image: python/django4-wsgi + command: /commands/run-downstream + depends_on: + - postgres + - redis + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # Runs only the background worker (image-only; only `app` builds). + worker: + image: python/django4-wsgi + command: /commands/run-worker + depends_on: + - postgres + - redis + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env volumes: - ./app:/app - ../integration:/integration diff --git a/python/django5-celery/app/appsignal_python_opentelemetry/settings.py b/python/django5-celery/app/appsignal_python_opentelemetry/settings.py index 0c7fbec62..78f131dab 100644 --- a/python/django5-celery/app/appsignal_python_opentelemetry/settings.py +++ b/python/django5-celery/app/appsignal_python_opentelemetry/settings.py @@ -27,7 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['app', 'localhost'] +ALLOWED_HOSTS = ['app', 'downstream', 'localhost'] # Application definition diff --git a/python/django5-celery/app/appsignal_python_opentelemetry/views.py b/python/django5-celery/app/appsignal_python_opentelemetry/views.py index 204a74119..a42cafd2b 100644 --- a/python/django5-celery/app/appsignal_python_opentelemetry/views.py +++ b/python/django5-celery/app/appsignal_python_opentelemetry/views.py @@ -1,3 +1,4 @@ +import os import requests import json import time @@ -152,7 +153,7 @@ def error_queue_inline(request): def make_request(request): - requests.get('https://www.appsignal.com/') + requests.get(os.environ.get("DOWNSTREAM_URL", "https://www.appsignal.com/")) return HttpResponse("I did a request to appsignal.com!") diff --git a/python/django5-celery/app/processmon.downstream.toml b/python/django5-celery/app/processmon.downstream.toml new file mode 100644 index 000000000..8f5cc974c --- /dev/null +++ b/python/django5-celery/app/processmon.downstream.toml @@ -0,0 +1,12 @@ +[[paths_to_watch]] +path = "/app" + +# Downstream web server on :4002 (no celery here; that is the worker split). +[processes.django] +command = "python3" +args = [ + "manage.py", + "runserver", + "0.0.0.0:4002" +] +working_dir = "/app" diff --git a/python/django5-celery/app/processmon.toml b/python/django5-celery/app/processmon.toml index 0f26e1af2..86fb3bc76 100644 --- a/python/django5-celery/app/processmon.toml +++ b/python/django5-celery/app/processmon.toml @@ -9,13 +9,3 @@ args = [ "0.0.0.0:4001" ] working_dir = "/app" - -[processes.celery] -command = "celery" -args = [ - "--app=tasks", - "worker", - "--loglevel=INFO", - "--queues=high-priority,low-priority" -] -working_dir = "/app" diff --git a/python/django5-celery/app/processmon.worker.toml b/python/django5-celery/app/processmon.worker.toml new file mode 100644 index 000000000..5a1ff7183 --- /dev/null +++ b/python/django5-celery/app/processmon.worker.toml @@ -0,0 +1,15 @@ +[[paths_to_watch]] +path = "/app" + +# The worker service runs only the background worker. Work enqueued by the web +# `app` service is performed here, so it reports under the worker's own app name +# (agent mode) or `worker` service name (collector mode). +[processes.celery] +command = "celery" +args = [ + "--app=tasks", + "worker", + "--loglevel=INFO", + "--queues=high-priority,low-priority" +] +working_dir = "/app" diff --git a/python/django5-celery/commands/run-downstream b/python/django5-celery/commands/run-downstream new file mode 100755 index 000000000..707620794 --- /dev/null +++ b/python/django5-celery/commands/run-downstream @@ -0,0 +1,22 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service builds the appsignal dist into the shared /integration +# volume and runs migrations, then serves HTTP. Wait for it to be reachable: +# that means the dist is built (so we install from it without racing the app's +# `hatch build`) and the schema is migrated (so we never migrate from here). +echo "Waiting for the app service to be ready..." +until python3 -c "import socket; socket.create_connection(('app', 4001), 2).close()" >/dev/null 2>&1; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Installing dependencies" +pip3 install -r requirements.txt +pip3 install /integration/dist/* --force-reinstall + +echo "Starting downstream processes" +/commands/processmon processmon.downstream.toml diff --git a/python/django5-celery/commands/run-worker b/python/django5-celery/commands/run-worker new file mode 100755 index 000000000..2df2ee274 --- /dev/null +++ b/python/django5-celery/commands/run-worker @@ -0,0 +1,23 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service builds the appsignal dist into the shared /integration +# volume and (for the Django apps) runs migrations, then serves HTTP. Wait for +# it to be reachable before installing here: that means the dist is built (so we +# install from it without racing the app's `hatch build`) and the schema is +# migrated (so this container never migrates). +echo "Waiting for the app service to be ready..." +until python3 -c "import socket; socket.create_connection(('app', 4001), 2).close()" >/dev/null 2>&1; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Installing dependencies" +pip3 install -r requirements.txt +pip3 install /integration/dist/* --force-reinstall + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/python/django5-celery/docker-compose.agent.yml b/python/django5-celery/docker-compose.agent.yml index fbffc1731..4bddff1a6 100644 --- a/python/django5-celery/docker-compose.agent.yml +++ b/python/django5-celery/docker-compose.agent.yml @@ -5,3 +5,9 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/django5-celery + downstream: + environment: + - APPSIGNAL_APP_NAME=python/django5-celery-downstream + worker: + environment: + - APPSIGNAL_APP_NAME=python/django5-celery-worker diff --git a/python/django5-celery/docker-compose.collector.yml b/python/django5-celery/docker-compose.collector.yml index 932dac9a5..03ec19794 100644 --- a/python/django5-celery/docker-compose.collector.yml +++ b/python/django5-celery/docker-compose.collector.yml @@ -6,4 +6,15 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/django5-celery-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + downstream: + environment: + - APPSIGNAL_APP_NAME=python/django5-celery-collector + - APPSIGNAL_SERVICE_NAME=downstream + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=python/django5-celery-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/python/django5-celery/docker-compose.shared.yml b/python/django5-celery/docker-compose.shared.yml index 71372be60..0c3ecfb75 100644 --- a/python/django5-celery/docker-compose.shared.yml +++ b/python/django5-celery/docker-compose.shared.yml @@ -28,6 +28,42 @@ services: - ../../appsignal_key.env environment: - PORT=4001 + - DOWNSTREAM_URL=http://downstream:4002/ + - APPSIGNAL_APP_ENV=dev + - APPSIGNAL_LOG_LEVEL=trace + volumes: + - ./app:/app + - ../integration:/integration + # Second instance of the same image (image-only; only `app` builds). Receives + # the HTTP request the `app` service makes, so the trace continues into it. + downstream: + image: python/django5-celery + command: /commands/run-downstream + depends_on: + - postgres + - redis + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env + environment: + - APPSIGNAL_APP_ENV=dev + - APPSIGNAL_LOG_LEVEL=trace + volumes: + - ./app:/app + - ../integration:/integration + # Runs only the background worker (image-only; only `app` builds). + worker: + image: python/django5-celery + command: /commands/run-worker + depends_on: + - postgres + - redis + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env + environment: - APPSIGNAL_APP_ENV=dev - APPSIGNAL_LOG_LEVEL=trace volumes: diff --git a/python/fastapi-databases/docker-compose.collector.yml b/python/fastapi-databases/docker-compose.collector.yml index a7d4d6f22..f7838f562 100644 --- a/python/fastapi-databases/docker-compose.collector.yml +++ b/python/fastapi-databases/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/fastapi-databases-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/python/fastapi/docker-compose.collector.yml b/python/fastapi/docker-compose.collector.yml index 9fb6468a4..31354247b 100644 --- a/python/fastapi/docker-compose.collector.yml +++ b/python/fastapi/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/fastapi-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/python/flask-pika/app/processmon.toml b/python/flask-pika/app/processmon.toml index a9842fc15..c74bb0fa7 100644 --- a/python/flask-pika/app/processmon.toml +++ b/python/flask-pika/app/processmon.toml @@ -9,10 +9,3 @@ args = [ "--port=4001" ] working_dir = "/app" - -[processes.consumer] -command = "python" -args = [ - "pika_consumer.py" -] -working_dir = "/app" diff --git a/python/flask-pika/app/processmon.worker.toml b/python/flask-pika/app/processmon.worker.toml new file mode 100644 index 000000000..560f6f307 --- /dev/null +++ b/python/flask-pika/app/processmon.worker.toml @@ -0,0 +1,12 @@ +[[paths_to_watch]] +path = "/app" + +# The worker service runs only the background worker. Work enqueued by the web +# `app` service is performed here, so it reports under the worker's own app name +# (agent mode) or `worker` service name (collector mode). +[processes.consumer] +command = "python" +args = [ + "pika_consumer.py" +] +working_dir = "/app" diff --git a/python/flask-pika/commands/run-worker b/python/flask-pika/commands/run-worker new file mode 100755 index 000000000..2df2ee274 --- /dev/null +++ b/python/flask-pika/commands/run-worker @@ -0,0 +1,23 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service builds the appsignal dist into the shared /integration +# volume and (for the Django apps) runs migrations, then serves HTTP. Wait for +# it to be reachable before installing here: that means the dist is built (so we +# install from it without racing the app's `hatch build`) and the schema is +# migrated (so this container never migrates). +echo "Waiting for the app service to be ready..." +until python3 -c "import socket; socket.create_connection(('app', 4001), 2).close()" >/dev/null 2>&1; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Installing dependencies" +pip3 install -r requirements.txt +pip3 install /integration/dist/* --force-reinstall + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/python/flask-pika/docker-compose.agent.yml b/python/flask-pika/docker-compose.agent.yml index f91fd9c65..308b995a3 100644 --- a/python/flask-pika/docker-compose.agent.yml +++ b/python/flask-pika/docker-compose.agent.yml @@ -5,3 +5,6 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/flask-pika + worker: + environment: + - APPSIGNAL_APP_NAME=python/flask-pika-worker diff --git a/python/flask-pika/docker-compose.collector.yml b/python/flask-pika/docker-compose.collector.yml index c24ef4fce..81415b1be 100644 --- a/python/flask-pika/docker-compose.collector.yml +++ b/python/flask-pika/docker-compose.collector.yml @@ -6,4 +6,10 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/flask-pika-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=python/flask-pika-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/python/flask-pika/docker-compose.shared.yml b/python/flask-pika/docker-compose.shared.yml index 1f93057e3..5fff1a869 100644 --- a/python/flask-pika/docker-compose.shared.yml +++ b/python/flask-pika/docker-compose.shared.yml @@ -22,6 +22,18 @@ services: volumes: - ./app:/app - ../integration:/integration + # Runs only the background worker (image-only; only `app` builds). + worker: + image: python/flask + command: /commands/run-worker + depends_on: + - rabbitmq + env_file: + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration tests: build: ../../support/server-tests image: server-tests diff --git a/python/flask-sqlalchemy/docker-compose.collector.yml b/python/flask-sqlalchemy/docker-compose.collector.yml index d0d5d1b64..d3b08022b 100644 --- a/python/flask-sqlalchemy/docker-compose.collector.yml +++ b/python/flask-sqlalchemy/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/flask-sqlalchemy-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/python/flask/docker-compose.collector.yml b/python/flask/docker-compose.collector.yml index 63e9c0b22..7be2ca6fb 100644 --- a/python/flask/docker-compose.collector.yml +++ b/python/flask/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/flask-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/python/starlette/docker-compose.collector.yml b/python/starlette/docker-compose.collector.yml index 6609b2fb5..efcb78225 100644 --- a/python/starlette/docker-compose.collector.yml +++ b/python/starlette/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=python/starlette-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/grape/docker-compose.collector.yml b/ruby/grape/docker-compose.collector.yml index 399479e47..a7905da74 100644 --- a/ruby/grape/docker-compose.collector.yml +++ b/ruby/grape/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/grape-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/hanami2-postgres/docker-compose.collector.yml b/ruby/hanami2-postgres/docker-compose.collector.yml index 18dd62592..a05e7758f 100644 --- a/ruby/hanami2-postgres/docker-compose.collector.yml +++ b/ruby/hanami2-postgres/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-hanami2-postgres-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rack/docker-compose.collector.yml b/ruby/rack/docker-compose.collector.yml index 8ade7cada..6695ba420 100644 --- a/ruby/rack/docker-compose.collector.yml +++ b/ruby/rack/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/rack-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails6-mysql/docker-compose.collector.yml b/ruby/rails6-mysql/docker-compose.collector.yml index a17f41c8c..91c62f6cc 100644 --- a/ruby/rails6-mysql/docker-compose.collector.yml +++ b/ruby/rails6-mysql/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-rails6-mysql-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails6-shakapacker/docker-compose.collector.yml b/ruby/rails6-shakapacker/docker-compose.collector.yml index f13c9a20a..ab23d8511 100644 --- a/ruby/rails6-shakapacker/docker-compose.collector.yml +++ b/ruby/rails6-shakapacker/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-rails6-shakapacker-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails6-shoryuken/app/Gemfile.lock b/ruby/rails6-shoryuken/app/Gemfile.lock index cd45e09c7..e6c31203e 100644 --- a/ruby/rails6-shoryuken/app/Gemfile.lock +++ b/ruby/rails6-shoryuken/app/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: /integration specs: - appsignal (4.8.0) + appsignal (4.8.6) logger rack (>= 2.0.0) diff --git a/ruby/rails6-shoryuken/app/app/controllers/tests_controller.rb b/ruby/rails6-shoryuken/app/app/controllers/tests_controller.rb index 9868aeb30..c557f9dd1 100644 --- a/ruby/rails6-shoryuken/app/app/controllers/tests_controller.rb +++ b/ruby/rails6-shoryuken/app/app/controllers/tests_controller.rb @@ -31,4 +31,12 @@ def shoryuken_batched_job end render :html => "Batched Shoryuken jobs queued, refresh to queue new ones!" end + + # Enqueue a native Shoryuken job onto the `downstream` queue, which only the + # separately instrumented downstream service drains. In collector mode this + # shows job trace propagation across two services. + def cross_service_job + DownstreamWorker.perform_async(:body => "Cross-service job queued") + render :html => "Cross-service Shoryuken job queued, refresh to queue a new one!" + end end diff --git a/ruby/rails6-shoryuken/app/app/views/tests/index.html.erb b/ruby/rails6-shoryuken/app/app/views/tests/index.html.erb index 2588b3362..6c5fcc6a1 100644 --- a/ruby/rails6-shoryuken/app/app/views/tests/index.html.erb +++ b/ruby/rails6-shoryuken/app/app/views/tests/index.html.erb @@ -7,4 +7,5 @@
  • <%= link_to "ActiveJob PerformanceJob", active_job_performance_job_path %>
  • <%= link_to "Native Shoryuken worker", shoryuken_native_job_path %>
  • <%= link_to "Batched Shoryuken worker", shoryuken_batched_job_path %>
  • +
  • <%= link_to "Cross-service Shoryuken worker", cross_service_job_path %>
  • diff --git a/ruby/rails6-shoryuken/app/app/workers/downstream_worker.rb b/ruby/rails6-shoryuken/app/app/workers/downstream_worker.rb new file mode 100644 index 000000000..aecd71eb7 --- /dev/null +++ b/ruby/rails6-shoryuken/app/app/workers/downstream_worker.rb @@ -0,0 +1,14 @@ +# A native Shoryuken worker enqueued by this app but performed by the +# separately-instrumented downstream service, which is the only worker draining +# the `downstream` queue. In collector mode the trace context rides along on an +# SQS message attribute, so the job's trace (under the downstream app) links back +# to the enqueue span (under this app) -- job trace propagation across services. +class DownstreamWorker + include Shoryuken::Worker + + shoryuken_options :queue => "downstream", :auto_delete => true, :body_parser => :json + + def perform(_sqs_msg, body) + puts "downstream performed Shoryuken job: #{body}" + end +end diff --git a/ruby/rails6-shoryuken/app/config/initializers/shoryuken.rb b/ruby/rails6-shoryuken/app/config/initializers/shoryuken.rb index fb9ed965e..7a3350b67 100644 --- a/ruby/rails6-shoryuken/app/config/initializers/shoryuken.rb +++ b/ruby/rails6-shoryuken/app/config/initializers/shoryuken.rb @@ -23,4 +23,5 @@ # route the "native" queue to NativeWorker. Rails.application.config.to_prepare do NativeWorker + DownstreamWorker end diff --git a/ruby/rails6-shoryuken/app/config/routes.rb b/ruby/rails6-shoryuken/app/config/routes.rb index 32e08a8f1..18f51893c 100644 --- a/ruby/rails6-shoryuken/app/config/routes.rb +++ b/ruby/rails6-shoryuken/app/config/routes.rb @@ -5,5 +5,6 @@ get "/active_job_error_job", to: "tests#active_job_error_job" get "/shoryuken_native_job", to: "tests#shoryuken_native_job" get "/shoryuken_batched_job", to: "tests#shoryuken_batched_job" + get "/cross_service_job", to: "tests#cross_service_job" root :to => "tests#index" end diff --git a/ruby/rails6-shoryuken/app/processmon.downstream.toml b/ruby/rails6-shoryuken/app/processmon.downstream.toml new file mode 100644 index 000000000..6d3a75ebb --- /dev/null +++ b/ruby/rails6-shoryuken/app/processmon.downstream.toml @@ -0,0 +1,20 @@ +[[paths_to_watch]] +path = "/app" +ignore = ["tmp", "log", "public/packs", "node_modules/.cache"] + +[[paths_to_watch]] +path = "/integration" + +# Only the downstream service runs this worker, draining just the `downstream` +# queue, so jobs enqueued by `app` are performed here under the downstream app +# name. (`app`'s worker uses the default/native queues, so it never picks these +# up.) No web server -- this service only runs jobs. +[processes.shoryuken] +command = "bundle" +args = [ + "exec", + "shoryuken", + "-q=downstream", + "-R" +] +working_dir = "/app" diff --git a/ruby/rails6-shoryuken/app/processmon.toml b/ruby/rails6-shoryuken/app/processmon.toml index 072c35113..22455f20e 100644 --- a/ruby/rails6-shoryuken/app/processmon.toml +++ b/ruby/rails6-shoryuken/app/processmon.toml @@ -5,18 +5,6 @@ ignore = ["tmp", "log", "public/packs", "node_modules/.cache"] [[paths_to_watch]] path = "/integration" -[processes.shoryuken] -command = "bundle" -args = [ - "exec", - "shoryuken", - "-q=default", - "-q=native", - "-q=batched", - "-R" -] -working_dir = "/app" - [processes.rails] command = "bin/rails" args = [ diff --git a/ruby/rails6-shoryuken/app/processmon.worker.toml b/ruby/rails6-shoryuken/app/processmon.worker.toml new file mode 100644 index 000000000..516c2c28b --- /dev/null +++ b/ruby/rails6-shoryuken/app/processmon.worker.toml @@ -0,0 +1,22 @@ +[[paths_to_watch]] +path = "/app" +ignore = ["tmp", "log", "public/packs", "node_modules/.cache"] + +[[paths_to_watch]] +path = "/integration" + +# The worker service runs only the background job processor. Jobs enqueued by +# the web `app` service are performed here, so the job's trace runs under the +# worker's own app name (agent mode) or `worker` service name (collector mode), +# linked back to the enqueue span in `app`. +[processes.shoryuken] +command = "bundle" +args = [ + "exec", + "shoryuken", + "-q=default", + "-q=native", + "-q=batched", + "-R" +] +working_dir = "/app" diff --git a/ruby/rails6-shoryuken/commands/run b/ruby/rails6-shoryuken/commands/run index ca8f48a56..010a2e6d1 100755 --- a/ruby/rails6-shoryuken/commands/run +++ b/ruby/rails6-shoryuken/commands/run @@ -24,5 +24,8 @@ echo "Create sqs queues in mock server" bundle exec shoryuken sqs create default bundle exec shoryuken sqs create native bundle exec shoryuken sqs create batched +# Owned by the downstream service, but create it here too so the queue exists +# when this app enqueues to it, regardless of the downstream service's timing. +bundle exec shoryuken sqs create downstream /commands/processmon processmon.toml diff --git a/ruby/rails6-shoryuken/commands/run-downstream b/ruby/rails6-shoryuken/commands/run-downstream new file mode 100755 index 000000000..159334c6f --- /dev/null +++ b/ruby/rails6-shoryuken/commands/run-downstream @@ -0,0 +1,37 @@ +#!/bin/bash + +set -eu + +cd /app + +# Point bundler at the shared vendor/bundle the `app` service installs into. +# (This app configures the path at runtime rather than via a BUNDLE_PATH env, so +# each container must set it for itself.) +bundle config set path vendor/bundle + +# The `app` service owns installing gems and running migrations into the shared +# /app and /integration volumes. Wait for that instead of installing +# concurrently, which would race on the shared vendor/bundle directory. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Also wait for the app to finish migrating. A DB-backed queue worker (e.g. Que) +# polls a table created by a migration; if it starts first it crashes, and +# processmon does not restart a crashed process. The app only serves HTTP after +# it migrates, so a reachable app means the schema is ready. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +# Make sure the queue exists before this worker starts polling it (the `app` +# service also creates it; SQS CreateQueue is idempotent). +echo "Ensuring the downstream SQS queue exists" +bundle exec shoryuken sqs create downstream || true + +echo "Starting downstream Shoryuken worker" +/commands/processmon processmon.downstream.toml diff --git a/ruby/rails6-shoryuken/commands/run-worker b/ruby/rails6-shoryuken/commands/run-worker new file mode 100755 index 000000000..e1aa38eca --- /dev/null +++ b/ruby/rails6-shoryuken/commands/run-worker @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service owns installing gems and running migrations into the shared +# /app and /integration volumes. Wait for both before starting the worker. +# +# Gems: `bundle check` is read-only, so polling it is safe. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Migrations: a DB-backed queue (que, solid_queue, delayed_job, good_job) polls +# a table created by a migration. If the worker starts before the app migrates, +# the process crashes and processmon does not restart it. The app only serves +# HTTP after it has migrated, so a reachable app means migrations are done. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/ruby/rails6-shoryuken/docker-compose.agent.yml b/ruby/rails6-shoryuken/docker-compose.agent.yml index b6ca95b17..47eb4701d 100644 --- a/ruby/rails6-shoryuken/docker-compose.agent.yml +++ b/ruby/rails6-shoryuken/docker-compose.agent.yml @@ -5,3 +5,9 @@ services: app: environment: - APPSIGNAL_APP_NAME=rails6-shoryuken + downstream: + environment: + - APPSIGNAL_APP_NAME=rails6-shoryuken-downstream + worker: + environment: + - APPSIGNAL_APP_NAME=rails6-shoryuken-worker diff --git a/ruby/rails6-shoryuken/docker-compose.collector.yml b/ruby/rails6-shoryuken/docker-compose.collector.yml index baa91282f..467cbc61f 100644 --- a/ruby/rails6-shoryuken/docker-compose.collector.yml +++ b/ruby/rails6-shoryuken/docker-compose.collector.yml @@ -6,4 +6,15 @@ services: app: environment: - APPSIGNAL_APP_NAME=rails6-shoryuken-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + downstream: + environment: + - APPSIGNAL_APP_NAME=rails6-shoryuken-collector + - APPSIGNAL_SERVICE_NAME=downstream + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=rails6-shoryuken-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails6-shoryuken/docker-compose.shared.yml b/ruby/rails6-shoryuken/docker-compose.shared.yml index d2df5cfc0..e8a1aecef 100644 --- a/ruby/rails6-shoryuken/docker-compose.shared.yml +++ b/ruby/rails6-shoryuken/docker-compose.shared.yml @@ -23,6 +23,41 @@ services: volumes: - ./app:/app - ../integration:/integration + # A second instance of the same image, running as a distinct AppSignal app. It + # drains the `downstream` SQS queue that `app` enqueues to, so the job's trace + # links back to the enqueuing app (job trace propagation across services). + downstream: + platform: "linux/amd64" + image: ruby/rails6-shoryuken + command: /commands/run-downstream + depends_on: + - motoserver + env_file: + - shoryuken.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # Runs only the background worker (see run-worker / processmon.worker.toml). + # Image-only (no build:) so `docker compose build` does not collide with the + # app service exporting the same image tag on Docker's containerd store. + worker: + platform: "linux/amd64" + image: ruby/rails6-shoryuken + command: /commands/run-worker + depends_on: + - motoserver + env_file: + - shoryuken.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # A second instance of the same image, running as a distinct AppSignal app. It + # drains the `downstream` SQS queue that `app` enqueues to, so the job's trace + # links back to the enqueuing app (job trace propagation across services). tests: build: ../../support/server-tests image: server-tests diff --git a/ruby/rails7-delayed-job/app/processmon.toml b/ruby/rails7-delayed-job/app/processmon.toml index 2ddbdc901..4018daf2e 100644 --- a/ruby/rails7-delayed-job/app/processmon.toml +++ b/ruby/rails7-delayed-job/app/processmon.toml @@ -10,13 +10,6 @@ ignore = [ [[paths_to_watch]] path = "/integration" -[processes.delayed_job] -command = "bin/rake" -args = [ - "jobs:work" -] -working_dir = "/app" - [processes.rails] command = "bin/rails" args = [ diff --git a/ruby/rails7-delayed-job/app/processmon.worker.toml b/ruby/rails7-delayed-job/app/processmon.worker.toml new file mode 100644 index 000000000..9d40330d0 --- /dev/null +++ b/ruby/rails7-delayed-job/app/processmon.worker.toml @@ -0,0 +1,22 @@ +[[paths_to_watch]] +path = "/app" +ignore = [ + "tmp", + "log", + "node_modules", + "public/packs" +] + +[[paths_to_watch]] +path = "/integration" + +# The worker service runs only the background job processor. Jobs enqueued by +# the web `app` service are performed here, so the job's trace runs under the +# worker's own app name (agent mode) or `worker` service name (collector mode), +# linked back to the enqueue span in `app`. +[processes.delayed_job] +command = "bin/rake" +args = [ + "jobs:work" +] +working_dir = "/app" diff --git a/ruby/rails7-delayed-job/commands/run-worker b/ruby/rails7-delayed-job/commands/run-worker new file mode 100755 index 000000000..e1aa38eca --- /dev/null +++ b/ruby/rails7-delayed-job/commands/run-worker @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service owns installing gems and running migrations into the shared +# /app and /integration volumes. Wait for both before starting the worker. +# +# Gems: `bundle check` is read-only, so polling it is safe. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Migrations: a DB-backed queue (que, solid_queue, delayed_job, good_job) polls +# a table created by a migration. If the worker starts before the app migrates, +# the process crashes and processmon does not restart it. The app only serves +# HTTP after it has migrated, so a reachable app means migrations are done. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/ruby/rails7-delayed-job/docker-compose.agent.yml b/ruby/rails7-delayed-job/docker-compose.agent.yml index 00e76013a..6e00a9870 100644 --- a/ruby/rails7-delayed-job/docker-compose.agent.yml +++ b/ruby/rails7-delayed-job/docker-compose.agent.yml @@ -5,3 +5,6 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-rails7-delayed-job + worker: + environment: + - APPSIGNAL_APP_NAME=ruby-rails7-delayed-job-worker diff --git a/ruby/rails7-delayed-job/docker-compose.collector.yml b/ruby/rails7-delayed-job/docker-compose.collector.yml index 9d25d7abc..13d1f7bbd 100644 --- a/ruby/rails7-delayed-job/docker-compose.collector.yml +++ b/ruby/rails7-delayed-job/docker-compose.collector.yml @@ -6,4 +6,10 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-rails7-delayed-job-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=ruby-rails7-delayed-job-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails7-delayed-job/docker-compose.shared.yml b/ruby/rails7-delayed-job/docker-compose.shared.yml index af9c763bb..5f3f78710 100644 --- a/ruby/rails7-delayed-job/docker-compose.shared.yml +++ b/ruby/rails7-delayed-job/docker-compose.shared.yml @@ -23,6 +23,20 @@ services: volumes: - ./app:/app - ../integration:/integration + # Runs only the background worker (see run-worker / processmon.worker.toml). + # Image-only (no build:) so `docker compose build` does not collide with the + # app service exporting the same image tag on Docker's containerd store. + worker: + image: ruby/rails7-delayed-job + command: /commands/run-worker + depends_on: + - postgres + env_file: + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration tests: build: ../../support/server-tests image: server-tests diff --git a/ruby/rails7-goodjob/app/processmon.toml b/ruby/rails7-goodjob/app/processmon.toml index 1ee66b533..0b1962e22 100644 --- a/ruby/rails7-goodjob/app/processmon.toml +++ b/ruby/rails7-goodjob/app/processmon.toml @@ -10,15 +10,6 @@ ignore = [ [[paths_to_watch]] path = "/integration" -[processes.good_job] -command = "bundle" -args = [ - "exec", - "good_job", - "start" -] -working_dir = "/app" - [processes.rails] command = "bin/rails" args = [ diff --git a/ruby/rails7-goodjob/app/processmon.worker.toml b/ruby/rails7-goodjob/app/processmon.worker.toml new file mode 100644 index 000000000..c5b59b84c --- /dev/null +++ b/ruby/rails7-goodjob/app/processmon.worker.toml @@ -0,0 +1,24 @@ +[[paths_to_watch]] +path = "/app" +ignore = [ + "tmp", + "log", + "node_modules", + "public/packs" +] + +[[paths_to_watch]] +path = "/integration" + +# The worker service runs only the background job processor. Jobs enqueued by +# the web `app` service are performed here, so the job's trace runs under the +# worker's own app name (agent mode) or `worker` service name (collector mode), +# linked back to the enqueue span in `app`. +[processes.good_job] +command = "bundle" +args = [ + "exec", + "good_job", + "start" +] +working_dir = "/app" diff --git a/ruby/rails7-goodjob/commands/run-worker b/ruby/rails7-goodjob/commands/run-worker new file mode 100755 index 000000000..e1aa38eca --- /dev/null +++ b/ruby/rails7-goodjob/commands/run-worker @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service owns installing gems and running migrations into the shared +# /app and /integration volumes. Wait for both before starting the worker. +# +# Gems: `bundle check` is read-only, so polling it is safe. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Migrations: a DB-backed queue (que, solid_queue, delayed_job, good_job) polls +# a table created by a migration. If the worker starts before the app migrates, +# the process crashes and processmon does not restart it. The app only serves +# HTTP after it has migrated, so a reachable app means migrations are done. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/ruby/rails7-goodjob/docker-compose.agent.yml b/ruby/rails7-goodjob/docker-compose.agent.yml index 1cbf79f1e..7cbfebae4 100644 --- a/ruby/rails7-goodjob/docker-compose.agent.yml +++ b/ruby/rails7-goodjob/docker-compose.agent.yml @@ -5,3 +5,6 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-rails7-goodjob + worker: + environment: + - APPSIGNAL_APP_NAME=ruby-rails7-goodjob-worker diff --git a/ruby/rails7-goodjob/docker-compose.collector.yml b/ruby/rails7-goodjob/docker-compose.collector.yml index 501050d69..631c2ff67 100644 --- a/ruby/rails7-goodjob/docker-compose.collector.yml +++ b/ruby/rails7-goodjob/docker-compose.collector.yml @@ -6,4 +6,10 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-rails7-goodjob-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=ruby-rails7-goodjob-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails7-goodjob/docker-compose.shared.yml b/ruby/rails7-goodjob/docker-compose.shared.yml index 6138d2eab..9ef3b5d3e 100644 --- a/ruby/rails7-goodjob/docker-compose.shared.yml +++ b/ruby/rails7-goodjob/docker-compose.shared.yml @@ -24,6 +24,21 @@ services: volumes: - ./app:/app - ../integration:/integration + # Runs only the background worker (see run-worker / processmon.worker.toml). + # Image-only (no build:) so `docker compose build` does not collide with the + # app service exporting the same image tag on Docker's containerd store. + worker: + image: ruby/rails7-goodjob + command: /commands/run-worker + depends_on: + - postgres + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration tests: build: ../../support/server-tests image: server-tests diff --git a/ruby/rails7-postgres/docker-compose.collector.yml b/ruby/rails7-postgres/docker-compose.collector.yml index 938d30519..2939c4621 100644 --- a/ruby/rails7-postgres/docker-compose.collector.yml +++ b/ruby/rails7-postgres/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-rails7-postgres-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails7-sequel/docker-compose.collector.yml b/ruby/rails7-sequel/docker-compose.collector.yml index bcf685cda..3c854c92c 100644 --- a/ruby/rails7-sequel/docker-compose.collector.yml +++ b/ruby/rails7-sequel/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-rails7-sequel-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails7-sidekiq/app/Gemfile b/ruby/rails7-sidekiq/app/Gemfile index b01d778ad..e4b48f96f 100644 --- a/ruby/rails7-sidekiq/app/Gemfile +++ b/ruby/rails7-sidekiq/app/Gemfile @@ -14,6 +14,16 @@ gem "excon" gem "http" gem "faraday" +# OpenTelemetry gems for AppSignal collector mode. Loaded only when +# APPSIGNAL_COLLECTOR_ENDPOINT is set; keep versions in sync with appsignal's +# lib/appsignal/opentelemetry/dependencies.rb. +gem "opentelemetry-sdk", ">= 1.8.0" +gem "opentelemetry-metrics-sdk", ">= 0.7.1" +gem "opentelemetry-logs-sdk", ">= 0.2.0" +gem "opentelemetry-exporter-otlp", ">= 0.30.0" +gem "opentelemetry-exporter-otlp-metrics", ">= 0.4.0" +gem "opentelemetry-exporter-otlp-logs", ">= 0.2.0" + # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] gem "sprockets-rails" diff --git a/ruby/rails7-sidekiq/app/Gemfile.lock b/ruby/rails7-sidekiq/app/Gemfile.lock index 2dee8db21..8b9485392 100644 --- a/ruby/rails7-sidekiq/app/Gemfile.lock +++ b/ruby/rails7-sidekiq/app/Gemfile.lock @@ -122,6 +122,11 @@ GEM rake globalid (1.2.1) activesupport (>= 6.1) + google-protobuf (4.33.6) + bigdecimal + rake (>= 13) + googleapis-common-protos-types (1.22.0) + google-protobuf (~> 4.26) http (5.3.1) addressable (~> 2.8) http-cookie (~> 1.0) @@ -178,6 +183,56 @@ GEM nio4r (2.7.3) nokogiri (1.16.5-aarch64-linux) racc (~> 1.4) + opentelemetry-api (1.8.0) + logger + opentelemetry-common (0.23.0) + opentelemetry-api (~> 1.0) + opentelemetry-exporter-otlp (0.32.0) + google-protobuf (>= 3.18) + googleapis-common-protos-types (~> 1.3) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-sdk (~> 1.10) + opentelemetry-semantic_conventions + opentelemetry-exporter-otlp-logs (0.3.0) + google-protobuf (>= 3.18) + googleapis-common-protos-types (~> 1.3) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-logs-api (~> 0.1) + opentelemetry-logs-sdk (~> 0.1) + opentelemetry-sdk + opentelemetry-semantic_conventions + opentelemetry-exporter-otlp-metrics (0.7.0) + google-protobuf (>= 3.18, < 5.0) + googleapis-common-protos-types (~> 1.3) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-metrics-api (~> 0.2) + opentelemetry-metrics-sdk (~> 0.5) + opentelemetry-sdk (~> 1.2) + opentelemetry-semantic_conventions + opentelemetry-logs-api (0.2.0) + opentelemetry-api (~> 1.0) + opentelemetry-logs-sdk (0.4.0) + opentelemetry-api (~> 1.2) + opentelemetry-logs-api (~> 0.1) + opentelemetry-sdk (~> 1.3) + opentelemetry-metrics-api (0.4.0) + opentelemetry-api (~> 1.0) + opentelemetry-metrics-sdk (0.12.0) + opentelemetry-api (~> 1.1) + opentelemetry-metrics-api (~> 0.2) + opentelemetry-sdk (~> 1.2) + opentelemetry-registry (0.4.0) + opentelemetry-api (~> 1.1) + opentelemetry-sdk (1.10.0) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-registry (~> 0.2) + opentelemetry-semantic_conventions + opentelemetry-semantic_conventions (1.36.0) + opentelemetry-api (~> 1.0) pg (1.5.4) psych (5.1.2) stringio @@ -309,6 +364,12 @@ DEPENDENCIES http importmap-rails jbuilder + opentelemetry-exporter-otlp (>= 0.30.0) + opentelemetry-exporter-otlp-logs (>= 0.2.0) + opentelemetry-exporter-otlp-metrics (>= 0.4.0) + opentelemetry-logs-sdk (>= 0.2.0) + opentelemetry-metrics-sdk (>= 0.7.1) + opentelemetry-sdk (>= 1.8.0) pg (~> 1.1) puma (~> 6.0) rails (~> 7.1.0) diff --git a/ruby/rails7-sidekiq/app/app/controllers/requests_controller.rb b/ruby/rails7-sidekiq/app/app/controllers/requests_controller.rb index e4af0eb3f..0cd75c41c 100644 --- a/ruby/rails7-sidekiq/app/app/controllers/requests_controller.rb +++ b/ruby/rails7-sidekiq/app/app/controllers/requests_controller.rb @@ -1,13 +1,21 @@ class RequestsController < ApplicationController skip_forgery_protection + # The HTTP client actions call a second, separately-instrumented app (the + # `downstream` service) so the injected `traceparent` is extracted there and + # the trace spans both apps. Falls back to calling this app itself when + # DOWNSTREAM_URL is unset, so the setup still works as a single service. + def self.downstream_url(path) + "#{ENV.fetch("DOWNSTREAM_URL", "http://localhost:4001")}#{path}" + end + def perform_excon_get - handle_response Excon.get("http://localhost:4001/requests/excon_get?query=param") + handle_response Excon.get(self.class.downstream_url("/requests/excon_get?query=param")) end def perform_excon_post handle_response Excon.post( - "http://localhost:4001/requests/excon_post", + self.class.downstream_url("/requests/excon_post"), :body => URI.encode_www_form(:language => "ruby", :class => "fog"), :headers => { "Content-Type" => "application/x-www-form-urlencoded" } ) @@ -15,7 +23,7 @@ def perform_excon_post def perform_excon_put handle_response Excon.put( - "http://localhost:4001/requests/excon_put", + self.class.downstream_url("/requests/excon_put"), :body => URI.encode_www_form(:language => "ruby", :class => "fog"), :headers => { "Content-Type" => "application/x-www-form-urlencoded" } ) @@ -23,26 +31,28 @@ def perform_excon_put def perform_excon_delete handle_response Excon.delete( - "http://localhost:4001/requests/excon_delete", + self.class.downstream_url("/requests/excon_delete"), :body => URI.encode_www_form(:language => "ruby", :class => "fog"), :headers => { "Content-Type" => "application/x-www-form-urlencoded" } ) end def perform_excon_head - handle_response Excon.head("http://localhost:4001/requests/excon_get") + handle_response Excon.head(self.class.downstream_url("/requests/excon_get")) end def perform_excon_options - handle_response Excon.options("http://localhost:4001/requests/excon_options") + handle_response Excon.options(self.class.downstream_url("/requests/excon_options")) end def perform_excon_trace - handle_response Excon.trace("http://localhost:4001/requests/excon_trace") + handle_response Excon.trace(self.class.downstream_url("/requests/excon_trace")) end + # Hits the downstream app so the request span continues into it. (Previously + # this called example.com, which is uninstrumented, so nothing continued.) def perform_http_rb_get - HTTP.get("https://example.com") + HTTP.get(self.class.downstream_url("/requests/http_rb_target")) redirect_to requests_path, :notice => %(Request "#{params[:action]}" made) end @@ -51,14 +61,14 @@ def perform_http_rb_get # These actions exercise that path so it records a `request.http_rb` event. def perform_http_rb_headers HTTP.headers("X-Custom-Header" => "AppSignal") - .get("http://localhost:4001/requests/http_rb_target") + .get(self.class.downstream_url("/requests/http_rb_target")) redirect_to requests_path, :notice => %(Request "#{params[:action]}" made) end # A followed redirect should stay a single `request.http_rb` event spanning # every hop, not one event per hop. def perform_http_rb_follow - HTTP.follow.get("http://localhost:4001/requests/http_rb_redirect") + HTTP.follow.get(self.class.downstream_url("/requests/http_rb_redirect")) redirect_to requests_path, :notice => %(Request "#{params[:action]}" made) end @@ -75,12 +85,12 @@ def http_rb_redirect # suppressing the downstream Net::HTTP event: the request should be recorded # once, as a `request.faraday` event. def perform_faraday_get - handle_response Faraday.get("http://localhost:4001/requests/faraday_get?query=param") + handle_response Faraday.get(self.class.downstream_url("/requests/faraday_get?query=param")) end def perform_faraday_post handle_response Faraday.post( - "http://localhost:4001/requests/faraday_post", + self.class.downstream_url("/requests/faraday_post"), URI.encode_www_form(:language => "ruby", :library => "faraday"), "Content-Type" => "application/x-www-form-urlencoded" ) diff --git a/ruby/rails7-sidekiq/app/app/controllers/workers_controller.rb b/ruby/rails7-sidekiq/app/app/controllers/workers_controller.rb index 478b5a8f0..9c96e812d 100644 --- a/ruby/rails7-sidekiq/app/app/controllers/workers_controller.rb +++ b/ruby/rails7-sidekiq/app/app/controllers/workers_controller.rb @@ -54,6 +54,14 @@ def queue redirect_to({ :action => :index }, :notice => "Worker queued") end + # Enqueue a job onto the `downstream` queue, which only the separately + # instrumented `downstream` service drains. In collector mode this shows job + # trace propagation across two services. + def cross_service + DownstreamWorker.perform_async("Cross-service test") + redirect_to({ :action => :index }, :notice => "Cross-service worker queued") + end + def handle_sidekiq(worker, args:, test:, future:) if future worker.perform_in(DELAY_DURATION.from_now, *args) diff --git a/ruby/rails7-sidekiq/app/app/views/workers/index.html.erb b/ruby/rails7-sidekiq/app/app/views/workers/index.html.erb index 8297b93fb..31b862ccf 100644 --- a/ruby/rails7-sidekiq/app/app/views/workers/index.html.erb +++ b/ruby/rails7-sidekiq/app/app/views/workers/index.html.erb @@ -21,6 +21,16 @@ <% end %> +

    Queue a cross-service Sidekiq worker

    + +

    Enqueues a job onto the downstream queue, performed by the +separately-instrumented downstream service. In collector mode the job's trace +links back to the enqueue span across both apps.

    + + +

    Sidekiq monitoring page

    + +

    Queue a cross-service job

    + +

    Enqueues a native Que job onto the downstream queue, performed by +the separately-instrumented downstream service. In collector mode the job's +trace links back to the enqueue span across both apps.

    + + diff --git a/ruby/rails8-que/app/config/routes.rb b/ruby/rails8-que/app/config/routes.rb index f33e417ee..c2521424c 100644 --- a/ruby/rails8-que/app/config/routes.rb +++ b/ruby/rails8-que/app/config/routes.rb @@ -3,6 +3,7 @@ resources :workers do collection do get :queue + get :cross_service end end diff --git a/ruby/rails8-que/app/processmon.downstream.toml b/ruby/rails8-que/app/processmon.downstream.toml new file mode 100644 index 000000000..5b3a92acf --- /dev/null +++ b/ruby/rails8-que/app/processmon.downstream.toml @@ -0,0 +1,26 @@ +[[paths_to_watch]] +path = "/app" +ignore = [ + "tmp", + "log", + "node_modules", + "public/packs" +] + +[[paths_to_watch]] +path = "/integration" + +# Only the downstream service runs this worker, draining just the `downstream` +# queue, so jobs enqueued by `app` are performed here under the downstream app +# name. (`app`'s worker uses the default queue, so it never picks these up.) +# No web server -- this service only runs jobs. +[processes.que] +command = "bundle" +args = [ + "exec", + "que", + "--queue-name", + "downstream", + "./config/environment.rb" +] +working_dir = "/app" diff --git a/ruby/rails8-que/app/processmon.toml b/ruby/rails8-que/app/processmon.toml index 6f5d6ed8b..0b1962e22 100644 --- a/ruby/rails8-que/app/processmon.toml +++ b/ruby/rails8-que/app/processmon.toml @@ -10,15 +10,6 @@ ignore = [ [[paths_to_watch]] path = "/integration" -[processes.que] -command = "bundle" -args = [ - "exec", - "que", - "./config/environment.rb" -] -working_dir = "/app" - [processes.rails] command = "bin/rails" args = [ diff --git a/ruby/rails8-que/app/processmon.worker.toml b/ruby/rails8-que/app/processmon.worker.toml new file mode 100644 index 000000000..c7b1f6ed0 --- /dev/null +++ b/ruby/rails8-que/app/processmon.worker.toml @@ -0,0 +1,24 @@ +[[paths_to_watch]] +path = "/app" +ignore = [ + "tmp", + "log", + "node_modules", + "public/packs" +] + +[[paths_to_watch]] +path = "/integration" + +# The worker service runs only the background job processor. Jobs enqueued by +# the web `app` service are performed here, so the job's trace runs under the +# worker's own app name (agent mode) or `worker` service name (collector mode), +# linked back to the enqueue span in `app`. +[processes.que] +command = "bundle" +args = [ + "exec", + "que", + "./config/environment.rb" +] +working_dir = "/app" diff --git a/ruby/rails8-que/commands/run-downstream b/ruby/rails8-que/commands/run-downstream new file mode 100755 index 000000000..735a24059 --- /dev/null +++ b/ruby/rails8-que/commands/run-downstream @@ -0,0 +1,27 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service owns installing gems and running migrations into the shared +# /app and /integration volumes. Wait for that instead of installing +# concurrently, which would race on the shared vendor/bundle directory. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Also wait for the app to finish migrating. A DB-backed queue worker (e.g. Que) +# polls a table created by a migration; if it starts first it crashes, and +# processmon does not restart a crashed process. The app only serves HTTP after +# it migrates, so a reachable app means the schema is ready. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Starting downstream Que worker" +/commands/processmon processmon.downstream.toml diff --git a/ruby/rails8-que/commands/run-worker b/ruby/rails8-que/commands/run-worker new file mode 100755 index 000000000..e1aa38eca --- /dev/null +++ b/ruby/rails8-que/commands/run-worker @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service owns installing gems and running migrations into the shared +# /app and /integration volumes. Wait for both before starting the worker. +# +# Gems: `bundle check` is read-only, so polling it is safe. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Migrations: a DB-backed queue (que, solid_queue, delayed_job, good_job) polls +# a table created by a migration. If the worker starts before the app migrates, +# the process crashes and processmon does not restart it. The app only serves +# HTTP after it has migrated, so a reachable app means migrations are done. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/ruby/rails8-que/docker-compose.agent.yml b/ruby/rails8-que/docker-compose.agent.yml index 6ac437c3d..58a903298 100644 --- a/ruby/rails8-que/docker-compose.agent.yml +++ b/ruby/rails8-que/docker-compose.agent.yml @@ -5,3 +5,9 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/rails8-que + downstream: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-que-downstream + worker: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-que-worker diff --git a/ruby/rails8-que/docker-compose.collector.yml b/ruby/rails8-que/docker-compose.collector.yml index 153d35461..41f07eab6 100644 --- a/ruby/rails8-que/docker-compose.collector.yml +++ b/ruby/rails8-que/docker-compose.collector.yml @@ -6,4 +6,15 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/rails8-que-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + downstream: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-que-collector + - APPSIGNAL_SERVICE_NAME=downstream + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-que-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails8-que/docker-compose.shared.yml b/ruby/rails8-que/docker-compose.shared.yml index f039028d5..0de128996 100644 --- a/ruby/rails8-que/docker-compose.shared.yml +++ b/ruby/rails8-que/docker-compose.shared.yml @@ -24,6 +24,39 @@ services: volumes: - ./app:/app - ../integration:/integration + # A second instance of the same image, running as a distinct AppSignal app. It + # drains the `downstream` Que queue that `app` enqueues to, so the job's trace + # links back to the enqueuing app (job trace propagation across services). + downstream: + image: ruby/rails8-que + command: /commands/run-downstream + depends_on: + - postgres + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # Runs only the background worker (see run-worker / processmon.worker.toml). + # Image-only (no build:) so `docker compose build` does not collide with the + # app service exporting the same image tag on Docker's containerd store. + worker: + image: ruby/rails8-que + command: /commands/run-worker + depends_on: + - postgres + env_file: + - postgres.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # A second instance of the same image, running as a distinct AppSignal app. It + # drains the `downstream` Que queue that `app` enqueues to, so the job's trace + # links back to the enqueuing app (job trace propagation across services). tests: build: ../../support/server-tests image: server-tests diff --git a/ruby/rails8-resque/app/app/controllers/workers_controller.rb b/ruby/rails8-resque/app/app/controllers/workers_controller.rb index cc2fecb59..1bec69da4 100644 --- a/ruby/rails8-resque/app/app/controllers/workers_controller.rb +++ b/ruby/rails8-resque/app/app/controllers/workers_controller.rb @@ -32,6 +32,14 @@ def queue redirect_to({ :action => :index }, :notice => "Worker queued") end + # Enqueue a native Resque job onto the `downstream` queue, which only the + # separately instrumented downstream service drains. In collector mode this + # shows job trace propagation across two services. + def cross_service + Resque.enqueue(DownstreamJob, "Cross-service test") + redirect_to({ :action => :index }, :notice => "Cross-service job queued") + end + private def handle_resque(worker, args) diff --git a/ruby/rails8-resque/app/app/jobs/downstream_job.rb b/ruby/rails8-resque/app/app/jobs/downstream_job.rb new file mode 100644 index 000000000..df3676342 --- /dev/null +++ b/ruby/rails8-resque/app/app/jobs/downstream_job.rb @@ -0,0 +1,13 @@ +# Enqueued by this app but performed by the separately-instrumented downstream +# service, which is the only worker draining the `downstream` queue. In collector +# mode the trace context rides along on the Resque payload, so the job's trace +# (under the downstream app) links back to the enqueue span (under this app) -- +# job trace propagation across two services. +class DownstreamJob + @queue = :downstream + + def self.perform(argument = nil) + sleep 1 + puts "downstream performed #{argument}!" + end +end diff --git a/ruby/rails8-resque/app/app/views/workers/index.html.erb b/ruby/rails8-resque/app/app/views/workers/index.html.erb index b252c1b9c..b5bd5e985 100644 --- a/ruby/rails8-resque/app/app/views/workers/index.html.erb +++ b/ruby/rails8-resque/app/app/views/workers/index.html.erb @@ -10,3 +10,13 @@ Job jobs (and mailers) record an enqueue.active_job event.

  • <%= link_to name, queue_workers_path(:worker => worker, :test => test) %>
  • <% end %> + +

    Queue a cross-service job

    + +

    Enqueues a native Resque job onto the downstream queue, performed +by the separately-instrumented downstream service. In collector mode the job's +trace links back to the enqueue span across both apps.

    + + diff --git a/ruby/rails8-resque/app/config/appsignal.rb b/ruby/rails8-resque/app/config/appsignal.rb index 321ba6f3b..b48b43a5d 100644 --- a/ruby/rails8-resque/app/config/appsignal.rb +++ b/ruby/rails8-resque/app/config/appsignal.rb @@ -1,5 +1,5 @@ Appsignal.configure do |config| - config.enable_rake_performance_instrumentation = true + config.enable_rake_performance_instrumentation = false config.activate_if_environment(:development, :production) end diff --git a/ruby/rails8-resque/app/config/routes.rb b/ruby/rails8-resque/app/config/routes.rb index f33e417ee..c2521424c 100644 --- a/ruby/rails8-resque/app/config/routes.rb +++ b/ruby/rails8-resque/app/config/routes.rb @@ -3,6 +3,7 @@ resources :workers do collection do get :queue + get :cross_service end end diff --git a/ruby/rails8-resque/app/lib/tasks/resque.rake b/ruby/rails8-resque/app/lib/tasks/resque.rake index a80250cc3..851547f5d 100644 --- a/ruby/rails8-resque/app/lib/tasks/resque.rake +++ b/ruby/rails8-resque/app/lib/tasks/resque.rake @@ -3,8 +3,10 @@ require "resque/tasks" namespace :resque do # `resque:work` runs this first. Loading the Rails environment makes the job # classes available and runs the initializer that points Resque at Redis. - # Default to working every queue so `rake resque:work` needs no arguments. + # Default to the `default` queue (not `*`) so this app's worker does not drain + # the `downstream` queue, which the separately-instrumented downstream service + # owns (it exports QUEUE=downstream to override this default). task :setup => :environment do - ENV["QUEUE"] ||= "*" + ENV["QUEUE"] ||= "default" end end diff --git a/ruby/rails8-resque/app/processmon.downstream.toml b/ruby/rails8-resque/app/processmon.downstream.toml new file mode 100644 index 000000000..23e246817 --- /dev/null +++ b/ruby/rails8-resque/app/processmon.downstream.toml @@ -0,0 +1,23 @@ +[[paths_to_watch]] +path = "/app" +ignore = [ + "tmp", + "log", + "node_modules", + "public/packs" +] + +[[paths_to_watch]] +path = "/integration" + +# Only the downstream service runs this worker, draining the `downstream` queue +# (set via QUEUE in run-downstream), so jobs enqueued by `app` are performed here +# under the downstream app name. No web server -- this service only runs jobs. +[processes.resque] +command = "bundle" +args = [ + "exec", + "rake", + "resque:work" +] +working_dir = "/app" diff --git a/ruby/rails8-resque/app/processmon.toml b/ruby/rails8-resque/app/processmon.toml index feebc4b54..0b1962e22 100644 --- a/ruby/rails8-resque/app/processmon.toml +++ b/ruby/rails8-resque/app/processmon.toml @@ -10,15 +10,6 @@ ignore = [ [[paths_to_watch]] path = "/integration" -[processes.resque] -command = "bundle" -args = [ - "exec", - "rake", - "resque:work" -] -working_dir = "/app" - [processes.rails] command = "bin/rails" args = [ diff --git a/ruby/rails8-resque/app/processmon.worker.toml b/ruby/rails8-resque/app/processmon.worker.toml new file mode 100644 index 000000000..eb661625a --- /dev/null +++ b/ruby/rails8-resque/app/processmon.worker.toml @@ -0,0 +1,24 @@ +[[paths_to_watch]] +path = "/app" +ignore = [ + "tmp", + "log", + "node_modules", + "public/packs" +] + +[[paths_to_watch]] +path = "/integration" + +# The worker service runs only the background job processor. Jobs enqueued by +# the web `app` service are performed here, so the job's trace runs under the +# worker's own app name (agent mode) or `worker` service name (collector mode), +# linked back to the enqueue span in `app`. +[processes.resque] +command = "bundle" +args = [ + "exec", + "rake", + "resque:work" +] +working_dir = "/app" diff --git a/ruby/rails8-resque/commands/run-downstream b/ruby/rails8-resque/commands/run-downstream new file mode 100755 index 000000000..11b4bba09 --- /dev/null +++ b/ruby/rails8-resque/commands/run-downstream @@ -0,0 +1,31 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service owns installing gems and running migrations into the shared +# /app and /integration volumes. Wait for that instead of installing +# concurrently, which would race on the shared vendor/bundle directory. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Also wait for the app to finish migrating. A DB-backed queue worker (e.g. Que) +# polls a table created by a migration; if it starts first it crashes, and +# processmon does not restart a crashed process. The app only serves HTTP after +# it migrates, so a reachable app means the schema is ready. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +# Drain only the `downstream` queue (overrides the resque:setup default), so +# this worker performs the jobs `app` enqueues there, under the downstream app. +export QUEUE=downstream + +echo "Starting downstream Resque worker" +/commands/processmon processmon.downstream.toml diff --git a/ruby/rails8-resque/commands/run-worker b/ruby/rails8-resque/commands/run-worker new file mode 100755 index 000000000..e1aa38eca --- /dev/null +++ b/ruby/rails8-resque/commands/run-worker @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service owns installing gems and running migrations into the shared +# /app and /integration volumes. Wait for both before starting the worker. +# +# Gems: `bundle check` is read-only, so polling it is safe. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Migrations: a DB-backed queue (que, solid_queue, delayed_job, good_job) polls +# a table created by a migration. If the worker starts before the app migrates, +# the process crashes and processmon does not restart it. The app only serves +# HTTP after it has migrated, so a reachable app means migrations are done. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/ruby/rails8-resque/docker-compose.agent.yml b/ruby/rails8-resque/docker-compose.agent.yml index f5a2acd78..c57ed84a1 100644 --- a/ruby/rails8-resque/docker-compose.agent.yml +++ b/ruby/rails8-resque/docker-compose.agent.yml @@ -5,3 +5,9 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/rails8-resque + downstream: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-resque-downstream + worker: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-resque-worker diff --git a/ruby/rails8-resque/docker-compose.collector.yml b/ruby/rails8-resque/docker-compose.collector.yml index df42569a5..ab3392ada 100644 --- a/ruby/rails8-resque/docker-compose.collector.yml +++ b/ruby/rails8-resque/docker-compose.collector.yml @@ -6,4 +6,15 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/rails8-resque-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + downstream: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-resque-collector + - APPSIGNAL_SERVICE_NAME=downstream + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-resque-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails8-resque/docker-compose.shared.yml b/ruby/rails8-resque/docker-compose.shared.yml index 42460eb90..e8a6e7dee 100644 --- a/ruby/rails8-resque/docker-compose.shared.yml +++ b/ruby/rails8-resque/docker-compose.shared.yml @@ -22,6 +22,39 @@ services: volumes: - ./app:/app - ../integration:/integration + # A second instance of the same image, running as a distinct AppSignal app. It + # drains the `downstream` Resque queue that `app` enqueues to, so the job's + # trace links back to the enqueuing app (job trace propagation across services). + downstream: + image: ruby/rails8-resque + command: /commands/run-downstream + depends_on: + - redis + env_file: + - redis.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # Runs only the background worker (see run-worker / processmon.worker.toml). + # Image-only (no build:) so `docker compose build` does not collide with the + # app service exporting the same image tag on Docker's containerd store. + worker: + image: ruby/rails8-resque + command: /commands/run-worker + depends_on: + - redis + env_file: + - redis.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # A second instance of the same image, running as a distinct AppSignal app. It + # drains the `downstream` Resque queue that `app` enqueues to, so the job's + # trace links back to the enqueuing app (job trace propagation across services). tests: build: ../../support/server-tests image: server-tests diff --git a/ruby/rails8-sidekiq/app/processmon.toml b/ruby/rails8-sidekiq/app/processmon.toml index 286194df4..0b1962e22 100644 --- a/ruby/rails8-sidekiq/app/processmon.toml +++ b/ruby/rails8-sidekiq/app/processmon.toml @@ -10,15 +10,6 @@ ignore = [ [[paths_to_watch]] path = "/integration" -[processes.sidekiq] -command = "bundle" -args = [ - "exec", - "sidekiq", - "--queue=default" -] -working_dir = "/app" - [processes.rails] command = "bin/rails" args = [ diff --git a/ruby/rails8-sidekiq/app/processmon.worker.toml b/ruby/rails8-sidekiq/app/processmon.worker.toml new file mode 100644 index 000000000..9db6a1d62 --- /dev/null +++ b/ruby/rails8-sidekiq/app/processmon.worker.toml @@ -0,0 +1,24 @@ +[[paths_to_watch]] +path = "/app" +ignore = [ + "tmp", + "log", + "node_modules", + "public/packs" +] + +[[paths_to_watch]] +path = "/integration" + +# The worker service drains the default queue. Jobs enqueued by the web `app` +# service are performed here, so the job's trace runs under the worker's own +# app name (agent mode) or `worker` service name (collector mode), linked back +# to the enqueue span in `app`. +[processes.sidekiq] +command = "bundle" +args = [ + "exec", + "sidekiq", + "--queue=default" +] +working_dir = "/app" diff --git a/ruby/rails8-sidekiq/commands/run-worker b/ruby/rails8-sidekiq/commands/run-worker new file mode 100755 index 000000000..e1aa38eca --- /dev/null +++ b/ruby/rails8-sidekiq/commands/run-worker @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu + +cd /app + +# The `app` service owns installing gems and running migrations into the shared +# /app and /integration volumes. Wait for both before starting the worker. +# +# Gems: `bundle check` is read-only, so polling it is safe. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Migrations: a DB-backed queue (que, solid_queue, delayed_job, good_job) polls +# a table created by a migration. If the worker starts before the app migrates, +# the process crashes and processmon does not restart it. The app only serves +# HTTP after it has migrated, so a reachable app means migrations are done. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +echo "Starting worker processes" +/commands/processmon processmon.worker.toml diff --git a/ruby/rails8-sidekiq/docker-compose.agent.yml b/ruby/rails8-sidekiq/docker-compose.agent.yml index 96d3bc0a6..ad85c2c9e 100644 --- a/ruby/rails8-sidekiq/docker-compose.agent.yml +++ b/ruby/rails8-sidekiq/docker-compose.agent.yml @@ -5,3 +5,6 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/rails8-sidekiq + worker: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-sidekiq-worker diff --git a/ruby/rails8-sidekiq/docker-compose.collector.yml b/ruby/rails8-sidekiq/docker-compose.collector.yml index e35609e07..60cb56c96 100644 --- a/ruby/rails8-sidekiq/docker-compose.collector.yml +++ b/ruby/rails8-sidekiq/docker-compose.collector.yml @@ -6,4 +6,10 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/rails8-sidekiq-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + worker: + environment: + - APPSIGNAL_APP_NAME=ruby/rails8-sidekiq-collector + - APPSIGNAL_SERVICE_NAME=worker - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/rails8-sidekiq/docker-compose.shared.yml b/ruby/rails8-sidekiq/docker-compose.shared.yml index bb1921af0..c92bc3970 100644 --- a/ruby/rails8-sidekiq/docker-compose.shared.yml +++ b/ruby/rails8-sidekiq/docker-compose.shared.yml @@ -28,6 +28,28 @@ services: volumes: - ./app:/app - ../integration:/integration + # A second container of the same image that runs only the background worker, + # draining the queue the web `app` service enqueues to. Splitting the worker + # into its own service attributes job traces to a separate app (agent mode) + # or service (collector mode) from the web requests. + # No `build:` here on purpose. The `app` service builds this shared image; + # if a second service also built the same tag, `docker compose build` fails + # on Docker's containerd image store ("image already exists"). `rake app:up` + # builds `app` first, so the image exists before `worker` starts. + worker: + image: ruby/rails8-sidekiq + command: /commands/run-worker + depends_on: + - postgres + - redis + env_file: + - postgres.env + - redis.env + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration tests: build: ../../support/server-tests image: server-tests diff --git a/ruby/sinatra-alpine/docker-compose.collector.yml b/ruby/sinatra-alpine/docker-compose.collector.yml index 953022b24..27672a9e2 100644 --- a/ruby/sinatra-alpine/docker-compose.collector.yml +++ b/ruby/sinatra-alpine/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-sinatra-alpine-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/sinatra-gvltools/docker-compose.collector.yml b/ruby/sinatra-gvltools/docker-compose.collector.yml index 684e0f17e..325e12651 100644 --- a/ruby/sinatra-gvltools/docker-compose.collector.yml +++ b/ruby/sinatra-gvltools/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby-sinatra-gvltools-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/webmachine1/docker-compose.collector.yml b/ruby/webmachine1/docker-compose.collector.yml index c4c726055..fd4117e3c 100644 --- a/ruby/webmachine1/docker-compose.collector.yml +++ b/ruby/webmachine1/docker-compose.collector.yml @@ -6,4 +6,5 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/webmachine1-collector + - APPSIGNAL_SERVICE_NAME=app - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/webmachine2/app/Gemfile.lock b/ruby/webmachine2/app/Gemfile.lock index e0414c5cb..8d549429e 100644 --- a/ruby/webmachine2/app/Gemfile.lock +++ b/ruby/webmachine2/app/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: /integration specs: - appsignal (4.8.0) + appsignal (4.8.6) logger rack (>= 2.0.0) diff --git a/ruby/webmachine2/app/app.rb b/ruby/webmachine2/app/app.rb index 1b4aa7dfd..28636fe19 100644 --- a/ruby/webmachine2/app/app.rb +++ b/ruby/webmachine2/app/app.rb @@ -1,4 +1,5 @@ require "webmachine" +require "net/http" require "appsignal" Appsignal.start @@ -27,6 +28,21 @@ def to_html end end +# Makes an HTTP request to a second, separately-instrumented webmachine app (the +# `downstream` service). AppSignal's Net::HTTP integration injects the current +# W3C trace context onto the request in collector mode, and the downstream app's +# Webmachine integration extracts it -- so the trace spans both apps. This +# exercises Webmachine's trace-context extraction, the one server path that is +# not shared with the Rack integrations. Falls back to calling itself when +# DOWNSTREAM_URL is unset. +class CallDownstreamResource < Webmachine::Resource + def to_html + url = ENV.fetch("DOWNSTREAM_URL", "http://localhost:4001") + Net::HTTP.get(URI("#{url}/")) + "Called downstream app" + end +end + class MyRootResource < Webmachine::Resource def to_html time = Time.now.utc @@ -38,6 +54,7 @@ def to_html @@ -48,5 +65,6 @@ def to_html Webmachine.application.routes do add ["slow"], MyPerformanceResource add ["error"], MyErrorResource + add ["call_downstream"], CallDownstreamResource add [], MyRootResource end diff --git a/ruby/webmachine2/commands/run-downstream b/ruby/webmachine2/commands/run-downstream new file mode 100755 index 000000000..678909c71 --- /dev/null +++ b/ruby/webmachine2/commands/run-downstream @@ -0,0 +1,32 @@ +#!/bin/bash + +set -eu + +cd /app + +# Point bundler at the shared vendor/bundle the `app` service installs into, so +# this container can find the gems (each container configures bundler itself). +bundle config set --local path vendor/bundle + +# The `app` service installs the gems and builds the extension into the shared +# volumes. Wait for that instead of installing concurrently, which would race on +# vendor/bundle. +echo "Waiting for the app service to install gems into the shared volume..." +until bundle check >/dev/null 2>&1; do + echo "Gems not ready yet, retrying..." + sleep 2 +done + +# Also wait for the app to finish migrating. A DB-backed queue worker (e.g. Que) +# polls a table created by a migration; if it starts first it crashes, and +# processmon does not restart a crashed process. The app only serves HTTP after +# it migrates, so a reachable app means the schema is ready. +echo "Waiting for the app service to finish migrating..." +until curl -s -o /dev/null "http://app:4001/up" 2>/dev/null; do + echo "App not ready yet, retrying..." + sleep 2 +done + +# Runs the same webmachine app, which binds to PORT (4002 here) via config.ru. +echo "Starting downstream webmachine app" +/commands/processmon processmon.toml diff --git a/ruby/webmachine2/docker-compose.agent.yml b/ruby/webmachine2/docker-compose.agent.yml index 8b7d8c5c5..92c8c9684 100644 --- a/ruby/webmachine2/docker-compose.agent.yml +++ b/ruby/webmachine2/docker-compose.agent.yml @@ -5,3 +5,6 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/webmachine2 + downstream: + environment: + - APPSIGNAL_APP_NAME=ruby/webmachine2-downstream diff --git a/ruby/webmachine2/docker-compose.collector.yml b/ruby/webmachine2/docker-compose.collector.yml index 726558df3..02cec5089 100644 --- a/ruby/webmachine2/docker-compose.collector.yml +++ b/ruby/webmachine2/docker-compose.collector.yml @@ -6,4 +6,10 @@ services: app: environment: - APPSIGNAL_APP_NAME=ruby/webmachine2-collector + - APPSIGNAL_SERVICE_NAME=app + - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 + downstream: + environment: + - APPSIGNAL_APP_NAME=ruby/webmachine2-collector + - APPSIGNAL_SERVICE_NAME=downstream - APPSIGNAL_COLLECTOR_ENDPOINT=http://appsignal-collector:8099 diff --git a/ruby/webmachine2/docker-compose.shared.yml b/ruby/webmachine2/docker-compose.shared.yml index 0ffabde9a..b0c5b6b0d 100644 --- a/ruby/webmachine2/docker-compose.shared.yml +++ b/ruby/webmachine2/docker-compose.shared.yml @@ -11,6 +11,25 @@ services: - "4001:4001" environment: - PORT=4001 + # The call_downstream resource makes an HTTP request to this second, + # separately-instrumented webmachine app so the trace spans both. + - DOWNSTREAM_URL=http://downstream:4002 + env_file: + - ../../appsignal.env + - ../../appsignal_key.env + volumes: + - ./app:/app + - ../integration:/integration + # A second instance of the same image on port 4002, running as a distinct + # AppSignal app. It receives the HTTP request `app` makes and extracts the + # trace context (Webmachine's own, non-Rack extraction path), continuing the + # trace across both apps. + downstream: + build: . + image: ruby/webmachine + command: /commands/run-downstream + environment: + - PORT=4002 env_file: - ../../appsignal.env - ../../appsignal_key.env