usefluency-workflow-telemetry 0.1.1__py3-none-any.whl

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.
Files changed (30) hide show
  1. fluency_workflow_telemetry/__init__.py +6 -0
  2. fluency_workflow_telemetry/configuration.py +151 -0
  3. fluency_workflow_telemetry/docs/LICENSE +34 -0
  4. fluency_workflow_telemetry/docs/README.md +36 -0
  5. fluency_workflow_telemetry/docs/SDK.md +248 -0
  6. fluency_workflow_telemetry/docs/examples/README.md +92 -0
  7. fluency_workflow_telemetry/docs/examples/agent-events.py +44 -0
  8. fluency_workflow_telemetry/docs/examples/agent-events.ts +31 -0
  9. fluency_workflow_telemetry/docs/examples/capture-context.py +41 -0
  10. fluency_workflow_telemetry/docs/examples/capture-context.ts +47 -0
  11. fluency_workflow_telemetry/docs/examples/collector.yaml +9 -0
  12. fluency_workflow_telemetry/docs/examples/environment.example +2 -0
  13. fluency_workflow_telemetry/docs/examples/existing-provider.py +35 -0
  14. fluency_workflow_telemetry/docs/examples/existing-provider.ts +28 -0
  15. fluency_workflow_telemetry/docs/examples/langchain-agent.py +37 -0
  16. fluency_workflow_telemetry/docs/examples/openai-agent.py +35 -0
  17. fluency_workflow_telemetry/docs/examples/openai-client.py +43 -0
  18. fluency_workflow_telemetry/docs/examples/pydantic-agent.py +40 -0
  19. fluency_workflow_telemetry/docs/examples/rpa.py +20 -0
  20. fluency_workflow_telemetry/docs/examples/rpa.ts +25 -0
  21. fluency_workflow_telemetry/docs/examples/vercel-agent.ts +65 -0
  22. fluency_workflow_telemetry/docs/examples/workflow.py +19 -0
  23. fluency_workflow_telemetry/docs/examples/workflow.ts +20 -0
  24. fluency_workflow_telemetry/events.py +59 -0
  25. fluency_workflow_telemetry/py.typed +0 -0
  26. usefluency_workflow_telemetry-0.1.1.dist-info/METADATA +49 -0
  27. usefluency_workflow_telemetry-0.1.1.dist-info/RECORD +30 -0
  28. usefluency_workflow_telemetry-0.1.1.dist-info/WHEEL +5 -0
  29. usefluency_workflow_telemetry-0.1.1.dist-info/licenses/LICENSE +34 -0
  30. usefluency_workflow_telemetry-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,6 @@
