Skip to content

Add instrumentation for asyncua (OPC UA client) #4491

@luke6Lh43

Description

@luke6Lh43

What problem do you want to solve?

Python applications communicating with industrial control systems (PLCs, SCADA, MES) via OPC UA currently have no first-class OpenTelemetry instrumentation. The most popular Python OPC UA library, asyncua (1.4k GitHub stars, actively maintained, used in industrial IoT gateways, MES integrations, and data historian pipelines), has no instrumentor in opentelemetry-python-contrib. Client-side Python instrumentation for asyncua would enable observability for Python-based industrial integrations.

Describe the solution you'd like

A new package opentelemetry-instrumentation-asyncua in opentelemetry-python-contrib, extending BaseInstrumentor and following the established patterns in this repo (structurally modeled on asyncpg, redis, and similar).

MVP scope (initial PR)

Traces for client-initiated OPC UA operations:

  • Client.connectopcua.connect span
  • Client.disconnectopcua.disconnect span
  • Node.read_valueopcua.read span
  • Node.write_valueopcua.write span
  • Node.call_methodopcua.call_method span

All spans are SpanKind.CLIENT. Attributes include:

  • opcua.operation (read / write / connect / etc.)
  • opcua.endpoint, server.address, server.port (target identification)
  • opcua.node_id (canonical OPC UA form via NodeId.to_string())
  • opcua.security_policy, opcua.security_mode (security posture)
  • opcua.method_id, opcua.method_args_count (for method calls)
  • opcua.variant_type (for writes)
  • error.type and span status on exceptions

asyncua's Node class is shared between client and server code paths, so wrappers filter out server-internal operations (e.g., address space setup reads/writes) by inspecting the node's session type — ensuring only genuine client-initiated calls produce spans.

Deferred to follow-up work

  • Subscriptions and monitored items (design question: data change notifications are long-lived server-push via client-initiated polling; likely modeled as CONSUMER spans with links to the subscription span, similar to existing messaging conventions)
  • Browse / address space navigation
  • Metrics (operation duration histogram, active subscriptions gauge)
  • OPC UA StatusCode extraction from response DataValue (would require wrapping lower-level methods where the full response is visible)
  • OPC UA-specific semantic conventions proposal (separate work in semantic-conventions)

Questions for maintainers

Before opening a PR, I'd appreciate guidance on three points:

  1. Semantic conventions approach: There are no stable OTel semantic conventions for OPC UA yet. Would you prefer:

    • (a) A provisional opcua.* attribute namespace, documented as experimental, with parallel work to propose stable conventions in semantic-conventions?
    • (b) Generic rpc.* conventions with rpc.system = "opcua"?
    • (c) Something else?

    My current prototype uses (a). Happy to change direction.

  2. Scope of MVP: Is the scope above acceptable for an initial PR, or would you expect subscriptions to be covered in the first drop? Subscriptions add design complexity (long-lived notifications, span/metric split) that I'd prefer to address in a focused follow-up.

  3. Package naming: I'm proposing opentelemetry-instrumentation-asyncua (library-specific, following the pymysql/mysql/mysqlclient precedent) rather than opentelemetry-instrumentation-opcua (protocol-specific). Does that work?

Describe alternatives you've considered

  • Manual instrumentation with the OTel SDK — works but requires every user to reimplement. A standard instrumentation in this repo is more valuable for the ecosystem.
  • Third-party PyPI package outside contrib — would fragment the ecosystem and reduce discoverability. Hosting in opentelemetry-python-contrib signals community ownership and ensures maintenance continuity alongside the rest of the instrumentation catalog.
  • Waiting for stable OPC UA semantic conventions before implementing — this could take a long time and leaves Python users without a standard instrumentation indefinitely. A provisional opcua.* namespace now, adjusted later to match stabilized conventions, seems more pragmatic.

Additional Context

I have a working local prototype implementing the MVP scope described above. It passes lint (ruff) and produces clean, verified spans end-to-end against a local asyncua server:

Span: opcua.connect (kind=CLIENT, duration=3.46 ms)
opcua.operation = connect
opcua.endpoint = opc.tcp://localhost:14840/test/
server.address = localhost
server.port = 14840
opcua.security_policy = None
opcua.security_mode = None
Span: opcua.read (kind=CLIENT, duration=0.54 ms)
opcua.operation = read
opcua.node_id = ns=2;i=2
Span: opcua.write (kind=CLIENT, duration=0.36 ms)
opcua.operation = write
opcua.node_id = ns=2;i=2
Span: opcua.disconnect (kind=CLIENT, duration=0.52 ms)
opcua.operation = disconnect
opcua.endpoint = opc.tcp://localhost:14840/test/

Would you like to implement a fix?

Yes

Tip

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it. Learn more here.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions