diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 0e829495..43dc19ca 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -28,7 +28,7 @@ jobs: virtualenvs-create: true virtualenvs-in-project: true - name: Install dependencies - run: poetry install --no-interaction + run: poetry install --no-interaction --all-extras - name: Generate Env File run: | diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 255c3fc9..f77282f0 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,7 +26,7 @@ jobs: virtualenvs-in-project: true - name: Install linting tools - run: poetry install + run: poetry install --all-extras - name: Run Black run: poetry run black . --check --verbose --diff --color diff --git a/examples/opentelemetry_instrumentation/client.py b/examples/opentelemetry_instrumentation/client.py new file mode 100644 index 00000000..554608c7 --- /dev/null +++ b/examples/opentelemetry_instrumentation/client.py @@ -0,0 +1,7 @@ +from dotenv import load_dotenv + +from hatchet_sdk import Hatchet + +load_dotenv() + +hatchet = Hatchet(debug=True) diff --git a/examples/opentelemetry_instrumentation/test_otel_instrumentation.py b/examples/opentelemetry_instrumentation/test_otel_instrumentation.py new file mode 100644 index 00000000..fa4e785d --- /dev/null +++ b/examples/opentelemetry_instrumentation/test_otel_instrumentation.py @@ -0,0 +1,60 @@ +import json + +import pytest +from opentelemetry.trace import NoOpTracerProvider + +from hatchet_sdk import Hatchet, Worker +from hatchet_sdk.clients.admin import TriggerWorkflowOptions +from hatchet_sdk.clients.events import PushEventOptions +from hatchet_sdk.opentelemetry.instrumentor import HatchetInstrumentor + +trace_provider = NoOpTracerProvider() + +instrumentor = HatchetInstrumentor(tracer_provider=trace_provider) +instrumentor.instrument() + +tracer = trace_provider.get_tracer(__name__) + + +def create_additional_metadata() -> dict[str, str]: + return instrumentor.inject_traceparent_into_metadata( + {"hello": "world"}, instrumentor.create_traceparent() + ) + + +def create_push_options() -> PushEventOptions: + return {"additional_metadata": create_additional_metadata()} + + +@pytest.mark.parametrize("worker", ["otel"], indirect=True) +def test_push_event(hatchet: Hatchet, worker: Worker) -> None: + key = "otel:event" + payload = {"test": "test"} + + with tracer.start_as_current_span("push_event"): + event = hatchet.event.push( + event_key=key, + payload=payload, + options=create_push_options(), + ) + + """Assert on `endswith` to ignore namespacing""" + assert event.key.endswith(key) + assert event.payload == json.dumps(payload) + + +@pytest.mark.skip("Failing in CI for unknown reason") +@pytest.mark.asyncio() +@pytest.mark.parametrize("worker", ["otel"], indirect=True) +async def test_run_workflow(aiohatchet: Hatchet, worker: Worker) -> None: + with tracer.start_as_current_span("run_workflow") as span: + workflow = aiohatchet.admin.run_workflow( + "OTelWorkflow", + {"test": "test"}, + options=TriggerWorkflowOptions( + additional_metadata=create_additional_metadata() + ), + ) + + with pytest.raises(Exception, match="Workflow Errors"): + await workflow.result() diff --git a/examples/opentelemetry_instrumentation/tracer.py b/examples/opentelemetry_instrumentation/tracer.py new file mode 100644 index 00000000..fb8b73f1 --- /dev/null +++ b/examples/opentelemetry_instrumentation/tracer.py @@ -0,0 +1,45 @@ +import os +from typing import cast + +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.trace import NoOpTracerProvider + +trace_provider: TracerProvider | NoOpTracerProvider + +if os.getenv("CI", "false") == "true": + trace_provider = NoOpTracerProvider() +else: + resource = Resource( + attributes={ + SERVICE_NAME: os.getenv("HATCHET_CLIENT_OTEL_SERVICE_NAME", "test-service") + } + ) + + headers = dict( + [ + cast( + tuple[str, str], + tuple( + os.getenv( + "HATCHET_CLIENT_OTEL_EXPORTER_OTLP_HEADERS", "foo=bar" + ).split("=") + ), + ) + ] + ) + + processor = BatchSpanProcessor( + OTLPSpanExporter( + endpoint=os.getenv( + "HATCHET_CLIENT_OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317" + ), + headers=headers, + ), + ) + + trace_provider = TracerProvider(resource=resource) + + trace_provider.add_span_processor(processor) diff --git a/examples/opentelemetry_instrumentation/triggers.py b/examples/opentelemetry_instrumentation/triggers.py new file mode 100644 index 00000000..3a7b8cb0 --- /dev/null +++ b/examples/opentelemetry_instrumentation/triggers.py @@ -0,0 +1,161 @@ +import asyncio + +from examples.opentelemetry_instrumentation.client import hatchet +from examples.opentelemetry_instrumentation.tracer import trace_provider +from hatchet_sdk.clients.admin import TriggerWorkflowOptions +from hatchet_sdk.clients.events import PushEventOptions +from hatchet_sdk.opentelemetry.instrumentor import HatchetInstrumentor + +instrumentor = HatchetInstrumentor(tracer_provider=trace_provider) +tracer = trace_provider.get_tracer(__name__) + + +def create_additional_metadata() -> dict[str, str]: + return instrumentor.inject_traceparent_into_metadata( + {"hello": "world"}, instrumentor.create_traceparent() + ) + + +def create_push_options() -> PushEventOptions: + return {"additional_metadata": create_additional_metadata()} + + +def push_event() -> None: + print("\npush_event") + with tracer.start_as_current_span("push_event") as span: + hatchet.event.push( + "otel:event", + {"test": "test"}, + options=create_push_options(), + ) + + +async def async_push_event() -> None: + print("\nasync_push_event") + with tracer.start_as_current_span("async_push_event") as span: + await hatchet.event.async_push( + "otel:event", {"test": "test"}, options=create_push_options() + ) + + +def bulk_push_event() -> None: + print("\nbulk_push_event") + with tracer.start_as_current_span("bulk_push_event") as span: + hatchet.event.bulk_push( + [ + { + "additional_metadata": create_additional_metadata(), + "key": "otel:event", + "payload": {"test": "test 1"}, + }, + { + "additional_metadata": create_additional_metadata(), + "key": "otel:event", + "payload": {"test": "test 2"}, + }, + ], + ) + + +async def async_bulk_push_event() -> None: + print("\nasync_bulk_push_event") + with tracer.start_as_current_span("bulk_push_event") as span: + await hatchet.event.async_bulk_push( + [ + { + "additional_metadata": create_additional_metadata(), + "key": "otel:event", + "payload": {"test": "test 1"}, + }, + { + "additional_metadata": create_additional_metadata(), + "key": "otel:event", + "payload": {"test": "test 2"}, + }, + ], + ) + + +def run_workflow() -> None: + print("\nrun_workflow") + with tracer.start_as_current_span("run_workflow") as span: + hatchet.admin.run_workflow( + "OTelWorkflow", + {"test": "test"}, + options=TriggerWorkflowOptions( + additional_metadata=create_additional_metadata() + ), + ) + + +async def async_run_workflow() -> None: + print("\nasync_run_workflow") + with tracer.start_as_current_span("async_run_workflow") as span: + await hatchet.admin.aio.run_workflow( + "OTelWorkflow", + {"test": "test"}, + options=TriggerWorkflowOptions( + additional_metadata=create_additional_metadata() + ), + ) + + +def run_workflows() -> None: + print("\nrun_workflows") + with tracer.start_as_current_span("run_workflows") as span: + hatchet.admin.run_workflows( + [ + { + "workflow_name": "OTelWorkflow", + "input": {"test": "test"}, + "options": TriggerWorkflowOptions( + additional_metadata=create_additional_metadata() + ), + }, + { + "workflow_name": "OTelWorkflow", + "input": {"test": "test 2"}, + "options": TriggerWorkflowOptions( + additional_metadata=create_additional_metadata() + ), + }, + ], + ) + + +async def async_run_workflows() -> None: + print("\nasync_run_workflows") + with tracer.start_as_current_span("async_run_workflows") as span: + await hatchet.admin.aio.run_workflows( + [ + { + "workflow_name": "OTelWorkflow", + "input": {"test": "test"}, + "options": TriggerWorkflowOptions( + additional_metadata=create_additional_metadata() + ), + }, + { + "workflow_name": "OTelWorkflow", + "input": {"test": "test 2"}, + "options": TriggerWorkflowOptions( + additional_metadata=create_additional_metadata() + ), + }, + ], + ) + + +async def main() -> None: + push_event() + await async_push_event() + bulk_push_event() + await async_bulk_push_event() + run_workflow() + # await async_run_workflow() + run_workflows() + # await async_run_workflows() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/opentelemetry_instrumentation/worker.py b/examples/opentelemetry_instrumentation/worker.py new file mode 100644 index 00000000..67e2c7ea --- /dev/null +++ b/examples/opentelemetry_instrumentation/worker.py @@ -0,0 +1,47 @@ +from examples.opentelemetry_instrumentation.client import hatchet +from examples.opentelemetry_instrumentation.tracer import trace_provider +from hatchet_sdk import Context +from hatchet_sdk.opentelemetry.instrumentor import HatchetInstrumentor + +HatchetInstrumentor( + tracer_provider=trace_provider, +).instrument() + + +@hatchet.workflow(on_events=["otel:event"]) +class OTelWorkflow: + @hatchet.step() + def your_spans_are_children_of_hatchet_span( + self, context: Context + ) -> dict[str, str]: + with trace_provider.get_tracer(__name__).start_as_current_span("step1"): + print("executed step") + return { + "foo": "bar", + } + + @hatchet.step() + def your_spans_are_still_children_of_hatchet_span(self, context: Context) -> None: + with trace_provider.get_tracer(__name__).start_as_current_span("step2"): + raise Exception("Manually instrumented step failed failed") + + @hatchet.step() + def this_step_is_still_instrumented(self, context: Context) -> dict[str, str]: + print("executed still-instrumented step") + return { + "still": "instrumented", + } + + @hatchet.step() + def this_step_is_also_still_instrumented(self, context: Context) -> None: + raise Exception("Still-instrumented step failed") + + +def main() -> None: + worker = hatchet.worker("otel-example-worker", max_runs=1) + worker.register_workflow(OTelWorkflow()) + worker.start() + + +if __name__ == "__main__": + main() diff --git a/hatchet_sdk/clients/admin.py b/hatchet_sdk/clients/admin.py index e3d345b1..18664cef 100644 --- a/hatchet_sdk/clients/admin.py +++ b/hatchet_sdk/clients/admin.py @@ -24,12 +24,6 @@ ) from hatchet_sdk.contracts.workflows_pb2_grpc import WorkflowServiceStub from hatchet_sdk.utils.serialization import flatten -from hatchet_sdk.utils.tracing import ( - create_carrier, - create_tracer, - inject_carrier_into_metadata, - parse_carrier_from_metadata, -) from hatchet_sdk.workflow_run import RunRef, WorkflowRunRef from ..loader import ClientConfig @@ -174,7 +168,6 @@ def __init__(self, config: ClientConfig): self.token = config.token self.listener_client = new_listener(config) self.namespace = config.namespace - self.otel_tracer = create_tracer(config=config) async def run( self, @@ -193,74 +186,46 @@ async def run( wrr.workflow_run_id, wrr.workflow_listener, wrr.workflow_run_event_listener ) + ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor @tenacity_retry async def run_workflow( self, workflow_name: str, input: any, options: TriggerWorkflowOptions = None ) -> WorkflowRunRef: - ctx = parse_carrier_from_metadata( - (options or {}).get("additional_metadata", {}) - ) + try: + if not self.pooled_workflow_listener: + self.pooled_workflow_listener = PooledWorkflowRunListener(self.config) - with self.otel_tracer.start_as_current_span( - f"hatchet.async_run_workflow.{workflow_name}", context=ctx - ) as span: - carrier = create_carrier() + namespace = self.namespace - try: - if not self.pooled_workflow_listener: - self.pooled_workflow_listener = PooledWorkflowRunListener( - self.config - ) - - namespace = self.namespace - - if ( - options is not None - and "namespace" in options - and options["namespace"] is not None - ): - namespace = options.pop("namespace") - - if namespace != "" and not workflow_name.startswith(self.namespace): - workflow_name = f"{namespace}{workflow_name}" - - if options is not None and "additional_metadata" in options: - options["additional_metadata"] = inject_carrier_into_metadata( - options["additional_metadata"], carrier - ) - span.set_attributes( - flatten( - options["additional_metadata"], parent_key="", separator="." - ) - ) - - request = self._prepare_workflow_request(workflow_name, input, options) - - span.add_event( - "Triggering workflow", attributes={"workflow_name": workflow_name} - ) + if ( + options is not None + and "namespace" in options + and options["namespace"] is not None + ): + namespace = options.pop("namespace") - resp: TriggerWorkflowResponse = await self.aio_client.TriggerWorkflow( - request, - metadata=get_metadata(self.token), - ) + if namespace != "" and not workflow_name.startswith(self.namespace): + workflow_name = f"{namespace}{workflow_name}" - span.add_event( - "Received workflow response", - attributes={"workflow_name": workflow_name}, - ) + request = self._prepare_workflow_request(workflow_name, input, options) - return WorkflowRunRef( - workflow_run_id=resp.workflow_run_id, - workflow_listener=self.pooled_workflow_listener, - workflow_run_event_listener=self.listener_client, - ) - except (grpc.RpcError, grpc.aio.AioRpcError) as e: - if e.code() == grpc.StatusCode.ALREADY_EXISTS: - raise DedupeViolationErr(e.details()) + resp: TriggerWorkflowResponse = await self.aio_client.TriggerWorkflow( + request, + metadata=get_metadata(self.token), + ) - raise e + return WorkflowRunRef( + workflow_run_id=resp.workflow_run_id, + workflow_listener=self.pooled_workflow_listener, + workflow_run_event_listener=self.listener_client, + ) + except (grpc.RpcError, grpc.aio.AioRpcError) as e: + if e.code() == grpc.StatusCode.ALREADY_EXISTS: + raise DedupeViolationErr(e.details()) + + raise e + ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor @tenacity_retry async def run_workflows( self, @@ -389,7 +354,6 @@ def __init__(self, config: ClientConfig): self.token = config.token self.listener_client = new_listener(config) self.namespace = config.namespace - self.otel_tracer = create_tracer(config=config) @tenacity_retry def put_workflow( @@ -459,78 +423,47 @@ def schedule_workflow( raise e - ## TODO: `options` is treated as a dict (wrong type hint) - ## TODO: `any` type hint should come from `typing` + ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor @tenacity_retry def run_workflow( self, workflow_name: str, input: any, options: TriggerWorkflowOptions = None ) -> WorkflowRunRef: - ctx = parse_carrier_from_metadata( - (options or {}).get("additional_metadata", {}) - ) + try: + if not self.pooled_workflow_listener: + self.pooled_workflow_listener = PooledWorkflowRunListener(self.config) - with self.otel_tracer.start_as_current_span( - f"hatchet.run_workflow.{workflow_name}", context=ctx - ) as span: - carrier = create_carrier() + namespace = self.namespace - try: - if not self.pooled_workflow_listener: - self.pooled_workflow_listener = PooledWorkflowRunListener( - self.config - ) - - namespace = self.namespace - - ## TODO: Factor this out - it's repeated a lot of places - if ( - options is not None - and "namespace" in options - and options["namespace"] is not None - ): - namespace = options.pop("namespace") - - if options is not None and "additional_metadata" in options: - options["additional_metadata"] = inject_carrier_into_metadata( - options["additional_metadata"], carrier - ) - - span.set_attributes( - flatten( - options["additional_metadata"], parent_key="", separator="." - ) - ) - - if namespace != "" and not workflow_name.startswith(self.namespace): - workflow_name = f"{namespace}{workflow_name}" - - request = self._prepare_workflow_request(workflow_name, input, options) - - span.add_event( - "Triggering workflow", attributes={"workflow_name": workflow_name} - ) + ## TODO: Factor this out - it's repeated a lot of places + if ( + options is not None + and "namespace" in options + and options["namespace"] is not None + ): + namespace = options.pop("namespace") - resp: TriggerWorkflowResponse = self.client.TriggerWorkflow( - request, - metadata=get_metadata(self.token), - ) + if namespace != "" and not workflow_name.startswith(self.namespace): + workflow_name = f"{namespace}{workflow_name}" - span.add_event( - "Received workflow response", - attributes={"workflow_name": workflow_name}, - ) + request = self._prepare_workflow_request(workflow_name, input, options) - return WorkflowRunRef( - workflow_run_id=resp.workflow_run_id, - workflow_listener=self.pooled_workflow_listener, - workflow_run_event_listener=self.listener_client, - ) - except (grpc.RpcError, grpc.aio.AioRpcError) as e: - if e.code() == grpc.StatusCode.ALREADY_EXISTS: - raise DedupeViolationErr(e.details()) + resp: TriggerWorkflowResponse = self.client.TriggerWorkflow( + request, + metadata=get_metadata(self.token), + ) + + return WorkflowRunRef( + workflow_run_id=resp.workflow_run_id, + workflow_listener=self.pooled_workflow_listener, + workflow_run_event_listener=self.listener_client, + ) + except (grpc.RpcError, grpc.aio.AioRpcError) as e: + if e.code() == grpc.StatusCode.ALREADY_EXISTS: + raise DedupeViolationErr(e.details()) - raise e + raise e + ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor @tenacity_retry def run_workflows( self, workflows: List[WorkflowRunDict], options: TriggerWorkflowOptions = None diff --git a/hatchet_sdk/clients/dispatcher/action_listener.py b/hatchet_sdk/clients/dispatcher/action_listener.py index fc2887bd..f5d20e8e 100644 --- a/hatchet_sdk/clients/dispatcher/action_listener.py +++ b/hatchet_sdk/clients/dispatcher/action_listener.py @@ -90,27 +90,24 @@ def __post_init__(self): self.additional_metadata = {} @property - def otel_attributes(self) -> dict[str, Any]: - return flatten( - xs={ - "worker_id": self.worker_id, - "tenant_id": self.tenant_id, - "workflow_run_id": self.workflow_run_id, - "get_group_key_run_id": self.get_group_key_run_id, - "job_id": self.job_id, - "job_name": self.job_name, - "job_run_id": self.job_run_id, - "step_id": self.step_id, - "step_run_id": self.step_run_id, - "retry_count": self.retry_count, - "child_workflow_index": self.child_workflow_index, - "child_workflow_key": self.child_workflow_key, - "parent_workflow_run_id": self.parent_workflow_run_id, - "action_payload": self.action_payload, - }, - parent_key="", - separator=".", - ) + def otel_attributes(self) -> dict[str, str | int]: + attrs: dict[str, str | int | None] = { + "hatchet.tenant_id": self.tenant_id, + "hatchet.worker_id": self.worker_id, + "hatchet.workflow_run_id": self.workflow_run_id, + "hatchet.step_id": self.step_id, + "hatchet.step_run_id": self.step_run_id, + "hatchet.retry_count": self.retry_count, + "hatchet.parent_workflow_run_id": self.parent_workflow_run_id, + "hatchet.child_workflow_index": self.child_workflow_index, + "hatchet.child_workflow_key": self.child_workflow_key, + "hatchet.action_payload": self.action_payload, + "hatchet.workflow_name": self.job_name, + "hatchet.action_name": self.action_id, + "hatchet.get_group_key_run_id": self.get_group_key_run_id, + } + + return {k: v for k, v in attrs.items() if v} START_STEP_RUN = 0 diff --git a/hatchet_sdk/clients/events.py b/hatchet_sdk/clients/events.py index 160b780e..cf6a2721 100644 --- a/hatchet_sdk/clients/events.py +++ b/hatchet_sdk/clients/events.py @@ -17,12 +17,6 @@ ) from hatchet_sdk.contracts.events_pb2_grpc import EventsServiceStub from hatchet_sdk.utils.serialization import flatten -from hatchet_sdk.utils.tracing import ( - create_carrier, - create_tracer, - inject_carrier_into_metadata, - parse_carrier_from_metadata, -) from ..loader import ClientConfig from ..metadata import get_metadata @@ -63,7 +57,6 @@ def __init__(self, client: EventsServiceStub, config: ClientConfig): self.client = client self.token = config.token self.namespace = config.namespace - self.otel_tracer = create_tracer(config=config) async def async_push( self, event_key, payload, options: Optional[PushEventOptions] = None @@ -79,54 +72,41 @@ async def async_bulk_push( ) -> List[Event]: return await asyncio.to_thread(self.bulk_push, events=events, options=options) + ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor @tenacity_retry def push(self, event_key, payload, options: PushEventOptions = None) -> Event: - ctx = parse_carrier_from_metadata( - (options or {}).get("additional_metadata", {}) - ) - - with self.otel_tracer.start_as_current_span( - "hatchet.push", context=ctx - ) as span: - carrier = create_carrier() - namespace = self.namespace - - if ( - options is not None - and "namespace" in options - and options["namespace"] is not None - ): - namespace = options.pop("namespace") - - namespaced_event_key = namespace + event_key - - try: - meta = inject_carrier_into_metadata( - dict() if options is None else options["additional_metadata"], - carrier, - ) - meta_bytes = None if meta is None else json.dumps(meta).encode("utf-8") - except Exception as e: - raise ValueError(f"Error encoding meta: {e}") + namespace = self.namespace - span.set_attributes(flatten(meta, parent_key="", separator=".")) + if ( + options is not None + and "namespace" in options + and options["namespace"] is not None + ): + namespace = options.pop("namespace") - try: - payload_bytes = json.dumps(payload).encode("utf-8") - except json.UnicodeEncodeError as e: - raise ValueError(f"Error encoding payload: {e}") + namespaced_event_key = namespace + event_key - request = PushEventRequest( - key=namespaced_event_key, - payload=payload_bytes, - eventTimestamp=proto_timestamp_now(), - additionalMetadata=meta_bytes, - ) + try: + meta = dict() if options is None else options["additional_metadata"] + meta_bytes = None if meta is None else json.dumps(meta).encode("utf-8") + except Exception as e: + raise ValueError(f"Error encoding meta: {e}") - span.add_event("Pushing event", attributes={"key": namespaced_event_key}) + try: + payload_bytes = json.dumps(payload).encode("utf-8") + except json.UnicodeEncodeError as e: + raise ValueError(f"Error encoding payload: {e}") + + request = PushEventRequest( + key=namespaced_event_key, + payload=payload_bytes, + eventTimestamp=proto_timestamp_now(), + additionalMetadata=meta_bytes, + ) - return self.client.Push(request, metadata=get_metadata(self.token)) + return self.client.Push(request, metadata=get_metadata(self.token)) + ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor @tenacity_retry def bulk_push( self, @@ -134,10 +114,6 @@ def bulk_push( options: BulkPushEventOptions = None, ) -> List[Event]: namespace = self.namespace - bulk_push_correlation_id = uuid4() - ctx = parse_carrier_from_metadata( - (options or {}).get("additional_metadata", {}) - ) if ( options is not None @@ -148,43 +124,30 @@ def bulk_push( bulk_events = [] for event in events: - with self.otel_tracer.start_as_current_span( - "hatchet.bulk_push", context=ctx - ) as span: - carrier = create_carrier() - span.set_attribute( - "bulk_push_correlation_id", str(bulk_push_correlation_id) - ) - - event_key = namespace + event["key"] - payload = event["payload"] - - try: - meta = inject_carrier_into_metadata( - event.get("additional_metadata", {}), carrier - ) - meta_bytes = json.dumps(meta).encode("utf-8") if meta else None - except Exception as e: - raise ValueError(f"Error encoding meta: {e}") - - span.set_attributes(flatten(meta, parent_key="", separator=".")) - - try: - payload_bytes = json.dumps(payload).encode("utf-8") - except json.UnicodeEncodeError as e: - raise ValueError(f"Error encoding payload: {e}") - - request = PushEventRequest( - key=event_key, - payload=payload_bytes, - eventTimestamp=proto_timestamp_now(), - additionalMetadata=meta_bytes, - ) - bulk_events.append(request) + event_key = namespace + event["key"] + payload = event["payload"] + + try: + meta = event.get("additional_metadata", {}) + meta_bytes = json.dumps(meta).encode("utf-8") if meta else None + except Exception as e: + raise ValueError(f"Error encoding meta: {e}") + + try: + payload_bytes = json.dumps(payload).encode("utf-8") + except json.UnicodeEncodeError as e: + raise ValueError(f"Error encoding payload: {e}") + + request = PushEventRequest( + key=event_key, + payload=payload_bytes, + eventTimestamp=proto_timestamp_now(), + additionalMetadata=meta_bytes, + ) + bulk_events.append(request) bulk_request = BulkPushEventRequest(events=bulk_events) - span.add_event("Pushing bulk events") response = self.client.BulkPush(bulk_request, metadata=get_metadata(self.token)) return response.events diff --git a/hatchet_sdk/loader.py b/hatchet_sdk/loader.py index 38b0b2bf..6d8884d6 100644 --- a/hatchet_sdk/loader.py +++ b/hatchet_sdk/loader.py @@ -1,7 +1,7 @@ -import json import os from logging import Logger, getLogger from typing import Dict, Optional +from warnings import warn import yaml @@ -39,10 +39,6 @@ def __init__( logger: Logger = None, grpc_max_recv_message_length: int = 4 * 1024 * 1024, # 4MB grpc_max_send_message_length: int = 4 * 1024 * 1024, # 4MB - otel_exporter_oltp_endpoint: str | None = None, - otel_service_name: str | None = None, - otel_exporter_oltp_headers: dict[str, str] | None = None, - otel_exporter_oltp_protocol: str | None = None, worker_healthcheck_port: int | None = None, worker_healthcheck_enabled: bool | None = None, worker_preset_labels: dict[str, str] = {}, @@ -56,10 +52,6 @@ def __init__( self.logInterceptor = logger self.grpc_max_recv_message_length = grpc_max_recv_message_length self.grpc_max_send_message_length = grpc_max_send_message_length - self.otel_exporter_oltp_endpoint = otel_exporter_oltp_endpoint - self.otel_service_name = otel_service_name - self.otel_exporter_oltp_headers = otel_exporter_oltp_headers - self.otel_exporter_oltp_protocol = otel_exporter_oltp_protocol self.worker_healthcheck_port = worker_healthcheck_port self.worker_healthcheck_enabled = worker_healthcheck_enabled self.worker_preset_labels = worker_preset_labels @@ -142,33 +134,6 @@ def get_config_value(key, env_var): tls_config = self._load_tls_config(config_data["tls"], host_port) - otel_exporter_oltp_endpoint = get_config_value( - "otel_exporter_oltp_endpoint", "HATCHET_CLIENT_OTEL_EXPORTER_OTLP_ENDPOINT" - ) - - otel_service_name = get_config_value( - "otel_service_name", "HATCHET_CLIENT_OTEL_SERVICE_NAME" - ) - - _oltp_headers = get_config_value( - "otel_exporter_oltp_headers", "HATCHET_CLIENT_OTEL_EXPORTER_OTLP_HEADERS" - ) - - if _oltp_headers: - try: - otel_header_key, api_key = _oltp_headers.split("=", maxsplit=1) - otel_exporter_oltp_headers = {otel_header_key: api_key} - except ValueError: - raise ValueError( - "HATCHET_CLIENT_OTEL_EXPORTER_OTLP_HEADERS must be in the format `key=value`" - ) - else: - otel_exporter_oltp_headers = None - - otel_exporter_oltp_protocol = get_config_value( - "otel_exporter_oltp_protocol", "HATCHET_CLIENT_OTEL_EXPORTER_OTLP_PROTOCOL" - ) - worker_healthcheck_port = int( get_config_value( "worker_healthcheck_port", "HATCHET_CLIENT_WORKER_HEALTHCHECK_PORT" @@ -196,6 +161,19 @@ def get_config_value(key, env_var): if autoscaling_target: worker_preset_labels["hatchet-autoscaling-target"] = autoscaling_target + legacy_otlp_headers = get_config_value( + "otel_exporter_otlp_endpoint", "HATCHET_CLIENT_OTEL_EXPORTER_OTLP_ENDPOINT" + ) + + legacy_otlp_headers = get_config_value( + "otel_exporter_otlp_headers", "HATCHET_CLIENT_OTEL_EXPORTER_OTLP_HEADERS" + ) + + if legacy_otlp_headers or legacy_otlp_headers: + warn( + "The `otel_exporter_otlp_*` fields are no longer supported as of SDK version `0.46.0`. Please see the documentation on OpenTelemetry at https://docs.hatchet.run/home/features/opentelemetry for more information on how to migrate to the new `HatchetInstrumentor`." + ) + return ClientConfig( tenant_id=tenant_id, tls_config=tls_config, @@ -207,10 +185,6 @@ def get_config_value(key, env_var): logger=defaults.logInterceptor, grpc_max_recv_message_length=grpc_max_recv_message_length, grpc_max_send_message_length=grpc_max_send_message_length, - otel_exporter_oltp_endpoint=otel_exporter_oltp_endpoint, - otel_service_name=otel_service_name, - otel_exporter_oltp_headers=otel_exporter_oltp_headers, - otel_exporter_oltp_protocol=otel_exporter_oltp_protocol, worker_healthcheck_port=worker_healthcheck_port, worker_healthcheck_enabled=worker_healthcheck_enabled, worker_preset_labels=worker_preset_labels, diff --git a/hatchet_sdk/opentelemetry/instrumentor.py b/hatchet_sdk/opentelemetry/instrumentor.py new file mode 100644 index 00000000..d13e3fc5 --- /dev/null +++ b/hatchet_sdk/opentelemetry/instrumentor.py @@ -0,0 +1,323 @@ +from importlib.metadata import version +from typing import Any, Callable, Collection, Coroutine + +try: + from opentelemetry.context import Context + from opentelemetry.instrumentation.instrumentor import ( # type: ignore[attr-defined] + BaseInstrumentor, + ) + from opentelemetry.instrumentation.utils import unwrap + from opentelemetry.metrics import MeterProvider, NoOpMeterProvider, get_meter + from opentelemetry.trace import ( + NoOpTracerProvider, + StatusCode, + TracerProvider, + get_tracer, + ) + from opentelemetry.trace.propagation.tracecontext import ( + TraceContextTextMapPropagator, + ) + from wrapt import wrap_function_wrapper # type: ignore[import-untyped] +except (RuntimeError, ImportError, ModuleNotFoundError): + raise ModuleNotFoundError( + "To use the HatchetInstrumentor, you must install Hatchet's `otel` extra using (e.g.) `pip install hatchet-sdk[otel]`" + ) + +import hatchet_sdk +from hatchet_sdk.clients.admin import ( + AdminClient, + TriggerWorkflowOptions, + WorkflowRunDict, +) +from hatchet_sdk.clients.dispatcher.action_listener import Action +from hatchet_sdk.clients.events import ( + BulkPushEventWithMetadata, + EventClient, + PushEventOptions, +) +from hatchet_sdk.contracts.events_pb2 import Event +from hatchet_sdk.worker.runner.runner import Runner +from hatchet_sdk.workflow_run import WorkflowRunRef + +hatchet_sdk_version = version("hatchet-sdk") + +InstrumentKwargs = TracerProvider | MeterProvider | None + + +class HatchetInstrumentor(BaseInstrumentor): # type: ignore[misc] + OTEL_TRACEPARENT_KEY = "traceparent" + + def __init__( + self, + tracer_provider: TracerProvider, + meter_provider: MeterProvider = NoOpMeterProvider(), + ): + self.tracer_provider = tracer_provider + self.meter_provider = meter_provider + + super().__init__() + + def create_traceparent(self) -> str | None: + carrier: dict[str, str] = {} + TraceContextTextMapPropagator().inject(carrier) + + return carrier.get("traceparent") + + def parse_carrier_from_metadata( + self, metadata: dict[str, str] | None + ) -> Context | None: + if not metadata: + return None + + traceparent = metadata.get(self.OTEL_TRACEPARENT_KEY) + + if not traceparent: + return None + + return TraceContextTextMapPropagator().extract( + {self.OTEL_TRACEPARENT_KEY: traceparent} + ) + + def inject_traceparent_into_metadata( + self, metadata: dict[str, str], traceparent: str | None + ) -> dict[str, str]: + if traceparent: + metadata[self.OTEL_TRACEPARENT_KEY] = traceparent + + return metadata + + def instrumentation_dependencies(self) -> Collection[str]: + return tuple() + + def _instrument(self, **kwargs: InstrumentKwargs) -> None: + self._tracer = get_tracer(__name__, hatchet_sdk_version, self.tracer_provider) + self._meter = get_meter(__name__, hatchet_sdk_version, self.meter_provider) + + wrap_function_wrapper( + hatchet_sdk, + "worker.runner.runner.Runner.handle_start_step_run", + self._wrap_handle_start_step_run, + ) + wrap_function_wrapper( + hatchet_sdk, + "worker.runner.runner.Runner.handle_start_group_key_run", + self._wrap_handle_get_group_key_run, + ) + wrap_function_wrapper( + hatchet_sdk, + "worker.runner.runner.Runner.handle_cancel_action", + self._wrap_handle_cancel_action, + ) + + wrap_function_wrapper( + hatchet_sdk, + "clients.events.EventClient.push", + self._wrap_push_event, + ) + + wrap_function_wrapper( + hatchet_sdk, + "clients.events.EventClient.bulk_push", + self._wrap_bulk_push_event, + ) + + wrap_function_wrapper( + hatchet_sdk, + "clients.admin.AdminClient.run_workflow", + self._wrap_run_workflow, + ) + + wrap_function_wrapper( + hatchet_sdk, + "clients.admin.AdminClientAioImpl.run_workflow", + self._wrap_async_run_workflow, + ) + + wrap_function_wrapper( + hatchet_sdk, + "clients.admin.AdminClient.run_workflows", + self._wrap_run_workflows, + ) + + wrap_function_wrapper( + hatchet_sdk, + "clients.admin.AdminClientAioImpl.run_workflows", + self._wrap_async_run_workflows, + ) + + ## IMPORTANT: Keep these types in sync with the wrapped method's signature + async def _wrap_handle_start_step_run( + self, + wrapped: Callable[[Action], Coroutine[None, None, Exception | None]], + instance: Runner, + args: tuple[Action], + kwargs: Any, + ) -> Exception | None: + action = args[0] + traceparent = self.parse_carrier_from_metadata(action.additional_metadata) + + with self._tracer.start_as_current_span( + "hatchet.start_step_run", + attributes=action.otel_attributes, + context=traceparent, + ) as span: + result = await wrapped(*args, **kwargs) + + if isinstance(result, Exception): + span.set_status(StatusCode.ERROR, str(result)) + + return result + + ## IMPORTANT: Keep these types in sync with the wrapped method's signature + async def _wrap_handle_get_group_key_run( + self, + wrapped: Callable[[Action], Coroutine[None, None, Exception | None]], + instance: Runner, + args: tuple[Action], + kwargs: Any, + ) -> Exception | None: + action = args[0] + + with self._tracer.start_as_current_span( + "hatchet.get_group_key_run", + attributes=action.otel_attributes, + ) as span: + result = await wrapped(*args, **kwargs) + + if isinstance(result, Exception): + span.set_status(StatusCode.ERROR, str(result)) + + return result + + ## IMPORTANT: Keep these types in sync with the wrapped method's signature + async def _wrap_handle_cancel_action( + self, + wrapped: Callable[[str], Coroutine[None, None, Exception | None]], + instance: Runner, + args: tuple[str], + kwargs: Any, + ) -> Exception | None: + step_run_id = args[0] + + with self._tracer.start_as_current_span( + "hatchet.cancel_step_run", + attributes={ + "hatchet.step_run_id": step_run_id, + }, + ): + return await wrapped(*args, **kwargs) + + ## IMPORTANT: Keep these types in sync with the wrapped method's signature + def _wrap_push_event( + self, + wrapped: Callable[[str, dict[str, Any], PushEventOptions | None], Event], + instance: EventClient, + args: tuple[ + str, + dict[str, Any], + PushEventOptions | None, + ], + kwargs: dict[str, str | dict[str, Any] | PushEventOptions | None], + ) -> Event: + with self._tracer.start_as_current_span( + "hatchet.push_event", + ): + return wrapped(*args, **kwargs) + + ## IMPORTANT: Keep these types in sync with the wrapped method's signature + def _wrap_bulk_push_event( + self, + wrapped: Callable[ + [list[BulkPushEventWithMetadata], PushEventOptions | None], list[Event] + ], + instance: EventClient, + args: tuple[ + list[BulkPushEventWithMetadata], + PushEventOptions | None, + ], + kwargs: dict[str, list[BulkPushEventWithMetadata] | PushEventOptions | None], + ) -> list[Event]: + with self._tracer.start_as_current_span( + "hatchet.bulk_push_event", + ): + return wrapped(*args, **kwargs) + + ## IMPORTANT: Keep these types in sync with the wrapped method's signature + def _wrap_run_workflow( + self, + wrapped: Callable[[str, Any, TriggerWorkflowOptions | None], WorkflowRunRef], + instance: AdminClient, + args: tuple[str, Any, TriggerWorkflowOptions | None], + kwargs: dict[str, str | Any | TriggerWorkflowOptions | None], + ) -> WorkflowRunRef: + with self._tracer.start_as_current_span( + "hatchet.run_workflow", + ): + return wrapped(*args, **kwargs) + + ## IMPORTANT: Keep these types in sync with the wrapped method's signature + async def _wrap_async_run_workflow( + self, + wrapped: Callable[ + [str, Any, TriggerWorkflowOptions | None], + Coroutine[None, None, WorkflowRunRef], + ], + instance: AdminClient, + args: tuple[str, Any, TriggerWorkflowOptions | None], + kwargs: dict[str, str | Any | TriggerWorkflowOptions | None], + ) -> WorkflowRunRef: + with self._tracer.start_as_current_span( + "hatchet.run_workflow", + ): + return await wrapped(*args, **kwargs) + + ## IMPORTANT: Keep these types in sync with the wrapped method's signature + def _wrap_run_workflows( + self, + wrapped: Callable[ + [list[WorkflowRunDict], TriggerWorkflowOptions | None], list[WorkflowRunRef] + ], + instance: AdminClient, + args: tuple[ + list[WorkflowRunDict], + TriggerWorkflowOptions | None, + ], + kwargs: dict[str, list[WorkflowRunDict] | TriggerWorkflowOptions | None], + ) -> list[WorkflowRunRef]: + with self._tracer.start_as_current_span( + "hatchet.run_workflows", + ): + return wrapped(*args, **kwargs) + + ## IMPORTANT: Keep these types in sync with the wrapped method's signature + async def _wrap_async_run_workflows( + self, + wrapped: Callable[ + [list[WorkflowRunDict], TriggerWorkflowOptions | None], + Coroutine[None, None, list[WorkflowRunRef]], + ], + instance: AdminClient, + args: tuple[ + list[WorkflowRunDict], + TriggerWorkflowOptions | None, + ], + kwargs: dict[str, list[WorkflowRunDict] | TriggerWorkflowOptions | None], + ) -> list[WorkflowRunRef]: + with self._tracer.start_as_current_span( + "hatchet.run_workflows", + ): + return await wrapped(*args, **kwargs) + + def _uninstrument(self, **kwargs: InstrumentKwargs) -> None: + self.tracer_provider = NoOpTracerProvider() + self.meter_provider = NoOpMeterProvider() + + unwrap(hatchet_sdk, "worker.runner.runner.Runner.handle_start_step_run") + unwrap(hatchet_sdk, "worker.runner.runner.Runner.handle_start_group_key_run") + unwrap(hatchet_sdk, "worker.runner.runner.Runner.handle_cancel_action") + unwrap(hatchet_sdk, "clients.events.EventClient.push") + unwrap(hatchet_sdk, "clients.events.EventClient.bulk_push") + unwrap(hatchet_sdk, "clients.admin.AdminClient.run_workflow") + unwrap(hatchet_sdk, "clients.admin.AdminClientAioImpl.run_workflow") + unwrap(hatchet_sdk, "clients.admin.AdminClient.run_workflows") + unwrap(hatchet_sdk, "clients.admin.AdminClientAioImpl.run_workflows") diff --git a/hatchet_sdk/utils/tracing.py b/hatchet_sdk/utils/tracing.py deleted file mode 100644 index afc398f7..00000000 --- a/hatchet_sdk/utils/tracing.py +++ /dev/null @@ -1,70 +0,0 @@ -import json -from functools import cache -from typing import Any - -from opentelemetry import trace -from opentelemetry.context import Context -from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.trace import NoOpTracerProvider, Tracer -from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator - -from hatchet_sdk.loader import ClientConfig - -OTEL_CARRIER_KEY = "__otel_carrier" - - -@cache -def create_tracer(config: ClientConfig) -> Tracer: - ## TODO: Figure out how to specify protocol here - resource = Resource( - attributes={SERVICE_NAME: config.otel_service_name or "hatchet.run"} - ) - - if config.otel_exporter_oltp_endpoint and config.otel_exporter_oltp_headers: - processor = BatchSpanProcessor( - OTLPSpanExporter( - endpoint=config.otel_exporter_oltp_endpoint, - headers=config.otel_exporter_oltp_headers, - ), - ) - - ## If tracer provider is already set, we don't need to override it - if not isinstance(trace.get_tracer_provider(), TracerProvider): - trace_provider = TracerProvider(resource=resource) - trace_provider.add_span_processor(processor) - trace.set_tracer_provider(trace_provider) - else: - if not isinstance(trace.get_tracer_provider(), NoOpTracerProvider): - trace.set_tracer_provider(NoOpTracerProvider()) - - return trace.get_tracer(__name__) - - -def create_carrier() -> dict[str, str]: - carrier: dict[str, str] = {} - TraceContextTextMapPropagator().inject(carrier) - - return carrier - - -def inject_carrier_into_metadata( - metadata: dict[Any, Any], carrier: dict[str, str] -) -> dict[Any, Any]: - if carrier: - metadata[OTEL_CARRIER_KEY] = carrier - - return metadata - - -def parse_carrier_from_metadata(metadata: dict[str, Any] | None) -> Context | None: - if not metadata: - return None - - return ( - TraceContextTextMapPropagator().extract(_ctx) - if (_ctx := metadata.get(OTEL_CARRIER_KEY)) - else None - ) diff --git a/hatchet_sdk/worker/runner/runner.py b/hatchet_sdk/worker/runner/runner.py index f72fb04b..6e27edd3 100644 --- a/hatchet_sdk/worker/runner/runner.py +++ b/hatchet_sdk/worker/runner/runner.py @@ -10,7 +10,6 @@ from threading import Thread, current_thread from typing import Any, Callable, Dict, Literal, Type, TypeVar, cast, overload -from opentelemetry.trace import StatusCode from pydantic import BaseModel from hatchet_sdk.client import new_client_raw @@ -32,7 +31,6 @@ ) from hatchet_sdk.loader import ClientConfig from hatchet_sdk.logger import logger -from hatchet_sdk.utils.tracing import create_tracer, parse_carrier_from_metadata from hatchet_sdk.utils.types import WorkflowValidator from hatchet_sdk.v2.callable import DurableContext from hatchet_sdk.worker.action_listener_process import ActionEvent @@ -88,43 +86,29 @@ def __init__( labels=labels, client=new_client_raw(config).dispatcher ) - self.otel_tracer = create_tracer(config=config) - def create_workflow_run_url(self, action: Action) -> str: return f"{self.config.server_url}/workflow-runs/{action.workflow_run_id}?tenant={action.tenant_id}" def run(self, action: Action) -> None: - ctx = parse_carrier_from_metadata(action.additional_metadata) - - with self.otel_tracer.start_as_current_span( - f"hatchet.worker.run.{action.step_id}", context=ctx - ) as span: - if self.worker_context.id() is None: - self.worker_context._worker_id = action.worker_id - - span.set_attributes(action.otel_attributes) - span.set_attribute("workflow_run_url", self.create_workflow_run_url(action)) - - match action.action_type: - case ActionType.START_STEP_RUN: - log = f"run: start step: {action.action_id}/{action.step_run_id}" - span.add_event(log) - logger.info(log) - asyncio.create_task(self.handle_start_step_run(action)) - case ActionType.CANCEL_STEP_RUN: - log = f"cancel: step run: {action.action_id}/{action.step_run_id}" - span.add_event(log) - logger.info(log) - asyncio.create_task(self.handle_cancel_action(action.step_run_id)) - case ActionType.START_GET_GROUP_KEY: - log = f"run: get group key: {action.action_id}/{action.get_group_key_run_id}" - span.add_event(log) - logger.info(log) - asyncio.create_task(self.handle_start_group_key_run(action)) - case _: - log = f"unknown action type: {action.action_type}" - span.add_event(log) - logger.error(log) + if self.worker_context.id() is None: + self.worker_context._worker_id = action.worker_id + + match action.action_type: + case ActionType.START_STEP_RUN: + log = f"run: start step: {action.action_id}/{action.step_run_id}" + logger.info(log) + asyncio.create_task(self.handle_start_step_run(action)) + case ActionType.CANCEL_STEP_RUN: + log = f"cancel: step run: {action.action_id}/{action.step_run_id}" + logger.info(log) + asyncio.create_task(self.handle_cancel_action(action.step_run_id)) + case ActionType.START_GET_GROUP_KEY: + log = f"run: get group key: {action.action_id}/{action.get_group_key_run_id}" + logger.info(log) + asyncio.create_task(self.handle_start_group_key_run(action)) + case _: + log = f"unknown action type: {action.action_type}" + logger.error(log) def step_run_callback(self, action: Action) -> Callable[[asyncio.Task[Any]], None]: def inner_callback(task: asyncio.Task[Any]) -> None: @@ -306,101 +290,87 @@ def create_context( validator_registry=self.validator_registry, ) - async def handle_start_step_run(self, action: Action) -> None: - with self.otel_tracer.start_as_current_span( - f"hatchet.worker.handle_start_step_run.{action.step_id}", - ) as span: - span.add_event("Starting step run") - span.set_attributes(action.otel_attributes) - - action_name = action.action_id + ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor + async def handle_start_step_run(self, action: Action) -> None | Exception: + action_name = action.action_id - # Find the corresponding action function from the registry - action_func = self.action_registry.get(action_name) + # Find the corresponding action function from the registry + action_func = self.action_registry.get(action_name) - context = self.create_context(action, action_func) + context = self.create_context(action, action_func) - self.contexts[action.step_run_id] = context + self.contexts[action.step_run_id] = context - if action_func: - self.event_queue.put( - ActionEvent( - action=action, - type=STEP_EVENT_TYPE_STARTED, - ) + if action_func: + self.event_queue.put( + ActionEvent( + action=action, + type=STEP_EVENT_TYPE_STARTED, ) + ) - loop = asyncio.get_event_loop() - task = loop.create_task( - self.async_wrapped_action_func( - context, action_func, action, action.step_run_id - ) + loop = asyncio.get_event_loop() + task = loop.create_task( + self.async_wrapped_action_func( + context, action_func, action, action.step_run_id ) - - task.add_done_callback(self.step_run_callback(action)) - self.tasks[action.step_run_id] = task - - try: - await task - span.set_status(StatusCode.OK) - except Exception as e: - # do nothing, this should be caught in the callback - span.set_status(StatusCode.ERROR) - span.record_exception(e) - - span.add_event("Finished step run") - - async def handle_start_group_key_run(self, action: Action) -> None: - with self.otel_tracer.start_as_current_span( - f"hatchet.worker.handle_start_step_run.{action.step_id}" - ) as span: - span.add_event("Starting group key run") - action_name = action.action_id - context = Context( - action, - self.dispatcher_client, - self.admin_client, - self.client.event, - self.client.rest, - self.client.workflow_listener, - self.workflow_run_event_listener, - self.worker_context, - self.client.config.namespace, ) - self.contexts[action.get_group_key_run_id] = context + task.add_done_callback(self.step_run_callback(action)) + self.tasks[action.step_run_id] = task - # Find the corresponding action function from the registry - action_func = self.action_registry.get(action_name) + try: + await task + except Exception as e: + return e - if action_func: - # send an event that the group key run has started - self.event_queue.put( - ActionEvent( - action=action, - type=GROUP_KEY_EVENT_TYPE_STARTED, - ) + return None + + ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor + async def handle_start_group_key_run(self, action: Action) -> Exception | None: + action_name = action.action_id + context = Context( + action, + self.dispatcher_client, + self.admin_client, + self.client.event, + self.client.rest, + self.client.workflow_listener, + self.workflow_run_event_listener, + self.worker_context, + self.client.config.namespace, + ) + + self.contexts[action.get_group_key_run_id] = context + + # Find the corresponding action function from the registry + action_func = self.action_registry.get(action_name) + + if action_func: + # send an event that the group key run has started + self.event_queue.put( + ActionEvent( + action=action, + type=GROUP_KEY_EVENT_TYPE_STARTED, ) + ) - loop = asyncio.get_event_loop() - task = loop.create_task( - self.async_wrapped_action_func( - context, action_func, action, action.get_group_key_run_id - ) + loop = asyncio.get_event_loop() + task = loop.create_task( + self.async_wrapped_action_func( + context, action_func, action, action.get_group_key_run_id ) + ) - task.add_done_callback(self.group_key_run_callback(action)) - self.tasks[action.get_group_key_run_id] = task + task.add_done_callback(self.group_key_run_callback(action)) + self.tasks[action.get_group_key_run_id] = task - try: - await task - span.set_status(StatusCode.OK) - except Exception as e: - # do nothing, this should be caught in the callback - span.set_status(StatusCode.ERROR) - span.record_exception(e) + try: + await task + except Exception as e: + return e - span.add_event("Finished group key run") + return None def force_kill_thread(self, thread: Thread) -> None: """Terminate a python threading.Thread.""" @@ -431,39 +401,31 @@ def force_kill_thread(self, thread: Thread) -> None: except Exception as e: logger.exception(f"Failed to terminate thread: {e}") + ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor async def handle_cancel_action(self, run_id: str) -> None: - with self.otel_tracer.start_as_current_span( - "hatchet.worker.handle_cancel_action" - ) as span: - span.add_event(f"Cancelling run id: {run_id}") - span.set_attribute("run_id", run_id) - - try: - # call cancel to signal the context to stop - if run_id in self.contexts: - context = self.contexts.get(run_id) - - if context: - context.cancel() + try: + # call cancel to signal the context to stop + if run_id in self.contexts: + context = self.contexts.get(run_id) - await asyncio.sleep(1) + if context: + context.cancel() - if run_id in self.tasks: - future = self.tasks.get(run_id) + await asyncio.sleep(1) - if future: - future.cancel() + if run_id in self.tasks: + future = self.tasks.get(run_id) - # check if thread is still running, if so, print a warning - if run_id in self.threads: - logger.warning( - f"Thread {self.threads[run_id].ident} with run id {run_id} is still running after cancellation. This could cause the thread pool to get blocked and prevent new tasks from running." - ) + if future: + future.cancel() - span.set_status(StatusCode.OK) - finally: - self.cleanup_run_id(run_id) - span.add_event(f"Finished cancelling run id: {run_id}") + # check if thread is still running, if so, print a warning + if run_id in self.threads: + logger.warning( + f"Thread {self.threads[run_id].ident} with run id {run_id} is still running after cancellation. This could cause the thread pool to get blocked and prevent new tasks from running." + ) + finally: + self.cleanup_run_id(run_id) def serialize_output(self, output: Any) -> str: diff --git a/poetry.lock b/poetry.lock index 603caef7..f6aa79cd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -6,6 +6,7 @@ version = "2.4.4" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, @@ -17,6 +18,7 @@ version = "3.11.11" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"}, {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"}, @@ -115,6 +117,7 @@ version = "2.9.1" description = "Simple retry client for aiohttp" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54"}, {file = "aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1"}, @@ -129,6 +132,7 @@ version = "1.3.2" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, @@ -143,6 +147,7 @@ version = "0.5.2" description = "Generator-based operators for asynchronous iteration" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "aiostream-0.5.2-py3-none-any.whl", hash = "sha256:054660370be9d37f6fe3ece3851009240416bd082e469fd90cc8673d3818cf71"}, {file = "aiostream-0.5.2.tar.gz", hash = "sha256:b71b519a2d66c38f0872403ab86417955b77352f08d9ad02ad46fc3926b389f4"}, @@ -157,6 +162,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -168,6 +174,8 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.11\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -179,6 +187,7 @@ version = "24.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, @@ -198,6 +207,7 @@ version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, @@ -212,6 +222,7 @@ version = "24.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["lint"] files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -258,6 +269,7 @@ version = "0.1.5" description = "Pure Python CEL Implementation" optional = false python-versions = ">=3.7, <4" +groups = ["main"] files = [ {file = "cel-python-0.1.5.tar.gz", hash = "sha256:d3911bb046bc3ed12792bd88ab453f72d98c66923b72a2fa016bcdffd96e2f98"}, {file = "cel_python-0.1.5-py3-none-any.whl", hash = "sha256:ac81fab8ba08b633700a45d84905be2863529c6a32935c9da7ef53fc06844f1a"}, @@ -278,6 +290,7 @@ version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -289,6 +302,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -390,6 +404,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["lint"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -404,17 +419,21 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev", "lint", "test"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {dev = "sys_platform == \"win32\"", lint = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [[package]] name = "deprecated" version = "1.2.15" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false +optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, @@ -432,6 +451,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev", "test"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -446,6 +467,7 @@ version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, @@ -545,8 +567,10 @@ files = [ name = "googleapis-common-protos" version = "1.66.0" description = "Common protobufs used in Google APIs" -optional = false +optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"}, {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"}, @@ -564,6 +588,7 @@ version = "1.69.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "grpcio-1.69.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2060ca95a8db295ae828d0fc1c7f38fb26ccd5edf9aa51a0f44251f5da332e97"}, {file = "grpcio-1.69.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2e52e107261fd8fa8fa457fe44bfadb904ae869d87c1280bf60f93ecd3e79278"}, @@ -631,6 +656,7 @@ version = "1.69.0" description = "Protobuf code generator for gRPC" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "grpcio_tools-1.69.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:8c210630faa581c3bd08953dac4ad21a7f49862f3b92d69686e9b436d2f1265d"}, {file = "grpcio_tools-1.69.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:09b66ea279fcdaebae4ec34b1baf7577af3b14322738aa980c1c33cfea71f7d7"}, @@ -700,6 +726,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -712,8 +739,10 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 name = "importlib-metadata" version = "8.5.0" description = "Read metadata from Python packages" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, @@ -737,6 +766,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev", "test"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -748,6 +778,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["lint"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -762,6 +793,7 @@ version = "1.0.1" description = "JSON Matching Expressions" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, @@ -773,6 +805,7 @@ version = "0.12.0" description = "a modern parsing library" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "lark-parser-0.12.0.tar.gz", hash = "sha256:15967db1f1214013dca65b1180745047b9be457d73da224fcda3d9dd4e96a138"}, {file = "lark_parser-0.12.0-py2.py3-none-any.whl", hash = "sha256:0eaf30cb5ba787fe404d73a7d6e61df97b21d5a63ac26c5008c78a494373c675"}, @@ -783,30 +816,13 @@ atomic-cache = ["atomicwrites"] nearley = ["js2py"] regex = ["regex"] -[[package]] -name = "loguru" -version = "0.7.3" -description = "Python logging made (stupidly) simple" -optional = false -python-versions = "<4.0,>=3.5" -files = [ - {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, - {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} -win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} - -[package.extras] -dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] - [[package]] name = "multidict" version = "6.1.0" description = "multidict implementation" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, @@ -911,6 +927,7 @@ version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["lint"] files = [ {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, @@ -970,6 +987,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["lint"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -981,6 +999,7 @@ version = "1.6.0" description = "Patch asyncio to allow nested event loops" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, @@ -990,8 +1009,10 @@ files = [ name = "opentelemetry-api" version = "1.29.0" description = "OpenTelemetry Python API" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8"}, {file = "opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf"}, @@ -1005,8 +1026,10 @@ importlib-metadata = ">=6.0,<=8.5.0" name = "opentelemetry-distro" version = "0.50b0" description = "OpenTelemetry Python Distro" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_distro-0.50b0-py3-none-any.whl", hash = "sha256:5fa2e2a99a047ea477fab53e73fb8088b907bda141e8440745b92eb2a84d74aa"}, {file = "opentelemetry_distro-0.50b0.tar.gz", hash = "sha256:3e059e00f53553ebd646d1162d1d3edf5d7c6d3ceafd54a49e74c90dc1c39a7d"}, @@ -1024,8 +1047,10 @@ otlp = ["opentelemetry-exporter-otlp (==1.29.0)"] name = "opentelemetry-exporter-otlp" version = "1.29.0" description = "OpenTelemetry Collector Exporters" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_exporter_otlp-1.29.0-py3-none-any.whl", hash = "sha256:b8da6e20f5b0ffe604154b1e16a407eade17ce310c42fb85bb4e1246fc3688ad"}, {file = "opentelemetry_exporter_otlp-1.29.0.tar.gz", hash = "sha256:ee7dfcccbb5e87ad9b389908452e10b7beeab55f70a83f41ce5b8c4efbde6544"}, @@ -1039,8 +1064,10 @@ opentelemetry-exporter-otlp-proto-http = "1.29.0" name = "opentelemetry-exporter-otlp-proto-common" version = "1.29.0" description = "OpenTelemetry Protobuf encoding" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_exporter_otlp_proto_common-1.29.0-py3-none-any.whl", hash = "sha256:a9d7376c06b4da9cf350677bcddb9618ed4b8255c3f6476975f5e38274ecd3aa"}, {file = "opentelemetry_exporter_otlp_proto_common-1.29.0.tar.gz", hash = "sha256:e7c39b5dbd1b78fe199e40ddfe477e6983cb61aa74ba836df09c3869a3e3e163"}, @@ -1053,8 +1080,10 @@ opentelemetry-proto = "1.29.0" name = "opentelemetry-exporter-otlp-proto-grpc" version = "1.29.0" description = "OpenTelemetry Collector Protobuf over gRPC Exporter" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_exporter_otlp_proto_grpc-1.29.0-py3-none-any.whl", hash = "sha256:5a2a3a741a2543ed162676cf3eefc2b4150e6f4f0a193187afb0d0e65039c69c"}, {file = "opentelemetry_exporter_otlp_proto_grpc-1.29.0.tar.gz", hash = "sha256:3d324d07d64574d72ed178698de3d717f62a059a93b6b7685ee3e303384e73ea"}, @@ -1073,8 +1102,10 @@ opentelemetry-sdk = ">=1.29.0,<1.30.0" name = "opentelemetry-exporter-otlp-proto-http" version = "1.29.0" description = "OpenTelemetry Collector Protobuf over HTTP Exporter" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_exporter_otlp_proto_http-1.29.0-py3-none-any.whl", hash = "sha256:b228bdc0f0cfab82eeea834a7f0ffdd2a258b26aa33d89fb426c29e8e934d9d0"}, {file = "opentelemetry_exporter_otlp_proto_http-1.29.0.tar.gz", hash = "sha256:b10d174e3189716f49d386d66361fbcf6f2b9ad81e05404acdee3f65c8214204"}, @@ -1093,8 +1124,10 @@ requests = ">=2.7,<3.0" name = "opentelemetry-instrumentation" version = "0.50b0" description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_instrumentation-0.50b0-py3-none-any.whl", hash = "sha256:b8f9fc8812de36e1c6dffa5bfc6224df258841fb387b6dfe5df15099daa10630"}, {file = "opentelemetry_instrumentation-0.50b0.tar.gz", hash = "sha256:7d98af72de8dec5323e5202e46122e5f908592b22c6d24733aad619f07d82979"}, @@ -1110,8 +1143,10 @@ wrapt = ">=1.0.0,<2.0.0" name = "opentelemetry-proto" version = "1.29.0" description = "OpenTelemetry Python Proto" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_proto-1.29.0-py3-none-any.whl", hash = "sha256:495069c6f5495cbf732501cdcd3b7f60fda2b9d3d4255706ca99b7ca8dec53ff"}, {file = "opentelemetry_proto-1.29.0.tar.gz", hash = "sha256:3c136aa293782e9b44978c738fff72877a4b78b5d21a64e879898db7b2d93e5d"}, @@ -1124,8 +1159,10 @@ protobuf = ">=5.0,<6.0" name = "opentelemetry-sdk" version = "1.29.0" description = "OpenTelemetry Python SDK" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_sdk-1.29.0-py3-none-any.whl", hash = "sha256:173be3b5d3f8f7d671f20ea37056710217959e774e2749d984355d1f9391a30a"}, {file = "opentelemetry_sdk-1.29.0.tar.gz", hash = "sha256:b0787ce6aade6ab84315302e72bd7a7f2f014b0fb1b7c3295b88afe014ed0643"}, @@ -1140,8 +1177,10 @@ typing-extensions = ">=3.7.4" name = "opentelemetry-semantic-conventions" version = "0.50b0" description = "OpenTelemetry Semantic Conventions" -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "opentelemetry_semantic_conventions-0.50b0-py3-none-any.whl", hash = "sha256:e87efba8fdb67fb38113efea6a349531e75ed7ffc01562f65b802fcecb5e115e"}, {file = "opentelemetry_semantic_conventions-0.50b0.tar.gz", hash = "sha256:02dc6dbcb62f082de9b877ff19a3f1ffaa3c306300fa53bfac761c4567c83d38"}, @@ -1157,10 +1196,12 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "lint", "test"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +markers = {main = "extra == \"otel\""} [[package]] name = "pathspec" @@ -1168,6 +1209,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["lint"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1179,6 +1221,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["lint"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1195,6 +1238,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev", "test"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1210,6 +1254,7 @@ version = "0.21.1" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, @@ -1224,6 +1269,7 @@ version = "0.2.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, @@ -1315,6 +1361,7 @@ version = "5.29.2" description = "" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "protobuf-5.29.2-cp310-abi3-win32.whl", hash = "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851"}, {file = "protobuf-5.29.2-cp310-abi3-win_amd64.whl", hash = "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9"}, @@ -1335,6 +1382,7 @@ version = "6.1.1" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["dev"] files = [ {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, @@ -1365,6 +1413,7 @@ version = "2.10.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, @@ -1385,6 +1434,7 @@ version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -1497,6 +1547,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev", "test"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -1519,6 +1570,7 @@ version = "0.23.8" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, @@ -1537,6 +1589,7 @@ version = "2.3.1" description = "pytest plugin to abort hanging tests" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, @@ -1551,6 +1604,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1565,6 +1619,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -1579,6 +1634,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1641,6 +1697,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1662,6 +1719,7 @@ version = "75.7.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "setuptools-75.7.0-py3-none-any.whl", hash = "sha256:84fb203f278ebcf5cd08f97d3fb96d3fbed4b629d500b29ad60d11e00769b183"}, {file = "setuptools-75.7.0.tar.gz", hash = "sha256:886ff7b16cd342f1d1defc16fc98c9ce3fde69e087a4e1983d7ab634e5f41f4f"}, @@ -1682,6 +1740,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1693,6 +1752,7 @@ version = "9.0.0" description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, @@ -1708,6 +1768,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev", "lint", "test"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -1749,6 +1811,7 @@ version = "5.29.1.20241207" description = "Typing stubs for protobuf" optional = false python-versions = ">=3.8" +groups = ["lint"] files = [ {file = "types_protobuf-5.29.1.20241207-py3-none-any.whl", hash = "sha256:92893c42083e9b718c678badc0af7a9a1307b92afe1599e5cba5f3d35b668b2f"}, {file = "types_protobuf-5.29.1.20241207.tar.gz", hash = "sha256:2ebcadb8ab3ef2e3e2f067e0882906d64ba0dc65fc5b0fd7a8b692315b4a0be9"}, @@ -1760,6 +1823,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "lint"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1771,6 +1835,7 @@ version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, @@ -1782,26 +1847,14 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "win32-setctime" -version = "1.2.0" -description = "A small Python utility to set file creation time on Windows" -optional = false -python-versions = ">=3.5" -files = [ - {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, - {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, -] - -[package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] - [[package]] name = "wrapt" version = "1.17.0" description = "Module for decorators, wrappers and monkey patching." -optional = false +optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, @@ -1876,6 +1929,7 @@ version = "1.18.3" description = "Yet another URL library" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, @@ -1970,8 +2024,10 @@ propcache = ">=0.2.0" name = "zipp" version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false +optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, @@ -1985,7 +2041,10 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] +[extras] +otel = ["opentelemetry-api", "opentelemetry-distro", "opentelemetry-exporter-otlp", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-instrumentation", "opentelemetry-sdk"] + [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" -content-hash = "414d63b255f80d13260cb3a9ecce29f782af46280bba79395554595a47c42f05" +content-hash = "0d25006ef0b235347f4183edbf435d4accccda2b9f554eb8f77067d4178e731c" diff --git a/pyproject.toml b/pyproject.toml index 27dd1d8c..23df3a4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hatchet-sdk" -version = "0.45.3" +version = "0.46.0" description = "" authors = ["Alexander Belanger "] readme = "README.md" @@ -19,7 +19,6 @@ grpcio-tools = [ python-dotenv = "^1.0.0" protobuf = "^5.29.1" pyyaml = "^6.0.1" -loguru = "^0.7.2" pydantic = "^2.6.3" python-dateutil = "^2.9.0.post0" urllib3 = ">=1.26.20" @@ -29,12 +28,12 @@ aiohttp = "^3.10.5" aiohttp-retry = "^2.8.3" tenacity = ">=8.4.1" cel-python = "^0.1.5" -opentelemetry-api = "^1.28.0" -opentelemetry-sdk = "^1.28.0" -opentelemetry-instrumentation = ">=0.49b0" -opentelemetry-distro = ">=0.49b0" -opentelemetry-exporter-otlp = "^1.28.0" -opentelemetry-exporter-otlp-proto-http = "^1.28.0" +opentelemetry-api = { version = "^1.28.0", optional = true } +opentelemetry-sdk = { version = "^1.28.0", optional = true } +opentelemetry-instrumentation = { version = ">=0.49b0", optional = true } +opentelemetry-distro = { version = ">=0.49b0", optional = true } +opentelemetry-exporter-otlp = { version = "^1.28.0", optional = true } +opentelemetry-exporter-otlp-proto-http = { version = "^1.28.0", optional = true } prometheus-client = "^0.21.1" [tool.poetry.group.dev.dependencies] @@ -51,6 +50,16 @@ isort = "^5.13.2" [tool.poetry.group.test.dependencies] pytest-timeout = "^2.3.1" +[tool.poetry.extras] +otel = [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-instrumentation", + "opentelemetry-distro", + "opentelemetry-exporter-otlp", + "opentelemetry-exporter-otlp-proto-http", +] + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" @@ -85,7 +94,7 @@ files = [ "hatchet_sdk/worker/runner/runner.py", "hatchet_sdk/workflow.py", "hatchet_sdk/utils/serialization.py", - "hatchet_sdk/utils/tracing.py", + "hatchet_sdk/opentelemetry/instrumentor.py", "hatchet_sdk/utils/types.py", "hatchet_sdk/utils/backoff.py", "examples/**/*.py", @@ -122,3 +131,4 @@ bulk_fanout = "examples.bulk_fanout.worker:main" retries_with_backoff = "examples.retries_with_backoff.worker:main" pydantic = "examples.pydantic.worker:main" v2_simple = "examples.v2.simple.worker:main" +otel = "examples.opentelemetry_instrumentation.worker:main"