opentelemetry-instrumentation-taskflow 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opentelemetry_instrumentation_taskflow-0.0.1/.gitignore +13 -0
- opentelemetry_instrumentation_taskflow-0.0.1/PKG-INFO +40 -0
- opentelemetry_instrumentation_taskflow-0.0.1/README.md +16 -0
- opentelemetry_instrumentation_taskflow-0.0.1/pyproject.toml +51 -0
- opentelemetry_instrumentation_taskflow-0.0.1/src/opentelemetry/instrumentation/taskflow/__init__.py +122 -0
- opentelemetry_instrumentation_taskflow-0.0.1/src/opentelemetry/instrumentation/taskflow/version.py +1 -0
- opentelemetry_instrumentation_taskflow-0.0.1/tests/__init__.py +0 -0
- opentelemetry_instrumentation_taskflow-0.0.1/tests/test_taskflow.py +134 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opentelemetry-instrumentation-taskflow
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: OpenStack taskflow instrumentation for OpenTelemetry
|
|
5
|
+
Author-email: Pham Le Gia Dai <daipham3213@gmail.com>
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: opentelemetry-api
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation
|
|
19
|
+
Requires-Dist: opentelemetry-semantic-conventions
|
|
20
|
+
Requires-Dist: opentelemetry-util-http
|
|
21
|
+
Provides-Extra: instruments
|
|
22
|
+
Requires-Dist: taskflow; extra == 'instruments'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# OpenTelemetry TaskFlow Instrumentation
|
|
26
|
+
|
|
27
|
+
OpenStack TaskFlow instrumentation for OpenTelemetry.
|
|
28
|
+
|
|
29
|
+
It records every `execute`/`revert` call on a `taskflow.task.Task` as a span
|
|
30
|
+
named `taskflow.task.<method>`, annotated with the task's class, method and
|
|
31
|
+
name. Because concrete tasks override `execute`/`revert`, the instrumentor wraps
|
|
32
|
+
`Task.__getattribute__` so those overrides are traced wherever they are defined.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from opentelemetry.instrumentation.taskflow import TaskflowInstrumentor
|
|
38
|
+
|
|
39
|
+
TaskflowInstrumentor().instrument()
|
|
40
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# OpenTelemetry TaskFlow Instrumentation
|
|
2
|
+
|
|
3
|
+
OpenStack TaskFlow instrumentation for OpenTelemetry.
|
|
4
|
+
|
|
5
|
+
It records every `execute`/`revert` call on a `taskflow.task.Task` as a span
|
|
6
|
+
named `taskflow.task.<method>`, annotated with the task's class, method and
|
|
7
|
+
name. Because concrete tasks override `execute`/`revert`, the instrumentor wraps
|
|
8
|
+
`Task.__getattribute__` so those overrides are traced wherever they are defined.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from opentelemetry.instrumentation.taskflow import TaskflowInstrumentor
|
|
14
|
+
|
|
15
|
+
TaskflowInstrumentor().instrument()
|
|
16
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "opentelemetry-instrumentation-taskflow"
|
|
3
|
+
description = "OpenStack taskflow instrumentation for OpenTelemetry"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Pham Le Gia Dai", email = "daipham3213@gmail.com" }
|
|
7
|
+
]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dynamic = ["version"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: Apache Software License",
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"opentelemetry-api",
|
|
24
|
+
"opentelemetry-instrumentation",
|
|
25
|
+
"opentelemetry-semantic-conventions",
|
|
26
|
+
"opentelemetry-util-http",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
instruments = [
|
|
31
|
+
"taskflow"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.entry-points.opentelemetry_instrumentor]
|
|
35
|
+
taskflow = "opentelemetry.instrumentation.taskflow:TaskflowInstrumentor"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.version]
|
|
38
|
+
path = "src/opentelemetry/instrumentation/taskflow/version.py"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
include = [
|
|
42
|
+
"/src",
|
|
43
|
+
"/tests",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/opentelemetry"]
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["hatchling"]
|
|
51
|
+
build-backend = "hatchling.build"
|
opentelemetry_instrumentation_taskflow-0.0.1/src/opentelemetry/instrumentation/taskflow/__init__.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentation for TaskFlow.
|
|
2
|
+
|
|
3
|
+
`TaskFlow <https://docs.openstack.org/taskflow/>`_ models work as *atoms* — most
|
|
4
|
+
commonly :class:`~taskflow.task.Task` objects that implement an ``execute``
|
|
5
|
+
method (to perform work) and an optional ``revert`` method (to roll it back when
|
|
6
|
+
a flow fails). This instrumentor records every ``execute``/``revert`` call as a
|
|
7
|
+
span named ``taskflow.task.<method>``, annotated with the task's class, method
|
|
8
|
+
and name.
|
|
9
|
+
|
|
10
|
+
Concrete tasks override ``execute``/``revert``, so patching the methods on
|
|
11
|
+
:class:`~taskflow.task.Task` itself would never see those overrides. Instead the
|
|
12
|
+
instrumentor wraps ``Task.__getattribute__`` so that accessing either method on
|
|
13
|
+
*any* Task instance returns a traced wrapper, regardless of where the method is
|
|
14
|
+
defined.
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
from opentelemetry.instrumentation.taskflow import TaskflowInstrumentor
|
|
19
|
+
|
|
20
|
+
TaskflowInstrumentor().instrument()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from functools import wraps
|
|
24
|
+
from typing import Collection
|
|
25
|
+
|
|
26
|
+
from opentelemetry import trace
|
|
27
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
28
|
+
from opentelemetry.instrumentation.taskflow.version import __version__
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from taskflow.task import Task
|
|
32
|
+
except ImportError:
|
|
33
|
+
Task = None
|
|
34
|
+
|
|
35
|
+
_instruments = ("taskflow",)
|
|
36
|
+
_TRACED_METHODS = frozenset(("execute", "revert"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _task_attributes(task, method):
|
|
40
|
+
"""Build the span attributes describing ``task`` and the called ``method``."""
|
|
41
|
+
task_type = type(task)
|
|
42
|
+
attributes = {
|
|
43
|
+
"taskflow.task.class": f"{task_type.__module__}.{task_type.__qualname__}",
|
|
44
|
+
"taskflow.task.method": method,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
task_name = getattr(task, "name", None)
|
|
48
|
+
if task_name is not None:
|
|
49
|
+
attributes["taskflow.task.name"] = task_name
|
|
50
|
+
|
|
51
|
+
return attributes
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _wrap_task_method(task, method_name, method, tracer):
|
|
55
|
+
"""Wrap a bound task method so each call is recorded as a span."""
|
|
56
|
+
|
|
57
|
+
@wraps(method)
|
|
58
|
+
def traced_method(*args, **kwargs):
|
|
59
|
+
with tracer.start_as_current_span(
|
|
60
|
+
f"taskflow.task.{method_name}",
|
|
61
|
+
attributes=_task_attributes(task, method_name),
|
|
62
|
+
record_exception=True,
|
|
63
|
+
set_status_on_exception=True,
|
|
64
|
+
):
|
|
65
|
+
return method(*args, **kwargs)
|
|
66
|
+
|
|
67
|
+
return traced_method
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _wrap_getattribute(getattribute, tracer):
|
|
71
|
+
"""Wrap ``Task.__getattribute__`` to trace traced-method lookups."""
|
|
72
|
+
|
|
73
|
+
def traced_getattribute(self, name):
|
|
74
|
+
attribute = getattribute(self, name)
|
|
75
|
+
if name in _TRACED_METHODS and callable(attribute):
|
|
76
|
+
return _wrap_task_method(self, name, attribute, tracer)
|
|
77
|
+
return attribute
|
|
78
|
+
|
|
79
|
+
return traced_getattribute
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TaskflowInstrumentor(BaseInstrumentor):
|
|
83
|
+
"""An instrumentor for TaskFlow task atoms.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
tracer_provider: ``TracerProvider`` used to obtain the tracer. Defaults
|
|
87
|
+
to the global provider.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
_original_getattribute = None
|
|
91
|
+
|
|
92
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
93
|
+
return _instruments
|
|
94
|
+
|
|
95
|
+
def _instrument(self, **kwargs):
|
|
96
|
+
if Task is None:
|
|
97
|
+
return # taskflow is not available
|
|
98
|
+
|
|
99
|
+
if TaskflowInstrumentor._original_getattribute is not None:
|
|
100
|
+
return # already instrumented
|
|
101
|
+
|
|
102
|
+
tracer = trace.get_tracer(
|
|
103
|
+
__name__,
|
|
104
|
+
__version__,
|
|
105
|
+
tracer_provider=kwargs.get("tracer_provider"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
TaskflowInstrumentor._original_getattribute = Task.__getattribute__
|
|
109
|
+
Task.__getattribute__ = _wrap_getattribute(
|
|
110
|
+
TaskflowInstrumentor._original_getattribute,
|
|
111
|
+
tracer,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _uninstrument(self, **kwargs):
|
|
115
|
+
if TaskflowInstrumentor._original_getattribute is None:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
Task.__getattribute__ = TaskflowInstrumentor._original_getattribute
|
|
119
|
+
TaskflowInstrumentor._original_getattribute = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
__all__ = ["TaskflowInstrumentor", "__version__"]
|
opentelemetry_instrumentation_taskflow-0.0.1/src/opentelemetry/instrumentation/taskflow/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from taskflow import task
|
|
3
|
+
|
|
4
|
+
from opentelemetry.instrumentation.taskflow import TaskflowInstrumentor
|
|
5
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
6
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
7
|
+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
|
|
8
|
+
InMemorySpanExporter,
|
|
9
|
+
)
|
|
10
|
+
from opentelemetry.trace import StatusCode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DemoTask(task.Task):
|
|
14
|
+
def execute(self, **kwargs):
|
|
15
|
+
return kwargs.get("value", "ok")
|
|
16
|
+
|
|
17
|
+
def revert(self, *args, **kwargs):
|
|
18
|
+
return kwargs.get("result", "reverted")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FailingTask(task.Task):
|
|
22
|
+
def execute(self, **kwargs):
|
|
23
|
+
raise ValueError(kwargs.get("message", "boom"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture(autouse=True)
|
|
27
|
+
def uninstrument_taskflow():
|
|
28
|
+
TaskflowInstrumentor().uninstrument()
|
|
29
|
+
yield
|
|
30
|
+
TaskflowInstrumentor().uninstrument()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def span_exporter():
|
|
35
|
+
return InMemorySpanExporter()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def instrumentor(span_exporter):
|
|
40
|
+
tracer_provider = TracerProvider()
|
|
41
|
+
tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
|
|
42
|
+
|
|
43
|
+
instrumentor = TaskflowInstrumentor()
|
|
44
|
+
instrumentor.instrument(tracer_provider=tracer_provider)
|
|
45
|
+
|
|
46
|
+
yield instrumentor
|
|
47
|
+
|
|
48
|
+
instrumentor.uninstrument()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def assert_task_span(span, *, method, task_name, task_class="DemoTask"):
|
|
52
|
+
assert span.name == f"taskflow.task.{method}"
|
|
53
|
+
assert span.attributes["taskflow.task.class"].endswith(f".{task_class}")
|
|
54
|
+
assert span.attributes["taskflow.task.method"] == method
|
|
55
|
+
assert span.attributes["taskflow.task.name"] == task_name
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_instrumentation_dependencies():
|
|
59
|
+
assert TaskflowInstrumentor().instrumentation_dependencies() == (
|
|
60
|
+
"taskflow",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize(
|
|
65
|
+
("method", "kwargs", "expected"),
|
|
66
|
+
(
|
|
67
|
+
("execute", {"value": "done"}, "done"),
|
|
68
|
+
("revert", {"result": "rolled-back"}, "rolled-back"),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
def test_task_lifecycle_methods_create_spans(
|
|
72
|
+
instrumentor,
|
|
73
|
+
span_exporter,
|
|
74
|
+
method,
|
|
75
|
+
kwargs,
|
|
76
|
+
expected,
|
|
77
|
+
):
|
|
78
|
+
result = getattr(DemoTask(name="demo-task"), method)(**kwargs)
|
|
79
|
+
|
|
80
|
+
spans = span_exporter.get_finished_spans()
|
|
81
|
+
assert result == expected
|
|
82
|
+
assert len(spans) == 1
|
|
83
|
+
assert_task_span(spans[0], method=method, task_name="demo-task")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_subclass_overrides_are_wrapped(instrumentor, span_exporter):
|
|
87
|
+
# ``execute`` is defined on the subclass, not on ``Task`` — wrapping
|
|
88
|
+
# ``__getattribute__`` is what lets the instrumentor see it.
|
|
89
|
+
assert DemoTask(name="subclass-task").execute(value="done") == "done"
|
|
90
|
+
|
|
91
|
+
spans = span_exporter.get_finished_spans()
|
|
92
|
+
assert len(spans) == 1
|
|
93
|
+
assert_task_span(spans[0], method="execute", task_name="subclass-task")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_instrument_twice_does_not_create_nested_duplicate_spans(
|
|
97
|
+
instrumentor,
|
|
98
|
+
span_exporter,
|
|
99
|
+
):
|
|
100
|
+
instrumentor.instrument()
|
|
101
|
+
|
|
102
|
+
assert DemoTask(name="demo-task").execute(value="done") == "done"
|
|
103
|
+
|
|
104
|
+
spans = span_exporter.get_finished_spans()
|
|
105
|
+
assert len(spans) == 1
|
|
106
|
+
assert_task_span(spans[0], method="execute", task_name="demo-task")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_uninstrument_restores_task_without_creating_spans(
|
|
110
|
+
instrumentor,
|
|
111
|
+
span_exporter,
|
|
112
|
+
):
|
|
113
|
+
instrumentor.uninstrument()
|
|
114
|
+
|
|
115
|
+
result = DemoTask(name="demo-task").execute(value="done")
|
|
116
|
+
|
|
117
|
+
assert result == "done"
|
|
118
|
+
assert span_exporter.get_finished_spans() == ()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_exception_is_recorded_on_span(instrumentor, span_exporter):
|
|
122
|
+
with pytest.raises(ValueError, match="boom"):
|
|
123
|
+
FailingTask(name="failing-task").execute(message="boom")
|
|
124
|
+
|
|
125
|
+
spans = span_exporter.get_finished_spans()
|
|
126
|
+
assert len(spans) == 1
|
|
127
|
+
assert_task_span(
|
|
128
|
+
spans[0],
|
|
129
|
+
method="execute",
|
|
130
|
+
task_name="failing-task",
|
|
131
|
+
task_class="FailingTask",
|
|
132
|
+
)
|
|
133
|
+
assert spans[0].status.status_code == StatusCode.ERROR
|
|
134
|
+
assert any(event.name == "exception" for event in spans[0].events)
|