nvidia-nat 1.3.0.dev2__py3-none-any.whl → 1.3.0rc1__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.
- aiq/__init__.py +2 -2
- nat/agent/base.py +24 -15
- nat/agent/dual_node.py +9 -4
- nat/agent/prompt_optimizer/prompt.py +68 -0
- nat/agent/prompt_optimizer/register.py +149 -0
- nat/agent/react_agent/agent.py +79 -47
- nat/agent/react_agent/register.py +41 -21
- nat/agent/reasoning_agent/reasoning_agent.py +11 -9
- nat/agent/register.py +1 -1
- nat/agent/rewoo_agent/agent.py +326 -148
- nat/agent/rewoo_agent/prompt.py +19 -22
- nat/agent/rewoo_agent/register.py +46 -26
- nat/agent/tool_calling_agent/agent.py +84 -28
- nat/agent/tool_calling_agent/register.py +51 -28
- nat/authentication/api_key/api_key_auth_provider.py +2 -2
- nat/authentication/credential_validator/bearer_token_validator.py +557 -0
- nat/authentication/http_basic_auth/http_basic_auth_provider.py +1 -1
- nat/authentication/interfaces.py +5 -2
- nat/authentication/oauth2/oauth2_auth_code_flow_provider.py +40 -20
- nat/authentication/oauth2/oauth2_resource_server_config.py +124 -0
- nat/authentication/register.py +0 -1
- nat/builder/builder.py +56 -24
- nat/builder/component_utils.py +9 -5
- nat/builder/context.py +46 -11
- nat/builder/eval_builder.py +16 -11
- nat/builder/framework_enum.py +1 -0
- nat/builder/front_end.py +1 -1
- nat/builder/function.py +378 -8
- nat/builder/function_base.py +3 -3
- nat/builder/function_info.py +6 -8
- nat/builder/user_interaction_manager.py +2 -2
- nat/builder/workflow.py +13 -1
- nat/builder/workflow_builder.py +281 -76
- nat/cli/cli_utils/config_override.py +2 -2
- nat/cli/commands/evaluate.py +1 -1
- nat/cli/commands/info/info.py +16 -6
- nat/cli/commands/info/list_channels.py +1 -1
- nat/cli/commands/info/list_components.py +7 -8
- nat/cli/commands/mcp/__init__.py +14 -0
- nat/cli/commands/mcp/mcp.py +986 -0
- nat/cli/commands/object_store/__init__.py +14 -0
- nat/cli/commands/object_store/object_store.py +227 -0
- nat/cli/commands/optimize.py +90 -0
- nat/cli/commands/registry/publish.py +2 -2
- nat/cli/commands/registry/pull.py +2 -2
- nat/cli/commands/registry/remove.py +2 -2
- nat/cli/commands/registry/search.py +15 -17
- nat/cli/commands/start.py +16 -5
- nat/cli/commands/uninstall.py +1 -1
- nat/cli/commands/workflow/templates/config.yml.j2 +0 -1
- nat/cli/commands/workflow/templates/pyproject.toml.j2 +4 -1
- nat/cli/commands/workflow/templates/register.py.j2 +0 -1
- nat/cli/commands/workflow/workflow_commands.py +9 -13
- nat/cli/entrypoint.py +8 -10
- nat/cli/register_workflow.py +38 -4
- nat/cli/type_registry.py +75 -6
- nat/control_flow/__init__.py +0 -0
- nat/control_flow/register.py +20 -0
- nat/control_flow/router_agent/__init__.py +0 -0
- nat/control_flow/router_agent/agent.py +329 -0
- nat/control_flow/router_agent/prompt.py +48 -0
- nat/control_flow/router_agent/register.py +91 -0
- nat/control_flow/sequential_executor.py +166 -0
- nat/data_models/agent.py +34 -0
- nat/data_models/api_server.py +10 -10
- nat/data_models/authentication.py +23 -9
- nat/data_models/common.py +1 -1
- nat/data_models/component.py +2 -0
- nat/data_models/component_ref.py +11 -0
- nat/data_models/config.py +41 -17
- nat/data_models/dataset_handler.py +1 -1
- nat/data_models/discovery_metadata.py +4 -4
- nat/data_models/evaluate.py +4 -1
- nat/data_models/function.py +34 -0
- nat/data_models/function_dependencies.py +14 -6
- nat/data_models/gated_field_mixin.py +242 -0
- nat/data_models/intermediate_step.py +3 -3
- nat/data_models/optimizable.py +119 -0
- nat/data_models/optimizer.py +149 -0
- nat/data_models/swe_bench_model.py +1 -1
- nat/data_models/temperature_mixin.py +44 -0
- nat/data_models/thinking_mixin.py +86 -0
- nat/data_models/top_p_mixin.py +44 -0
- nat/embedder/nim_embedder.py +1 -1
- nat/embedder/openai_embedder.py +1 -1
- nat/embedder/register.py +0 -1
- nat/eval/config.py +3 -1
- nat/eval/dataset_handler/dataset_handler.py +71 -7
- nat/eval/evaluate.py +86 -31
- nat/eval/evaluator/base_evaluator.py +1 -1
- nat/eval/evaluator/evaluator_model.py +13 -0
- nat/eval/intermediate_step_adapter.py +1 -1
- nat/eval/rag_evaluator/evaluate.py +2 -2
- nat/eval/rag_evaluator/register.py +3 -3
- nat/eval/register.py +4 -1
- nat/eval/remote_workflow.py +3 -3
- nat/eval/runtime_evaluator/__init__.py +14 -0
- nat/eval/runtime_evaluator/evaluate.py +123 -0
- nat/eval/runtime_evaluator/register.py +100 -0
- nat/eval/swe_bench_evaluator/evaluate.py +6 -6
- nat/eval/trajectory_evaluator/evaluate.py +1 -1
- nat/eval/trajectory_evaluator/register.py +1 -1
- nat/eval/tunable_rag_evaluator/evaluate.py +4 -7
- nat/eval/utils/eval_trace_ctx.py +89 -0
- nat/eval/utils/weave_eval.py +18 -9
- nat/experimental/decorators/experimental_warning_decorator.py +27 -7
- nat/experimental/test_time_compute/functions/plan_select_execute_function.py +7 -3
- nat/experimental/test_time_compute/functions/ttc_tool_orchestration_function.py +3 -3
- nat/experimental/test_time_compute/functions/ttc_tool_wrapper_function.py +1 -1
- nat/experimental/test_time_compute/models/strategy_base.py +5 -4
- nat/experimental/test_time_compute/register.py +0 -1
- nat/experimental/test_time_compute/selection/llm_based_output_merging_selector.py +1 -3
- nat/front_ends/console/authentication_flow_handler.py +82 -30
- nat/front_ends/console/console_front_end_plugin.py +8 -5
- nat/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +52 -17
- nat/front_ends/fastapi/dask_client_mixin.py +65 -0
- nat/front_ends/fastapi/fastapi_front_end_config.py +36 -5
- nat/front_ends/fastapi/fastapi_front_end_controller.py +4 -4
- nat/front_ends/fastapi/fastapi_front_end_plugin.py +135 -4
- nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +481 -281
- nat/front_ends/fastapi/job_store.py +518 -99
- nat/front_ends/fastapi/main.py +11 -19
- nat/front_ends/fastapi/message_handler.py +13 -14
- nat/front_ends/fastapi/message_validator.py +17 -19
- nat/front_ends/fastapi/response_helpers.py +4 -4
- nat/front_ends/fastapi/step_adaptor.py +2 -2
- nat/front_ends/fastapi/utils.py +57 -0
- nat/front_ends/mcp/introspection_token_verifier.py +73 -0
- nat/front_ends/mcp/mcp_front_end_config.py +10 -1
- nat/front_ends/mcp/mcp_front_end_plugin.py +45 -13
- nat/front_ends/mcp/mcp_front_end_plugin_worker.py +116 -8
- nat/front_ends/mcp/tool_converter.py +44 -14
- nat/front_ends/register.py +0 -1
- nat/front_ends/simple_base/simple_front_end_plugin_base.py +3 -1
- nat/llm/aws_bedrock_llm.py +24 -12
- nat/llm/azure_openai_llm.py +13 -6
- nat/llm/litellm_llm.py +69 -0
- nat/llm/nim_llm.py +20 -8
- nat/llm/openai_llm.py +14 -6
- nat/llm/register.py +4 -1
- nat/llm/utils/env_config_value.py +2 -3
- nat/llm/utils/thinking.py +215 -0
- nat/meta/pypi.md +9 -9
- nat/object_store/register.py +0 -1
- nat/observability/exporter/base_exporter.py +3 -3
- nat/observability/exporter/file_exporter.py +1 -1
- nat/observability/exporter/processing_exporter.py +309 -81
- nat/observability/exporter/span_exporter.py +1 -1
- nat/observability/exporter_manager.py +7 -7
- nat/observability/mixin/file_mixin.py +7 -7
- nat/observability/mixin/redaction_config_mixin.py +42 -0
- nat/observability/mixin/tagging_config_mixin.py +62 -0
- nat/observability/mixin/type_introspection_mixin.py +420 -107
- nat/observability/processor/batching_processor.py +5 -7
- nat/observability/processor/falsy_batch_filter_processor.py +55 -0
- nat/observability/processor/processor.py +3 -0
- nat/observability/processor/processor_factory.py +70 -0
- nat/observability/processor/redaction/__init__.py +24 -0
- nat/observability/processor/redaction/contextual_redaction_processor.py +125 -0
- nat/observability/processor/redaction/contextual_span_redaction_processor.py +66 -0
- nat/observability/processor/redaction/redaction_processor.py +177 -0
- nat/observability/processor/redaction/span_header_redaction_processor.py +92 -0
- nat/observability/processor/span_tagging_processor.py +68 -0
- nat/observability/register.py +6 -4
- nat/profiler/calc/calc_runner.py +3 -4
- nat/profiler/callbacks/agno_callback_handler.py +1 -1
- nat/profiler/callbacks/langchain_callback_handler.py +6 -6
- nat/profiler/callbacks/llama_index_callback_handler.py +3 -3
- nat/profiler/callbacks/semantic_kernel_callback_handler.py +3 -3
- nat/profiler/data_frame_row.py +1 -1
- nat/profiler/decorators/framework_wrapper.py +62 -13
- nat/profiler/decorators/function_tracking.py +160 -3
- nat/profiler/forecasting/models/forecasting_base_model.py +3 -1
- nat/profiler/inference_optimization/bottleneck_analysis/simple_stack_analysis.py +1 -1
- nat/profiler/inference_optimization/data_models.py +3 -3
- nat/profiler/inference_optimization/experimental/prefix_span_analysis.py +7 -8
- nat/profiler/inference_optimization/token_uniqueness.py +1 -1
- nat/profiler/parameter_optimization/__init__.py +0 -0
- nat/profiler/parameter_optimization/optimizable_utils.py +93 -0
- nat/profiler/parameter_optimization/optimizer_runtime.py +67 -0
- nat/profiler/parameter_optimization/parameter_optimizer.py +153 -0
- nat/profiler/parameter_optimization/parameter_selection.py +107 -0
- nat/profiler/parameter_optimization/pareto_visualizer.py +380 -0
- nat/profiler/parameter_optimization/prompt_optimizer.py +384 -0
- nat/profiler/parameter_optimization/update_helpers.py +66 -0
- nat/profiler/profile_runner.py +14 -9
- nat/profiler/utils.py +4 -2
- nat/registry_handlers/local/local_handler.py +2 -2
- nat/registry_handlers/package_utils.py +1 -2
- nat/registry_handlers/pypi/pypi_handler.py +23 -26
- nat/registry_handlers/register.py +3 -4
- nat/registry_handlers/rest/rest_handler.py +12 -13
- nat/retriever/milvus/retriever.py +2 -2
- nat/retriever/nemo_retriever/retriever.py +1 -1
- nat/retriever/register.py +0 -1
- nat/runtime/loader.py +2 -2
- nat/runtime/runner.py +3 -2
- nat/runtime/session.py +43 -8
- nat/settings/global_settings.py +16 -5
- nat/tool/chat_completion.py +5 -2
- nat/tool/code_execution/local_sandbox/local_sandbox_server.py +3 -3
- nat/tool/datetime_tools.py +49 -9
- nat/tool/document_search.py +2 -2
- nat/tool/github_tools.py +450 -0
- nat/tool/nvidia_rag.py +1 -1
- nat/tool/register.py +2 -9
- nat/tool/retriever.py +3 -2
- nat/utils/callable_utils.py +70 -0
- nat/utils/data_models/schema_validator.py +3 -3
- nat/utils/exception_handlers/automatic_retries.py +104 -51
- nat/utils/exception_handlers/schemas.py +1 -1
- nat/utils/io/yaml_tools.py +2 -2
- nat/utils/log_levels.py +25 -0
- nat/utils/reactive/base/observable_base.py +2 -2
- nat/utils/reactive/base/observer_base.py +1 -1
- nat/utils/reactive/observable.py +2 -2
- nat/utils/reactive/observer.py +4 -4
- nat/utils/reactive/subscription.py +1 -1
- nat/utils/settings/global_settings.py +6 -8
- nat/utils/type_converter.py +4 -3
- nat/utils/type_utils.py +9 -5
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc1.dist-info}/METADATA +42 -16
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc1.dist-info}/RECORD +230 -189
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc1.dist-info}/entry_points.txt +1 -0
- nat/cli/commands/info/list_mcp.py +0 -304
- nat/tool/github_tools/create_github_commit.py +0 -133
- nat/tool/github_tools/create_github_issue.py +0 -87
- nat/tool/github_tools/create_github_pr.py +0 -106
- nat/tool/github_tools/get_github_file.py +0 -106
- nat/tool/github_tools/get_github_issue.py +0 -166
- nat/tool/github_tools/get_github_pr.py +0 -256
- nat/tool/github_tools/update_github_issue.py +0 -100
- nat/tool/mcp/exceptions.py +0 -142
- nat/tool/mcp/mcp_client.py +0 -255
- nat/tool/mcp/mcp_tool.py +0 -96
- nat/utils/exception_handlers/mcp.py +0 -211
- /nat/{tool/github_tools → agent/prompt_optimizer}/__init__.py +0 -0
- /nat/{tool/mcp → authentication/credential_validator}/__init__.py +0 -0
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc1.dist-info}/WHEEL +0 -0
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc1.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc1.dist-info}/licenses/LICENSE.md +0 -0
- {nvidia_nat-1.3.0.dev2.dist-info → nvidia_nat-1.3.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from typing import TypeVar
|
|
18
|
+
|
|
19
|
+
from nat.observability.processor.processor import Processor
|
|
20
|
+
from nat.utils.type_utils import override
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
FalsyT = TypeVar("FalsyT")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FalsyBatchFilterProcessor(Processor[list[FalsyT], list[FalsyT]]):
|
|
28
|
+
"""Processor that filters out falsy items from a batch."""
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
async def process(self, item: list[FalsyT]) -> list[FalsyT]:
|
|
32
|
+
"""Filter out falsy items from a batch.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
item (list[FalsyT]): The batch of items to filter.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
list[FalsyT]: The filtered batch.
|
|
39
|
+
"""
|
|
40
|
+
return [i for i in item if i]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DictBatchFilterProcessor(FalsyBatchFilterProcessor[dict]):
|
|
44
|
+
"""Processor that filters out empty dict items from a batch."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ListBatchFilterProcessor(FalsyBatchFilterProcessor[list]):
|
|
49
|
+
"""Processor that filters out empty list items from a batch."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SetBatchFilterProcessor(FalsyBatchFilterProcessor[set]):
|
|
54
|
+
"""Processor that filters out empty set items from a batch."""
|
|
55
|
+
pass
|
|
@@ -58,6 +58,9 @@ class Processor(Generic[InputT, OutputT], TypeIntrospectionMixin, ABC):
|
|
|
58
58
|
chained processors.
|
|
59
59
|
"""
|
|
60
60
|
|
|
61
|
+
# All processors automatically use this for signature checking
|
|
62
|
+
_signature_method = 'process'
|
|
63
|
+
|
|
61
64
|
@abstractmethod
|
|
62
65
|
async def process(self, item: InputT) -> OutputT:
|
|
63
66
|
"""Process an item and return a potentially different type.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from nat.observability.processor.processor import Processor
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def processor_factory(processor_class: type, from_type: type[Any], to_type: type[Any]) -> type[Processor]:
|
|
22
|
+
"""Create a concrete processor class from a processor class and types.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
processor_class (type): The processor class to create a concrete instance of
|
|
26
|
+
from_type (type[Any]): The type of the input data
|
|
27
|
+
to_type (type[Any]): The type of the output data
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
type[Processor]: The concrete processor class
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
class ConcreteProcessor(processor_class[from_type, to_type]): # type: ignore
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
return ConcreteProcessor
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def processor_factory_from_type(processor_class: type, from_type: type[Any]) -> type[Processor]:
|
|
40
|
+
"""Create a concrete processor class from a processor class and input type.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
processor_class (type): The processor class to create a concrete instance of
|
|
44
|
+
from_type (type[Any]): The type of the input data
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
type[Processor]: The concrete processor class
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
class ConcreteProcessor(processor_class[from_type]): # type: ignore
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return ConcreteProcessor
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def processor_factory_to_type(processor_class: type, to_type: type[Any]) -> type[Processor]:
|
|
57
|
+
"""Create a concrete processor class from a processor class and output type.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
processor_class (type): The processor class to create a concrete instance of
|
|
61
|
+
to_type (type[Any]): The type of the output data
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
type[Processor]: The concrete processor class
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
class ConcreteProcessor(processor_class[to_type]): # type: ignore
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
return ConcreteProcessor
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from .redaction_processor import RedactionContext
|
|
17
|
+
from .redaction_processor import RedactionContextState
|
|
18
|
+
from .span_header_redaction_processor import SpanHeaderRedactionProcessor
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SpanHeaderRedactionProcessor",
|
|
22
|
+
"RedactionContext",
|
|
23
|
+
"RedactionContextState",
|
|
24
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from abc import abstractmethod
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from typing import Any
|
|
20
|
+
from typing import TypeVar
|
|
21
|
+
|
|
22
|
+
from nat.observability.processor.redaction.redaction_processor import RedactionContext
|
|
23
|
+
from nat.observability.processor.redaction.redaction_processor import RedactionContextState
|
|
24
|
+
from nat.observability.processor.redaction.redaction_processor import RedactionInputT
|
|
25
|
+
from nat.observability.processor.redaction.redaction_processor import RedactionProcessor
|
|
26
|
+
from nat.utils.type_utils import override
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Type variable for the data type extracted from context
|
|
31
|
+
RedactionDataT = TypeVar('RedactionDataT')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ContextualRedactionProcessor(RedactionProcessor[RedactionInputT, RedactionDataT]):
|
|
35
|
+
"""Generic processor with context-aware caching for any data type.
|
|
36
|
+
|
|
37
|
+
Provides a framework for redaction processors that need to:
|
|
38
|
+
- Extract data from the request context (headers, cookies, query params, etc.)
|
|
39
|
+
- Execute callbacks to determine redaction decisions
|
|
40
|
+
- Cache results within the request context to avoid redundant callback executions
|
|
41
|
+
- Handle race conditions with atomic operations
|
|
42
|
+
|
|
43
|
+
This class handles all the generic caching, context management, and callback
|
|
44
|
+
execution logic. Subclasses only need to implement data extraction and validation.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
callback: Callable that determines if redaction should occur based on extracted data
|
|
48
|
+
enabled: Whether the processor is enabled
|
|
49
|
+
force_redact: If True, always redact regardless of data checks
|
|
50
|
+
redaction_value: The value to replace redacted attributes with
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
callback: Callable[..., Any],
|
|
56
|
+
enabled: bool,
|
|
57
|
+
force_redact: bool,
|
|
58
|
+
redaction_value: str,
|
|
59
|
+
):
|
|
60
|
+
self.callback = callback
|
|
61
|
+
self.enabled = enabled
|
|
62
|
+
self.force_redact = force_redact
|
|
63
|
+
self.redaction_value = redaction_value
|
|
64
|
+
self._redaction_context = RedactionContext(RedactionContextState())
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def extract_data_from_context(self) -> RedactionDataT | None:
|
|
68
|
+
"""Extract the relevant data from the context for redaction decision.
|
|
69
|
+
|
|
70
|
+
This method must be implemented by subclasses to extract their specific
|
|
71
|
+
data type (headers, cookies, query params, etc.) from the request context
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
RedactionDataT | None: The extracted data, or None if no relevant data found
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def validate_data(self, data: RedactionDataT) -> bool:
|
|
80
|
+
"""Validate that the extracted data is suitable for callback execution.
|
|
81
|
+
|
|
82
|
+
This method allows subclasses to implement their own validation logic
|
|
83
|
+
(e.g., checking if headers exist, if cookies are not empty, etc.).
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
data (RedactionDataT): The extracted data to validate
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
bool: True if the data is valid for callback execution, False otherwise
|
|
90
|
+
"""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
@override
|
|
94
|
+
async def should_redact(self, item: RedactionInputT) -> bool:
|
|
95
|
+
"""Determine if this span should be redacted based on extracted data.
|
|
96
|
+
|
|
97
|
+
Extracts the relevant data from the context, validates it, and passes it to the
|
|
98
|
+
callback function to determine if redaction should occur. Results are cached
|
|
99
|
+
within the request context to avoid redundant callback executions.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
item (RedactionInputT): The item to check
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
bool: True if the span should be redacted, False otherwise
|
|
106
|
+
"""
|
|
107
|
+
# If force_redact is enabled, always redact regardless of other conditions
|
|
108
|
+
if self.force_redact:
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
if not self.enabled:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
# Extract data using subclass implementation
|
|
115
|
+
data = self.extract_data_from_context()
|
|
116
|
+
if data is None:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# Validate data using subclass implementation
|
|
120
|
+
if not self.validate_data(data):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
# Use the generic caching framework for callback execution
|
|
124
|
+
async with self._redaction_context.redaction_manager() as manager:
|
|
125
|
+
return await manager.redaction_check(self.callback, data)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from nat.data_models.span import Span
|
|
20
|
+
from nat.observability.processor.redaction.contextual_redaction_processor import ContextualRedactionProcessor
|
|
21
|
+
from nat.observability.processor.redaction.redaction_processor import RedactionDataT
|
|
22
|
+
from nat.utils.type_utils import override
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ContextualSpanRedactionProcessor(ContextualRedactionProcessor[Span, RedactionDataT]):
|
|
26
|
+
"""Processor that redacts the Span based on the Span attributes.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
attributes: List of span attribute keys to redact
|
|
30
|
+
callback: Callable that determines if redaction should occur
|
|
31
|
+
enabled: Whether the processor is enabled
|
|
32
|
+
force_redact: If True, always redact regardless of callback
|
|
33
|
+
redaction_value: The value to replace redacted attributes with
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self,
|
|
37
|
+
attributes: list[str],
|
|
38
|
+
callback: Callable[..., Any],
|
|
39
|
+
enabled: bool,
|
|
40
|
+
force_redact: bool,
|
|
41
|
+
redaction_value: str,
|
|
42
|
+
redaction_tag: str | None = None):
|
|
43
|
+
super().__init__(callback=callback, enabled=enabled, force_redact=force_redact, redaction_value=redaction_value)
|
|
44
|
+
self.attributes = attributes
|
|
45
|
+
self.redaction_tag = redaction_tag
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
async def redact_item(self, item: Span) -> Span:
|
|
49
|
+
"""Redact specified attributes in the span.
|
|
50
|
+
|
|
51
|
+
Replaces the values of configured attributes with the redaction value.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
item (Span): The span to redact
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Span: The span with redacted attributes
|
|
58
|
+
"""
|
|
59
|
+
for key in self.attributes:
|
|
60
|
+
if key in item.attributes:
|
|
61
|
+
item.set_attribute(key, self.redaction_value)
|
|
62
|
+
|
|
63
|
+
if self.redaction_tag:
|
|
64
|
+
item.set_attribute(self.redaction_tag, True)
|
|
65
|
+
|
|
66
|
+
return item
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from abc import abstractmethod
|
|
18
|
+
from collections.abc import AsyncGenerator
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from contextlib import asynccontextmanager
|
|
21
|
+
from contextvars import ContextVar
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from dataclasses import field
|
|
24
|
+
from typing import Any
|
|
25
|
+
from typing import Generic
|
|
26
|
+
from typing import TypeVar
|
|
27
|
+
|
|
28
|
+
from nat.observability.processor.processor import Processor
|
|
29
|
+
from nat.utils.callable_utils import ainvoke_any
|
|
30
|
+
from nat.utils.type_utils import override
|
|
31
|
+
|
|
32
|
+
RedactionInputT = TypeVar('RedactionInputT')
|
|
33
|
+
RedactionDataT = TypeVar('RedactionDataT')
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RedactionProcessor(Processor[RedactionInputT, RedactionInputT], Generic[RedactionInputT, RedactionDataT]):
|
|
39
|
+
"""Abstract base class for redaction processors."""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def should_redact(self, item: RedactionInputT) -> bool:
|
|
43
|
+
"""Determine if this item should be redacted.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
item (RedactionInputT): The item to check.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
bool: True if the item should be redacted, False otherwise.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def redact_item(self, item: RedactionInputT) -> RedactionInputT:
|
|
55
|
+
"""Redact the item.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
item (RedactionInputT): The item to redact.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
RedactionInputT: The redacted item.
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@override
|
|
66
|
+
async def process(self, item: RedactionInputT) -> RedactionInputT:
|
|
67
|
+
"""Perform redaction on the item if it should be redacted.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
item (RedactionInputT): The item to process.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
RedactionInputT: The processed item.
|
|
74
|
+
"""
|
|
75
|
+
if await self.should_redact(item):
|
|
76
|
+
return await self.redact_item(item)
|
|
77
|
+
return item
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class RedactionContextState:
|
|
82
|
+
"""Generic context state for redaction results.
|
|
83
|
+
|
|
84
|
+
Stores the redaction result in a context variable to avoid redundant
|
|
85
|
+
callback executions within the same request context.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
redaction_result: ContextVar[bool
|
|
89
|
+
| None] = field(default_factory=lambda: ContextVar("redaction_result", default=None))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class RedactionManager(Generic[RedactionDataT]):
|
|
93
|
+
"""Generic manager for atomic redaction operations.
|
|
94
|
+
|
|
95
|
+
Handles state mutations and ensures atomic callback execution
|
|
96
|
+
with proper result caching within a request context.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
RedactionDataT: The type of data being processed for redaction decisions.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, context_state: RedactionContextState):
|
|
103
|
+
self._context_state = context_state
|
|
104
|
+
|
|
105
|
+
def set_redaction_result(self, result: bool) -> None:
|
|
106
|
+
"""Set the redaction result in the context.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
result (bool): The redaction result to cache.
|
|
110
|
+
"""
|
|
111
|
+
self._context_state.redaction_result.set(result)
|
|
112
|
+
|
|
113
|
+
def clear_redaction_result(self) -> None:
|
|
114
|
+
"""Clear the cached redaction result from the context."""
|
|
115
|
+
self._context_state.redaction_result.set(None)
|
|
116
|
+
|
|
117
|
+
async def redaction_check(self, callback: Callable[..., Any], data: RedactionDataT) -> bool:
|
|
118
|
+
"""Execute redaction callback with atomic result caching.
|
|
119
|
+
|
|
120
|
+
Checks for existing cached results first, then executes the callback
|
|
121
|
+
and caches the result atomically. Since data is static per request,
|
|
122
|
+
subsequent calls within the same context return the cached result.
|
|
123
|
+
|
|
124
|
+
Supports sync/async functions, generators, and async generators.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
callback (Callable[..., Any]): The callback to execute (sync/async function, generator, etc.).
|
|
128
|
+
data (RedactionDataT): The data to pass to the callback for redaction decision.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
bool: True if the item should be redacted, False otherwise.
|
|
132
|
+
"""
|
|
133
|
+
# Check if we already have a result for this context
|
|
134
|
+
existing_result = self._context_state.redaction_result.get()
|
|
135
|
+
if existing_result is not None:
|
|
136
|
+
return existing_result
|
|
137
|
+
|
|
138
|
+
# Execute callback and cache result
|
|
139
|
+
result_value = await ainvoke_any(callback, data)
|
|
140
|
+
result = bool(result_value)
|
|
141
|
+
self.set_redaction_result(result)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RedactionContext(Generic[RedactionDataT]):
|
|
146
|
+
"""Generic context provider for redaction operations.
|
|
147
|
+
|
|
148
|
+
Provides read-only access to redaction state and manages the
|
|
149
|
+
RedactionManager lifecycle through async context managers.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
RedactionDataT: The type of data being processed for redaction decisions.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, context: RedactionContextState):
|
|
156
|
+
self._context_state: RedactionContextState = context
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def redaction_result(self) -> bool | None:
|
|
160
|
+
"""Get the current redaction result from context.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
bool | None: The cached redaction result, or None if not set.
|
|
164
|
+
"""
|
|
165
|
+
return self._context_state.redaction_result.get()
|
|
166
|
+
|
|
167
|
+
@asynccontextmanager
|
|
168
|
+
async def redaction_manager(self) -> AsyncGenerator[RedactionManager[RedactionDataT], None]:
|
|
169
|
+
"""Provide a redaction manager within an async context.
|
|
170
|
+
|
|
171
|
+
Creates and yields a RedactionManager instance for atomic
|
|
172
|
+
redaction operations within the current context.
|
|
173
|
+
|
|
174
|
+
Yields:
|
|
175
|
+
RedactionManager[RedactionDataT]: Manager instance for redaction operations.
|
|
176
|
+
"""
|
|
177
|
+
yield RedactionManager(self._context_state)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from starlette.datastructures import Headers
|
|
21
|
+
|
|
22
|
+
from nat.builder.context import Context
|
|
23
|
+
from nat.observability.processor.redaction.contextual_span_redaction_processor import ContextualSpanRedactionProcessor
|
|
24
|
+
from nat.utils.type_utils import override
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SpanHeaderRedactionProcessor(ContextualSpanRedactionProcessor[dict[str, Any]]):
|
|
30
|
+
"""Processor that redacts the Span based on multiple headers and callback logic.
|
|
31
|
+
|
|
32
|
+
Uses context-scoped atomic updates to avoid redundant callback executions within a single context.
|
|
33
|
+
Since headers are static per request, the callback result is cached for the entire context using
|
|
34
|
+
an asynccontextmanager to ensure atomic operations.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
headers: List of header keys to extract and pass to the callback
|
|
38
|
+
attributes: List of Span attribute keys to redact
|
|
39
|
+
callback: Callable that determines if redaction should occur
|
|
40
|
+
enabled: Whether the processor is enabled (default: True)
|
|
41
|
+
force_redact: If True, always redact regardless of header checks (default: False)
|
|
42
|
+
redaction_value: The value to replace redacted attributes with (default: "[REDACTED]")
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self,
|
|
46
|
+
headers: list[str],
|
|
47
|
+
attributes: list[str],
|
|
48
|
+
callback: Callable[..., Any],
|
|
49
|
+
enabled: bool = True,
|
|
50
|
+
force_redact: bool = False,
|
|
51
|
+
redaction_value: str = "[REDACTED]",
|
|
52
|
+
redaction_tag: str | None = None):
|
|
53
|
+
# Initialize the base class with common parameters
|
|
54
|
+
super().__init__(attributes=attributes,
|
|
55
|
+
callback=callback,
|
|
56
|
+
enabled=enabled,
|
|
57
|
+
force_redact=force_redact,
|
|
58
|
+
redaction_value=redaction_value,
|
|
59
|
+
redaction_tag=redaction_tag)
|
|
60
|
+
# Store header-specific configuration
|
|
61
|
+
self.headers = headers
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
def extract_data_from_context(self) -> dict[str, Any] | None:
|
|
65
|
+
"""Extract header data from the context.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
dict[str, Any] | None: Dictionary of header names to values, or None if no headers.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
context = Context.get()
|
|
72
|
+
headers: Headers | None = context.metadata.headers
|
|
73
|
+
|
|
74
|
+
if headers is None or not self.headers:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
header_map: dict[str, Any] = {header: headers.get(header, None) for header in self.headers}
|
|
78
|
+
|
|
79
|
+
return header_map
|
|
80
|
+
|
|
81
|
+
@override
|
|
82
|
+
def validate_data(self, data: dict[str, Any]) -> bool:
|
|
83
|
+
"""Validate that the extracted headers are suitable for callback execution.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
data (dict[str, Any]): The extracted header dictionary.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
bool: True if headers exist and are not all None, False otherwise.
|
|
90
|
+
"""
|
|
91
|
+
# Skip callback if no headers were found (all None values)
|
|
92
|
+
return bool(data) and not all(value is None for value in data.values())
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from enum import Enum
|
|
20
|
+
|
|
21
|
+
from nat.data_models.span import Span
|
|
22
|
+
from nat.observability.processor.processor import Processor
|
|
23
|
+
from nat.utils.type_utils import override
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SpanTaggingProcessor(Processor[Span, Span]):
|
|
29
|
+
"""Processor that tags spans with multiple key-value metadata attributes.
|
|
30
|
+
|
|
31
|
+
This processor adds custom tags to spans by setting attributes with a configurable prefix.
|
|
32
|
+
Tags are applied for each key-value pair in the tags dictionary. The processor uses
|
|
33
|
+
a span prefix (configurable via NAT_SPAN_PREFIX environment variable) to namespace
|
|
34
|
+
the tag attributes.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
tags: Mapping of tag keys to their values. Values can be enums (converted to strings) or strings
|
|
38
|
+
span_prefix: The prefix to use for tag attributes (default: from NAT_SPAN_PREFIX env var or "nat")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, tags: Mapping[str, Enum | str] | None = None, span_prefix: str | None = None):
|
|
42
|
+
self.tags = tags or {}
|
|
43
|
+
|
|
44
|
+
if span_prefix is None:
|
|
45
|
+
span_prefix = os.getenv("NAT_SPAN_PREFIX", "nat").strip() or "nat"
|
|
46
|
+
|
|
47
|
+
self._span_prefix = span_prefix
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
async def process(self, item: Span) -> Span:
|
|
51
|
+
"""Tag the span with all configured tags.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
item (Span): The span to tag
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Span: The tagged span with all configured tags applied
|
|
58
|
+
"""
|
|
59
|
+
for tag_key, tag_value in self.tags.items():
|
|
60
|
+
key = str(tag_key).strip()
|
|
61
|
+
if not key:
|
|
62
|
+
continue
|
|
63
|
+
value_str = str(tag_value.value) if isinstance(tag_value, Enum) else str(tag_value)
|
|
64
|
+
if value_str == "":
|
|
65
|
+
continue
|
|
66
|
+
item.set_attribute(f"{self._span_prefix}.{key}", value_str)
|
|
67
|
+
|
|
68
|
+
return item
|