Skip to content

Commit

Permalink
Update handling error in query (Azure#20765)
Browse files Browse the repository at this point in the history
* 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 <krpratic@microsoft.com>

* changes

Co-authored-by: Krista Pratico <krpratic@microsoft.com>
  • Loading branch information
rakshith91 and kristapratico authored Sep 24, 2021
1 parent 1a9b633 commit 0475f51
Show file tree
Hide file tree
Showing 17 changed files with 459 additions and 789 deletions.
8 changes: 5 additions & 3 deletions sdk/monitor/azure-monitor-query/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions sdk/monitor/azure-monitor-query/azure/monitor/query/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,7 +26,7 @@
Metric,
MetricValue,
MetricClass,
MetricAvailability
MetricAvailability,
)

from ._version import VERSION
Expand All @@ -36,8 +35,9 @@
"MetricAggregationType",
"LogsQueryClient",
"LogsQueryResult",
"LogsQueryPartialResult",
"LogsQueryStatus",
"LogsQueryError",
"QueryPartialErrorException",
"LogsTable",
"LogsTableRow",
"LogsBatchQuery",
Expand All @@ -51,7 +51,7 @@
"Metric",
"MetricValue",
"MetricClass",
"MetricAvailability"
"MetricAvailability",
]

__version__ = VERSION
70 changes: 16 additions & 54 deletions sdk/monitor/azure-monitor-query/azure/monitor/query/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
84 changes: 52 additions & 32 deletions sdk/monitor/azure-monitor-query/azure/monitor/query/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down
Loading

0 comments on commit 0475f51

Please sign in to comment.