splunk-otel-util-genai 0.1.3__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 (31) hide show
  1. opentelemetry/util/genai/__init__.py +17 -0
  2. opentelemetry/util/genai/_fsspec_upload/__init__.py +39 -0
  3. opentelemetry/util/genai/_fsspec_upload/fsspec_hook.py +184 -0
  4. opentelemetry/util/genai/attributes.py +60 -0
  5. opentelemetry/util/genai/callbacks.py +24 -0
  6. opentelemetry/util/genai/config.py +184 -0
  7. opentelemetry/util/genai/debug.py +183 -0
  8. opentelemetry/util/genai/emitters/__init__.py +25 -0
  9. opentelemetry/util/genai/emitters/composite.py +186 -0
  10. opentelemetry/util/genai/emitters/configuration.py +324 -0
  11. opentelemetry/util/genai/emitters/content_events.py +153 -0
  12. opentelemetry/util/genai/emitters/evaluation.py +519 -0
  13. opentelemetry/util/genai/emitters/metrics.py +308 -0
  14. opentelemetry/util/genai/emitters/span.py +774 -0
  15. opentelemetry/util/genai/emitters/spec.py +48 -0
  16. opentelemetry/util/genai/emitters/utils.py +961 -0
  17. opentelemetry/util/genai/environment_variables.py +200 -0
  18. opentelemetry/util/genai/handler.py +1002 -0
  19. opentelemetry/util/genai/instruments.py +44 -0
  20. opentelemetry/util/genai/interfaces.py +58 -0
  21. opentelemetry/util/genai/plugins.py +114 -0
  22. opentelemetry/util/genai/span_context.py +80 -0
  23. opentelemetry/util/genai/types.py +440 -0
  24. opentelemetry/util/genai/upload_hook.py +119 -0
  25. opentelemetry/util/genai/utils.py +182 -0
  26. opentelemetry/util/genai/version.py +15 -0
  27. splunk_otel_util_genai-0.1.3.dist-info/METADATA +70 -0
  28. splunk_otel_util_genai-0.1.3.dist-info/RECORD +31 -0
  29. splunk_otel_util_genai-0.1.3.dist-info/WHEEL +4 -0
  30. splunk_otel_util_genai-0.1.3.dist-info/entry_points.txt +5 -0
  31. splunk_otel_util_genai-0.1.3.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,440 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import time
