Async Click Instrumentation for OpenTelemetry
This library provides OpenTelemetry tracing for applications built with `asyncclick`, an asynchronous fork of the `click` library. It's part of the `opentelemetry-python-contrib` project, which maintains a monthly release cadence, and the instrumentation itself is currently in beta status, indicating potential for API changes.
Common errors
-
ModuleNotFoundError: No module named 'asyncclick'
cause The `asyncclick` library, which this instrumentation instruments, is not installed in the environment.fixInstall `asyncclick`: `pip install asyncclick`. -
No traces appear in the console/exporter output.
cause The OpenTelemetry SDK (TracerProvider, SpanProcessor, Exporter) is not correctly initialized, or the `AsyncClickInstrumentor().instrument()` method was not called.fixEnsure you have correctly configured a `TracerProvider` with a `SpanProcessor` and an `Exporter` (e.g., `ConsoleSpanExporter`) and that `AsyncClickInstrumentor().instrument()` is called before `asyncclick` commands are executed. -
TypeError: object async_function can't be used in 'await' expression
cause Attempting to run an `asyncclick` command directly without `asyncio.run()` or `asyncclick.main()` or `anyio.run()` in a synchronous context.fixEnsure your async Click application is run within an `asyncio` event loop. For instance, wrap your CLI invocation: `asyncio.run(cli.main())` (if `asyncclick` supports a direct `main` equivalent for `asyncio.run`) or manually invoke commands within an `asyncio.run` context as shown in the quickstart.
Warnings
- beta This instrumentation package is currently in beta. This means its API and functionality might change in future releases, potentially introducing breaking changes or incomplete features. It is generally not recommended for production environments where stability is critical.
- gotcha The `opentelemetry-instrumentation-asyncclick` package only provides instrumentation for `asyncclick`. It does not automatically install `asyncclick` itself. You must explicitly install `asyncclick` (or `click` if using the standard one) for your application to function.
- gotcha This instrumentation provides manual instrumentation. If you are looking for zero-code automatic instrumentation, you would typically use `opentelemetry-instrument` CLI, which detects and instruments supported libraries at runtime. However, for `asyncclick`, explicit `instrument()` call is typically required.
Install
-
pip install opentelemetry-instrumentation-asyncclick asyncclick opentelemetry-sdk
Imports
- AsyncClickInstrumentor
from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor
Quickstart
import asyncio
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor
import asyncclick as click
# Configure OpenTelemetry SDK
resource = Resource.create({"service.name": "asyncclick-app"})
tracer_provider = TracerProvider(resource=resource)
span_processor = SimpleSpanProcessor(ConsoleSpanExporter())
tracer_provider.add_span_processor(span_processor)
trace.set_tracer_provider(tracer_provider)
# Enable asyncclick instrumentation
AsyncClickInstrumentor().instrument()
# Define an async click command
@click.group()
async def cli():
"A simple async Click CLI."
pass
@cli.command()
@click.option('--name', default='World', help='Name to greet.')
async def hello(name):
"Greets the given name."
current_span = trace.get_current_span()
current_span.set_attribute("greeting.name", name)
click.echo(f"Hello, {name}!")
await asyncio.sleep(0.05)
click.echo("Greeting complete.")
@cli.command()
async def goodbye():
"Says goodbye."
click.echo("Goodbye!")
await asyncio.sleep(0.01)
async def main():
# Simulate running a command, e.g., 'python your_app.py hello --name Alice'
# In a real scenario, click.Group.main() would handle argv parsing.
# For this example, we'll manually invoke the command.
ctx = click.Context(cli, info_name='cli')
await cli.invoke(ctx)
# You can also simulate calling specific commands directly
# await hello.callback(name="Bob")
# await goodbye.callback()
if __name__ == '__main__':
# This part typically handles the command line invocation
# For demonstration, we'll run the 'hello' command as if it was passed via argv
# Note: asyncclick.main() internally uses asyncio.run() or anyio.run()
# We are directly running an async function, so manually setup invocation.
# This mimics `cli(['hello', '--name', 'OpenTelemetry'])` but for async context
async def _run_cli_command():
ctx = click.Context(cli, obj={}, auto_envvar_prefix='OTEL')
await cli.invoke(ctx)
await hello.invoke(ctx, name='OpenTelemetry')
await goodbye.invoke(ctx)
asyncio.run(_run_cli_command())
# Ensure all spans are exported before exiting
tracer_provider.shutdown()