plugins/lisa-rails/skills/ops-verify-telemetry/SKILL.md
Verify OpenTelemetry traces are being collected and exported to AWS X-Ray for Rails applications. Check collector health, trace export, and CloudWatch metrics.
npx skillsauth add codyswanngt/lisa ops-verify-telemetryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Verify OpenTelemetry traces are being collected and exported to X-Ray.
Argument: $ARGUMENTS — check type (traces, metrics, all; default: all)
Gemfile for OpenTelemetry gem configuration:
opentelemetry-sdkopentelemetry-exporter-otlpopentelemetry-instrumentation-all (or individual instrumentation gems)config/initializers/opentelemetry.rb (or similar) for SDK configurationconfig/deploy.yml or environment files for OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAMEdocker-compose.yml for OpenTelemetry Collector sidecar configuration (if present)bundle list | grep -i opentelemetry
grep -r "OpenTelemetry" config/initializers/ app/
echo "OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-not set}"
echo "OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-not set}"
Make a request to the local app and check that traces are produced:
# Make a request that should generate a trace
curl -sf http://localhost:3000/up -w "\nHTTP %{http_code}\n"
# Check Rails logs for OpenTelemetry output (if configured to log)
grep -i "otel\|opentelemetry\|trace_id" log/development.log | tail -10
# Capture the status BEFORE the pipe. A pipeline reports its LAST stage's exit
# code, so `docker compose logs ... | tail -20 || echo "No otel-collector"`
# never prints the fallback: `tail` succeeds even when there is no such
# service, and the absent sidecar reads as a silent pass.
#
# `|| status=$?`, not `; status=$?`: under `set -e` the `;` form exits before
# the assignment, so the branch that reports the failure never runs.
#
# Ask whether the service EXISTS separately from whether its logs could be
# read. `docker compose logs` also exits non-zero for a stopped daemon, a
# permission failure, and an unparseable compose file; answering all of those
# with "No otel-collector service" turns a broken telemetry stack into a
# reassuring sentence — the same silent-measurement failure one level up.
services_status=0
services=$(docker compose config --services 2>&1) || services_status=$?
if [ "$services_status" -ne 0 ]; then
echo "FAILED ($services_status) — docker compose config could not be read:"
printf '%s\n' "$services"
elif ! printf '%s\n' "$services" | grep -qx otel-collector; then
echo "No otel-collector service in Docker Compose"
else
logs_status=0
docker compose logs otel-collector >otel.log 2>&1 || logs_status=$?
if [ "$logs_status" -eq 0 ]; then
tail -n 20 otel.log
else
echo "FAILED ($logs_status) — otel-collector is configured but its logs could not be read:"
cat otel.log
fi
fi
aws xray get-trace-summaries \
--region {aws-region} \
--start-time $(ruby -r time -e 'puts (Time.now.utc - 30 * 60).iso8601') \
--end-time $(ruby -r time -e 'puts Time.now.utc.iso8601') \
--query 'TraceSummaries | length(@)' \
--output text
aws xray get-trace-summaries \
--region {aws-region} \
--start-time $(ruby -r time -e 'puts (Time.now.utc - 30 * 60).iso8601') \
--end-time $(ruby -r time -e 'puts Time.now.utc.iso8601') \
--filter-expression "service(\"{service-name}\")" \
--query 'TraceSummaries[:10].{TraceId:Id,Duration:Duration,StatusCode:Http.HttpStatus,URL:Http.HttpURL,ResponseTime:ResponseTime}' \
--output table
aws xray get-trace-summaries \
--region {aws-region} \
--start-time $(ruby -r time -e 'puts (Time.now.utc - 3600).iso8601') \
--end-time $(ruby -r time -e 'puts Time.now.utc.iso8601') \
--filter-expression "service(\"{service-name}\") AND fault = true" \
--query 'TraceSummaries[:10].{TraceId:Id,Duration:Duration,StatusCode:Http.HttpStatus,URL:Http.HttpURL}' \
--output table
aws xray batch-get-traces \
--region {aws-region} \
--trace-ids "{trace-id}" \
--query 'Traces[0].Segments[].Document' \
--output text | jq '.'
aws xray get-service-graph \
--region {aws-region} \
--start-time $(ruby -r time -e 'puts (Time.now.utc - 3600).iso8601') \
--end-time $(ruby -r time -e 'puts Time.now.utc.iso8601') \
--query 'Services[].{Name:Name,Type:Type,Edges:Edges[].{Ref:ReferenceId,Latency:ResponseTimeHistogram[0].Average}}' \
--output table
aws cloudwatch list-metrics \
--region {aws-region} \
--namespace "{app_name}" \
--query 'Metrics[].{MetricName:MetricName,Dimensions:Dimensions[].{Name:Name,Value:Value}}' \
--output table
aws cloudwatch get-metric-statistics \
--region {aws-region} \
--namespace "{app_name}" \
--metric-name "{metric-name}" \
--start-time $(ruby -r time -e 'puts (Time.now.utc - 3600).iso8601') \
--end-time $(ruby -r time -e 'puts Time.now.utc.iso8601') \
--period 300 \
--statistics Average Sum \
--output table
aws cloudwatch describe-alarms \
--region {aws-region} \
--alarm-name-prefix "{app_name}" \
--query 'MetricAlarms[].{Name:AlarmName,State:StateValue,Metric:MetricName,Threshold:Threshold}' \
--output table
For checking telemetry configuration in deployed environments:
kamal app exec --roles=web "bin/rails runner \"
puts 'OTEL_EXPORTER_OTLP_ENDPOINT: ' + ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', 'NOT SET')
puts 'OTEL_SERVICE_NAME: ' + ENV.fetch('OTEL_SERVICE_NAME', 'NOT SET')
puts 'OTEL_TRACES_EXPORTER: ' + ENV.fetch('OTEL_TRACES_EXPORTER', 'NOT SET')
\"" -d {environment}
| Check | Status | Details | |-------|--------|---------| | OTel gems installed | OK/FAIL | opentelemetry-sdk v1.x.x | | OTel initializer present | OK/FAIL | config/initializers/opentelemetry.rb | | OTLP endpoint configured | OK/FAIL | https://otel-collector:4318 | | Traces in X-Ray (last 30m) | OK/FAIL | 245 traces found | | Error traces (last 1h) | OK/WARN | 3 fault traces | | CloudWatch metrics | OK/FAIL | 12 custom metrics published | | CloudWatch alarms | OK/WARN | 1 alarm in ALARM state |
| Trace ID | Duration | Status | URL | |----------|----------|--------|-----| | 1-abc123 | 45ms | 200 | GET /up | | 1-def456 | 120ms | 200 | GET /api/v1/users | | 1-ghi789 | 2300ms | 500 | POST /api/v1/orders |
Flag concerns:
development
Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents (Claude Code, Codex, Cursor, OpenCode, Antigravity, Copilot) are already installed, asks which credential manager the machine uses (Bitwarden, 1Password, Doppler, Vault, AWS, or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent, headless by default, and emits a Dockerfile for a spin-up/spin-down environment. Run it on a new machine, in a container, or before cloning anything.
tools
Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv.
tools
Bring a developer's machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked. Same manifest, same pins, same installers as lisa-setup-remote-env; what differs is consent and that the pin is a floor rather than an equality. Run it on a fresh checkout, after a manifest change, or when a tool fails at the moment of use.
tools
Route one unit of work to a remote execution surface. Reads the executionEnv parameter (local by default, codex-cloud or claude-web today), verifies the environment is provisioned and bound to this repository, submits a thin skill invocation, records the task identifier to .lisa/remote-dispatch.json, and exits without polling. Routing only — the remote runs the identical skill from the identical repository. Composable and inline: other skills invoke it via the Skill tool rather than users calling it directly.