17
+ from contextvars import Token
18
+ from dataclasses import dataclass, field
19
+ from dataclasses import fields as dataclass_fields
20
+ from enum import Enum
21
+ from typing import Any, Dict, List, Literal, Optional, Type, Union
22
+ from uuid import UUID, uuid4
23
+
24
+ from opentelemetry.semconv._incubating.attributes import (
25
+ gen_ai_attributes as GenAIAttributes,
26
+ )
27
+ from opentelemetry.trace import Span, SpanContext
28
+
29
+ # Backward compatibility: older semconv builds may miss new GEN_AI attributes
30
+ if not hasattr(GenAIAttributes, "GEN_AI_PROVIDER_NAME"):
31
+ GenAIAttributes.GEN_AI_PROVIDER_NAME = "gen_ai.provider.name"
32
+ from opentelemetry.util.types import AttributeValue
33
+
34
+ ContextToken = Token # simple alias; avoid TypeAlias warning tools
35
+
36
+
37
+ class ContentCapturingMode(Enum):
38
+ # Do not capture content (default).
39
+ NO_CONTENT = 0
40
+ # Only capture content in spans.
41
+ SPAN_ONLY = 1
42
+ # Only capture content in events.
43
+ EVENT_ONLY = 2
44
+ # Capture content in both spans and events.
45
+ SPAN_AND_EVENT = 3
46
+
47
+
48
+ def _new_input_messages() -> list["InputMessage"]: # quotes for forward ref
49
+ return []
50
+
51
+
52
+ def _new_output_messages() -> list["OutputMessage"]: # quotes for forward ref
53
+ return []
54
+
55
+
56
+ def _new_str_any_dict() -> dict[str, Any]:
57
+ return {}
58
+
59
+
60
+ @dataclass(kw_only=True)
61
+ class GenAI:
62
+ """Base type for all GenAI telemetry entities."""
63
+
64
+ context_token: Optional[ContextToken] = None
65
+ span: Optional[Span] = None
66
+ span_context: Optional[SpanContext] = None
67
+ trace_id: Optional[int] = None
68
+ span_id: Optional[int] = None
69
+ trace_flags: Optional[int] = None
70
+ start_time: float = field(default_factory=time.time)
71
+ end_time: Optional[float] = None
72
+ provider: Optional[str] = field(
73
+ default=None,
74
+ metadata={"semconv": GenAIAttributes.GEN_AI_PROVIDER_NAME},
75
+ )
76
+ framework: Optional[str] = None
77
+ attributes: Dict[str, Any] = field(default_factory=_new_str_any_dict)
78
+ run_id: UUID = field(default_factory=uuid4)
79
+ parent_run_id: Optional[UUID] = None
80
+ agent_name: Optional[str] = field(
81
+ default=None,
82
+ metadata={"semconv": GenAIAttributes.GEN_AI_AGENT_NAME},
83
+ )
84
+ agent_id: Optional[str] = field(
85
+ default=None,
86
+ metadata={"semconv": GenAIAttributes.GEN_AI_AGENT_ID},
87
+ )
88
+ system: Optional[str] = field(
89
+ default=None,
90
+ metadata={"semconv": GenAIAttributes.GEN_AI_SYSTEM},
91
+ )
92
+ conversation_id: Optional[str] = field(
93
+ default=None,
94
+ metadata={"semconv": GenAIAttributes.GEN_AI_CONVERSATION_ID},
95
+ )
96
+ data_source_id: Optional[str] = field(
97
+ default=None,
98
+ metadata={"semconv": GenAIAttributes.GEN_AI_DATA_SOURCE_ID},
99
+ )
100
+ sample_for_evaluation: Optional[bool] = field(default=True)
101
+
102
+ def semantic_convention_attributes(self) -> dict[str, Any]:
103
+ """Return semantic convention attributes defined on this dataclass."""
104
+
105
+ result: dict[str, Any] = {}
106
+ for data_field in dataclass_fields(self):
107
+ semconv_key = data_field.metadata.get("semconv")
108
+ if not semconv_key:
109
+ continue
110
+ value = getattr(self, data_field.name)
111
+ if value is None:
112
+ continue
113
+ if isinstance(value, list) and not value:
114
+ continue
115
+ result[semconv_key] = value
116
+ return result
117
+
118
+
119
+ @dataclass()
120
+ class ToolCall(GenAI):
121
+ """Represents a single tool call invocation (Phase 4)."""
122
+
123
+ arguments: Any
124
+ name: str
125
+ id: Optional[str]
126
+ type: Literal["tool_call"] = "tool_call"
127
+
128
+
129
+ @dataclass()
130
+ class ToolCallResponse:
131
+ response: Any
132
+ id: Optional[str]
133
+ type: Literal["tool_call_response"] = "tool_call_response"
134
+
135
+
136
+ FinishReason = Literal[
137
+ "content_filter", "error", "length", "stop", "tool_calls"
138
+ ]
139
+
140
+
141
+ @dataclass()
142
+ class Text:
143
+ content: str
144
+ type: Literal["text"] = "text"
145
+
146
+
147
+ MessagePart = Union[Text, "ToolCall", ToolCallResponse, Any]
148
+
149
+
150
+ @dataclass()
151
+ class InputMessage:
152
+ role: str
153
+ parts: list[MessagePart]
154
+
155
+
156
+ @dataclass()
157
+ class OutputMessage:
158
+ role: str
159
+ parts: list[MessagePart]
160
+ finish_reason: Union[str, FinishReason]
161
+
162
+
163
+ @dataclass
164
+ class LLMInvocation(GenAI):
165
+ """Represents a single large language model invocation.
166
+
167
+ Only fields tagged with ``metadata["semconv"]`` are emitted as
168
+ semantic-convention attributes by the span emitters. Additional fields are
169
+ util-only helpers or inputs to alternative span flavors (e.g. Traceloop).
170
+ """
171
+
172
+ request_model: str = field(
173
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_MODEL}
174
+ )
175
+ input_messages: List[InputMessage] = field(
176
+ default_factory=_new_input_messages
177
+ )
178
+ # Traceloop compatibility relies on enumerating these lists into prefixed attributes.
179
+ output_messages: List[OutputMessage] = field(
180
+ default_factory=_new_output_messages
181
+ )
182
+ operation: str = field(
183
+ default=GenAIAttributes.GenAiOperationNameValues.CHAT.value,
184
+ metadata={"semconv": GenAIAttributes.GEN_AI_OPERATION_NAME},
185
+ )
186
+ response_model_name: Optional[str] = field(
187
+ default=None,
188
+ metadata={"semconv": GenAIAttributes.GEN_AI_RESPONSE_MODEL},
189
+ )
190
+ response_id: Optional[str] = field(
191
+ default=None,
192
+ metadata={"semconv": GenAIAttributes.GEN_AI_RESPONSE_ID},
193
+ )
194
+ input_tokens: Optional[AttributeValue] = field(
195
+ default=None,
196
+ metadata={"semconv": GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS},
197
+ )
198
+ output_tokens: Optional[AttributeValue] = field(
199
+ default=None,
200
+ metadata={"semconv": GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS},
201
+ )
202
+ # Structured function/tool definitions for semantic convention emission
203
+ request_functions: list[dict[str, Any]] = field(default_factory=list)
204
+ request_temperature: Optional[float] = field(
205
+ default=None,
206
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE},
207
+ )
208
+ request_top_p: Optional[float] = field(
209
+ default=None,
210
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_TOP_P},
211
+ )
212
+ request_top_k: Optional[int] = field(
213
+ default=None,
214
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_TOP_K},
215
+ )
216
+ request_frequency_penalty: Optional[float] = field(
217
+ default=None,
218
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY},
219
+ )
220
+ request_presence_penalty: Optional[float] = field(
221
+ default=None,
222
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY},
223
+ )
224
+ request_stop_sequences: List[str] = field(
225
+ default_factory=list,
226
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES},
227
+ )
228
+ request_max_tokens: Optional[int] = field(
229
+ default=None,
230
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS},
231
+ )
232
+ request_choice_count: Optional[int] = field(
233
+ default=None,
234
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT},
235
+ )
236
+ request_seed: Optional[int] = field(
237
+ default=None,
238
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_SEED},
239
+ )
240
+ request_encoding_formats: List[str] = field(
241
+ default_factory=list,
242
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_ENCODING_FORMATS},
243
+ )
244
+ output_type: Optional[str] = field(
245
+ default=None,
246
+ metadata={"semconv": GenAIAttributes.GEN_AI_OUTPUT_TYPE},
247
+ )
248
+ response_finish_reasons: List[str] = field(
249
+ default_factory=list,
250
+ metadata={"semconv": GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS},
251
+ )
252
+ request_service_tier: Optional[str] = field(
253
+ default=None,
254
+ metadata={
255
+ "semconv": GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER
256
+ },
257
+ )
258
+ response_service_tier: Optional[str] = field(
259
+ default=None,
260
+ metadata={
261
+ "semconv": GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER
262
+ },
263
+ )
264
+ response_system_fingerprint: Optional[str] = field(
265
+ default=None,
266
+ metadata={
267
+ "semconv": GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SYSTEM_FINGERPRINT
268
+ },
269
+ )
270
+
271
+
272
+ @dataclass
273
+ class Error:
274
+ message: str
275
+ type: Type[BaseException]
276
+
277
+
278
+ @dataclass
279
+ class EvaluationResult:
280
+ """Represents the outcome of a single evaluation metric.
281
+
282
+ Additional fields (e.g., judge model, threshold) can be added without
283
+ breaking callers that rely only on the current contract.
284
+ """
285
+
286
+ metric_name: str
287
+ score: Optional[float] = None
288
+ label: Optional[str] = None
289
+ explanation: Optional[str] = None
290
+ error: Optional[Error] = None
291
+ attributes: Dict[str, Any] = field(default_factory=dict)
292
+
293
+
294
+ @dataclass
295
+ class EmbeddingInvocation(GenAI):
296
+ """Represents a single embedding model invocation."""
297
+
298
+ operation_name: str = field(
299
+ default=GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value,
300
+ metadata={"semconv": GenAIAttributes.GEN_AI_OPERATION_NAME},
301
+ )
302
+ request_model: str = field(
303
+ default="",
304
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_MODEL},
305
+ )
306
+ input_texts: list[str] = field(default_factory=list)
307
+ dimension_count: Optional[int] = None
308
+ server_port: Optional[int] = None
309
+ server_address: Optional[str] = None
310
+ input_tokens: Optional[int] = field(
311
+ default=None,
312
+ metadata={"semconv": GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS},
313
+ )
314
+ encoding_formats: list[str] = field(
315
+ default_factory=list,
316
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_ENCODING_FORMATS},
317
+ )
318
+ error_type: Optional[str] = None
319
+
320
+
321
+ @dataclass
322
+ class Workflow(GenAI):
323
+ """Represents a workflow orchestrating multiple agents and steps.
324
+
325
+ A workflow is the top-level orchestration unit in agentic AI systems,
326
+ coordinating agents and steps to achieve a complex goal. Workflows are optional
327
+ and typically used in multi-agent or multi-step scenarios.
328
+
329
+ Attributes:
330
+ name: Identifier for the workflow (e.g., "customer_support_pipeline")
331
+ workflow_type: Type of orchestration (e.g., "sequential", "parallel", "graph", "dynamic")
332
+ description: Human-readable description of the workflow's purpose
333
+ framework: Framework implementing the workflow (e.g., "langgraph", "crewai", "autogen")
334
+ initial_input: User's initial query/request that triggered the workflow
335
+ final_output: Final response/result produced by the workflow
336
+ attributes: Additional custom attributes for workflow-specific metadata
337
+ start_time: Timestamp when workflow started
338
+ end_time: Timestamp when workflow completed
339
+ span: OpenTelemetry span associated with this workflow
340
+ context_token: Context token for span management
341
+ run_id: Unique identifier for this workflow execution
342
+ parent_run_id: Optional parent workflow/trace identifier
343
+ """
344
+
345
+ name: str
346
+ workflow_type: Optional[str] = None # sequential, parallel, graph, dynamic
347
+ description: Optional[str] = None
348
+ initial_input: Optional[str] = None # User's initial query/request
349
+ final_output: Optional[str] = None # Final response/result
350
+
351
+
352
+ @dataclass
353
+ class _BaseAgent(GenAI):
354
+ """Shared fields for agent lifecycle phases."""
355
+
356
+ name: str
357
+ agent_type: Optional[str] = (
358
+ None # researcher, planner, executor, critic, etc.
359
+ )
360
+ description: Optional[str] = field(
361
+ default=None,
362
+ metadata={"semconv": GenAIAttributes.GEN_AI_AGENT_DESCRIPTION},
363
+ )
364
+ model: Optional[str] = field(
365
+ default=None,
366
+ metadata={"semconv": GenAIAttributes.GEN_AI_REQUEST_MODEL},
367
+ ) # primary model if applicable
368
+ tools: list[str] = field(default_factory=list) # available tool names
369
+ system_instructions: Optional[str] = None # System prompt/instructions
370
+
371
+
372
+ @dataclass
373
+ class AgentCreation(_BaseAgent):
374
+ """Represents agent creation/initialisation."""
375
+
376
+ operation: Literal["create_agent"] = field(
377
+ init=False,
378
+ default="create_agent",
379
+ metadata={"semconv": GenAIAttributes.GEN_AI_OPERATION_NAME},
380
+ )
381
+ input_context: Optional[str] = None # optional initial context
382
+
383
+
384
+ @dataclass
385
+ class AgentInvocation(_BaseAgent):
386
+ """Represents agent execution (`invoke_agent`)."""
387
+
388
+ operation: Literal["invoke_agent"] = field(
389
+ init=False,
390
+ default="invoke_agent",
391
+ metadata={"semconv": GenAIAttributes.GEN_AI_OPERATION_NAME},
392
+ )
393
+ input_context: Optional[str] = None # Input for invoke operations
394
+ output_result: Optional[str] = None # Output for invoke operations
395
+
396
+
397
+ @dataclass
398
+ class Step(GenAI):
399
+ """Represents a discrete unit of work in an agentic AI system.
400
+
401
+ Steps can be orchestrated at the workflow level (assigned to agents) or
402
+ decomposed internally by agents during execution. This design supports both
403
+ scenarios through flexible parent relationships.
404
+ """
405
+
406
+ name: str
407
+ objective: Optional[str] = None # what the step aims to achieve
408
+ step_type: Optional[str] = (
409
+ None # planning, execution, reflection, tool_use, etc.
410
+ )
411
+ source: Optional[Literal["workflow", "agent"]] = (
412
+ None # where step originated
413
+ )
414
+ assigned_agent: Optional[str] = None # for workflow-assigned steps
415
+ status: Optional[str] = None # pending, in_progress, completed, failed
416
+ description: Optional[str] = None
417
+ input_data: Optional[str] = None # Input data/context for the step
418
+ output_data: Optional[str] = None # Output data/result from the step
419
+
420
+
421
+ __all__ = [
422
+ # existing exports intentionally implicit before; making explicit for new additions
423
+ "ContentCapturingMode",
424
+ "ToolCall",
425
+ "ToolCallResponse",
426
+ "Text",
427
+ "InputMessage",
428
+ "OutputMessage",
429
+ "GenAI",
430
+ "LLMInvocation",
431
+ "EmbeddingInvocation",
432
+ "Error",
433
+ "EvaluationResult",
434
+ # agentic AI types
435
+ "Workflow",
436
+ "AgentCreation",
437
+ "AgentInvocation",
438
+ "Step",
439
+ # backward compatibility normalization helpers
440
+ ]
@@ -0,0 +1,119 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """This module defines the generic hooks for GenAI content uploading
16
+
17
+ The hooks are specified as part of semconv in `Uploading content to external storage
18
+ <https://github.com/open-telemetry/semantic-conventions/blob/v1.37.0/docs/gen-ai/gen-ai-spans.md#uploading-content-to-external-storage>`__.
19
+
20
+ This module defines the `UploadHook` type that custom implementations should implement, and a
21
+ `load_upload_hook` function to load it from an entry point.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from os import environ
28
+ from typing import Any, Protocol, cast, runtime_checkable
29
+
30
+ from opentelemetry._logs import LogRecord
31
+ from opentelemetry.trace import Span
32
+ from opentelemetry.util._importlib_metadata import (
33
+ entry_points, # pyright: ignore[reportUnknownVariableType]
34
+ )
35
+ from opentelemetry.util.genai import types
36
+ from opentelemetry.util.genai.environment_variables import (
37
+ OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK,
38
+ )
39
+
40
+ _logger = logging.getLogger(__name__)
41
+
42
+
43
+ @runtime_checkable
44
+ class UploadHook(Protocol):
45
+ """A hook to upload GenAI content to an external storage.
46
+
47
+ This is the interface for a hook that can be
48
+ used to upload GenAI content to an external storage. The hook is a
49
+ callable that takes the inputs, outputs, and system instruction of a
50
+ GenAI interaction, as well as the span and log record associated with
51
+ it.
52
+
53
+ The hook can be used to upload the content to any external storage,
54
+ such as a database, a file system, or a cloud storage service.
55
+
56
+ The span and log_record arguments should be provided based on the content capturing mode
57
+ :func:`~opentelemetry.util.genai.utils.get_content_capturing_mode`.
58
+
59
+ Args:
60
+ inputs: The inputs of the GenAI interaction.
61
+ outputs: The outputs of the GenAI interaction.
62
+ system_instruction: The system instruction of the GenAI
63
+ interaction.
64
+ span: The span associated with the GenAI interaction.
65
+ log_record: The event log associated with the GenAI
66
+ interaction.
67
+ """
68
+
69
+ def upload(
70
+ self,
71
+ *,
72
+ inputs: list[types.InputMessage],
73
+ outputs: list[types.OutputMessage],
74
+ system_instruction: list[types.MessagePart],
75
+ span: Span | None = None,
76
+ log_record: LogRecord | None = None,
77
+ ) -> None: ...
78
+
79
+
80
+ class _NoOpUploadHook(UploadHook):
81
+ def upload(self, **kwargs: Any) -> None:
82
+ return None
83
+
84
+
85
+ def load_upload_hook() -> UploadHook:
86
+ """Load the upload hook from entry point or return a noop implementation
87
+
88
+ This function loads an upload hook from the entry point group
89
+ ``opentelemetry_genai_upload_hook`` with name coming from
90
+ :envvar:`OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK`. If one can't be found, returns a no-op
91
+ implementation.
92
+ """
93
+ hook_name = environ.get(OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK, None)
94
+ if not hook_name:
95
+ return _NoOpUploadHook()
96
+
97
+ for entry_point in entry_points(group="opentelemetry_genai_upload_hook"): # pyright: ignore[reportUnknownVariableType]
98
+ name = cast(str, entry_point.name) # pyright: ignore[reportUnknownMemberType]
99
+ try:
100
+ if hook_name != name:
101
+ continue
102
+
103
+ hook = entry_point.load()() # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
104
+ if not isinstance(hook, UploadHook):
105
+ _logger.debug("%s is not a valid UploadHook. Using noop", name)
106
+ continue
107
+
108
+ _logger.debug("Using UploadHook %s", name)
109
+ return hook
110
+
111
+ except Exception: # pylint: disable=broad-except
112
+ _logger.exception(
113
+ "UploadHook %s configuration failed. Using noop", name
114
+ )
115
+
116
+ return _NoOpUploadHook()
117
+
118
+
119
+ __all__ = ["UploadHook", "load_upload_hook"]