1
+ """Standard OTLP startup configuration for customer workflow telemetry."""
2
+
3
+ from .configuration import TelemetryHandle, configure
4
+ from .events import AgentEvents
5
+
6
+ __all__ = ["AgentEvents", "TelemetryHandle", "configure"]
@@ -0,0 +1,151 @@
1
+ """Configure standard OTLP export without owning application instrumentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import os
7
+ import threading
8
+ from dataclasses import dataclass, field
9
+ from importlib.metadata import version
10
+ from urllib.parse import urlsplit
11
+
12
+ from opentelemetry import trace
13
+ from opentelemetry.exporter.otlp.proto.http import Compression
14
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
15
+ from opentelemetry.sdk.resources import Resource
16
+ from opentelemetry.sdk.trace import TracerProvider
17
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
18
+
19
+ from .events import AgentEvents
20
+
21
+ _lock = threading.RLock()
22
+ _handles: dict[TracerProvider, TelemetryHandle] = {}
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class _Configuration:
27
+ endpoint: str
28
+ api_key: str = field(repr=False)
29
+ service_name: str | None
30
+ max_queue_size: int
31
+ max_export_batch_size: int
32
+ schedule_delay_millis: int
33
+ timeout_seconds: float
34
+
35
+
36
+ class TelemetryHandle:
37
+ """Own only the Fluency batch processor; the application's provider stays usable."""
38
+
39
+ def __init__(self, provider: TracerProvider, processor: BatchSpanProcessor, config: _Configuration) -> None:
40
+ self.provider = provider
41
+ self.events = AgentEvents(provider.get_tracer("usefluency-workflow-telemetry", version("usefluency-workflow-telemetry")))
42
+ self._processor = processor
43
+ self._lifecycle_lock = threading.RLock()
44
+ self._config = config
45
+ self._closed = False
46
+
47
+ @property
48
+ def closed(self) -> bool:
49
+ return self._closed
50
+
51
+ def flush(self) -> None:
52
+ """Block while attempting to drain ended spans; no total deadline or delivery acknowledgment."""
53
+ with self._lifecycle_lock:
54
+ if not self._closed:
55
+ self._processor.force_flush()
56
+
57
+ def shutdown(self) -> None:
58
+ """Drain and stop Fluency export without stopping other exporters.
59
+
60
+ Uses OTel's shutdown behavior; unsent spans can be lost. Inspect
61
+ exporter logs for outcomes. Finish active work before shutting down.
62
+ """
63
+ with self._lifecycle_lock:
64
+ if not self._closed:
65
+ self._closed = True
66
+ self._processor.shutdown()
67
+
68
+
69
+ def configure(
70
+ *,
71
+ traces_endpoint: str | None = None,
72
+ api_key: str | None = None,
73
+ service_name: str | None = None,
74
+ provider: TracerProvider | None = None,
75
+ max_queue_size: int = 2048,
76
+ max_export_batch_size: int = 512,
77
+ schedule_delay_millis: int = 5000,
78
+ timeout_seconds: float = 10,
79
+ ) -> TelemetryHandle:
80
+ """Add batched OTLP/HTTP export to a supplied, existing, or new global provider.
81
+
82
+ Endpoint is the full traces URL, used unchanged. Framework instrumentation must
83
+ be configured separately. Existing resources, sampling and exporters are kept.
84
+ """
85
+ endpoint = traces_endpoint if traces_endpoint is not None else os.getenv("FLUENCY_OTLP_TRACES_ENDPOINT", "")
86
+ key = api_key if api_key is not None else os.getenv("FLUENCY_WORKFLOW_API_KEY", "")
87
+ url = urlsplit(endpoint)
88
+ if url.scheme not in {"http", "https"} or not url.hostname or url.username or url.password or url.query or url.fragment:
89
+ raise ValueError("traces_endpoint must be a full HTTP(S) traces URL without credentials, query or fragment")
90
+ if url.scheme == "http" and url.hostname not in {"localhost", "127.0.0.1", "::1"}:
91
+ raise ValueError("Use HTTPS for non-local telemetry endpoints")
92
+ if not key.strip() or any(c.isspace() for c in key):
93
+ raise ValueError("Provide api_key or FLUENCY_WORKFLOW_API_KEY without whitespace")
94
+ if service_name is not None and not service_name.strip():
95
+ raise ValueError("service_name must not be empty")
96
+ if any(type(value) is not int for value in (max_queue_size, max_export_batch_size, schedule_delay_millis)):
97
+ raise ValueError("Batch sizes and delay must be integers")
98
+ if max_export_batch_size > 1024:
99
+ raise ValueError("max_export_batch_size must not exceed the receiver's 1024 span limit")
100
+ if max_queue_size <= 0 or not 0 < max_export_batch_size <= max_queue_size or schedule_delay_millis <= 0:
101
+ raise ValueError("Batch sizes and delay must be positive; batch size must not exceed queue size")
102
+ if not math.isfinite(timeout_seconds) or timeout_seconds <= 0:
103
+ raise ValueError("timeout_seconds must be finite and positive")
104
+ config = _Configuration(endpoint, key, service_name, max_queue_size, max_export_batch_size, schedule_delay_millis, timeout_seconds)
105
+
106
+ with _lock:
107
+ candidate = provider if provider is not None else trace.get_tracer_provider()
108
+ new_global = provider is None and isinstance(candidate, trace.ProxyTracerProvider)
109
+ if new_global:
110
+ attributes = {"service.name": service_name} if service_name is not None else {}
111
+ target = TracerProvider(resource=Resource.create(attributes))
112
+ elif isinstance(candidate, TracerProvider):
113
+ target = candidate
114
+ else:
115
+ raise TypeError("An OpenTelemetry SDK TracerProvider is required; the existing provider will not be replaced")
116
+
117
+ existing = _handles.get(target)
118
+ if existing is not None:
119
+ if existing._closed:
120
+ raise RuntimeError("Fluency telemetry has shut down for this provider; configure once at application startup")
121
+ if existing._config != config:
122
+ raise ValueError("Fluency export is already configured differently on this provider")
123
+ return existing
124
+ if not new_global and service_name is not None and target.resource.attributes.get("service.name") != service_name:
125
+ raise ValueError("Set service_name on the existing provider's Resource; configure does not replace resources")
126
+
127
+ exporter = OTLPSpanExporter(
128
+ endpoint=endpoint,
129
+ headers={
130
+ "Authorization": f"Bearer {key}",
131
+ "User-Agent": (
132
+ f"usefluency-workflow-telemetry/{version('usefluency-workflow-telemetry')} "
133
+ f"OTel-OTLP-Exporter-Python/{version('opentelemetry-exporter-otlp-proto-http')}"
134
+ ),
135
+ },
136
+ timeout=timeout_seconds,
137
+ compression=Compression.NoCompression,
138
+ )
139
+ processor = BatchSpanProcessor(
140
+ exporter, max_queue_size=max_queue_size, max_export_batch_size=max_export_batch_size, schedule_delay_millis=schedule_delay_millis
141
+ )
142
+ if new_global:
143
+ trace.set_tracer_provider(target)
144
+ if trace.get_tracer_provider() is not target:
145
+ processor.shutdown()
146
+ target.shutdown()
147
+ raise RuntimeError("Another provider was installed during setup; pass the existing provider explicitly")
148
+ target.add_span_processor(processor)
149
+ handle = TelemetryHandle(target, processor, config)
150
+ _handles[target] = handle
151
+ return handle
@@ -0,0 +1,34 @@
1
+ Fluency Workflow Telemetry SDK Proprietary License
2
+
3
+ Copyright (c) 2026 Fluency. All rights reserved.
4
+
5
+ This software and its documentation (the "SDK") are proprietary and confidential
6
+ to Fluency.
7
+
8
+ Subject to a written agreement with Fluency that authorizes access to the Fluency
9
+ services, Fluency grants the customer a limited, non-exclusive, non-transferable,
10
+ non-sublicensable right to install, copy, and use the SDK solely for the customer's
11
+ internal operation of its automated workflows with the Fluency services during the term
12
+ of that agreement.
13
+
14
+ Except to the extent expressly permitted by that written agreement or applicable
15
+ law, the customer may not:
16
+
17
+ 1. distribute, publish, sublicense, sell, rent, lease, or otherwise make the SDK
18
+ available to any third party;
19
+ 2. modify, adapt, translate, or create derivative works of the SDK;
20
+ 3. reverse engineer, decompile, or disassemble the SDK; or
21
+ 4. use the SDK to develop or operate a product or service that competes with the
22
+ Fluency services.
23
+
24
+ The customer may make a reasonable number of copies of the SDK solely for backup,
25
+ testing, deployment, and disaster recovery connected with its authorized use. All
26
+ copies must retain this license and all proprietary notices.
27
+
28
+ Third-party software included with or used by the SDK remains subject to its own
29
+ license terms. No rights are granted to Fluency trademarks, service marks, or logos.
30
+
31
+ The SDK is provided under the warranties, disclaimers, liability limits, and other
32
+ terms in the customer's written agreement with Fluency. If this license conflicts
33
+ with that agreement, the agreement controls. If no such agreement is in effect, no
34
+ license to use the SDK is granted.
@@ -0,0 +1,36 @@
1
+ # Workflow telemetry SDKs
2
+
3
+ Capture the work your agents and automations perform in Fluency. Connect existing OpenTelemetry instrumentation, then add business identifiers and outcomes with the SDK's event helpers.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pip install usefluency-workflow-telemetry
9
+ npm install @usefluency/workflow-telemetry
10
+ ```
11
+
12
+ Python requires 3.12 or later and imports as `fluency_workflow_telemetry`. JavaScript and TypeScript require Node.js 24 or later; ESM and CommonJS are supported.
13
+
14
+ ## Get started
15
+
16
+ Your Fluency contact provides your organization's regional endpoint and ingestion key. Configure them in your server-side environment:
17
+
18
+ ```sh
19
+ FLUENCY_OTLP_TRACES_ENDPOINT=https://YOUR-REGIONAL-ENDPOINT/ext/otel/v1/traces
20
+ FLUENCY_WORKFLOW_API_KEY=YOUR-ORGANIZATION-API-KEY
21
+ ```
22
+
23
+ Choose the guide for your application:
24
+
25
+ - [LangChain and LangGraph](https://docs.usefluency.com/fluency-workflow-telemetry-langchain): connect a Python agent, capture its tools, and attach business context.
26
+ - [Vercel AI SDK](https://docs.usefluency.com/fluency-workflow-telemetry-vercel): capture Node.js model calls, tools, and streaming work.
27
+ - [OpenAI](https://docs.usefluency.com/fluency-workflow-telemetry-openai): connect direct Python API calls or Agents SDK workflows while preserving existing tracing.
28
+ - [RPA and custom workflows](https://docs.usefluency.com/fluency-workflow-telemetry-rpa): record steps and outcomes from Python or TypeScript automations.
29
+ - [Existing OpenTelemetry](https://docs.usefluency.com/fluency-workflow-telemetry-opentelemetry): add Fluency while keeping your current telemetry destination.
30
+ - [Business context and capture](https://docs.usefluency.com/fluency-workflow-telemetry-business-context): enrich an operation or record a business action.
31
+
32
+ After `configure()`, use `telemetry.events` for `enrich`, `capture`, and timed operations. Framework integrations capture the operations they instrument; the exporter alone does not discover agent or bot activity.
33
+
34
+ Exact methods and lifecycle behavior are in the [SDK reference](SDK.md). [Executable examples](examples/README.md) accompany the guides. Maintainers use [release instructions](https://github.com/farmorsoftware/fluency-monorepo/blob/main/packages/workflow-telemetry/RELEASING.md).
35
+
36
+ This software is proprietary; see [LICENSE](LICENSE).
@@ -0,0 +1,248 @@
1
+ # Fluency Workflow Telemetry SDK reference
2
+
3
+ ## Overview
4
+
5
+ Python and Node.js adapters for standard OpenTelemetry export, plus optional instrumentation helpers. For onboarding, credentials, and Collector setup, use the [customer integration guide](https://docs.usefluency.com/fluency-workflow-telemetry-sdk). For the meaning of spans, span events, and capture, see [business context](https://docs.usefluency.com/fluency-workflow-telemetry-business-context).
6
+
7
+ ## Install
8
+
9
+ Python requires 3.12 or later; Node.js requires 24 or later. Install the SDK from PyPI or npm:
10
+
11
+ ```sh
12
+ pip install usefluency-workflow-telemetry
13
+ npm install @usefluency/workflow-telemetry
14
+ ```
15
+
16
+ ## SDK reference
17
+
18
+ ### Initialize the client
19
+
20
+ | Python `configure()` | TypeScript `configure()` | Default |
21
+ | ----------------------- | ------------------------ | --------------------------------- |
22
+ | `traces_endpoint` | `tracesEndpoint` | `FLUENCY_OTLP_TRACES_ENDPOINT` |
23
+ | `api_key` | `apiKey` | `FLUENCY_WORKFLOW_API_KEY` |
24
+ | `service_name` | `serviceName` | OTel resource default/environment |
25
+ | `max_queue_size` | `maxQueueSize` | 2048 |
26
+ | `max_export_batch_size` | `maxExportBatchSize` | 512 |
27
+ | `schedule_delay_millis` | `scheduleDelayMillis` | 5000 |
28
+ | `timeout_seconds` | `timeoutSeconds` | 10 |
29
+
30
+ The endpoint is a full traces URL. It must use HTTPS except for loopback, and must
31
+ not contain credentials, a query or a fragment. Keys cannot be empty or contain
32
+ whitespace. Batch sizes/delay must be positive integers, batch size cannot exceed
33
+ queue size or 1024, and timeout must be finite and positive.
34
+
35
+ Python additionally accepts `provider=existing_provider`. TypeScript integrates
36
+ with application-owned providers using `createSpanProcessor(options)`, which
37
+ accepts the same options except `serviceName`; set service metadata on the
38
+ application's resource. Add the returned `processor` to the provider's
39
+ `spanProcessors` constructor option exactly once. See the existing-provider examples below.
40
+
41
+ Python reuses an existing global SDK provider when `provider` is omitted.
42
+ TypeScript `configure()` requires an unconfigured global provider. Neither replaces
43
+ an application's provider, existing resource, sampler or exporters. Both new-provider
44
+ paths honor OTel resource environment attributes; explicit service name takes precedence.
45
+
46
+ `configure()` is idempotent for identical configuration and rejects conflicting
47
+ configuration. Configure after worker forking. Framework instrumentation must use
48
+ the same provider; an exporter does not create spans.
49
+
50
+ ### Use an existing OpenTelemetry provider
51
+
52
+ ```python
53
+ from fluency_workflow_telemetry import configure
54
+
55
+ telemetry = configure(provider=existing_provider)
56
+ ```
57
+
58
+ Use the `TracerProvider` already used by your framework or application. An explicitly supplied provider is not installed globally. Existing exporters, sampling policy, and resource attributes remain in place.
59
+
60
+ If you omit `provider`, `configure()` reuses an existing global OTel SDK provider or creates one when none has been installed. Initialize it before your framework's instrumentation. If that framework owns provider setup, initialize the framework first and pass its provider explicitly.
61
+
62
+ Configure `service.name` and other resource attributes on an existing provider itself. Passing a different `service_name` to `configure()` raises an error rather than replacing its resource.
63
+
64
+ For TypeScript, create the Fluency processor once and include it alongside your existing processors:
65
+
66
+ ```typescript
67
+ import { createSpanProcessor } from '@usefluency/workflow-telemetry';
68
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
69
+
70
+ const telemetry = createSpanProcessor();
71
+ const provider = new NodeTracerProvider({
72
+ spanProcessors: [existingProcessor, telemetry.processor],
73
+ // Keep your application's resource and sampler configuration here.
74
+ });
75
+ provider.register();
76
+ ```
77
+
78
+ `existingProcessor` represents your existing exporter pipeline. You can also pass the processor to `NodeSDK` through `spanProcessors`. OpenTelemetry JavaScript accepts processors during provider construction; update your startup configuration if your provider is already running. Each `createSpanProcessor()` call creates an independent export branch.
79
+
80
+ ### Rotate the API key
81
+
82
+ The credential is fixed at startup. Update your secret configuration and restart with the new key before revoking the old one. The SDK does not refresh tokens or hot-reload keys.
83
+
84
+ ### Lifecycle and adapter methods
85
+
86
+ | Python handle | TypeScript handle | Meaning |
87
+ | ------------- | ----------------------------- | ------------------------------------------------------------ |
88
+ | `provider` | `provider` from `configure()` | Provider used by application instrumentation |
89
+ | — | `processor` | Processor to attach during application provider construction |
90
+ | `flush()` | `await flush()` | Attempt to drain ended spans |
91
+ | `shutdown()` | `await shutdown()` | Drain and stop Fluency export |
92
+ | `closed` | `closed` | Shutdown has started |
93
+
94
+ ### Handle SDK diagnostics
95
+
96
+ Use the official OpenTelemetry exporter diagnostics through your application's
97
+ logging integration. The SDK does not install a global logger or expose delivery
98
+ counters. HTTP success can include partial rejection: Python's locked exporter
99
+ reports success without inspecting that body, while JavaScript logs partial
100
+ success through OTel `diag` and still reports export success. Neither result is
101
+ a per-span storage receipt. The receiver records authoritative accepted,
102
+ inserted, duplicate and rejected counts after the transaction commits.
103
+
104
+ Both lifecycle methods return no delivery status (`None` / `Promise<void>`).
105
+
106
+ ### Delivery, flushing, and shutdown
107
+
108
+ Finish active spans, streams and framework-specific buffers before shutdown.
109
+ Python methods block; async applications should call them on a worker thread.
110
+ The timeout applies to each official export attempt and retry budget, not the full
111
+ drain. Shutdown stops only Fluency's
112
+ processor; the application remains responsible for its provider and other exporters.
113
+ A shut-down handle cannot be restarted.
114
+
115
+ Queues are in memory. Failed batches are not retained for a later flush. Crashes,
116
+ overflow, sampling and exhausted retries can lose spans. Collector durability is
117
+ an optional deployment choice with its own capacity and recovery limits.
118
+
119
+ ## Record business context
120
+
121
+ Use native span attributes for facts about an operation and `span.add_event()` / `span.addEvent()` for a milestone within it. Both are retained. A customer agent event is a product concept; it need not be represented by a separate OTel span. The SDK does not extract nested span events into independent business records.
122
+
123
+ ### Optional Fluency attribute conventions
124
+
125
+ These are Fluency conventions, not OpenTelemetry standard keys or required ingestion fields. All values below are strings. Native OTel calls and the optional helpers produce the same evidence. Existing customer-defined and framework attributes remain intact.
126
+
127
+ | Attribute | Meaning and scope |
128
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
129
+ | `fluency.run.id` | Customer's business execution identifier; may correlate several traces. Set on relevant spans. It does not change deduplication or merge traces. |
130
+ | `fluency.run.name` | Business operation name, such as `Review supplier invoice`. |
131
+ | `fluency.artifact.source` | Source namespace, including system/environment where needed, such as `erp:production:invoices`. |
132
+ | `fluency.artifact.external_id` | Record identifier within that source; attach to the operation touching that record. |
133
+ | `fluency.artifact.type` | Customer's record type, such as `invoice`; no automatic KG classification. |
134
+ | `fluency.outcome` | Declared business result, such as `approved`, `declined`, or `escalated`, on the operation reaching that result. |
135
+
136
+ Absent fields remain unknown. Technical Error/OK is not a business outcome. Conversation/session IDs are retained as correlation evidence, not automatically treated as business run IDs. Declared attributes must take precedence over any future inferred interpretation. Record multiple affected artifacts in separate operation spans or customer-defined attributes; do not overwrite a single artifact field repeatedly and expect a history.
137
+
138
+ ### Optional agent-event helpers
139
+
140
+ `AgentEvents(tracer)` uses an application-supplied tracer. With no argument, it obtains a tracer from the global provider. It never configures a provider, exporter, sampler, or logger. Configure export separately. With no SDK provider, native OTel no-op behavior applies.
141
+
142
+ | Python | TypeScript | Behavior |
143
+ | ---------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
144
+ | `enrich(attributes, span=...)` | `enrich(attributes, span?)` | Sets attributes on the supplied or active recording span. Returns false if absent, ended, or nonrecording. Creates no span. |
145
+ | `capture(name, attributes=..., parent=None)` | `capture(name, attributes?, parent?)` | Starts and immediately ends a span with equal start/end timestamps. Returns its `SpanContext`, not a delivery receipt. |
146
+ | `with events.span(name, attributes=...)` | `await events.trace(name, callback, attributes?)` | Starts an active operation span, ends it on exit, and preserves the operation's exception. TypeScript always returns a Promise. |
147
+ | `with events.context(attributes)` | `events.withContext(attributes, callback)` | Scopes attributes to calls through this helper instance. Creates no span and does not establish trace parentage. |
148
+
149
+ Python operation helpers use native OTel exception semantics. TypeScript records Error/string exceptions, marks Error, rethrows the original value, and ends in `finally`. Python cancellation ends the span but follows native OTel status behavior. Consume streams inside the block/callback; returning an iterator does not keep it open.
150
+
151
+ ### How capture joins an existing trace
152
+
153
+ `capture()` uses the **active OTel context at the call site**, unless an explicit parent span is supplied. Selecting the same tracer, passing the same business ID, or constructing `AgentEvents` inside a span does not save a parent for later calls.
154
+
155
+ | Context at the call | Result with an installed SDK |
156
+ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
157
+ | Valid active parent span | Same trace ID; a new span ID; `parent_span_id` is the active parent's span ID. |
158
+ | Extracted remote parent activated by instrumentation | Same distributed trace, subject to the provider's sampler. |
159
+ | No valid active parent | New root span and a new trace ID. |
160
+ | Nonrecording valid parent | Parent relationship still comes from context; recording/export depends on the configured sampler. |
161
+
162
+ Capture ends only its new span. It neither ends nor becomes the customer's active span. Multiple captures under one active parent are siblings. Native `addEvent` instead appends an annotation to the existing span and creates no new span ID. Child capture can be exported before its parent ends; an annotation waits for its containing span to end with standard processors.
163
+
164
+ ```text
165
+ Trace A
166
+ Review invoice existing active span
167
+ event: invoice.approved native annotation, no separate span ID
168
+ Approval notification queued capture: new child span, ended immediately
169
+
170
+ Trace B
171
+ Standalone reconciliation capture outside any active span
172
+ ```
173
+
174
+ The executable [Python](examples/capture-context.py) and [TypeScript](examples/capture-context.ts) examples assert these IDs, parentage, active-context preservation and coexistence with another exporter.
175
+
176
+ ```python
177
+ from fluency_workflow_telemetry import AgentEvents
178
+
179
+ events = AgentEvents(tracer) # tracer from the customer's configured provider
180
+ with tracer.start_as_current_span("Review invoice") as parent:
181
+ parent.add_event("invoice.approved", {"customer.invoice.id": "INV-123"})
182
+ child = events.capture("Approval notification queued")
183
+ assert child.trace_id == parent.get_span_context().trace_id
184
+ # The parent's context has now been restored to its outer context.
185
+ ```
186
+
187
+ ```typescript
188
+ import { AgentEvents } from '@usefluency/workflow-telemetry';
189
+
190
+ const events = new AgentEvents(tracer); // existing configured provider/context manager
191
+ tracer.startActiveSpan('Review invoice', (parent) => {
192
+ try {
193
+ parent.addEvent('invoice.approved', { 'customer.invoice.id': 'INV-123' });
194
+ const child = events.capture('Approval notification queued');
195
+ // child.traceId === parent.spanContext().traceId
196
+ } finally {
197
+ parent.end();
198
+ }
199
+ });
200
+ ```
201
+
202
+ A span held in a variable is not necessarily active: Python `start_span()` and Node `startSpan()` do not activate it. Prefer your framework's active span or the native active-span APIs above. For an explicit parent, use Python `trace.use_span(parent, end_on_exit=False)` or Node `context.with(trace.setSpan(context.active(), parent), callback)`. The helper has no separate parent/trace-ID argument. Do not put trace IDs in attributes expecting them to change parentage.
203
+
204
+ ### Scoped attributes and async work
205
+
206
+ Nested helper scopes override outer ones; per-call attributes override scoped attributes. Context restores on exit, including failures. Scoped values do not automatically enrich framework spans, native span events, or outbound baggage. Use `enrich()` on the relevant active span, or native attribute/event calls. Parent attributes do not automatically copy to child spans.
207
+
208
+ Python contexts work across `await` in async functions. Node requires the application's context manager, normally installed by `NodeSDK` or `NodeTracerProvider.register()`. Await asynchronous work within the active-span scope. Detached work and HTTP/queue boundaries require the application's normal context propagation; the helper cannot reconstruct a missing parent from a run ID. Tasks deliberately started within a context can inherit it even if the parent ends before the task does.
209
+
210
+ ### Sampling and discrete actions
211
+
212
+ Capture and ordinary spans use the same configured sampler and exporters. Capture does not bypass sampling, recover dropped spans, or create a Fluency-only delivery channel. Its returned context does not prove that a span was recorded or received. Both existing and Fluency destinations can receive helper spans and added attributes. Use destination-specific filtering if their permitted data differs.
213
+
214
+ ### Concurrency and nesting
215
+
216
+ Use the active operation context to define parentage and helper scopes for repeated attributes. These are independent mechanisms: setting `fluency.run.id` does not start, join, deduplicate, or merge traces. Standard OTel network instrumentation or explicit propagation carries trace context across services. See [OpenTelemetry context and span creation](https://opentelemetry.io/docs/specs/otel/trace/api/).
217
+
218
+ ## Verify the integration
219
+
220
+ Run the [packaged examples](examples/README.md), inspect exporter diagnostics, and share the region, UTC time and trace/span IDs with Fluency onboarding for receipt confirmation. Flush and shutdown are not acceptance receipts. Internal trace-inspection endpoints are not part of this SDK's customer API.
221
+
222
+ ## Design references
223
+
224
+ Use the official [OTel tracing API](https://opentelemetry.io/docs/specs/otel/trace/api/) for parentage and span events, the [tracing SDK](https://opentelemetry.io/docs/specs/otel/trace/sdk/) for sampling/export/lifecycle, and the [OTLP exporter specification](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) for endpoint configuration. Optional Fluency helpers preserve those semantics.
225
+
226
+ <a id="what-appears-in-fluency"></a>
227
+ <a id="what-fluency-provides"></a>
228
+ <a id="choose-an-integration-path"></a>
229
+ <a id="typescript-quick-start"></a>
230
+ <a id="python-quick-start"></a>
231
+ <a id="opentelemetry-and-other-frameworks"></a>
232
+ <a id="send-from-an-existing-collector"></a>
233
+ <a id="stable-ids-and-retries"></a>
234
+ <a id="failed-operations"></a>
235
+ <a id="content-controls"></a>
236
+ <a id="runtime-behavior"></a>
237
+
238
+ Integration tutorials and receiver limits now live in the [customer guide](https://docs.usefluency.com/fluency-workflow-telemetry-sdk) and [ingestion reference](https://docs.usefluency.com/fluency-workflow-telemetry-api).
239
+
240
+ ## Configured business helpers
241
+
242
+ Python `configure(...)` returns `telemetry.events` bound to the selected provider. Standalone Node `configure(...)` returns `telemetry.events` bound to the provider it creates. Node `createSpanProcessor()` does not own a provider: construct `new AgentEvents(provider.getTracer('your-application'))` after creating your application provider.
243
+
244
+ `enrich` updates an active or explicitly supplied recording span. `capture` creates and ends a separate span. Python `capture(name, attributes=..., parent=span)` and TypeScript `capture(name, attributes, span)` accept an explicit parent. When omitted, the active OTel context supplies the parent. Supplying a parent does not activate or end that span. It is useful for framework callback spans, such as LangChain's `get_current_span()`.
245
+
246
+ A native span event is an annotation on its containing span. Captured action spans have their own identity and use ordinary provider sampling and export. A returned span context is not a delivery receipt.
247
+
248
+ Choose your framework in the [integration guides](https://docs.usefluency.com/fluency-workflow-telemetry-sdk) for a complete runnable example and existing-provider setup.
@@ -0,0 +1,92 @@
1
+ # Workflow telemetry examples
2
+
3
+ ## Overview
4
+
5
+ Start with the example that matches your application. Customer walkthroughs live in [Fluency's integration guides](https://docs.usefluency.com/fluency-workflow-telemetry-sdk).
6
+
7
+ ## Setup
8
+
9
+ Install `usefluency-workflow-telemetry` for Python 3.12+, or `@usefluency/workflow-telemetry` for Node.js 24+. Supply `FLUENCY_OTLP_TRACES_ENDPOINT` and `FLUENCY_WORKFLOW_API_KEY` through your application's environment. See [environment.example](environment.example).
10
+
11
+ TypeScript examples use an ESM application (`"type": "module"` in `package.json`). Install `@opentelemetry/api` and the OTel SDK packages imported by an example when using provider-level examples.
12
+
13
+ ## Choose an example
14
+
15
+ | Example | What it demonstrates |
16
+ | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `langchain-agent.py` | LangChain's LangGraph-backed agent reviews an invoice. OpenInference captures model/tools; explicit tool parentage attaches business context and approval to the correct operation. |
18
+ | `vercel-agent.ts` | Vercel AI SDK model/tool calls, concurrent runs, streaming, and business capture inside a tool. |
19
+ | `openai-agent.py` | OpenAI Agents runs and tools, retaining existing Agents tracing processors. |
20
+ | `openai-client.py` | Direct OpenAI Responses calls and streaming, recording business completion only after a completed response. |
21
+ | `rpa.py` / `rpa.ts` | A custom automation posts an invoice: timed run, ERP step, and successful business action. Replace the demo callback with your ERP operation. |
22
+ | `agent-events.py` / `agent-events.ts` | Concurrent runs, scoped business attributes, and a handled policy failure. |
23
+ | `capture-context.py` / `capture-context.ts` | Native span annotation, child capture, and standalone capture; checks the trace relationships. |
24
+ | `existing-provider.py` / `existing-provider.ts` | Add a Fluency destination while retaining an existing exporter. Python also demonstrates using native OTel without the Fluency SDK. |
25
+ | `workflow.py` / `workflow.ts` | Minimal native OTel root/child instrumentation and an approval annotation. |
26
+ | `pydantic-agent.py` | Existing Pydantic AI example using a deterministic test model, tool instrumentation, and business capture. |
27
+
28
+ The files are included in npm's `examples/` directory and Python's `fluency_workflow_telemetry/docs/examples/` directory. Copy the selected example into your application directory so it resolves your installed dependencies.
29
+
30
+ ## LangChain and LangGraph
31
+
32
+ ```sh
33
+ pip install usefluency-workflow-telemetry openinference-instrumentation-langchain langchain langgraph langchain-openai
34
+ python langchain-agent.py
35
+ ```
36
+
37
+ Set `OPENAI_API_KEY` and `OPENAI_MODEL` for this example. Use a model available to your account that supports tools. The approval function is illustrative: perform actual policy checks before recording approval. Keep your existing model/provider configuration when adding telemetry to a real agent.
38
+
39
+ The release tests pin LangChain 1.4.6, LangGraph 1.2.15, langchain-openai 1.7.2, OpenAI 3.31.0, and OpenInference LangChain 0.1.79. These dependencies are optional; the core SDK does not install them. See [the customer guide](https://docs.usefluency.com/fluency-workflow-telemetry-langchain) for new/existing provider setup.
40
+
41
+ ## Vercel AI SDK
42
+
43
+ ```sh
44
+ npm install @usefluency/workflow-telemetry ai@^7 @ai-sdk/otel@^1 @ai-sdk/openai@^4 @opentelemetry/api
45
+ npm install --save-dev @types/json-schema
46
+ node vercel-agent.ts
47
+ ```
48
+
49
+ Set `OPENAI_API_KEY` and `OPENAI_MODEL`. The example uses AI SDK 7's `registerTelemetry(new OpenTelemetry())` and a demo approval tool; replace it with actual validation and approval before recording success. Keep existing model/gateway configuration in your application. The release test pins AI SDK 7.0.142, `@ai-sdk/otel` 1.0.142 and `@ai-sdk/openai` 4.0.95. See [the customer guide](https://docs.usefluency.com/fluency-workflow-telemetry-vercel) for existing providers and Next.js startup.
50
+
51
+ ## OpenAI Python
52
+
53
+ For direct Responses API calls:
54
+
55
+ ```sh
56
+ pip install usefluency-workflow-telemetry openai openinference-instrumentation-openai
57
+ python openai-client.py
58
+ ```
59
+
60
+ For OpenAI Agents SDK:
61
+
62
+ ```sh
63
+ pip install usefluency-workflow-telemetry openai-agents openinference-instrumentation-openai-agents
64
+ python openai-agent.py
65
+ ```
66
+
67
+ Both examples use `OPENAI_API_KEY` and `OPENAI_MODEL`. The Agents bridge uses `exclusive_processor=False` to retain existing Agents tracing processors. Do not also instrument its underlying OpenAI client just for this integration. The direct-client example includes `stream_review`; consume it inside `contextlib.aclosing` so stopping early closes the operation without recording success.
68
+
69
+ Release tests pin OpenAI 3.31.0, Agents 0.24.0, OpenInference OpenAI 0.1.65 and OpenInference Agents 2.5.3. These remain optional dependencies. The [OpenAI guide](https://docs.usefluency.com/fluency-workflow-telemetry-openai) covers both [direct API calls](https://docs.usefluency.com/fluency-workflow-telemetry-openai#direct-api-calls) and the [Agents SDK](https://docs.usefluency.com/fluency-workflow-telemetry-openai#agents-sdk), each with new/existing tracing setup.
70
+
71
+ ## RPA and custom workflows
72
+
73
+ ```sh
74
+ python rpa.py
75
+ node rpa.ts
76
+ ```
77
+
78
+ These examples need no model credentials. A successful run produces `Post invoice` with `Submit to ERP` and `Invoice posted` children. The release test also injects an ERP failure and verifies that exceptions reach the caller and success is not recorded.
79
+
80
+ ## Other examples
81
+
82
+ Run `python <example>.py` or `node <example>.ts`. For `pydantic-agent.py`, also install `pydantic-ai-slim==2.55.0`; its `TestModel` makes no paid request.
83
+
84
+ [collector.yaml](collector.yaml) is a merge fragment for an existing Collector configuration. It has not been tested as a live Collector deployment.
85
+
86
+ ## Lifecycle and verification
87
+
88
+ Configure once at startup. Keep streaming work inside the operation's scope, await in-flight work, and shut down before a script exits. In an async Python service shutdown hook, use `await asyncio.to_thread(telemetry.shutdown)`. Reusable serverless processes should flush at invocation completion rather than permanently shutting down after each invocation.
89
+
90
+ The release smoke test installs the npm package, wheel, and source distribution in clean environments. It exercises native examples, helpers, LangChain, Vercel AI, OpenAI and RPA through a local OTLP receiver and decodes the requests to verify business context and parentage. Framework tests use real libraries with deterministic model responses; new and existing providers are exercised in separate processes. They also check that an existing exporter remains usable after Fluency shuts down.
91
+
92
+ The suite does not establish production receipt, live-model behavior, Collector deployment, Next.js deployment, OpenAI Agents streaming, or distributed HTTP/queue propagation. Use your Fluency contact to confirm a first run against your organization's regional endpoint.
@@ -0,0 +1,44 @@
1
+ """Capture concurrent agent actions and a handled failure using synthetic data."""
2
+
3
+ import asyncio
4
+
5
+ from fluency_workflow_telemetry import AgentEvents, configure
6
+
7
+
8
+ def check_policy(invoice_id):
9
+ if invoice_id == "invoice-b":
10
+ raise ValueError("Synthetic policy lookup failure")
11
+
12
+
13
+ async def main():
14
+ telemetry = configure(service_name="invoice-agent")
15
+ events = AgentEvents(telemetry.provider.get_tracer("invoice-example"))
16
+
17
+ async def review(invoice_id):
18
+ with events.context({"fluency.run.id": invoice_id}), events.span("Review invoice"):
19
+ await asyncio.sleep(0)
20
+ try:
21
+ with events.span("Check policy"):
22
+ check_policy(invoice_id)
23
+ except ValueError:
24
+ outcome = "escalated"
25
+ else:
26
+ outcome = "approved"
27
+ events.capture(
28
+ "Invoice decision",
29
+ attributes={
30
+ "fluency.artifact.source": "erp:demo:invoices",
31
+ "fluency.artifact.external_id": invoice_id,
32
+ "fluency.artifact.type": "invoice",
33
+ "fluency.outcome": outcome,
34
+ },
35
+ )
36
+
37
+ try:
38
+ await asyncio.gather(review("invoice-a"), review("invoice-b"))
39
+ finally:
40
+ await asyncio.to_thread(telemetry.shutdown)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ asyncio.run(main())