From 0475f5158730e739d283d375ea418f428fba5c02 Mon Sep 17 00:00:00 2001 From: Rakshith Bhyravabhotla Date: Fri, 24 Sep 2021 12:35:04 -0700 Subject: [PATCH] Update handling error in query (#20765) * samples * hide row * Handle errors * lint * lint * extract inner message * lint * lint * lint * api view fixes * more changes * Update sdk/monitor/azure-monitor-query/CHANGELOG.md * Apply suggestions from code review Co-authored-by: Krista Pratico * changes Co-authored-by: Krista Pratico --- sdk/monitor/azure-monitor-query/CHANGELOG.md | 8 +- .../azure/monitor/query/__init__.py | 14 +- .../azure/monitor/query/_exceptions.py | 70 +--- .../azure/monitor/query/_helpers.py | 84 +++-- .../azure/monitor/query/_logs_query_client.py | 93 ++--- .../monitor/query/_metrics_query_client.py | 30 +- .../azure/monitor/query/_models.py | 323 ++++++++++-------- .../azure/monitor/query/aio/__init__.py | 5 +- .../azure/monitor/query/aio/_helpers_asyc.py | 22 +- .../query/aio/_logs_query_client_async.py | 78 ++--- .../query/aio/_metrics_query_client_async.py | 50 +-- .../samples/champion_scenarios.md | 288 ---------------- .../samples/sample_batch_query.py | 26 +- .../samples/sample_log_query_client.py | 11 +- .../tests/async/test_exceptions_async.py | 85 +---- .../tests/test_exceptions.py | 55 +-- .../tests/test_logs_client.py | 6 +- 17 files changed, 459 insertions(+), 789 deletions(-) delete mode 100644 sdk/monitor/azure-monitor-query/samples/champion_scenarios.md diff --git a/sdk/monitor/azure-monitor-query/CHANGELOG.md b/sdk/monitor/azure-monitor-query/CHANGELOG.md index 5f2d861f2b22..93a955a2004f 100644 --- a/sdk/monitor/azure-monitor-query/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-query/CHANGELOG.md @@ -4,14 +4,16 @@ ### Features Added -- Added `QueryPartialErrorException` and `LogsQueryError` to handle errors. -- Added `partial_error` and `is_error` attributes to `LogsQueryResult`. -- Added an option `allow_partial_errors` that defaults to False, which can be set to not throw if there are any partial errors. +- Added `LogsQueryPartialResult` and `LogsQueryError` to handle errors. +- Added `status` attribute to `LogsQueryResult`. +- Added `LogsQueryStatus` Enum to describe the status of a result. - Added a new `LogsTableRow` type that represents a single row in a table. ### Breaking Changes - `LogsQueryResult` now iterates over the tables directly as a convinience. +- `query` API now returns a union of `LogsQueryPartialResult` and `LogsQueryResult`. +- `query_batch` API now returns a union of `LogsQueryPartialResult`, `LogsQueryError` and `LogsQueryResult`. - `metric_namespace` is renamed to `namespace` and is a keyword-only argument in `list_metric_definitions` API. ### Bugs Fixed diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/__init__.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/__init__.py index 1171a0562dfc..497c0e804256 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/__init__.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/__init__.py @@ -7,15 +7,14 @@ from ._logs_query_client import LogsQueryClient from ._metrics_query_client import MetricsQueryClient -from ._exceptions import ( - LogsQueryError, - QueryPartialErrorException -) +from ._exceptions import LogsQueryError from ._models import ( MetricAggregationType, LogsQueryResult, LogsTable, + LogsQueryPartialResult, + LogsQueryStatus, LogsTableRow, MetricsResult, LogsBatchQuery, @@ -27,7 +26,7 @@ Metric, MetricValue, MetricClass, - MetricAvailability + MetricAvailability, ) from ._version import VERSION @@ -36,8 +35,9 @@ "MetricAggregationType", "LogsQueryClient", "LogsQueryResult", + "LogsQueryPartialResult", + "LogsQueryStatus", "LogsQueryError", - "QueryPartialErrorException", "LogsTable", "LogsTableRow", "LogsBatchQuery", @@ -51,7 +51,7 @@ "Metric", "MetricValue", "MetricClass", - "MetricAvailability" + "MetricAvailability", ] __version__ = VERSION diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/_exceptions.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/_exceptions.py index f849f93ff6ee..658df77d7261 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/_exceptions.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/_exceptions.py @@ -4,7 +4,8 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from azure.core.exceptions import HttpResponseError +from ._models import LogsQueryStatus + class LogsQueryError(object): """The code and message for an error. @@ -15,65 +16,26 @@ class LogsQueryError(object): :vartype code: str :ivar message: A human readable error message. :vartype message: str - :ivar details: error details. - :vartype details: list[~monitor_query_client.models.ErrorDetail] - :ivar innererror: Inner error details if they exist. - :vartype innererror: ~azure.monitor.query.LogsQueryError - :ivar additional_properties: Additional properties that can be provided on the error info - object. - :vartype additional_properties: object - :ivar bool is_error: Boolean check for error item when iterating over list of - results. Always True for an instance of a LogsQueryError. + :ivar status: status for error item when iterating over list of + results. Always "Failure" for an instance of a LogsQueryError. + :vartype status: ~azure.monitor.query.LogsQueryStatus """ - def __init__( - self, - **kwargs - ): - self.code = kwargs.get('code', None) - self.message = kwargs.get('message', None) - self.details = kwargs.get('details', None) - self.innererror = kwargs.get('innererror', None) - self.additional_properties = kwargs.get('additional_properties', None) - self.is_error = True + + def __init__(self, **kwargs): + self.code = kwargs.get("code", None) + self.message = kwargs.get("message", None) + self.status = LogsQueryStatus.FAILURE @classmethod def _from_generated(cls, generated): if not generated: return None - details = None - if generated.details is not None: - details = [d.serialize() for d in generated.details] + + innererror = generated + while innererror.innererror is not None: + innererror = innererror.innererror + message = innererror.message return cls( code=generated.code, - message=generated.message, - innererror=cls._from_generated(generated.innererror) if generated.innererror else None, - additional_properties=generated.additional_properties, - details=details, + message=message, ) - -class QueryPartialErrorException(HttpResponseError): - """There is a partial failure in query operation. This is thrown for a single query operation - when allow_partial_errors is set to False. - - :ivar code: A machine readable error code. - :vartype code: str - :ivar message: A human readable error message. - :vartype message: str - :ivar details: error details. - :vartype details: list[~monitor_query_client.models.ErrorDetail] - :ivar innererror: Inner error details if they exist. - :vartype innererror: ~azure.monitor.query.LogsQueryError - :ivar additional_properties: Additional properties that can be provided on the error info - object. - :vartype additional_properties: object - """ - - def __init__(self, **kwargs): - error = kwargs.pop('error', None) - if error: - self.code = error.code - self.message = error.message - self.details = [d.serialize() for d in error.details] if error.details else None - self.innererror = LogsQueryError._from_generated(error.innererror) if error.innererror else None - self.additional_properties = error.additional_properties - super(QueryPartialErrorException, self).__init__(message=self.message) diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/_helpers.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/_helpers.py index 5adf5bc095d9..dc65b9e48967 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/_helpers.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/_helpers.py @@ -13,101 +13,121 @@ if TYPE_CHECKING: from azure.core.credentials import TokenCredential + def get_authentication_policy( - credential, # type: TokenCredential + credential, # type: TokenCredential ): # type: (...) -> BearerTokenCredentialPolicy - """Returns the correct authentication policy - """ + """Returns the correct authentication policy""" if credential is None: raise ValueError("Parameter 'credential' must not be None.") if hasattr(credential, "get_token"): - return BearerTokenCredentialPolicy(credential, "https://api.loganalytics.io/.default") + return BearerTokenCredentialPolicy( + credential, "https://api.loganalytics.io/.default" + ) raise TypeError("Unsupported credential") + def get_metrics_authentication_policy( - credential, # type: TokenCredential + credential, # type: TokenCredential ): # type: (...) -> BearerTokenCredentialPolicy - """Returns the correct authentication policy - """ + """Returns the correct authentication policy""" if credential is None: raise ValueError("Parameter 'credential' must not be None.") if hasattr(credential, "get_token"): - return BearerTokenCredentialPolicy(credential, "https://management.azure.com/.default") + return BearerTokenCredentialPolicy( + credential, "https://management.azure.com/.default" + ) raise TypeError("Unsupported credential") -def order_results(request_order, mapping, obj, err, allow_partial_errors=False): + +def order_results(request_order, mapping, **kwargs): ordered = [mapping[id] for id in request_order] results = [] for item in ordered: if not item.body.error: - results.append(obj._from_generated(item.body)) # pylint: disable=protected-access + results.append( + kwargs.get("obj")._from_generated(item.body) # pylint: disable=protected-access + ) else: error = item.body.error - if allow_partial_errors and error.code == 'PartialError': - res = obj._from_generated(item.body) # pylint: disable=protected-access - res.partial_error = err._from_generated(error) # pylint: disable=protected-access + if error.code == "PartialError": + res = kwargs.get("partial_err")._from_generated( # pylint: disable=protected-access + item.body, kwargs.get("raise_with") + ) results.append(res) else: - results.append(err._from_generated(error)) # pylint: disable=protected-access + results.append( + kwargs.get("err")._from_generated(error) # pylint: disable=protected-access + ) return results + def construct_iso8601(timespan=None): if not timespan: return None try: start, end, duration = None, None, None - if isinstance(timespan[1], datetime): # we treat thi as start_time, end_time + if isinstance(timespan[1], datetime): # we treat thi as start_time, end_time start, end = timespan[0], timespan[1] - elif isinstance(timespan[1], timedelta): # we treat this as start_time, duration + elif isinstance( + timespan[1], timedelta + ): # we treat this as start_time, duration start, duration = timespan[0], timespan[1] else: - raise ValueError('Tuple must be a start datetime with a timedelta or an end datetime.') + raise ValueError( + "Tuple must be a start datetime with a timedelta or an end datetime." + ) except TypeError: - duration = timespan # it means only duration (timedelta) is provideds + duration = timespan # it means only duration (timedelta) is provideds if duration: try: - duration = 'PT{}S'.format(duration.total_seconds()) + duration = "PT{}S".format(duration.total_seconds()) except AttributeError: - raise ValueError('timespan must be a timedelta or a tuple.') + raise ValueError("timespan must be a timedelta or a tuple.") iso_str = None if start is not None: start = Serializer.serialize_iso(start) if end is not None: end = Serializer.serialize_iso(end) - iso_str = start + '/' + end + iso_str = start + "/" + end elif duration is not None: - iso_str = start + '/' + duration - else: # means that an invalid value None that is provided with start_time - raise ValueError("Duration or end_time cannot be None when provided with start_time.") + iso_str = start + "/" + duration + else: # means that an invalid value None that is provided with start_time + raise ValueError( + "Duration or end_time cannot be None when provided with start_time." + ) else: iso_str = duration return iso_str + def native_col_type(col_type, value): - if col_type == 'datetime': + if col_type == "datetime": value = Deserializer.deserialize_iso(value) - elif col_type in ('timespan', 'guid'): + elif col_type in ("timespan", "guid"): value = str(value) return value + def process_row(col_types, row): return [native_col_type(col_types[ind], val) for ind, val in enumerate(row)] + def process_error(error, model): try: - model = model._from_generated(error.model.error) # pylint: disable=protected-access - except AttributeError: # model can be none + model = model._from_generated( # pylint: disable=protected-access + error.model.error + ) + except AttributeError: # model can be none pass - raise HttpResponseError( - message=error.message, - response=error.response, - model=model) + raise HttpResponseError(message=error.message, response=error.response, model=model) + def process_prefer(server_timeout, include_statistics, include_visualization): prefer = "" diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/_logs_query_client.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/_logs_query_client.py index ccb68fdd6414..da0d8e173605 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/_logs_query_client.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/_logs_query_client.py @@ -5,16 +5,22 @@ # license information. # -------------------------------------------------------------------------- -from typing import TYPE_CHECKING, Any, Union, Sequence, Dict, List +from typing import TYPE_CHECKING, Any, Union, Sequence, Dict, List, cast from azure.core.exceptions import HttpResponseError from azure.core.tracing.decorator import distributed_trace from ._generated._monitor_query_client import MonitorQueryClient from ._generated.models import BatchRequest, QueryBody as LogsQueryBody -from ._helpers import get_authentication_policy, construct_iso8601, order_results, process_error, process_prefer -from ._models import LogsBatchQuery, LogsQueryResult -from ._exceptions import LogsQueryError, QueryPartialErrorException +from ._helpers import ( + get_authentication_policy, + construct_iso8601, + order_results, + process_error, + process_prefer, +) +from ._models import LogsBatchQuery, LogsQueryResult, LogsQueryPartialResult +from ._exceptions import LogsQueryError if TYPE_CHECKING: from azure.core.credentials import TokenCredential @@ -42,7 +48,7 @@ class LogsQueryClient(object): def __init__(self, credential, **kwargs): # type: (TokenCredential, Any) -> None - self._endpoint = kwargs.pop('endpoint', 'https://api.loganalytics.io/v1') + self._endpoint = kwargs.pop("endpoint", "https://api.loganalytics.io/v1") self._client = MonitorQueryClient( credential=credential, authentication_policy=get_authentication_policy(credential), @@ -53,7 +59,7 @@ def __init__(self, credential, **kwargs): @distributed_trace def query(self, workspace_id, query, **kwargs): - # type: (str, str, Any) -> LogsQueryResult + # type: (str, str, Any) -> Union[LogsQueryResult, LogsQueryPartialResult] """Execute an Analytics query. Executes an Analytics query for data. @@ -64,7 +70,7 @@ def query(self, workspace_id, query, **kwargs): :param query: The Kusto query. Learn more about the `Kusto query syntax `_. :type query: str - :keyword timespan: The timespan for which to query the data. This can be a timedelta, + :keyword timespan: Required. The timespan for which to query the data. This can be a timedelta, a timedelta and a start datetime, or a start datetime/end datetime. :paramtype timespan: ~datetime.timedelta or tuple[~datetime.datetime, ~datetime.timedelta] or tuple[~datetime.datetime, ~datetime.datetime] @@ -77,10 +83,8 @@ def query(self, workspace_id, query, **kwargs): :keyword additional_workspaces: A list of workspaces that are included in the query. These can be qualified workspace names, workspace Ids, or Azure resource Ids. :paramtype additional_workspaces: list[str] - :keyword allow_partial_errors: Defaults to False. If set to true, partial errors are not thrown. - :paramtype allow_partial_errors: bool :return: LogsQueryResult, or the result of cls(response) - :rtype: ~azure.monitor.query.LogsQueryResult + :rtype: Union[~azure.monitor.query.LogsQueryResult, ~azure.monitor.query.LogsQueryPartialResult] :raises: ~azure.core.exceptions.HttpResponseError .. admonition:: Example: @@ -92,46 +96,50 @@ def query(self, workspace_id, query, **kwargs): :dedent: 0 :caption: Get a response for a single Log Query """ - allow_partial_errors = kwargs.pop('allow_partial_errors', False) - if 'timespan' not in kwargs: - raise TypeError("query() missing 1 required keyword-only argument: 'timespan'") - timespan = construct_iso8601(kwargs.pop('timespan')) + if "timespan" not in kwargs: + raise TypeError( + "query() missing 1 required keyword-only argument: 'timespan'" + ) + timespan = construct_iso8601(kwargs.pop("timespan")) include_statistics = kwargs.pop("include_statistics", False) include_visualization = kwargs.pop("include_visualization", False) server_timeout = kwargs.pop("server_timeout", None) workspaces = kwargs.pop("additional_workspaces", None) - prefer = process_prefer(server_timeout, include_statistics, include_visualization) + prefer = process_prefer( + server_timeout, include_statistics, include_visualization + ) body = LogsQueryBody( - query=query, - timespan=timespan, - workspaces=workspaces, - **kwargs + query=query, timespan=timespan, workspaces=workspaces, **kwargs ) try: - generated_response = self._query_op.execute( # pylint: disable=protected-access - workspace_id=workspace_id, - body=body, - prefer=prefer, - **kwargs + generated_response = ( + self._query_op.execute( # pylint: disable=protected-access + workspace_id=workspace_id, body=body, prefer=prefer, **kwargs + ) ) except HttpResponseError as err: process_error(err, LogsQueryError) - response = LogsQueryResult._from_generated(generated_response) # pylint: disable=protected-access + response = None if not generated_response.error: - return response - if not allow_partial_errors: - raise QueryPartialErrorException(error=generated_response.error) - response.partial_error = LogsQueryError._from_generated( # pylint: disable=protected-access - generated_response.error + response = LogsQueryResult._from_generated( # pylint: disable=protected-access + generated_response + ) + else: + response = LogsQueryPartialResult._from_generated( # pylint: disable=protected-access + generated_response, LogsQueryError ) return response @distributed_trace - def query_batch(self, queries, **kwargs): - # type: (Union[Sequence[Dict], Sequence[LogsBatchQuery]], Any) -> List[LogsQueryResult] + def query_batch( + self, + queries, # type: Union[Sequence[Dict], Sequence[LogsBatchQuery]] + **kwargs # type: Any + ): + # type: (...) -> List[Union[LogsQueryResult, LogsQueryPartialResult, LogsQueryError]] """Execute a list of analytics queries. Each request can be either a LogQueryRequest object or an equivalent serialized model. @@ -139,11 +147,9 @@ def query_batch(self, queries, **kwargs): :param queries: The list of Kusto queries to execute. :type queries: list[dict] or list[~azure.monitor.query.LogsBatchQuery] - :keyword bool allow_partial_errors: If set to True, a `LogsQueryResult` object is returned - when a partial error occurs. The error can be accessed using the `partial_error` - attribute in the object. :return: List of LogsQueryResult, or the result of cls(response) - :rtype: list[~azure.monitor.query.LogsQueryResult] + :rtype: list[Union[~azure.monitor.query.LogsQueryResult, ~azure.monitor.query.LogsQueryPartialResult, + ~azure.monitor.query.LogsQueryError] :raises: ~azure.core.exceptions.HttpResponseError .. admonition:: Example: @@ -155,25 +161,28 @@ def query_batch(self, queries, **kwargs): :dedent: 0 :caption: Get a response for multiple Log Queries. """ - allow_partial_errors = kwargs.pop('allow_partial_errors', False) try: queries = [LogsBatchQuery(**q) for q in queries] except (KeyError, TypeError): pass - queries = [q._to_generated() for q in queries] # pylint: disable=protected-access + queries = [ + cast(LogsBatchQuery, q)._to_generated() for q in queries # pylint: disable=protected-access + ] try: request_order = [req.id for req in queries] except AttributeError: - request_order = [req['id'] for req in queries] + request_order = [req["id"] for req in queries] batch = BatchRequest(requests=queries) generated = self._query_op.batch(batch, **kwargs) mapping = {item.id: item for item in generated.responses} return order_results( request_order, mapping, - LogsQueryResult, - LogsQueryError, - allow_partial_errors) + obj=LogsQueryResult, + err=LogsQueryError, + partial_err=LogsQueryPartialResult, + raise_with=LogsQueryError, + ) def close(self): # type: () -> None diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/_metrics_query_client.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/_metrics_query_client.py index 78f168bb59de..0e41b59db673 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/_metrics_query_client.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/_metrics_query_client.py @@ -44,7 +44,7 @@ class MetricsQueryClient(object): def __init__(self, credential, **kwargs): # type: (TokenCredential, Any) -> None - endpoint = kwargs.pop('endpoint', 'https://management.azure.com') + endpoint = kwargs.pop("endpoint", "https://management.azure.com") self._client = MonitorQueryClient( credential=credential, base_url=endpoint, @@ -116,8 +116,12 @@ def query(self, resource_uri, metric_names, **kwargs): kwargs.setdefault("top", kwargs.pop("max_results", None)) kwargs.setdefault("interval", kwargs.pop("granularity", None)) kwargs.setdefault("orderby", kwargs.pop("order_by", None)) - generated = self._metrics_op.list(resource_uri, connection_verify=False, **kwargs) - return MetricsResult._from_generated(generated) # pylint: disable=protected-access + generated = self._metrics_op.list( + resource_uri, connection_verify=False, **kwargs + ) + return MetricsResult._from_generated( # pylint: disable=protected-access + generated + ) @distributed_trace def list_metric_namespaces(self, resource_uri, **kwargs): @@ -133,7 +137,7 @@ def list_metric_namespaces(self, resource_uri, **kwargs): :rtype: ~azure.core.paging.ItemPaged[~azure.monitor.query.MetricNamespace] :raises: ~azure.core.exceptions.HttpResponseError """ - start_time = kwargs.pop('start_time', None) + start_time = kwargs.pop("start_time", None) if start_time: start_time = Serializer.serialize_iso(start_time) return self._namespace_op.list( @@ -142,10 +146,12 @@ def list_metric_namespaces(self, resource_uri, **kwargs): cls=kwargs.pop( "cls", lambda objs: [ - MetricNamespace._from_generated(x) for x in objs # pylint: disable=protected-access - ] + MetricNamespace._from_generated(x) # pylint: disable=protected-access + for x in objs + ], ), - **kwargs) + **kwargs + ) @distributed_trace def list_metric_definitions(self, resource_uri, **kwargs): @@ -160,17 +166,19 @@ def list_metric_definitions(self, resource_uri, **kwargs): :rtype: ~azure.core.paging.ItemPaged[~azure.monitor.query.MetricDefinition] :raises: ~azure.core.exceptions.HttpResponseError """ - metric_namespace = kwargs.pop('namespace', None) + metric_namespace = kwargs.pop("namespace", None) return self._definitions_op.list( resource_uri, metric_namespace, cls=kwargs.pop( "cls", lambda objs: [ - MetricDefinition._from_generated(x) for x in objs # pylint: disable=protected-access - ] + MetricDefinition._from_generated(x) # pylint: disable=protected-access + for x in objs + ], ), - **kwargs) + **kwargs + ) def close(self): # type: () -> None diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/_models.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/_models.py index daa4977803fc..18c1bd3ae510 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/_models.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/_models.py @@ -12,7 +12,7 @@ from ._helpers import construct_iso8601, process_row from ._generated.models import ( BatchQueryRequest as InternalLogQueryRequest, - BatchQueryResponse + BatchQueryResponse, ) @@ -26,24 +26,26 @@ class LogsTable(object): :ivar columns: The labels of columns in this table. :vartype columns: list[str] :ivar column_types: The types of columns in this table. - :vartype columns: list[object] + :vartype column_types: list[object] :ivar rows: Required. The resulting rows from this query. :vartype rows: list[~azure.monitor.query.LogsTableRow] """ + def __init__(self, **kwargs): # type: (Any) -> None - self.name = kwargs.pop('name', None) # type: str - self.columns = kwargs.pop('columns', None) # type: Optional[str] - self.columns_types = kwargs.pop('column_types', None) # type: Optional[Any] - _rows = kwargs.pop('rows', None) + self.name = kwargs.pop("name", None) # type: str + self.columns = kwargs.pop("columns", None) # type: Optional[str] + self.columns_types = kwargs.pop("column_types", None) # type: Optional[Any] + _rows = kwargs.pop("rows", None) self.rows = [ LogsTableRow( row=row, row_index=ind, col_types=self.columns_types, - columns=self.columns - ) for ind, row in enumerate(_rows) - ] + columns=self.columns, + ) + for ind, row in enumerate(_rows) + ] @classmethod def _from_generated(cls, generated): @@ -51,42 +53,40 @@ def _from_generated(cls, generated): name=generated.name, columns=[col.name for col in generated.columns], column_types=[col.type for col in generated.columns], - rows=generated.rows + rows=generated.rows, ) class LogsTableRow(object): """Represents a single row in logs table. - ivar list row: The collection of values in the row. - ivar int row_index: The index of the row in the table + :ivar int index: The index of the row in the table """ + def __init__(self, **kwargs): # type: (Any) -> None - _col_types = kwargs['col_types'] - row = kwargs['row'] - self.row = process_row(_col_types, row) - self.row_index = kwargs['row_index'] - _columns = kwargs['columns'] - self._row_dict = { - _columns[i]: self.row[i] for i in range(len(self.row)) - } + _col_types = kwargs["col_types"] + row = kwargs["row"] + self._row = process_row(_col_types, row) + self.index = kwargs["row_index"] + _columns = kwargs["columns"] + self._row_dict = {_columns[i]: self._row[i] for i in range(len(self._row))} def __iter__(self): - """This will iterate over the row directly. - """ - return iter(self.row) + """This will iterate over the row directly.""" + return iter(self._row) def __getitem__(self, column): """This type must be subscriptable directly to row. Must be gettableby both column name and row index - Example: row[0] -> returns the first element of row and - row[column_name] -> returns the row element against the given column name. + + :param column: The name of the column or the index of the element in a row. + :type column: str or int """ try: return self._row_dict[column] except KeyError: - return self.row[column] + return self._row[column] class MetricsResult(object): @@ -111,6 +111,7 @@ class MetricsResult(object): :ivar metrics: Required. The value of the collection. :vartype metrics: list[~azure.monitor.query.Metric] """ + def __init__(self, **kwargs): # type: (Any) -> None self.cost = kwargs.get("cost", None) @@ -130,9 +131,12 @@ def _from_generated(cls, generated): granularity=generated.interval, namespace=generated.namespace, resource_region=generated.resourceregion, - metrics=[Metric._from_generated(m) for m in generated.value] # pylint: disable=protected-access + metrics=[ + Metric._from_generated(m) for m in generated.value # pylint: disable=protected-access + ], ) + class LogsBatchQuery(object): """A single request in a batch. @@ -143,7 +147,7 @@ class LogsBatchQuery(object): :param query: The Analytics query. Learn more about the `Analytics query syntax `_. :type query: str - :keyword timespan: The timespan for which to query the data. This can be a timedelta, + :keyword timespan: Required. The timespan for which to query the data. This can be a timedelta, a timedelta and a start datetime, or a start datetime/end datetime. :paramtype timespan: ~datetime.timedelta or tuple[~datetime.datetime, ~datetime.timedelta] or tuple[~datetime.datetime, ~datetime.datetime] @@ -158,10 +162,14 @@ class LogsBatchQuery(object): visualization to show. """ - def __init__(self, workspace_id, query, **kwargs): #pylint: disable=super-init-not-called + def __init__( + self, workspace_id, query, **kwargs + ): # pylint: disable=super-init-not-called # type: (str, str, Any) -> None - if 'timespan' not in kwargs: - raise TypeError("LogsBatchQuery() missing 1 required keyword-only argument: 'timespan'") + if "timespan" not in kwargs: + raise TypeError( + "LogsBatchQuery() missing 1 required keyword-only argument: 'timespan'" + ) include_statistics = kwargs.pop("include_statistics", False) include_visualization = kwargs.pop("include_visualization", False) server_timeout = kwargs.pop("server_timeout", None) @@ -177,24 +185,24 @@ def __init__(self, workspace_id, query, **kwargs): #pylint: disable=super-init-n prefer += "," prefer += "include-render=true" - headers = {'Prefer': prefer} - timespan = construct_iso8601(kwargs.pop('timespan')) + headers = {"Prefer": prefer} + timespan = construct_iso8601(kwargs.pop("timespan")) additional_workspaces = kwargs.pop("additional_workspaces", None) self.id = str(uuid.uuid4()) self.body = { - "query": query, "timespan": timespan, "workspaces": additional_workspaces + "query": query, + "timespan": timespan, + "workspaces": additional_workspaces, } self.headers = headers self.workspace = workspace_id def _to_generated(self): return InternalLogQueryRequest( - id=self.id, - body=self.body, - headers=self.headers, - workspace=self.workspace + id=self.id, body=self.body, headers=self.headers, workspace=self.workspace ) + class LogsQueryResult(object): """The LogsQueryResult. @@ -206,21 +214,16 @@ class LogsQueryResult(object): :ivar visualization: This will include a visualization property in the response that specifies the type of visualization selected by the query and any properties for that visualization. :vartype visualization: object - :ivar partial_error: Any error info. This is none except in the case where `allow_partial_errors` - is explicitly set to True. - :vartype partial_error: ~azure.core.exceptions.HttpResponseError - :ivar bool is_error: Boolean check for error item when iterating over list of - results. Always False for an instance of a LogsQueryResult. + :ivar status: The status of the result. + Always 'Success' for an instance of a LogsQueryResult. + :vartype status: ~azure.monitor.query.LogsQueryStatus """ - def __init__( - self, - **kwargs - ): - self.tables = kwargs.get('tables', None) - self.partial_error = None - self.statistics = kwargs.get('statistics', None) - self.visualization = kwargs.get('visualization', None) - self.is_error = False + + def __init__(self, **kwargs): + self.tables = kwargs.get("tables", None) + self.statistics = kwargs.get("statistics", None) + self.visualization = kwargs.get("visualization", None) + self.status = LogsQueryStatus.SUCCESS def __iter__(self): return iter(self.tables) @@ -234,10 +237,9 @@ def _from_generated(cls, generated): generated = generated.body if generated.tables is not None: tables = [ - LogsTable._from_generated( # pylint: disable=protected-access - table - ) for table in generated.tables - ] + LogsTable._from_generated(table) # pylint: disable=protected-access + for table in generated.tables + ] return cls( tables=tables, statistics=generated.statistics, @@ -246,8 +248,7 @@ def _from_generated(cls, generated): class MetricNamespaceClassification(str, Enum): - """Kind of namespace - """ + """Kind of namespace""" PLATFORM = "Platform" CUSTOM = "Custom" @@ -268,15 +269,13 @@ class MetricNamespace(object): :ivar namespace_classification: Kind of namespace. Possible values include: "Platform", "Custom", "Qos". :vartype namespace_classification: str or ~azure.monitor.query.MetricNamespaceClassification """ - def __init__( - self, - **kwargs - ): - self.id = kwargs.get('id', None) - self.type = kwargs.get('type', None) - self.name = kwargs.get('name', None) - self.fully_qualified_namespace = kwargs.get('fully_qualified_namespace', None) - self.namespace_classification = kwargs.get('namespace_classification', None) + + def __init__(self, **kwargs): + self.id = kwargs.get("id", None) + self.type = kwargs.get("type", None) + self.name = kwargs.get("name", None) + self.fully_qualified_namespace = kwargs.get("fully_qualified_namespace", None) + self.namespace_classification = kwargs.get("namespace_classification", None) @classmethod def _from_generated(cls, generated): @@ -290,13 +289,12 @@ def _from_generated(cls, generated): type=generated.type, name=generated.name, fully_qualified_namespace=fully_qualified_namespace, - namespace_classification=generated.classification + namespace_classification=generated.classification, ) class MetricClass(str, Enum): - """The class of the metric. - """ + """The class of the metric.""" AVAILABILITY = "Availability" TRANSACTIONS = "Transactions" @@ -305,7 +303,7 @@ class MetricClass(str, Enum): SATURATION = "Saturation" -class MetricDefinition(object): #pylint: disable=too-many-instance-attributes +class MetricDefinition(object): # pylint: disable=too-many-instance-attributes """Metric definition class specifies the metadata for a metric. :ivar dimension_required: Flag to indicate whether the dimension is required. @@ -338,22 +336,28 @@ class MetricDefinition(object): #pylint: disable=too-many-instance-attributes string. :vartype dimensions: list[str] """ - def __init__( - self, - **kwargs - ): + + def __init__(self, **kwargs): # type: (Any) -> None - self.dimension_required = kwargs.get('dimension_required', None) # type: Optional[bool] - self.resource_id = kwargs.get('resource_id', None) # type: Optional[str] - self.namespace = kwargs.get('namespace', None) # type: Optional[str] - self.name = kwargs.get('name', None) # type: Optional[str] - self.unit = kwargs.get('unit', None) # type: Optional[str] - self.primary_aggregation_type = kwargs.get('primary_aggregation_type', None) # type: Optional[str] - self.supported_aggregation_types = kwargs.get('supported_aggregation_types', None) # type: Optional[str] - self.metric_availabilities = kwargs.get('metric_availabilities', None) # type: List[MetricAvailability] - self.id = kwargs.get('id', None) # type: Optional[str] - self.dimensions = kwargs.get('dimensions', None) # type: Optional[List[str]] - self.metric_class = kwargs.get('metric_class', None) # type: Optional[str] + self.dimension_required = kwargs.get( + "dimension_required", None + ) # type: Optional[bool] + self.resource_id = kwargs.get("resource_id", None) # type: Optional[str] + self.namespace = kwargs.get("namespace", None) # type: Optional[str] + self.name = kwargs.get("name", None) # type: Optional[str] + self.unit = kwargs.get("unit", None) # type: Optional[str] + self.primary_aggregation_type = kwargs.get( + "primary_aggregation_type", None + ) # type: Optional[str] + self.supported_aggregation_types = kwargs.get( + "supported_aggregation_types", None + ) # type: Optional[str] + self.metric_availabilities = kwargs.get( + "metric_availabilities", None + ) # type: List[MetricAvailability] + self.id = kwargs.get("id", None) # type: Optional[str] + self.dimensions = kwargs.get("dimensions", None) # type: Optional[List[str]] + self.metric_class = kwargs.get("metric_class", None) # type: Optional[str] @classmethod def _from_generated(cls, generated): @@ -372,14 +376,16 @@ def _from_generated(cls, generated): supported_aggregation_types=generated.supported_aggregation_types, metric_class=generated.metric_class, metric_availabilities=[ - MetricAvailability._from_generated( # pylint: disable=protected-access + MetricAvailability._from_generated( # pylint: disable=protected-access val - ) for val in generated.metric_availabilities - ], + ) + for val in generated.metric_availabilities + ], id=generated.id, - dimensions=dimensions + dimensions=dimensions, ) + class MetricValue(object): """Represents a metric value. @@ -399,17 +405,15 @@ class MetricValue(object): values that contributed to the average value. :vartype count: float """ - def __init__( - self, - **kwargs - ): + + def __init__(self, **kwargs): # type: (Any) -> None - self.timestamp = kwargs['timestamp'] - self.average = kwargs.get('average', None) - self.minimum = kwargs.get('minimum', None) - self.maximum = kwargs.get('maximum', None) - self.total = kwargs.get('total', None) - self.count = kwargs.get('count', None) + self.timestamp = kwargs["timestamp"] + self.average = kwargs.get("average", None) + self.minimum = kwargs.get("minimum", None) + self.maximum = kwargs.get("maximum", None) + self.total = kwargs.get("total", None) + self.count = kwargs.get("count", None) @classmethod def _from_generated(cls, generated): @@ -421,9 +425,10 @@ def _from_generated(cls, generated): minimum=generated.minimum, maximum=generated.maximum, total=generated.total, - count=generated.count + count=generated.count, ) + class Metric(object): """The result data of a query. @@ -444,17 +449,15 @@ class Metric(object): :ivar display_description: Detailed description of this metric. :vartype display_description: str """ - def __init__( - self, - **kwargs - ): + + def __init__(self, **kwargs): # type: (Any) -> None - self.id = kwargs['id'] - self.type = kwargs['type'] - self.name = kwargs['name'] - self.unit = kwargs['unit'] - self.timeseries = kwargs['timeseries'] - self.display_description = kwargs['display_description'] + self.id = kwargs["id"] + self.type = kwargs["type"] + self.name = kwargs["name"] + self.unit = kwargs["unit"] + self.timeseries = kwargs["timeseries"] + self.display_description = kwargs["display_description"] @classmethod def _from_generated(cls, generated): @@ -466,8 +469,9 @@ def _from_generated(cls, generated): name=generated.name.value, unit=generated.unit, timeseries=[ - TimeSeriesElement._from_generated(t) for t in generated.timeseries # pylint: disable=protected-access - ], + TimeSeriesElement._from_generated(t) # pylint: disable=protected-access + for t in generated.timeseries + ], display_description=generated.display_description, ) @@ -483,17 +487,14 @@ class TimeSeriesElement(object): """ _attribute_map = { - 'metadata_values': {'key': 'metadata_values', 'type': '[MetadataValue]'}, - 'data': {'key': 'data', 'type': '[MetricValue]'}, + "metadata_values": {"key": "metadata_values", "type": "[MetadataValue]"}, + "data": {"key": "data", "type": "[MetricValue]"}, } - def __init__( - self, - **kwargs - ): + def __init__(self, **kwargs): # type: (Any) -> None - self.metadata_values = kwargs.get('metadatavalues', None) - self.data = kwargs.get('data', None) + self.metadata_values = kwargs.get("metadatavalues", None) + self.data = kwargs.get("data", None) @classmethod def _from_generated(cls, generated): @@ -503,7 +504,9 @@ def _from_generated(cls, generated): metadata_values={ obj.name.value: obj.value for obj in generated.metadatavalues }, - data=[MetricValue._from_generated(val) for val in generated.data] # pylint: disable=protected-access + data=[ + MetricValue._from_generated(val) for val in generated.data # pylint: disable=protected-access + ], ) @@ -518,27 +521,21 @@ class MetricAvailability(object): a duration 'PT1M', 'P1D', etc. :vartype retention: ~datetime.timedelta """ - def __init__( - self, - **kwargs - ): + + def __init__(self, **kwargs): # type: (Any) -> None - self.granularity = kwargs.get('granularity', None) - self.retention = kwargs.get('retention', None) + self.granularity = kwargs.get("granularity", None) + self.retention = kwargs.get("retention", None) @classmethod def _from_generated(cls, generated): if not generated: return cls() - return cls( - granularity=generated.time_grain, - retention=generated.retention - ) + return cls(granularity=generated.time_grain, retention=generated.retention) class MetricAggregationType(str, Enum): - """The aggregation type of the metric. - """ + """The aggregation type of the metric.""" NONE = "None" AVERAGE = "Average" @@ -549,8 +546,7 @@ class MetricAggregationType(str, Enum): class MetricUnit(str, Enum): - """The unit of the metric. - """ + """The unit of the metric.""" COUNT = "Count" BYTES = "Bytes" @@ -565,3 +561,58 @@ class MetricUnit(str, Enum): MILLI_CORES = "MilliCores" NANO_CORES = "NanoCores" BITS_PER_SECOND = "BitsPerSecond" + + +class LogsQueryPartialResult(object): + """The LogsQueryPartialResult. + + :ivar partial_data: The list of tables, columns and rows. + :vartype partial_data: list[~azure.monitor.query.LogsTable] + :ivar statistics: This will include a statistics property in the response that describes various + performance statistics such as query execution time and resource usage. + :vartype statistics: object + :ivar visualization: This will include a visualization property in the response that specifies the type of + visualization selected by the query and any properties for that visualization. + :vartype visualization: object + :ivar partial_error: The partial errror info + :vartype partial_error: ~azure.monitor.query.LogsQueryError + :ivar status: The status of the result. Always 'PartialError' for an instance of a LogsQueryPartialResult. + :vartype status: ~azure.monitor.query.LogsQueryStatus + """ + + def __init__(self, **kwargs): + self.partial_data = kwargs.get("partial_data", None) + self.partial_error = kwargs.get("partial_error", None) + self.statistics = kwargs.get("statistics", None) + self.visualization = kwargs.get("visualization", None) + self.status = LogsQueryStatus.PARTIAL + + def __iter__(self): + return iter(self.partial_data) + + @classmethod + def _from_generated(cls, generated, error): # pylint: disable=arguments-differ + if not generated: + return cls() + partial_data = None + if isinstance(generated, BatchQueryResponse): + generated = generated.body + if generated.tables is not None: + partial_data = [ + LogsTable._from_generated(table) # pylint: disable=protected-access + for table in generated.tables + ] + return cls( + partial_data=partial_data, + partial_error=error._from_generated(generated.error), # pylint: disable=protected-access + statistics=generated.statistics, + visualization=generated.render, + ) + + +class LogsQueryStatus(str, Enum): + """The status of the result object.""" + + PARTIAL = "PartialError" + SUCCESS = "Success" + FAILURE = "Failure" diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/__init__.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/__init__.py index 9dbf3b056c78..8b144299baf9 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/__init__.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/__init__.py @@ -7,7 +7,4 @@ from ._logs_query_client_async import LogsQueryClient from ._metrics_query_client_async import MetricsQueryClient -__all__ = [ - "LogsQueryClient", - "MetricsQueryClient" -] +__all__ = ["LogsQueryClient", "MetricsQueryClient"] diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_helpers_asyc.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_helpers_asyc.py index 0dc26a8903a4..ffecbec48927 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_helpers_asyc.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_helpers_asyc.py @@ -8,30 +8,34 @@ from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy if TYPE_CHECKING: - from azure.core.credentials import TokenCredential + from azure.core.credentials_async import AsyncTokenCredential + def get_authentication_policy( - credential: 'TokenCredential' + credential: "AsyncTokenCredential", ) -> AsyncBearerTokenCredentialPolicy: - """Returns the correct authentication policy - """ + """Returns the correct authentication policy""" if credential is None: raise ValueError("Parameter 'credential' must not be None.") if hasattr(credential, "get_token"): - return AsyncBearerTokenCredentialPolicy(credential, "https://api.loganalytics.io/.default") + return AsyncBearerTokenCredentialPolicy( + credential, "https://api.loganalytics.io/.default" + ) raise TypeError("Unsupported credential") + def get_metrics_authentication_policy( - credential: 'TokenCredential' + credential: "AsyncTokenCredential", ) -> AsyncBearerTokenCredentialPolicy: - """Returns the correct authentication policy - """ + """Returns the correct authentication policy""" if credential is None: raise ValueError("Parameter 'credential' must not be None.") if hasattr(credential, "get_token"): - return AsyncBearerTokenCredentialPolicy(credential, "https://management.azure.com/.default") + return AsyncBearerTokenCredentialPolicy( + credential, "https://management.azure.com/.default" + ) raise TypeError("Unsupported credential") diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_logs_query_client_async.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_logs_query_client_async.py index e31806a110fa..40d3c5124904 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_logs_query_client_async.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_logs_query_client_async.py @@ -14,9 +14,9 @@ from .._generated.models import BatchRequest, QueryBody as LogsQueryBody from .._helpers import construct_iso8601, order_results, process_error, process_prefer -from .._models import LogsQueryResult, LogsBatchQuery +from .._models import LogsQueryResult, LogsBatchQuery, LogsQueryPartialResult from ._helpers_asyc import get_authentication_policy -from .._exceptions import LogsQueryError, QueryPartialErrorException +from .._exceptions import LogsQueryError if TYPE_CHECKING: from azure.core.credentials_async import AsyncTokenCredential @@ -32,7 +32,7 @@ class LogsQueryClient(object): """ def __init__(self, credential: "AsyncTokenCredential", **kwargs: Any) -> None: - self._endpoint = kwargs.pop('endpoint', 'https://api.loganalytics.io/v1') + self._endpoint = kwargs.pop("endpoint", "https://api.loganalytics.io/v1") self._client = MonitorQueryClient( credential=credential, authentication_policy=get_authentication_policy(credential), @@ -47,8 +47,11 @@ async def query( workspace_id: str, query: str, *, - timespan: Union[timedelta, Tuple[datetime, timedelta], Tuple[datetime, datetime]], - **kwargs: Any) -> LogsQueryResult: + timespan: Union[ + timedelta, Tuple[datetime, timedelta], Tuple[datetime, datetime] + ], + **kwargs: Any + ) -> Union[LogsQueryResult, LogsQueryPartialResult]: """Execute an Analytics query. Executes an Analytics query for data. @@ -59,7 +62,7 @@ async def query( :param query: The Kusto query. Learn more about the `Kusto query syntax `_. :type query: str - :param timespan: The timespan for which to query the data. This can be a timedelta, + :param timespan: Required. The timespan for which to query the data. This can be a timedelta, a timedelta and a start datetime, or a start datetime/end datetime. :type timespan: ~datetime.timedelta or tuple[~datetime.datetime, ~datetime.timedelta] or tuple[~datetime.datetime, ~datetime.datetime] @@ -73,50 +76,46 @@ async def query( These can be qualified workspace names, workspace Ids or Azure resource Ids. :paramtype additional_workspaces: list[str] :return: QueryResults, or the result of cls(response) - :rtype: ~azure.monitor.query.LogsQueryResult + :rtype: ~azure.monitor.query.LogsQueryResult or ~azure.monitor.query.LogsQueryPartialResult :raises: ~azure.core.exceptions.HttpResponseError """ - allow_partial_errors = kwargs.pop('allow_partial_errors', False) timespan = construct_iso8601(timespan) include_statistics = kwargs.pop("include_statistics", False) include_visualization = kwargs.pop("include_visualization", False) server_timeout = kwargs.pop("server_timeout", None) additional_workspaces = kwargs.pop("additional_workspaces", None) - prefer = process_prefer(server_timeout, include_statistics, include_visualization) + prefer = process_prefer( + server_timeout, include_statistics, include_visualization + ) body = LogsQueryBody( - query=query, - timespan=timespan, - workspaces=additional_workspaces, - **kwargs + query=query, timespan=timespan, workspaces=additional_workspaces, **kwargs ) try: - generated_response = await self._query_op.execute( # pylint: disable=protected-access - workspace_id=workspace_id, - body=body, - prefer=prefer, - **kwargs + generated_response = ( + await self._query_op.execute( # pylint: disable=protected-access + workspace_id=workspace_id, body=body, prefer=prefer, **kwargs + ) ) except HttpResponseError as err: process_error(err, LogsQueryError) - response = LogsQueryResult._from_generated(generated_response) # pylint: disable=protected-access + response = None if not generated_response.error: - return response - if not allow_partial_errors: - raise QueryPartialErrorException(error=generated_response.error) - response.partial_error = LogsQueryError._from_generated( # pylint: disable=protected-access - generated_response.error + response = LogsQueryResult._from_generated( # pylint: disable=protected-access + generated_response + ) + else: + response = LogsQueryPartialResult._from_generated( # pylint: disable=protected-access + generated_response, LogsQueryError ) return response @distributed_trace_async async def query_batch( - self, - queries: Union[Sequence[Dict], Sequence[LogsBatchQuery]], - **kwargs: Any - ) -> List[LogsQueryResult]: + self, queries: Union[Sequence[Dict], Sequence[LogsBatchQuery]], **kwargs: Any + ) -> List[Union[LogsQueryResult, LogsQueryError, LogsQueryPartialResult]]: """Execute a list of analytics queries. Each request can be either a LogQueryRequest object or an equivalent serialized model. @@ -124,32 +123,33 @@ async def query_batch( :param queries: The list of Kusto queries to execute. :type queries: list[dict] or list[~azure.monitor.query.LogsBatchQuery] - :keyword bool allow_partial_errors: If set to True, a `LogsQueryResult` object is returned - when a partial error occurs. The error can be accessed using the `partial_error` - attribute in the object. :return: list of LogsQueryResult objects, or the result of cls(response) - :rtype: list[~azure.monitor.query.LogsQueryResult] + :rtype: list[~azure.monitor.query.LogsQueryResult or ~azure.monitor.query.LogsQueryPartialResult + or ~azure.monitor.query.LogsQueryError] :raises: ~azure.core.exceptions.HttpResponseError """ - allow_partial_errors = kwargs.pop('allow_partial_errors', False) try: - queries = [LogsBatchQuery(**q) for q in queries] + queries = [LogsBatchQuery(**q) for q in queries] # type: ignore except (KeyError, TypeError): pass - queries = [q._to_generated() for q in queries] # pylint: disable=protected-access + queries = [ + q._to_generated() for q in queries # pylint: disable=protected-access + ] try: request_order = [req.id for req in queries] except AttributeError: - request_order = [req['id'] for req in queries] + request_order = [req["id"] for req in queries] batch = BatchRequest(requests=queries) generated = await self._query_op.batch(batch, **kwargs) mapping = {item.id: item for item in generated.responses} return order_results( request_order, mapping, - LogsQueryResult, - LogsQueryError, - allow_partial_errors) + obj=LogsQueryResult, + err=LogsQueryError, + partial_err=LogsQueryPartialResult, + raise_with=LogsQueryError, + ) async def __aenter__(self) -> "LogsQueryClient": await self._client.__aenter__() diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_metrics_query_client_async.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_metrics_query_client_async.py index 0d06cbe8a0b1..c13a6759598c 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_metrics_query_client_async.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_metrics_query_client_async.py @@ -25,6 +25,7 @@ if TYPE_CHECKING: from azure.core.credentials_async import AsyncTokenCredential + class MetricsQueryClient(object): """MetricsQueryClient @@ -35,7 +36,7 @@ class MetricsQueryClient(object): """ def __init__(self, credential: "AsyncTokenCredential", **kwargs: Any) -> None: - endpoint = kwargs.pop('endpoint', 'https://management.azure.com') + endpoint = kwargs.pop("endpoint", "https://management.azure.com") self._client = MonitorQueryClient( credential=credential, base_url=endpoint, @@ -48,11 +49,8 @@ def __init__(self, credential: "AsyncTokenCredential", **kwargs: Any) -> None: @distributed_trace_async async def query( - self, - resource_uri: str, - metric_names: List, - **kwargs: Any - ) -> MetricsResult: + self, resource_uri: str, metric_names: List, **kwargs: Any + ) -> MetricsResult: """Lists the metric values for a resource. **Note**: Although the start_time, end_time, duration are optional parameters, it is highly @@ -95,7 +93,7 @@ async def query( :rtype: ~azure.monitor.query.MetricsResult :raises: ~azure.core.exceptions.HttpResponseError """ - timespan = construct_iso8601(kwargs.pop('timespan', None)) + timespan = construct_iso8601(kwargs.pop("timespan", None)) kwargs.setdefault("metricnames", ",".join(metric_names)) kwargs.setdefault("timespan", timespan) kwargs.setdefault("top", kwargs.pop("max_results", None)) @@ -104,11 +102,17 @@ async def query( aggregations = kwargs.pop("aggregations", None) if aggregations: kwargs.setdefault("aggregation", ",".join(aggregations)) - generated = await self._metrics_op.list(resource_uri, connection_verify=False, **kwargs) - return MetricsResult._from_generated(generated) # pylint: disable=protected-access + generated = await self._metrics_op.list( + resource_uri, connection_verify=False, **kwargs + ) + return MetricsResult._from_generated( # pylint: disable=protected-access + generated + ) @distributed_trace - def list_metric_namespaces(self, resource_uri: str, **kwargs: Any) -> AsyncItemPaged[MetricNamespace]: + def list_metric_namespaces( + self, resource_uri: str, **kwargs: Any + ) -> AsyncItemPaged[MetricNamespace]: """Lists the metric namespaces for the resource. :param resource_uri: The identifier of the resource. @@ -120,7 +124,7 @@ def list_metric_namespaces(self, resource_uri: str, **kwargs: Any) -> AsyncItemP :rtype: ~azure.core.paging.AsyncItemPaged[:class: `~azure.monitor.query.MetricNamespace`] :raises: ~azure.core.exceptions.HttpResponseError """ - start_time = kwargs.pop('start_time', None) + start_time = kwargs.pop("start_time", None) if start_time: start_time = Serializer.serialize_iso(start_time) return self._namespace_op.list( @@ -129,17 +133,17 @@ def list_metric_namespaces(self, resource_uri: str, **kwargs: Any) -> AsyncItemP cls=kwargs.pop( "cls", lambda objs: [ - MetricNamespace._from_generated(x) for x in objs # pylint: disable=protected-access - ] + MetricNamespace._from_generated(x) # pylint: disable=protected-access + for x in objs + ], ), - **kwargs) + **kwargs + ) @distributed_trace def list_metric_definitions( - self, - resource_uri: str, - **kwargs: Any - ) -> AsyncItemPaged[MetricDefinition]: + self, resource_uri: str, **kwargs: Any + ) -> AsyncItemPaged[MetricDefinition]: """Lists the metric definitions for the resource. :param resource_uri: The identifier of the resource. @@ -150,17 +154,19 @@ def list_metric_definitions( :rtype: ~azure.core.paging.AsyncItemPaged[:class: `~azure.monitor.query.MetricDefinition`] :raises: ~azure.core.exceptions.HttpResponseError """ - metric_namespace = kwargs.pop('namespace', None) + metric_namespace = kwargs.pop("namespace", None) return self._definitions_op.list( resource_uri, metric_namespace, cls=kwargs.pop( "cls", lambda objs: [ - MetricDefinition._from_generated(x) for x in objs # pylint: disable=protected-access - ] + MetricDefinition._from_generated(x) # pylint: disable=protected-access + for x in objs + ], ), - **kwargs) + **kwargs + ) async def __aenter__(self) -> "MetricsQueryClient": await self._client.__aenter__() diff --git a/sdk/monitor/azure-monitor-query/samples/champion_scenarios.md b/sdk/monitor/azure-monitor-query/samples/champion_scenarios.md deleted file mode 100644 index d104a65ead8f..000000000000 --- a/sdk/monitor/azure-monitor-query/samples/champion_scenarios.md +++ /dev/null @@ -1,288 +0,0 @@ -## Azure Monitor Query Champion Scenarios - -This document covers the basic champion Scenarios to use the package. - -### Authenticate the client - -Consider the following example, which creates and authenticates clients for both logs and metrics querying: - -```python -from azure.identity import DefaultAzureCredential -from azure.monitor.query import LogsQueryClient, MetricsQueryClient - -credential = DefaultAzureCredential() -logs_client = LogsQueryClient(credential) -metrics_client = MetricsQueryClient(credential) -``` - -### Make a simple query to the service - -* Each row is converted into a native python data type. For example, time is a datetime object instead of string. - -#### Results in tabular form - -```python -import os -import pandas as pd -from datetime import timedelta -from azure.monitor.query import LogsQueryClient -from azure.identity import DefaultAzureCredential - -def query(): - query = """AppRequests | - summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId""" - - response = client.query(os.environ['LOG_WORKSPACE_ID'], query, - timespan=timedelta(days=1)) - - if not response.tables: - return None - - primary_table = response.tables[0] - df = pd.DataFrame(table.rows, columns=table.columns) - return df - -if __name__ == '__main__': - print(query()) - -""" - TimeGenerated _ResourceId avgRequestDuration -0 2021-05-27T08:40:00Z /subscriptions/... 27.307699999999997 -1 2021-05-27T08:50:00Z /subscriptions/... 18.11655 -2 2021-05-27T09:00:00Z /subscriptions/... 24.5271 -""" - -``` - -#### Results in Key Value form - -```python -import os -import pandas as pd -from datetime import timedelta -from azure.monitor.query import LogsQueryClient -from azure.identity import DefaultAzureCredential - -def query(): - query = """AppRequests | - summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId""" - - response = client.query(os.environ['LOG_WORKSPACE_ID'], query, - timespan=timedelta(days=1)) - - if not response.tables: - return None - - primary_table = response.tables[0] - df = pd.DataFrame(table.rows, columns=table.columns) - return df.to_dict(orient='records') - -if __name__ == '__main__': - print(query()) - - -""" -[ - { - 'TimeGenerated': Timestamp('2021-08-24 01:10:00+0000'), - '_ResourceId': '/subscriptions/faa080af....', - 'avgRequestDuration': 19.7987 - }, - { - 'TimeGenerated': Timestamp('2021-08-24 01:10:00+0000'), - '_ResourceId': '/subscriptions/faa08....', - 'avgRequestDuration': 33.9654 - }, - { - 'TimeGenerated': Timestamp('2021-08-24 01:10:00+0000'), - '_ResourceId': '/subscriptions/faa080....', - 'avgRequestDuration': 44.13115 - } -] -""" - -``` - -### Run multiple queries in 1 api call - -* batch_query returns the results as a list in the same order in which the requests were sent. -* Each item in the result will have an error attribute if there is an error. - -#### Results in tabular form - -```python -from datetime import datetime, timedelta -import os -import pandas as pd -from azure.monitor.query import LogsQueryClient, LogsBatchQuery -from azure.identity import DefaultAzureCredential - - -credential = DefaultAzureCredential() - -client = LogsQueryClient(credential) - -requests = [ - LogsBatchQuery( - query="AzureActivity | summarize count()", - timespan=timedelta(hours=1), - workspace_id= os.environ['LOG_WORKSPACE_ID'] - ), - LogsBatchQuery( - query= """AppRequests | take 5 | - summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId""", - timespan=(datetime(2021, 6, 2), timedelta(hours=1)), - workspace_id= os.environ['LOG_WORKSPACE_ID'] - ), - LogsBatchQuery( - query= """AppRequests | take 5 | - summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId""", - workspace_id= os.environ['LOG_WORKSPACE_ID'], - timespan=(datetime(2021, 6, 2), datetime(2021, 6, 3)), - include_statistics=True - ), -] -results = client.query_batch(requests) - -for response in results: - if response.error is not None: - error = response.error.innererror - print(error) - - table = response.tables[0] - df = pd.DataFrame(table.rows, columns=table.columns) - print(df) - print("\n\n-------------------------\n\n") - -""" - count_ -0 2 - - -------------------------- - - - TimeGenerated _ResourceId avgRequestDuration -0 2021-06-02 00:20:00+00:00 /subscriptions/... 18.12380 -1 2021-06-02 00:00:00+00:00 /subscriptions/... 20.84805 -2 2021-06-02 00:10:00+00:00 /subscriptions/... 19.72410 -3 2021-06-02 00:30:00+00:00 /subscriptions/... 19.41265 -4 2021-06-02 00:40:00+00:00 /subscriptions/... 19.17145 - - -------------------------- - - - - TimeGenerated _ResourceId avgRequestDuration -0 2021-06-02 00:20:00+00:00 /subscriptions/... 18.12380 -1 2021-06-02 00:00:00+00:00 /subscriptions/... 20.84805 -2 2021-06-02 00:10:00+00:00 /subscriptions/... 19.72410 -3 2021-06-02 00:30:00+00:00 /subscriptions/... 19.41265 -4 2021-06-02 00:40:00+00:00 /subscriptions/... 19.17145 - - - -------------------------- -""" -``` - -#### Results in Key Value form - - -Very Simlar to above: - -```python -for response in results: - if response.error is not None: - error = response.error.innererror - print(error) - - table = response.tables[0] - df = pd.DataFrame(table.rows, columns=table.columns) - print(df.to_dict(orient='records')) - print("\n\n-------------------------\n\n") -``` - -### Run a complex query to set server timeout for more than 3 minutes. - -```python -import os -import pandas as pd -from azure.core.serialization import NULL -from azure.monitor.query import LogsQueryClient -from azure.identity import DefaultAzureCredential - - -credential = DefaultAzureCredential() - -client = LogsQueryClient(credential) - -response = client.query( - os.environ['LOG_WORKSPACE_ID'], - "range x from 1 to 10000000000 step 1 | count", - timespan=NULL, # can pass None too - server_timeout=600 - ) - -### results in server timeout -``` - -### Run a metrics Query - -```python -import os -from datetime import timedelta -from azure.monitor.query import MetricsQueryClient, MetricAggregationType -from azure.identity import DefaultAzureCredential - -credential = DefaultAzureCredential() - -client = MetricsQueryClient(credential) - -metrics_uri = os.environ['METRICS_RESOURCE_URI'] -response = client.query( - metrics_uri, - metric_names=["Ingress"], - timespan=timedelta(hours=2), - granularity=timedelta(minutes=5), - aggregations=[MetricAggregationType.AVERAGE], - ) - -for metric in response.metrics: - print(metric.name + ' -- ' + metric.display_description) - for time_series_element in metric.timeseries: - for metric_value in time_series_element.data: - print('The ingress at {} is {}'.format( - metric_value.timestamp, - metric_value.average - )) - -""" -Ingress -- The amount of ingress data, in bytes. This number includes ingress from an external client into Azure Storage as well as ingress within Azure. -The ingress at 2021-08-23 23:58:00+00:00 is 567.4285714285714 -The ingress at 2021-08-24 00:03:00+00:00 is 812.0 -The ingress at 2021-08-24 00:08:00+00:00 is 812.0 -The ingress at 2021-08-24 00:13:00+00:00 is 812.0 -The ingress at 2021-08-24 00:18:00+00:00 is 812.0 -The ingress at 2021-08-24 00:23:00+00:00 is 3623.3333333333335 -The ingress at 2021-08-24 00:28:00+00:00 is 1082.75 -The ingress at 2021-08-24 00:33:00+00:00 is 1160.6666666666667 -The ingress at 2021-08-24 00:38:00+00:00 is 1060.75 -The ingress at 2021-08-24 00:43:00+00:00 is 1081.75 -The ingress at 2021-08-24 00:48:00+00:00 is 1061.25 -The ingress at 2021-08-24 00:53:00+00:00 is 1160.3333333333333 -The ingress at 2021-08-24 00:58:00+00:00 is 1082.0 -The ingress at 2021-08-24 01:03:00+00:00 is 1628.6666666666667 -The ingress at 2021-08-24 01:08:00+00:00 is 794.6666666666666 -The ingress at 2021-08-24 01:13:00+00:00 is 1060.25 -The ingress at 2021-08-24 01:18:00+00:00 is 1160.0 -The ingress at 2021-08-24 01:23:00+00:00 is 1082.0 -The ingress at 2021-08-24 01:28:00+00:00 is 1060.5 -The ingress at 2021-08-24 01:33:00+00:00 is 1630.0 -The ingress at 2021-08-24 01:38:00+00:00 is 795.0 -The ingress at 2021-08-24 01:43:00+00:00 is 827.6 -The ingress at 2021-08-24 01:48:00+00:00 is 1250.5 -The ingress at 2021-08-24 01:53:00+00:00 is 1061.75 -""" -``` \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-query/samples/sample_batch_query.py b/sdk/monitor/azure-monitor-query/samples/sample_batch_query.py index 43444c369c3c..ce956d7c27fb 100644 --- a/sdk/monitor/azure-monitor-query/samples/sample_batch_query.py +++ b/sdk/monitor/azure-monitor-query/samples/sample_batch_query.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta import os import pandas as pd -from azure.monitor.query import LogsQueryClient, LogsBatchQuery +from azure.monitor.query import LogsQueryClient, LogsBatchQuery, LogsQueryStatus, LogsQueryPartialResult from azure.identity import DefaultAzureCredential @@ -33,17 +33,23 @@ include_statistics=True ), ] -responses = client.query_batch(requests, allow_partial_errors=False) - -for response in responses: - if not response.is_error: - table = response.tables[0] +results = client.query_batch(requests) + +for res in results: + if res.status == LogsQueryStatus.FAILURE: + # this will be a LogsQueryError + print(res.message) + elif res.status == LogsQueryStatus.PARTIAL: + ## this will be a LogsQueryPartialResult + print(res.partial_error.message) + table = res.tables[0] + df = pd.DataFrame(table.rows, columns=table.columns) + print(df) + elif res.status == LogsQueryStatus.SUCCESS: + ## this will be a LogsQueryResult + table = res.tables[0] df = pd.DataFrame(table.rows, columns=table.columns) print(df) - print("\n\n-------------------------\n\n") - else: - error = response - print(error.message) # [END send_query_batch] \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-query/samples/sample_log_query_client.py b/sdk/monitor/azure-monitor-query/samples/sample_log_query_client.py index d16d38513f65..64eba9420502 100644 --- a/sdk/monitor/azure-monitor-query/samples/sample_log_query_client.py +++ b/sdk/monitor/azure-monitor-query/samples/sample_log_query_client.py @@ -4,7 +4,7 @@ import os import pandas as pd from datetime import timedelta -from azure.monitor.query import LogsQueryClient, QueryPartialErrorException +from azure.monitor.query import LogsQueryClient, LogsQueryStatus from azure.core.exceptions import HttpResponseError from azure.identity import DefaultAzureCredential @@ -17,17 +17,18 @@ # Response time trend # request duration over the last 12 hours. # [START send_logs_query] -query = """AppRequests | take 5""" +query = """AppRadfequests | take 5""" # returns LogsQueryResult try: response = client.query(os.environ['LOG_WORKSPACE_ID'], query, timespan=timedelta(days=1)) + if response.status == LogsQueryStatus.PARTIAL: + # handle error here + error = response.partial_error + print(error.message) for table in response: df = pd.DataFrame(data=table.rows, columns=table.columns) print(df) -except QueryPartialErrorException as err: - print("this is a partial error") - print(err.details) except HttpResponseError as err: print("something fatal happened") print (err) diff --git a/sdk/monitor/azure-monitor-query/tests/async/test_exceptions_async.py b/sdk/monitor/azure-monitor-query/tests/async/test_exceptions_async.py index cf46756a6ddf..99f3b2db2a02 100644 --- a/sdk/monitor/azure-monitor-query/tests/async/test_exceptions_async.py +++ b/sdk/monitor/azure-monitor-query/tests/async/test_exceptions_async.py @@ -3,7 +3,7 @@ import os from azure.identity.aio import ClientSecretCredential from azure.core.exceptions import HttpResponseError -from azure.monitor.query import LogsBatchQuery, LogsQueryError,LogsQueryResult, QueryPartialErrorException +from azure.monitor.query import LogsBatchQuery, LogsQueryError,LogsQueryResult, LogsQueryPartialResult from azure.monitor.query.aio import LogsQueryClient def _credential(): @@ -30,18 +30,8 @@ async def test_logs_single_query_partial_exception_not_allowed(): query = """let Weight = 92233720368547758; range x from 1 to 3 step 1 | summarize percentilesw(x, Weight * 100, 50)""" - with pytest.raises(QueryPartialErrorException) as err: - await client.query(os.environ['LOG_WORKSPACE_ID'], query, timespan=timedelta(days=1)) - -@pytest.mark.live_test_only -@pytest.mark.asyncio -async def test_logs_single_query_partial_exception_allowed(): - credential = _credential() - client = LogsQueryClient(credential) - query = """let Weight = 92233720368547758; - range x from 1 to 3 step 1 - | summarize percentilesw(x, Weight * 100, 50)""" - response = await client.query(os.environ['LOG_WORKSPACE_ID'], query, timespan=timedelta(days=1), allow_partial_errors=True) + response = await client.query(os.environ['LOG_WORKSPACE_ID'], query, timespan=timedelta(days=1)) + assert response.__class__ == LogsQueryPartialResult assert response.partial_error is not None assert response.partial_error.code == 'PartialError' assert response.partial_error.__class__ == LogsQueryError @@ -76,7 +66,7 @@ async def test_logs_batch_query_fatal_exception(): ), ] with pytest.raises(HttpResponseError): - await client.query_batch(requests, allow_partial_errors=True) + await client.query_batch(requests) @pytest.mark.live_test_only @pytest.mark.asyncio @@ -90,38 +80,7 @@ async def test_logs_batch_query_partial_exception_not_allowed(): workspace_id= os.environ['LOG_WORKSPACE_ID'] ), LogsBatchQuery( - query= """AppRequests | take 10""", - timespan=(datetime(2021, 6, 2), timedelta(days=1)), - workspace_id= os.environ['LOG_WORKSPACE_ID'] - ), - LogsBatchQuery( - query= """let Weight = 92233720368547758; - range x from 1 to 3 step 1 - | summarize percentilesw(x, Weight * 100, 50)""", - workspace_id= os.environ['LOG_WORKSPACE_ID'], - timespan=(datetime(2021, 6, 2), datetime(2021, 6, 3)), - include_statistics=True - ), - ] - responses = await client.query_batch(requests) - r1, r2, r3 = responses[0], responses[1], responses[2] - assert r1.__class__ == LogsQueryResult - assert r2.__class__ == LogsQueryResult - assert r3.__class__ == LogsQueryError - -@pytest.mark.live_test_only -@pytest.mark.asyncio -async def test_logs_batch_query_partial_exception_allowed(): - credential = _credential() - client = LogsQueryClient(credential) - requests = [ - LogsBatchQuery( - query="AzureActivity | summarize count()", - timespan=timedelta(hours=1), - workspace_id= os.environ['LOG_WORKSPACE_ID'] - ), - LogsBatchQuery( - query= """AppRequests | take 10""", + query= """bad query | take 10""", timespan=(datetime(2021, 6, 2), timedelta(days=1)), workspace_id= os.environ['LOG_WORKSPACE_ID'] ), @@ -134,38 +93,8 @@ async def test_logs_batch_query_partial_exception_allowed(): include_statistics=True ), ] - responses = await client.query_batch(requests, allow_partial_errors=True) - r1, r2, r3 = responses[0], responses[1], responses[2] - assert r1.__class__ == LogsQueryResult - assert r2.__class__ == LogsQueryResult - assert r3.__class__ == LogsQueryResult - assert r3.partial_error is not None - -@pytest.mark.live_test_only -@pytest.mark.asyncio -async def test_logs_batch_query_non_fatal_exception(): - credential = _credential() - client = LogsQueryClient(credential) - requests = [ - LogsBatchQuery( - query="AzureActivity | summarize count()", - timespan=timedelta(hours=1), - workspace_id= os.environ['LOG_WORKSPACE_ID'] - ), - LogsBatchQuery( - query= """AppRequests | take 10""", - timespan=(datetime(2021, 6, 2), timedelta(days=1)), - workspace_id= os.environ['LOG_WORKSPACE_ID'] - ), - LogsBatchQuery( - query= """Bad Query""", - workspace_id= os.environ['LOG_WORKSPACE_ID'], - timespan=(datetime(2021, 6, 2), datetime(2021, 6, 3)), - include_statistics=True - ), - ] responses = await client.query_batch(requests) r1, r2, r3 = responses[0], responses[1], responses[2] assert r1.__class__ == LogsQueryResult - assert r2.__class__ == LogsQueryResult - assert r3.__class__ == LogsQueryError + assert r2.__class__ == LogsQueryError + assert r3.__class__ == LogsQueryPartialResult diff --git a/sdk/monitor/azure-monitor-query/tests/test_exceptions.py b/sdk/monitor/azure-monitor-query/tests/test_exceptions.py index 92d28cd75dd9..df4a351802f2 100644 --- a/sdk/monitor/azure-monitor-query/tests/test_exceptions.py +++ b/sdk/monitor/azure-monitor-query/tests/test_exceptions.py @@ -3,7 +3,7 @@ import os from azure.identity import ClientSecretCredential from azure.core.exceptions import HttpResponseError -from azure.monitor.query import LogsQueryClient, LogsBatchQuery, LogsQueryError,LogsQueryResult, QueryPartialErrorException +from azure.monitor.query import LogsQueryClient, LogsBatchQuery, LogsQueryError, LogsQueryResult, LogsQueryPartialResult def _credential(): credential = ClientSecretCredential( @@ -21,24 +21,16 @@ def test_logs_single_query_fatal_exception(): client.query('bad_workspace_id', 'AppRequests', timespan=None) @pytest.mark.live_test_only -def test_logs_single_query_partial_exception_not_allowed(): +def test_logs_single_query_partial_exception(): credential = _credential() client = LogsQueryClient(credential) query = """let Weight = 92233720368547758; range x from 1 to 3 step 1 | summarize percentilesw(x, Weight * 100, 50)""" - with pytest.raises(QueryPartialErrorException) as err: - client.query(os.environ['LOG_WORKSPACE_ID'], query, timespan=timedelta(days=1)) - -@pytest.mark.live_test_only -def test_logs_single_query_partial_exception_allowed(): - credential = _credential() - client = LogsQueryClient(credential) - query = """let Weight = 92233720368547758; - range x from 1 to 3 step 1 - | summarize percentilesw(x, Weight * 100, 50)""" - response = client.query(os.environ['LOG_WORKSPACE_ID'], query, timespan=timedelta(days=1), allow_partial_errors=True) + response = client.query(os.environ['LOG_WORKSPACE_ID'], query, timespan=timedelta(days=1)) + assert response.__class__ == LogsQueryPartialResult assert response.partial_error is not None + assert response.partial_data is not None assert response.partial_error.code == 'PartialError' assert response.partial_error.__class__ == LogsQueryError @@ -71,10 +63,10 @@ def test_logs_batch_query_fatal_exception(): ), ] with pytest.raises(HttpResponseError): - responses = client.query_batch(requests, allow_partial_errors=True) + responses = client.query_batch(requests) @pytest.mark.live_test_only -def test_logs_batch_query_partial_exception_not_allowed(): +def test_logs_batch_query_partial_exception(): credential = _credential() client = LogsQueryClient(credential) requests = [ @@ -101,38 +93,7 @@ def test_logs_batch_query_partial_exception_not_allowed(): r1, r2, r3 = responses[0], responses[1], responses[2] assert r1.__class__ == LogsQueryResult assert r2.__class__ == LogsQueryResult - assert r3.__class__ == LogsQueryError - -@pytest.mark.live_test_only -def test_logs_batch_query_partial_exception_allowed(): - credential = _credential() - client = LogsQueryClient(credential) - requests = [ - LogsBatchQuery( - query="AzureActivity | summarize count()", - timespan=timedelta(hours=1), - workspace_id= os.environ['LOG_WORKSPACE_ID'] - ), - LogsBatchQuery( - query= """AppRequests | take 10""", - timespan=(datetime(2021, 6, 2), timedelta(days=1)), - workspace_id= os.environ['LOG_WORKSPACE_ID'] - ), - LogsBatchQuery( - query= """let Weight = 92233720368547758; - range x from 1 to 3 step 1 - | summarize percentilesw(x, Weight * 100, 50)""", - workspace_id= os.environ['LOG_WORKSPACE_ID'], - timespan=(datetime(2021, 6, 2), datetime(2021, 6, 3)), - include_statistics=True - ), - ] - responses = client.query_batch(requests, allow_partial_errors=True) - r1, r2, r3 = responses[0], responses[1], responses[2] - assert r1.__class__ == LogsQueryResult - assert r2.__class__ == LogsQueryResult - assert r3.__class__ == LogsQueryResult - assert r3.partial_error is not None + assert r3.__class__ == LogsQueryPartialResult @pytest.mark.live_test_only def test_logs_batch_query_non_fatal_exception(): diff --git a/sdk/monitor/azure-monitor-query/tests/test_logs_client.py b/sdk/monitor/azure-monitor-query/tests/test_logs_client.py index a9036055eb0d..6f3d85170e0e 100644 --- a/sdk/monitor/azure-monitor-query/tests/test_logs_client.py +++ b/sdk/monitor/azure-monitor-query/tests/test_logs_client.py @@ -3,7 +3,7 @@ import os from azure.identity import ClientSecretCredential from azure.core.exceptions import HttpResponseError -from azure.monitor.query import LogsQueryClient, LogsBatchQuery, LogsQueryError, LogsTable, LogsQueryResult, LogsTableRow +from azure.monitor.query import LogsQueryClient, LogsBatchQuery, LogsQueryError, LogsTable, LogsQueryResult, LogsTableRow, LogsQueryPartialResult def _credential(): credential = ClientSecretCredential( @@ -58,9 +58,11 @@ def test_logs_single_query_with_partial_success(): query = """let Weight = 92233720368547758; range x from 1 to 3 step 1 | summarize percentilesw(x, Weight * 100, 50)""" - response = client.query(os.environ['LOG_WORKSPACE_ID'], query, timespan=None, allow_partial_errors=True) + response = client.query(os.environ['LOG_WORKSPACE_ID'], query, timespan=None) assert response.partial_error is not None + assert response.partial_data is not None + assert response.__class__ == LogsQueryPartialResult @pytest.mark.skip("https://github.com/Azure/azure-sdk-for-python/issues/19917") @pytest.mark.live_test_only