Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update handling error in query #20765

Merged
merged 14 commits into from
Sep 24, 2021
Merged
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`.
kristapratico marked this conversation as resolved.
Show resolved Hide resolved
- `metric_namespace` is renamed to `namespace` and is a keyword-only argument in `list_metric_definitions` API.

### Bugs Fixed
Expand Down
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,
kristapratico marked this conversation as resolved.
Show resolved Hide resolved
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
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