answerrocket-client 0.2.105__tar.gz → 0.2.106__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/PKG-INFO +1 -1
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/__init__.py +1 -1
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/client.py +2 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/data.py +6 -72
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/graphql/client.py +1 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/graphql/schema.py +100 -1
- answerrocket_client-0.2.106/answer_rocket/observability.py +241 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answerrocket_client.egg-info/PKG-INFO +1 -1
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answerrocket_client.egg-info/SOURCES.txt +3 -1
- answerrocket_client-0.2.106/test/test_observability.py +437 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/auth.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/chat.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/client_config.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/config.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/email.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/error.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/graphql/__init__.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/graphql/sdk_operations.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/layouts.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/llm.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/output.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/skill.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/types.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/util/__init__.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/util/meta_data_frame.py +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answerrocket_client.egg-info/dependency_links.txt +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answerrocket_client.egg-info/requires.txt +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answerrocket_client.egg-info/top_level.txt +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/pyproject.toml +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/readme.md +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/setup.cfg +0 -0
- {answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/test/test_client.py +0 -0
|
@@ -10,6 +10,7 @@ from answer_rocket.skill import Skill
|
|
|
10
10
|
from answer_rocket.llm import Llm
|
|
11
11
|
from answer_rocket.layouts import DynamicLayouts
|
|
12
12
|
from answer_rocket.email import Email
|
|
13
|
+
from answer_rocket.observability import Observability
|
|
13
14
|
|
|
14
15
|
class AnswerRocketClient:
|
|
15
16
|
"""
|
|
@@ -41,6 +42,7 @@ class AnswerRocketClient:
|
|
|
41
42
|
self.llm = Llm(self._client_config, self._gql_client)
|
|
42
43
|
self.dynamic_layouts = DynamicLayouts(self._client_config, self._gql_client)
|
|
43
44
|
self.email = Email(self._client_config, self._gql_client)
|
|
45
|
+
self.observability = Observability(self._client_config, self._gql_client)
|
|
44
46
|
|
|
45
47
|
def can_connect(self) -> bool:
|
|
46
48
|
"""
|
|
@@ -2452,33 +2452,9 @@ class Data:
|
|
|
2452
2452
|
self, dataset_id: UUID
|
|
2453
2453
|
) -> List[TrackedDimensionAttribute]:
|
|
2454
2454
|
"""
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
Parameters
|
|
2459
|
-
----------
|
|
2460
|
-
dataset_id : UUID
|
|
2461
|
-
Dataset the starred values belong to.
|
|
2462
|
-
|
|
2463
|
-
Returns
|
|
2464
|
-
-------
|
|
2465
|
-
List[TrackedDimensionAttribute]
|
|
2466
|
-
One entry per dimension the caller has stars in. Each entry exposes
|
|
2467
|
-
``dimension_attribute_id``, ``dimension_name``, and ``values``
|
|
2468
|
-
(currently-active starred values for that dimension).
|
|
2469
|
-
|
|
2470
|
-
Examples
|
|
2471
|
-
--------
|
|
2472
|
-
>>> attrs = max.tracked_values.get_tracked_dimension_values(
|
|
2473
|
-
... dataset_id=uuid.UUID("...")
|
|
2474
|
-
... )
|
|
2475
|
-
>>> for attr in attrs:
|
|
2476
|
-
... print(attr.dimension_name, attr.values)
|
|
2477
|
-
"""
|
|
2478
|
-
query_args = {"datasetId": str(dataset_id)}
|
|
2479
|
-
op = Operations.query.get_tracked_dimension_values
|
|
2480
|
-
result = self._gql_client.submit(op, query_args)
|
|
2481
|
-
return result.get_tracked_dimension_values
|
|
2455
|
+
DEPRECATED in v0.2.106
|
|
2456
|
+
"""
|
|
2457
|
+
raise DeprecationWarning("Deprecated in v0.2.106")
|
|
2482
2458
|
|
|
2483
2459
|
def get_all_tracked_dimension_values(
|
|
2484
2460
|
self,
|
|
@@ -2489,48 +2465,6 @@ class Data:
|
|
|
2489
2465
|
sort: Optional[list] = None,
|
|
2490
2466
|
) -> TrackedDimensionValuesPage:
|
|
2491
2467
|
"""
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
Parameters
|
|
2496
|
-
----------
|
|
2497
|
-
offset : int, default 0
|
|
2498
|
-
Zero-based offset for paging.
|
|
2499
|
-
limit : int, default 100
|
|
2500
|
-
Max rows to return.
|
|
2501
|
-
dataset_id : UUID, optional
|
|
2502
|
-
If provided, scope results to a single dataset.
|
|
2503
|
-
filters : dict, optional
|
|
2504
|
-
AG-Grid filterModel JSON (per-column filters). Keys are column ids
|
|
2505
|
-
(e.g. ``"userName"``, ``"value"``, ``"addedUtc"``); values follow
|
|
2506
|
-
AG-Grid's text/set/date filter shapes.
|
|
2507
|
-
sort : list, optional
|
|
2508
|
-
AG-Grid sortModel JSON (list of ``{"colId": ..., "sort": "asc" |
|
|
2509
|
-
"desc", "sortIndex": ...}`` entries).
|
|
2510
|
-
|
|
2511
|
-
Returns
|
|
2512
|
-
-------
|
|
2513
|
-
TrackedDimensionValuesPage
|
|
2514
|
-
Object with ``total_count`` (count after filters but before paging)
|
|
2515
|
-
and ``rows`` (the requested page of joined user/dataset rows).
|
|
2516
|
-
|
|
2517
|
-
Examples
|
|
2518
|
-
--------
|
|
2519
|
-
>>> page = max.tracked_values.get_all_tracked_dimension_values(
|
|
2520
|
-
... dataset_id=uuid.UUID("..."),
|
|
2521
|
-
... limit=500,
|
|
2522
|
-
... )
|
|
2523
|
-
>>> print(page.total_count)
|
|
2524
|
-
>>> for row in page.rows:
|
|
2525
|
-
... print(row.user_name, row.dimension_name, row.value)
|
|
2526
|
-
"""
|
|
2527
|
-
query_args: dict[str, Any] = {
|
|
2528
|
-
"offset": offset,
|
|
2529
|
-
"limit": limit,
|
|
2530
|
-
"datasetId": str(dataset_id) if dataset_id is not None else None,
|
|
2531
|
-
"filters": filters,
|
|
2532
|
-
"sort": sort,
|
|
2533
|
-
}
|
|
2534
|
-
op = Operations.query.get_all_tracked_dimension_values
|
|
2535
|
-
result = self._gql_client.submit(op, query_args)
|
|
2536
|
-
return result.get_all_tracked_dimension_values
|
|
2468
|
+
DEPRECATED in v0.2.106
|
|
2469
|
+
"""
|
|
2470
|
+
raise DeprecationWarning("Deprecated in v0.2.106")
|
|
@@ -335,6 +335,22 @@ class MaxDomainEntity(sgqlc.types.Interface):
|
|
|
335
335
|
attributes = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(MaxDomainAttribute))), graphql_name='attributes')
|
|
336
336
|
|
|
337
337
|
|
|
338
|
+
class AnyValue(sgqlc.types.Type):
|
|
339
|
+
__schema__ = schema
|
|
340
|
+
__field_names__ = ('string_value', 'bool_value', 'int_value', 'double_value', 'array_value')
|
|
341
|
+
string_value = sgqlc.types.Field(String, graphql_name='stringValue')
|
|
342
|
+
bool_value = sgqlc.types.Field(Boolean, graphql_name='boolValue')
|
|
343
|
+
int_value = sgqlc.types.Field(String, graphql_name='intValue')
|
|
344
|
+
double_value = sgqlc.types.Field(Float, graphql_name='doubleValue')
|
|
345
|
+
array_value = sgqlc.types.Field('ArrayValue', graphql_name='arrayValue')
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class ArrayValue(sgqlc.types.Type):
|
|
349
|
+
__schema__ = schema
|
|
350
|
+
__field_names__ = ('values',)
|
|
351
|
+
values = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(AnyValue))), graphql_name='values')
|
|
352
|
+
|
|
353
|
+
|
|
338
354
|
class AsyncSkillRunResponse(sgqlc.types.Type):
|
|
339
355
|
__schema__ = schema
|
|
340
356
|
__field_names__ = ('execution_id', 'success', 'code', 'error')
|
|
@@ -763,6 +779,20 @@ class HydratedReport(sgqlc.types.Type):
|
|
|
763
779
|
meta = sgqlc.types.Field(JSON, graphql_name='meta')
|
|
764
780
|
|
|
765
781
|
|
|
782
|
+
class InstrumentationScope(sgqlc.types.Type):
|
|
783
|
+
__schema__ = schema
|
|
784
|
+
__field_names__ = ('name', 'version')
|
|
785
|
+
name = sgqlc.types.Field(sgqlc.types.non_null(String), graphql_name='name')
|
|
786
|
+
version = sgqlc.types.Field(String, graphql_name='version')
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
class KeyValue(sgqlc.types.Type):
|
|
790
|
+
__schema__ = schema
|
|
791
|
+
__field_names__ = ('key', 'value')
|
|
792
|
+
key = sgqlc.types.Field(sgqlc.types.non_null(String), graphql_name='key')
|
|
793
|
+
value = sgqlc.types.Field(sgqlc.types.non_null(AnyValue), graphql_name='value')
|
|
794
|
+
|
|
795
|
+
|
|
766
796
|
class LayoutPdfResponse(sgqlc.types.Type):
|
|
767
797
|
__schema__ = schema
|
|
768
798
|
__field_names__ = ('success', 'code', 'error', 'pdf')
|
|
@@ -1636,6 +1666,27 @@ class Mutation(sgqlc.types.Type):
|
|
|
1636
1666
|
)
|
|
1637
1667
|
|
|
1638
1668
|
|
|
1669
|
+
class ObservabilityTrace(sgqlc.types.Type):
|
|
1670
|
+
__schema__ = schema
|
|
1671
|
+
__field_names__ = ('resource_spans',)
|
|
1672
|
+
resource_spans = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null('ResourceSpans'))), graphql_name='resourceSpans')
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
class ObservabilityTracesResult(sgqlc.types.Type):
|
|
1676
|
+
__schema__ = schema
|
|
1677
|
+
__field_names__ = ('count', 'has_more', 'next_cursor', 'traces')
|
|
1678
|
+
count = sgqlc.types.Field(sgqlc.types.non_null(Int), graphql_name='count')
|
|
1679
|
+
has_more = sgqlc.types.Field(sgqlc.types.non_null(Boolean), graphql_name='hasMore')
|
|
1680
|
+
next_cursor = sgqlc.types.Field(String, graphql_name='nextCursor')
|
|
1681
|
+
traces = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(ObservabilityTrace))), graphql_name='traces')
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
class OtlpResource(sgqlc.types.Type):
|
|
1685
|
+
__schema__ = schema
|
|
1686
|
+
__field_names__ = ('attributes',)
|
|
1687
|
+
attributes = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(KeyValue))), graphql_name='attributes')
|
|
1688
|
+
|
|
1689
|
+
|
|
1639
1690
|
class PagedChatArtifacts(sgqlc.types.Type):
|
|
1640
1691
|
__schema__ = schema
|
|
1641
1692
|
__field_names__ = ('total_rows', 'rows')
|
|
@@ -1696,7 +1747,7 @@ class ParameterDefinition(sgqlc.types.Type):
|
|
|
1696
1747
|
|
|
1697
1748
|
class Query(sgqlc.types.Type):
|
|
1698
1749
|
__schema__ = schema
|
|
1699
|
-
__field_names__ = ('ping', 'current_user', 'get_copilot_skill_artifact_by_path', 'get_copilots', 'get_copilot_info', 'get_copilot_skill', 'run_copilot_skill', 'get_skill_components', 'get_copilot_hydrated_reports', 'get_async_skill_run_status', 'get_copilot_test_run', 'get_paged_copilot_test_runs', 'get_copilot_question_folders', 'get_max_agent_workflow', 'execute_sql_query', 'execute_rql_query', 'get_databases', 'get_database', 'get_database_tables', 'get_dataset_id', 'get_dataset', 'get_dataset2', 'get_datasets', 'get_domain_object', 'get_domain_object_by_name', 'get_grounded_value', 'get_database_kshots', 'get_database_kshot_by_id', 'get_dataset_kshots', 'get_dataset_kshot_by_id', 'run_max_sql_gen', 'run_sql_ai', 'generate_visualization', 'llmapi_config_for_sdk', 'generate_embeddings', 'get_max_llm_prompt', 'user_chat_threads', 'user_chat_entries', 'chat_thread', 'chat_entry', 'user', 'all_chat_entries', 'skill_memory', 'chat_completion', 'narrative_completion', 'narrative_completion_with_prompt', 'sql_completion', 'research_completion', 'chat_completion_with_prompt', 'research_completion_with_prompt', 'get_chat_artifact', 'get_chat_artifacts', 'get_dynamic_layout', 'get_tracked_dimension_values', 'get_all_tracked_dimension_values')
|
|
1750
|
+
__field_names__ = ('ping', 'current_user', 'get_copilot_skill_artifact_by_path', 'get_copilots', 'get_copilot_info', 'get_copilot_skill', 'run_copilot_skill', 'get_skill_components', 'get_copilot_hydrated_reports', 'get_async_skill_run_status', 'get_copilot_test_run', 'get_paged_copilot_test_runs', 'get_copilot_question_folders', 'get_max_agent_workflow', 'execute_sql_query', 'execute_rql_query', 'get_databases', 'get_database', 'get_database_tables', 'get_dataset_id', 'get_dataset', 'get_dataset2', 'get_datasets', 'get_domain_object', 'get_domain_object_by_name', 'get_grounded_value', 'get_database_kshots', 'get_database_kshot_by_id', 'get_dataset_kshots', 'get_dataset_kshot_by_id', 'run_max_sql_gen', 'run_sql_ai', 'generate_visualization', 'llmapi_config_for_sdk', 'generate_embeddings', 'get_max_llm_prompt', 'user_chat_threads', 'user_chat_entries', 'chat_thread', 'chat_entry', 'user', 'all_chat_entries', 'skill_memory', 'chat_completion', 'narrative_completion', 'narrative_completion_with_prompt', 'sql_completion', 'research_completion', 'chat_completion_with_prompt', 'research_completion_with_prompt', 'get_chat_artifact', 'get_chat_artifacts', 'get_dynamic_layout', 'get_tracked_dimension_values', 'get_all_tracked_dimension_values', 'observability_traces')
|
|
1700
1751
|
ping = sgqlc.types.Field(String, graphql_name='ping')
|
|
1701
1752
|
current_user = sgqlc.types.Field(MaxUser, graphql_name='currentUser')
|
|
1702
1753
|
get_copilot_skill_artifact_by_path = sgqlc.types.Field(CopilotSkillArtifact, graphql_name='getCopilotSkillArtifactByPath', args=sgqlc.types.ArgDict((
|
|
@@ -1989,6 +2040,18 @@ class Query(sgqlc.types.Type):
|
|
|
1989
2040
|
('sort', sgqlc.types.Arg(JSON, graphql_name='sort', default=None)),
|
|
1990
2041
|
))
|
|
1991
2042
|
)
|
|
2043
|
+
observability_traces = sgqlc.types.Field(sgqlc.types.non_null(ObservabilityTracesResult), graphql_name='observabilityTraces', args=sgqlc.types.ArgDict((
|
|
2044
|
+
('since', sgqlc.types.Arg(sgqlc.types.non_null(String), graphql_name='since', default=None)),
|
|
2045
|
+
('limit', sgqlc.types.Arg(Int, graphql_name='limit', default=None)),
|
|
2046
|
+
))
|
|
2047
|
+
)
|
|
2048
|
+
|
|
2049
|
+
|
|
2050
|
+
class ResourceSpans(sgqlc.types.Type):
|
|
2051
|
+
__schema__ = schema
|
|
2052
|
+
__field_names__ = ('resource', 'scope_spans')
|
|
2053
|
+
resource = sgqlc.types.Field(sgqlc.types.non_null(OtlpResource), graphql_name='resource')
|
|
2054
|
+
scope_spans = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null('ScopeSpans'))), graphql_name='scopeSpans')
|
|
1992
2055
|
|
|
1993
2056
|
|
|
1994
2057
|
class RunMaxSqlGenResponse(sgqlc.types.Type):
|
|
@@ -2019,6 +2082,13 @@ class RunSqlAiResponse(sgqlc.types.Type):
|
|
|
2019
2082
|
prior_runs = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null('RunSqlAiResponse'))), graphql_name='priorRuns')
|
|
2020
2083
|
|
|
2021
2084
|
|
|
2085
|
+
class ScopeSpans(sgqlc.types.Type):
|
|
2086
|
+
__schema__ = schema
|
|
2087
|
+
__field_names__ = ('scope', 'spans')
|
|
2088
|
+
scope = sgqlc.types.Field(sgqlc.types.non_null(InstrumentationScope), graphql_name='scope')
|
|
2089
|
+
spans = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null('Span'))), graphql_name='spans')
|
|
2090
|
+
|
|
2091
|
+
|
|
2022
2092
|
class SharedThread(sgqlc.types.Type):
|
|
2023
2093
|
__schema__ = schema
|
|
2024
2094
|
__field_names__ = ('id', 'user_id', 'original_thread_id', 'copilot_id', 'shared_by', 'last_updated_utc', 'created_utc', 'is_deleted', 'link_to_shared_thread')
|
|
@@ -2066,6 +2136,35 @@ class SkillParameter(sgqlc.types.Type):
|
|
|
2066
2136
|
match_values = sgqlc.types.Field(MatchValues, graphql_name='matchValues')
|
|
2067
2137
|
|
|
2068
2138
|
|
|
2139
|
+
class Span(sgqlc.types.Type):
|
|
2140
|
+
__schema__ = schema
|
|
2141
|
+
__field_names__ = ('trace_id', 'span_id', 'parent_span_id', 'name', 'kind', 'start_time_unix_nano', 'end_time_unix_nano', 'attributes', 'events', 'status')
|
|
2142
|
+
trace_id = sgqlc.types.Field(sgqlc.types.non_null(String), graphql_name='traceId')
|
|
2143
|
+
span_id = sgqlc.types.Field(sgqlc.types.non_null(String), graphql_name='spanId')
|
|
2144
|
+
parent_span_id = sgqlc.types.Field(String, graphql_name='parentSpanId')
|
|
2145
|
+
name = sgqlc.types.Field(sgqlc.types.non_null(String), graphql_name='name')
|
|
2146
|
+
kind = sgqlc.types.Field(sgqlc.types.non_null(String), graphql_name='kind')
|
|
2147
|
+
start_time_unix_nano = sgqlc.types.Field(sgqlc.types.non_null(String), graphql_name='startTimeUnixNano')
|
|
2148
|
+
end_time_unix_nano = sgqlc.types.Field(sgqlc.types.non_null(String), graphql_name='endTimeUnixNano')
|
|
2149
|
+
attributes = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(KeyValue))), graphql_name='attributes')
|
|
2150
|
+
events = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null('SpanEvent'))), graphql_name='events')
|
|
2151
|
+
status = sgqlc.types.Field('SpanStatus', graphql_name='status')
|
|
2152
|
+
|
|
2153
|
+
|
|
2154
|
+
class SpanEvent(sgqlc.types.Type):
|
|
2155
|
+
__schema__ = schema
|
|
2156
|
+
__field_names__ = ('time_unix_nano', 'name', 'attributes')
|
|
2157
|
+
time_unix_nano = sgqlc.types.Field(String, graphql_name='timeUnixNano')
|
|
2158
|
+
name = sgqlc.types.Field(sgqlc.types.non_null(String), graphql_name='name')
|
|
2159
|
+
attributes = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(KeyValue))), graphql_name='attributes')
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
class SpanStatus(sgqlc.types.Type):
|
|
2163
|
+
__schema__ = schema
|
|
2164
|
+
__field_names__ = ('code',)
|
|
2165
|
+
code = sgqlc.types.Field(String, graphql_name='code')
|
|
2166
|
+
|
|
2167
|
+
|
|
2069
2168
|
class TrackedDimensionAttribute(sgqlc.types.Type):
|
|
2070
2169
|
__schema__ = schema
|
|
2071
2170
|
__field_names__ = ('dimension_attribute_id', 'dimension_name', 'values')
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observability client for AnswerRocket.
|
|
3
|
+
|
|
4
|
+
Fetches pre-assembled OTLP/JSON traces from the observabilityTraces GraphQL
|
|
5
|
+
endpoint and delivers them to callers for forwarding to any OTel collector's
|
|
6
|
+
/v1/traces endpoint.
|
|
7
|
+
|
|
8
|
+
OTLP assembly is performed server-side in Kotlin (ObservabilityPublicSchemaModule).
|
|
9
|
+
Each trace is a complete OTLP ExportTraceServiceRequest object.
|
|
10
|
+
|
|
11
|
+
Usage patterns:
|
|
12
|
+
|
|
13
|
+
# Single page
|
|
14
|
+
batch = client.observability.get_traces(since="2026-05-01T00:00:00Z")
|
|
15
|
+
|
|
16
|
+
# All pages from a cursor
|
|
17
|
+
for batch in client.observability.iter_traces(since="2026-05-01T00:00:00Z"):
|
|
18
|
+
forward_to_collector(batch)
|
|
19
|
+
|
|
20
|
+
# Continuous polling loop
|
|
21
|
+
for batch in client.observability.poll_traces(since="2026-05-01T00:00:00Z"):
|
|
22
|
+
forward_to_collector(batch)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import time
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
from typing import Callable, Generator, Iterator
|
|
32
|
+
|
|
33
|
+
from answer_rocket.client_config import ClientConfig
|
|
34
|
+
from answer_rocket.graphql.client import GraphQlClient
|
|
35
|
+
|
|
36
|
+
_logger = logging.getLogger("answer_rocket.observability")
|
|
37
|
+
|
|
38
|
+
DEFAULT_LIMIT = 100
|
|
39
|
+
DEFAULT_POLL_INTERVAL_SECONDS = 60.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class TracesBatch:
|
|
44
|
+
"""One page of OTLP/JSON traces."""
|
|
45
|
+
success: bool = False
|
|
46
|
+
error: str | None = None
|
|
47
|
+
count: int = 0
|
|
48
|
+
has_more: bool = False
|
|
49
|
+
next_cursor: str | None = None
|
|
50
|
+
traces: list[dict] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Observability:
|
|
54
|
+
def __init__(self, config: ClientConfig, gql_client: GraphQlClient):
|
|
55
|
+
self._config = config
|
|
56
|
+
self._gql_client = gql_client
|
|
57
|
+
|
|
58
|
+
def get_traces(
|
|
59
|
+
self,
|
|
60
|
+
since: str | datetime,
|
|
61
|
+
limit: int = DEFAULT_LIMIT,
|
|
62
|
+
) -> TracesBatch:
|
|
63
|
+
since_str = _to_iso(since)
|
|
64
|
+
try:
|
|
65
|
+
op = self._gql_client.query()
|
|
66
|
+
obs = op.observability_traces(since=since_str, limit=limit)
|
|
67
|
+
obs.count()
|
|
68
|
+
obs.has_more()
|
|
69
|
+
obs.next_cursor()
|
|
70
|
+
# The full OTLP span tree is strongly typed in the schema, so every field
|
|
71
|
+
# (including attribute KeyValue/AnyValue) must be selected explicitly.
|
|
72
|
+
resource_spans = obs.traces().resource_spans()
|
|
73
|
+
_select_key_values(resource_spans.resource().attributes())
|
|
74
|
+
scope_spans = resource_spans.scope_spans()
|
|
75
|
+
scope = scope_spans.scope()
|
|
76
|
+
scope.name()
|
|
77
|
+
scope.version()
|
|
78
|
+
spans = scope_spans.spans()
|
|
79
|
+
spans.trace_id()
|
|
80
|
+
spans.span_id()
|
|
81
|
+
spans.parent_span_id()
|
|
82
|
+
spans.name()
|
|
83
|
+
spans.kind()
|
|
84
|
+
spans.start_time_unix_nano()
|
|
85
|
+
spans.end_time_unix_nano()
|
|
86
|
+
_select_key_values(spans.attributes())
|
|
87
|
+
events = spans.events()
|
|
88
|
+
events.time_unix_nano()
|
|
89
|
+
events.name()
|
|
90
|
+
_select_key_values(events.attributes())
|
|
91
|
+
spans.status().code()
|
|
92
|
+
|
|
93
|
+
result = self._gql_client.submit(op)
|
|
94
|
+
page = result.observability_traces
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
return TracesBatch(success=False, error=str(exc))
|
|
97
|
+
|
|
98
|
+
return TracesBatch(
|
|
99
|
+
success=True,
|
|
100
|
+
count=page.count,
|
|
101
|
+
has_more=page.has_more,
|
|
102
|
+
next_cursor=page.next_cursor,
|
|
103
|
+
traces=[_trace_to_otlp(t) for t in (page.traces or [])],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def iter_traces(
|
|
107
|
+
self,
|
|
108
|
+
since: str | datetime,
|
|
109
|
+
limit: int = DEFAULT_LIMIT,
|
|
110
|
+
) -> Iterator[list[dict]]:
|
|
111
|
+
cursor: str | datetime = since
|
|
112
|
+
while True:
|
|
113
|
+
result = self.get_traces(cursor, limit=limit)
|
|
114
|
+
if not result.success:
|
|
115
|
+
_logger.error("iter_traces page error: %s", result.error)
|
|
116
|
+
return
|
|
117
|
+
if result.traces:
|
|
118
|
+
yield result.traces
|
|
119
|
+
if not result.has_more or not result.next_cursor:
|
|
120
|
+
return
|
|
121
|
+
cursor = result.next_cursor
|
|
122
|
+
|
|
123
|
+
def poll_traces(
|
|
124
|
+
self,
|
|
125
|
+
since: str | datetime,
|
|
126
|
+
on_batch: Callable[[list[dict]], None] | None = None,
|
|
127
|
+
limit: int = DEFAULT_LIMIT,
|
|
128
|
+
poll_interval_seconds: float = DEFAULT_POLL_INTERVAL_SECONDS,
|
|
129
|
+
max_iterations: int | None = None,
|
|
130
|
+
) -> Generator[list[dict], None, None]:
|
|
131
|
+
cursor: str | datetime = since
|
|
132
|
+
iterations = 0
|
|
133
|
+
while max_iterations is None or iterations < max_iterations:
|
|
134
|
+
result = self.get_traces(cursor, limit=limit)
|
|
135
|
+
if not result.success:
|
|
136
|
+
_logger.warning("poll_traces error (will retry): %s", result.error)
|
|
137
|
+
time.sleep(poll_interval_seconds)
|
|
138
|
+
iterations += 1
|
|
139
|
+
continue
|
|
140
|
+
if result.traces:
|
|
141
|
+
if on_batch is not None:
|
|
142
|
+
on_batch(result.traces)
|
|
143
|
+
yield result.traces
|
|
144
|
+
if result.next_cursor:
|
|
145
|
+
cursor = result.next_cursor
|
|
146
|
+
else:
|
|
147
|
+
time.sleep(poll_interval_seconds)
|
|
148
|
+
iterations += 1
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _to_iso(dt: str | datetime) -> str:
|
|
152
|
+
if isinstance(dt, datetime):
|
|
153
|
+
if dt.tzinfo is None:
|
|
154
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
155
|
+
return dt.isoformat()
|
|
156
|
+
return dt
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# GraphQL selection + reconstruction of OTLP/JSON from the typed span tree.
|
|
161
|
+
#
|
|
162
|
+
# The server emits a fully-typed, versionable span tree; collectors want plain OTLP
|
|
163
|
+
# JSON. These helpers select every typed field and map the objects back to the OTLP
|
|
164
|
+
# wire shape, omitting absent optional fields so the output matches an OTLP exporter.
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def _select_key_values(kv_selector) -> None:
|
|
168
|
+
"""Select an OTLP KeyValue list, including one level of AnyValue.arrayValue."""
|
|
169
|
+
kv_selector.key()
|
|
170
|
+
value = kv_selector.value()
|
|
171
|
+
value.string_value()
|
|
172
|
+
value.bool_value()
|
|
173
|
+
value.int_value()
|
|
174
|
+
value.double_value()
|
|
175
|
+
inner = value.array_value().values()
|
|
176
|
+
inner.string_value()
|
|
177
|
+
inner.bool_value()
|
|
178
|
+
inner.int_value()
|
|
179
|
+
inner.double_value()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _trace_to_otlp(trace) -> dict:
|
|
183
|
+
return {"resourceSpans": [_resource_spans_to_otlp(rs) for rs in (trace.resource_spans or [])]}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _resource_spans_to_otlp(rs) -> dict:
|
|
187
|
+
return {
|
|
188
|
+
"resource": {"attributes": [_key_value_to_otlp(kv) for kv in (rs.resource.attributes or [])]},
|
|
189
|
+
"scopeSpans": [_scope_spans_to_otlp(ss) for ss in (rs.scope_spans or [])],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _scope_spans_to_otlp(ss) -> dict:
|
|
194
|
+
scope: dict = {"name": ss.scope.name}
|
|
195
|
+
if ss.scope.version is not None:
|
|
196
|
+
scope["version"] = ss.scope.version
|
|
197
|
+
return {"scope": scope, "spans": [_span_to_otlp(s) for s in (ss.spans or [])]}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _span_to_otlp(s) -> dict:
|
|
201
|
+
out = {
|
|
202
|
+
"traceId": s.trace_id,
|
|
203
|
+
"spanId": s.span_id,
|
|
204
|
+
"name": s.name,
|
|
205
|
+
"kind": s.kind,
|
|
206
|
+
"startTimeUnixNano": s.start_time_unix_nano,
|
|
207
|
+
"endTimeUnixNano": s.end_time_unix_nano,
|
|
208
|
+
"attributes": [_key_value_to_otlp(kv) for kv in (s.attributes or [])],
|
|
209
|
+
"events": [_event_to_otlp(e) for e in (s.events or [])],
|
|
210
|
+
}
|
|
211
|
+
if s.parent_span_id is not None:
|
|
212
|
+
out["parentSpanId"] = s.parent_span_id
|
|
213
|
+
if s.status is not None:
|
|
214
|
+
out["status"] = {"code": s.status.code}
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _event_to_otlp(e) -> dict:
|
|
219
|
+
out = {"name": e.name, "attributes": [_key_value_to_otlp(kv) for kv in (e.attributes or [])]}
|
|
220
|
+
if e.time_unix_nano is not None:
|
|
221
|
+
out["timeUnixNano"] = e.time_unix_nano
|
|
222
|
+
return out
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _key_value_to_otlp(kv) -> dict:
|
|
226
|
+
return {"key": kv.key, "value": _any_value_to_otlp(kv.value)}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _any_value_to_otlp(v) -> dict:
|
|
230
|
+
"""Map a typed OTLP AnyValue back to its single-key wire form."""
|
|
231
|
+
if v.string_value is not None:
|
|
232
|
+
return {"stringValue": v.string_value}
|
|
233
|
+
if v.bool_value is not None:
|
|
234
|
+
return {"boolValue": v.bool_value}
|
|
235
|
+
if v.int_value is not None:
|
|
236
|
+
return {"intValue": v.int_value}
|
|
237
|
+
if v.double_value is not None:
|
|
238
|
+
return {"doubleValue": v.double_value}
|
|
239
|
+
if v.array_value is not None:
|
|
240
|
+
return {"arrayValue": {"values": [_any_value_to_otlp(x) for x in (v.array_value.values or [])]}}
|
|
241
|
+
return {}
|
{answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answerrocket_client.egg-info/SOURCES.txt
RENAMED
|
@@ -11,6 +11,7 @@ answer_rocket/email.py
|
|
|
11
11
|
answer_rocket/error.py
|
|
12
12
|
answer_rocket/layouts.py
|
|
13
13
|
answer_rocket/llm.py
|
|
14
|
+
answer_rocket/observability.py
|
|
14
15
|
answer_rocket/output.py
|
|
15
16
|
answer_rocket/skill.py
|
|
16
17
|
answer_rocket/types.py
|
|
@@ -25,4 +26,5 @@ answerrocket_client.egg-info/SOURCES.txt
|
|
|
25
26
|
answerrocket_client.egg-info/dependency_links.txt
|
|
26
27
|
answerrocket_client.egg-info/requires.txt
|
|
27
28
|
answerrocket_client.egg-info/top_level.txt
|
|
28
|
-
test/test_client.py
|
|
29
|
+
test/test_client.py
|
|
30
|
+
test/test_observability.py
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Tests for the Observability client (GraphQL-backed, OTLP assembled server-side)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
7
|
+
|
|
8
|
+
from unittest.mock import MagicMock, patch
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from answer_rocket.observability import Observability, TracesBatch, DEFAULT_LIMIT
|
|
12
|
+
from answer_rocket.client_config import ClientConfig
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Helpers
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
_SAMPLE_OTLP_TRACE = {
|
|
19
|
+
"resourceSpans": [{
|
|
20
|
+
"resource": {"attributes": [{"key": "service.name", "value": {"stringValue": "answerrocket-copilot"}}]},
|
|
21
|
+
"scopeSpans": [{"scope": {"name": "answerrocket.copilot"}, "spans": [
|
|
22
|
+
{"traceId": "abc123", "spanId": "def456", "name": "chat.pipeline",
|
|
23
|
+
"kind": "SPAN_KIND_SERVER", "startTimeUnixNano": "1000", "endTimeUnixNano": "2000",
|
|
24
|
+
"attributes": [], "events": [], "status": {"code": "STATUS_CODE_OK"}},
|
|
25
|
+
]}],
|
|
26
|
+
}]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _typed_any_value(v):
|
|
31
|
+
"""Mimic a typed sgqlc AnyValue from an OTLP value dict (one populated variant)."""
|
|
32
|
+
m = MagicMock()
|
|
33
|
+
m.string_value = v.get("stringValue")
|
|
34
|
+
m.bool_value = v.get("boolValue")
|
|
35
|
+
m.int_value = v.get("intValue")
|
|
36
|
+
m.double_value = v.get("doubleValue")
|
|
37
|
+
if "arrayValue" in v:
|
|
38
|
+
arr = MagicMock(); arr.values = [_typed_any_value(x) for x in v["arrayValue"]["values"]]
|
|
39
|
+
m.array_value = arr
|
|
40
|
+
else:
|
|
41
|
+
m.array_value = None
|
|
42
|
+
return m
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _typed_key_value(kv):
|
|
46
|
+
m = MagicMock(); m.key = kv["key"]; m.value = _typed_any_value(kv["value"])
|
|
47
|
+
return m
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _typed_event(e):
|
|
51
|
+
m = MagicMock(); m.name = e["name"]; m.time_unix_nano = e.get("timeUnixNano")
|
|
52
|
+
m.attributes = [_typed_key_value(kv) for kv in e.get("attributes", [])]
|
|
53
|
+
return m
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _typed_span(s):
|
|
57
|
+
"""Mimic a typed sgqlc Span object built from an OTLP span dict."""
|
|
58
|
+
m = MagicMock()
|
|
59
|
+
m.trace_id = s["traceId"]
|
|
60
|
+
m.span_id = s["spanId"]
|
|
61
|
+
m.name = s["name"]
|
|
62
|
+
m.kind = s["kind"]
|
|
63
|
+
m.start_time_unix_nano = s["startTimeUnixNano"]
|
|
64
|
+
m.end_time_unix_nano = s["endTimeUnixNano"]
|
|
65
|
+
m.attributes = [_typed_key_value(kv) for kv in s.get("attributes", [])]
|
|
66
|
+
m.events = [_typed_event(e) for e in s.get("events", [])] # always a list (non-null in schema)
|
|
67
|
+
m.parent_span_id = s.get("parentSpanId") # None when absent
|
|
68
|
+
if "status" in s:
|
|
69
|
+
st = MagicMock(); st.code = s["status"]["code"]; m.status = st
|
|
70
|
+
else:
|
|
71
|
+
m.status = None
|
|
72
|
+
return m
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _typed_scope_spans(ss):
|
|
76
|
+
m = MagicMock()
|
|
77
|
+
sc = MagicMock(); sc.name = ss["scope"]["name"]; sc.version = ss["scope"].get("version")
|
|
78
|
+
m.scope = sc
|
|
79
|
+
m.spans = [_typed_span(s) for s in ss["spans"]]
|
|
80
|
+
return m
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _typed_resource_spans(rs):
|
|
84
|
+
m = MagicMock()
|
|
85
|
+
res = MagicMock(); res.attributes = [_typed_key_value(kv) for kv in rs["resource"]["attributes"]]
|
|
86
|
+
m.resource = res
|
|
87
|
+
m.scope_spans = [_typed_scope_spans(ss) for ss in rs["scopeSpans"]]
|
|
88
|
+
return m
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _typed_trace(otlp):
|
|
92
|
+
"""Mimic the typed sgqlc ObservabilityTrace tree built from an OTLP dict."""
|
|
93
|
+
t = MagicMock()
|
|
94
|
+
t.resource_spans = [_typed_resource_spans(rs) for rs in otlp["resourceSpans"]]
|
|
95
|
+
return t
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _make_gql_page(traces=None, count=None, has_more=False, next_cursor=None):
|
|
99
|
+
"""Build a mock sgqlc result whose `traces` are typed objects (as the server returns)."""
|
|
100
|
+
otlp = traces or []
|
|
101
|
+
page = MagicMock()
|
|
102
|
+
page.count = count if count is not None else len(otlp)
|
|
103
|
+
page.has_more = has_more
|
|
104
|
+
page.next_cursor = next_cursor
|
|
105
|
+
page.traces = [_typed_trace(t) for t in otlp]
|
|
106
|
+
return page
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _make_client():
|
|
110
|
+
config = MagicMock(spec=ClientConfig)
|
|
111
|
+
config.tenant = "test_tenant"
|
|
112
|
+
gql_client = MagicMock()
|
|
113
|
+
return config, gql_client, Observability(config, gql_client)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Basic attribute tests
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def test_client_has_observability_attr():
|
|
121
|
+
_, _, obs = _make_client()
|
|
122
|
+
assert isinstance(obs, Observability)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_to_iso_string_passthrough():
|
|
126
|
+
from answer_rocket.observability import _to_iso
|
|
127
|
+
assert _to_iso("2026-01-01T00:00:00Z") == "2026-01-01T00:00:00Z"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_to_iso_datetime_naive():
|
|
131
|
+
from answer_rocket.observability import _to_iso
|
|
132
|
+
dt = datetime(2026, 1, 1, 12, 0, 0)
|
|
133
|
+
result = _to_iso(dt)
|
|
134
|
+
assert "2026-01-01" in result
|
|
135
|
+
assert "+00:00" in result or "UTC" in result
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_to_iso_datetime_aware():
|
|
139
|
+
from answer_rocket.observability import _to_iso
|
|
140
|
+
dt = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
|
141
|
+
result = _to_iso(dt)
|
|
142
|
+
assert "2026-01-01" in result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# get_traces
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def test_get_traces_success():
|
|
150
|
+
config, gql_client, obs = _make_client()
|
|
151
|
+
mock_result = MagicMock()
|
|
152
|
+
mock_result.observability_traces = _make_gql_page(
|
|
153
|
+
traces=[_SAMPLE_OTLP_TRACE], count=1, has_more=False
|
|
154
|
+
)
|
|
155
|
+
gql_client.submit.return_value = mock_result
|
|
156
|
+
|
|
157
|
+
batch = obs.get_traces("2026-01-01T00:00:00Z")
|
|
158
|
+
|
|
159
|
+
assert batch.success is True
|
|
160
|
+
assert batch.count == 1
|
|
161
|
+
assert batch.has_more is False
|
|
162
|
+
assert batch.next_cursor is None
|
|
163
|
+
assert len(batch.traces) == 1
|
|
164
|
+
assert batch.traces[0] == _SAMPLE_OTLP_TRACE
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_get_traces_passes_correct_variables():
|
|
168
|
+
config, gql_client, obs = _make_client()
|
|
169
|
+
mock_result = MagicMock()
|
|
170
|
+
mock_result.observability_traces = _make_gql_page()
|
|
171
|
+
gql_client.submit.return_value = mock_result
|
|
172
|
+
|
|
173
|
+
mock_op = MagicMock()
|
|
174
|
+
mock_obs_field = MagicMock()
|
|
175
|
+
mock_op.observability_traces.return_value = mock_obs_field
|
|
176
|
+
gql_client.query.return_value = mock_op
|
|
177
|
+
|
|
178
|
+
obs.get_traces("2026-05-01T00:00:00Z", limit=50)
|
|
179
|
+
|
|
180
|
+
mock_op.observability_traces.assert_called_once_with(since="2026-05-01T00:00:00Z", limit=50)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_get_traces_pagination_fields():
|
|
184
|
+
config, gql_client, obs = _make_client()
|
|
185
|
+
mock_result = MagicMock()
|
|
186
|
+
mock_result.observability_traces = _make_gql_page(
|
|
187
|
+
traces=[_SAMPLE_OTLP_TRACE], count=1, has_more=True, next_cursor="2026-06-01T00:00:00Z"
|
|
188
|
+
)
|
|
189
|
+
gql_client.submit.return_value = mock_result
|
|
190
|
+
|
|
191
|
+
batch = obs.get_traces("2026-01-01T00:00:00Z")
|
|
192
|
+
|
|
193
|
+
assert batch.has_more is True
|
|
194
|
+
assert batch.next_cursor == "2026-06-01T00:00:00Z"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_get_traces_connection_error():
|
|
198
|
+
config, gql_client, obs = _make_client()
|
|
199
|
+
gql_client.submit.side_effect = ConnectionError("timeout")
|
|
200
|
+
|
|
201
|
+
batch = obs.get_traces("2026-01-01T00:00:00Z")
|
|
202
|
+
|
|
203
|
+
assert batch.success is False
|
|
204
|
+
assert "timeout" in batch.error
|
|
205
|
+
assert batch.traces == []
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_get_traces_empty_page():
|
|
209
|
+
config, gql_client, obs = _make_client()
|
|
210
|
+
mock_result = MagicMock()
|
|
211
|
+
mock_result.observability_traces = _make_gql_page(traces=[], count=0, has_more=False)
|
|
212
|
+
gql_client.submit.return_value = mock_result
|
|
213
|
+
|
|
214
|
+
batch = obs.get_traces("2026-01-01T00:00:00Z")
|
|
215
|
+
|
|
216
|
+
assert batch.success is True
|
|
217
|
+
assert batch.count == 0
|
|
218
|
+
assert batch.traces == []
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
# iter_traces
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
def test_iter_traces_follows_pages():
|
|
226
|
+
config, gql_client, obs = _make_client()
|
|
227
|
+
page1 = MagicMock()
|
|
228
|
+
page1.observability_traces = _make_gql_page(
|
|
229
|
+
traces=[_SAMPLE_OTLP_TRACE], count=1, has_more=True, next_cursor="cursor_1"
|
|
230
|
+
)
|
|
231
|
+
page2 = MagicMock()
|
|
232
|
+
page2.observability_traces = _make_gql_page(
|
|
233
|
+
traces=[_SAMPLE_OTLP_TRACE], count=1, has_more=False
|
|
234
|
+
)
|
|
235
|
+
gql_client.submit.side_effect = [page1, page2]
|
|
236
|
+
|
|
237
|
+
batches = list(obs.iter_traces("2026-01-01T00:00:00Z"))
|
|
238
|
+
|
|
239
|
+
assert len(batches) == 2
|
|
240
|
+
assert gql_client.submit.call_count == 2
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_iter_traces_stops_on_empty():
|
|
244
|
+
config, gql_client, obs = _make_client()
|
|
245
|
+
mock_result = MagicMock()
|
|
246
|
+
mock_result.observability_traces = _make_gql_page(traces=[], has_more=False)
|
|
247
|
+
gql_client.submit.return_value = mock_result
|
|
248
|
+
|
|
249
|
+
batches = list(obs.iter_traces("2026-01-01T00:00:00Z"))
|
|
250
|
+
|
|
251
|
+
assert batches == []
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_iter_traces_stops_on_error():
|
|
255
|
+
config, gql_client, obs = _make_client()
|
|
256
|
+
gql_client.submit.side_effect = Exception("server error")
|
|
257
|
+
|
|
258
|
+
batches = list(obs.iter_traces("2026-01-01T00:00:00Z"))
|
|
259
|
+
|
|
260
|
+
assert batches == []
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
# poll_traces
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
def test_poll_traces_max_iterations():
|
|
268
|
+
config, gql_client, obs = _make_client()
|
|
269
|
+
mock_result = MagicMock()
|
|
270
|
+
mock_result.observability_traces = _make_gql_page(
|
|
271
|
+
traces=[_SAMPLE_OTLP_TRACE], count=1, has_more=False
|
|
272
|
+
)
|
|
273
|
+
gql_client.submit.return_value = mock_result
|
|
274
|
+
|
|
275
|
+
with patch("time.sleep"):
|
|
276
|
+
batches = list(obs.poll_traces("2026-01-01T00:00:00Z", max_iterations=3))
|
|
277
|
+
|
|
278
|
+
assert len(batches) == 3
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_poll_traces_skips_empty_batches():
|
|
282
|
+
config, gql_client, obs = _make_client()
|
|
283
|
+
mock_result = MagicMock()
|
|
284
|
+
mock_result.observability_traces = _make_gql_page(traces=[], has_more=False)
|
|
285
|
+
gql_client.submit.return_value = mock_result
|
|
286
|
+
|
|
287
|
+
with patch("time.sleep"):
|
|
288
|
+
batches = list(obs.poll_traces("2026-01-01T00:00:00Z", max_iterations=2))
|
|
289
|
+
|
|
290
|
+
assert batches == []
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_poll_traces_on_batch_callback():
|
|
294
|
+
config, gql_client, obs = _make_client()
|
|
295
|
+
mock_result = MagicMock()
|
|
296
|
+
mock_result.observability_traces = _make_gql_page(
|
|
297
|
+
traces=[_SAMPLE_OTLP_TRACE], count=1, has_more=False
|
|
298
|
+
)
|
|
299
|
+
gql_client.submit.return_value = mock_result
|
|
300
|
+
|
|
301
|
+
received = []
|
|
302
|
+
with patch("time.sleep"):
|
|
303
|
+
for _ in obs.poll_traces("2026-01-01T00:00:00Z", on_batch=received.extend, max_iterations=1):
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
assert len(received) == 1
|
|
307
|
+
assert received[0] == _SAMPLE_OTLP_TRACE
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
# Cursor handling
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
def test_get_traces_converts_datetime_cursor_to_iso():
|
|
315
|
+
config, gql_client, obs = _make_client()
|
|
316
|
+
mock_result = MagicMock()
|
|
317
|
+
mock_result.observability_traces = _make_gql_page()
|
|
318
|
+
gql_client.submit.return_value = mock_result
|
|
319
|
+
mock_op = MagicMock()
|
|
320
|
+
gql_client.query.return_value = mock_op
|
|
321
|
+
|
|
322
|
+
obs.get_traces(datetime(2026, 5, 1, 12, 0, 0, tzinfo=timezone.utc), limit=10)
|
|
323
|
+
|
|
324
|
+
_, kwargs = mock_op.observability_traces.call_args
|
|
325
|
+
assert kwargs["since"].startswith("2026-05-01T12:00:00")
|
|
326
|
+
assert kwargs["limit"] == 10
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_iter_traces_threads_next_cursor_as_since():
|
|
330
|
+
"""Page 2 must be requested with `since` == page 1's next_cursor (no re-scan, no gaps)."""
|
|
331
|
+
config, gql_client, obs = _make_client()
|
|
332
|
+
page1 = MagicMock()
|
|
333
|
+
page1.observability_traces = _make_gql_page(
|
|
334
|
+
traces=[_SAMPLE_OTLP_TRACE], has_more=True, next_cursor="2026-05-01T12:00:01Z"
|
|
335
|
+
)
|
|
336
|
+
page2 = MagicMock()
|
|
337
|
+
page2.observability_traces = _make_gql_page(traces=[_SAMPLE_OTLP_TRACE], has_more=False)
|
|
338
|
+
gql_client.submit.side_effect = [page1, page2]
|
|
339
|
+
|
|
340
|
+
mock_op = MagicMock()
|
|
341
|
+
gql_client.query.return_value = mock_op
|
|
342
|
+
|
|
343
|
+
list(obs.iter_traces("2026-05-01T00:00:00Z"))
|
|
344
|
+
|
|
345
|
+
calls = mock_op.observability_traces.call_args_list
|
|
346
|
+
assert len(calls) == 2
|
|
347
|
+
assert calls[0].kwargs["since"] == "2026-05-01T00:00:00Z"
|
|
348
|
+
assert calls[1].kwargs["since"] == "2026-05-01T12:00:01Z"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# Contract: SDK faithfully passes server-assembled OTLP through (ties both PRs)
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
def _load_golden_trace():
|
|
356
|
+
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures", "example_otlp_trace.json")
|
|
357
|
+
with open(path) as f:
|
|
358
|
+
return json.load(f)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_contract_reconstructs_real_server_trace_to_identical_otlp():
|
|
362
|
+
config, gql_client, obs = _make_client()
|
|
363
|
+
golden = _load_golden_trace()
|
|
364
|
+
mock_result = MagicMock()
|
|
365
|
+
mock_result.observability_traces = _make_gql_page(traces=[golden], count=1, has_more=False)
|
|
366
|
+
gql_client.submit.return_value = mock_result
|
|
367
|
+
|
|
368
|
+
batch = obs.get_traces("2026-05-01T00:00:00Z")
|
|
369
|
+
|
|
370
|
+
assert batch.success is True
|
|
371
|
+
assert batch.count == 1
|
|
372
|
+
# The typed span tree must reconstruct byte-identically to the server's OTLP.
|
|
373
|
+
assert batch.traces[0] == golden
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def test_reconstruction_handles_typed_attributes_and_optionals():
|
|
377
|
+
from answer_rocket.observability import _span_to_otlp
|
|
378
|
+
full = _typed_span({
|
|
379
|
+
"traceId": "t", "spanId": "s", "name": "n", "kind": "SPAN_KIND_INTERNAL",
|
|
380
|
+
"startTimeUnixNano": "1", "endTimeUnixNano": "2",
|
|
381
|
+
"attributes": [
|
|
382
|
+
{"key": "ar.run_id", "value": {"stringValue": "v"}},
|
|
383
|
+
{"key": "ar.tokens.total", "value": {"intValue": "42"}},
|
|
384
|
+
{"key": "ar.cost", "value": {"doubleValue": 0.01}},
|
|
385
|
+
{"key": "ar.flag", "value": {"boolValue": True}},
|
|
386
|
+
{"key": "ar.answer.suggestions", "value": {"arrayValue": {"values": [{"stringValue": "a"}, {"stringValue": "b"}]}}},
|
|
387
|
+
],
|
|
388
|
+
"events": [{"name": "e", "attributes": []}],
|
|
389
|
+
"parentSpanId": "p", "status": {"code": "STATUS_CODE_OK"},
|
|
390
|
+
})
|
|
391
|
+
assert _span_to_otlp(full) == {
|
|
392
|
+
"traceId": "t", "spanId": "s", "name": "n", "kind": "SPAN_KIND_INTERNAL",
|
|
393
|
+
"startTimeUnixNano": "1", "endTimeUnixNano": "2",
|
|
394
|
+
"attributes": [
|
|
395
|
+
{"key": "ar.run_id", "value": {"stringValue": "v"}},
|
|
396
|
+
{"key": "ar.tokens.total", "value": {"intValue": "42"}},
|
|
397
|
+
{"key": "ar.cost", "value": {"doubleValue": 0.01}},
|
|
398
|
+
{"key": "ar.flag", "value": {"boolValue": True}},
|
|
399
|
+
{"key": "ar.answer.suggestions", "value": {"arrayValue": {"values": [{"stringValue": "a"}, {"stringValue": "b"}]}}},
|
|
400
|
+
],
|
|
401
|
+
"events": [{"name": "e", "attributes": []}],
|
|
402
|
+
"parentSpanId": "p", "status": {"code": "STATUS_CODE_OK"},
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Optional parentSpanId/status are omitted when absent; events is always a (possibly empty) list.
|
|
406
|
+
minimal = _typed_span({
|
|
407
|
+
"traceId": "t", "spanId": "s", "name": "root", "kind": "SPAN_KIND_SERVER",
|
|
408
|
+
"startTimeUnixNano": "1", "endTimeUnixNano": "2", "attributes": [], "events": [],
|
|
409
|
+
})
|
|
410
|
+
out = _span_to_otlp(minimal)
|
|
411
|
+
assert "parentSpanId" not in out
|
|
412
|
+
assert "status" not in out
|
|
413
|
+
assert out["events"] == []
|
|
414
|
+
assert out["attributes"] == []
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_contract_returned_trace_is_valid_otlp():
|
|
418
|
+
config, gql_client, obs = _make_client()
|
|
419
|
+
golden = _load_golden_trace()
|
|
420
|
+
mock_result = MagicMock()
|
|
421
|
+
mock_result.observability_traces = _make_gql_page(traces=[golden], count=1)
|
|
422
|
+
gql_client.submit.return_value = mock_result
|
|
423
|
+
|
|
424
|
+
trace = obs.get_traces("2026-05-01T00:00:00Z").traces[0]
|
|
425
|
+
|
|
426
|
+
spans = trace["resourceSpans"][0]["scopeSpans"][0]["spans"]
|
|
427
|
+
names = [s["name"] for s in spans]
|
|
428
|
+
assert "chat.pipeline" in names
|
|
429
|
+
assert any(n.startswith("phase.") for n in names)
|
|
430
|
+
assert "skill.narrative" in names
|
|
431
|
+
# All spans share one traceId; every span has the required OTLP fields.
|
|
432
|
+
trace_ids = {s["traceId"] for s in spans}
|
|
433
|
+
assert len(trace_ids) == 1
|
|
434
|
+
for s in spans:
|
|
435
|
+
assert s["spanId"]
|
|
436
|
+
assert s["startTimeUnixNano"] and s["endTimeUnixNano"]
|
|
437
|
+
assert int(s["startTimeUnixNano"]) <= int(s["endTimeUnixNano"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/graphql/__init__.py
RENAMED
|
File without changes
|
{answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/graphql/sdk_operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{answerrocket_client-0.2.105 → answerrocket_client-0.2.106}/answer_rocket/util/meta_data_frame.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|