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.connect → opcua.connect span
Client.disconnect → opcua.disconnect span
Node.read_value → opcua.read span
Node.write_value → opcua.write span
Node.call_method → opcua.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:
-
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.
-
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.
-
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.
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 inopentelemetry-python-contrib. Client-side Python instrumentation forasyncuawould enable observability for Python-based industrial integrations.Describe the solution you'd like
A new package
opentelemetry-instrumentation-asyncuainopentelemetry-python-contrib, extendingBaseInstrumentorand following the established patterns in this repo (structurally modeled onasyncpg,redis, and similar).MVP scope (initial PR)
Traces for client-initiated OPC UA operations:
Client.connect→opcua.connectspanClient.disconnect→opcua.disconnectspanNode.read_value→opcua.readspanNode.write_value→opcua.writespanNode.call_method→opcua.call_methodspanAll 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 viaNodeId.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.typeand span status on exceptionsasyncua's
Nodeclass 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
CONSUMERspans with links to the subscription span, similar to existing messaging conventions)StatusCodeextraction from responseDataValue(would require wrapping lower-level methods where the full response is visible)semantic-conventions)Questions for maintainers
Before opening a PR, I'd appreciate guidance on three points:
Semantic conventions approach: There are no stable OTel semantic conventions for OPC UA yet. Would you prefer:
opcua.*attribute namespace, documented as experimental, with parallel work to propose stable conventions insemantic-conventions?rpc.*conventions withrpc.system = "opcua"?My current prototype uses (a). Happy to change direction.
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.
Package naming: I'm proposing
opentelemetry-instrumentation-asyncua(library-specific, following thepymysql/mysql/mysqlclientprecedent) rather thanopentelemetry-instrumentation-opcua(protocol-specific). Does that work?Describe alternatives you've considered
opentelemetry-python-contribsignals community ownership and ensures maintenance continuity alongside the rest of the instrumentation catalog.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:
Would you like to implement a fix?
Yes
Tip
React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding
+1orme too, to help us triage it. Learn more here.