diff --git a/sdk/monitor/azure-monitor-query/CHANGELOG.md b/sdk/monitor/azure-monitor-query/CHANGELOG.md index b509ac76fa83..bd8f683f9cd2 100644 --- a/sdk/monitor/azure-monitor-query/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-query/CHANGELOG.md @@ -9,6 +9,7 @@ - `workspaces`, `workspace_ids`, `qualified_names` and `azure_resource_ids` are now merged into a single `additional_workspaces` list in the query API. - The `LogQueryRequest` object now takes in a `workspace_id` and `additional_workspaces` instead of `workspace`. - `aggregation` param is now a list instead of a string in the `query` method. +- `duration` must now be provided as a timedelta instead of a string. ### Key Bugs Fixed diff --git a/sdk/monitor/azure-monitor-query/README.md b/sdk/monitor/azure-monitor-query/README.md index 4bf44092d18e..719db95b887d 100644 --- a/sdk/monitor/azure-monitor-query/README.md +++ b/sdk/monitor/azure-monitor-query/README.md @@ -153,6 +153,7 @@ This sample shows sending multiple queries at the same time using batch query AP ```Python import os +from datetime import timedelta import pandas as pd from azure.monitor.query import LogsQueryClient, LogsQueryRequest from azure.identity import ClientSecretCredential @@ -169,13 +170,13 @@ client = LogsQueryClient(credential) requests = [ LogsQueryRequest( query="AzureActivity | summarize count()", - duration="PT1H", + duration=timedelta(hours=1), workspace= os.environ['LOG_WORKSPACE_ID'] ), LogsQueryRequest( query= """AppRequests | take 10 | summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId""", - duration="PT1H", + duration=timedelta(hours=1), start_time=datetime(2021, 6, 2), workspace= os.environ['LOG_WORKSPACE_ID'] ), @@ -228,6 +229,7 @@ This example shows getting the metrics for an EventGrid subscription. The resour ```Python import os +from datetime import timedelta from azure.monitor.query import MetricsQueryClient from azure.identity import ClientSecretCredential @@ -245,7 +247,7 @@ response = client.query( metrics_uri, metric_names=["PublishSuccessCount"], start_time=datetime(2021, 5, 25), - duration='P1D' + duration=timedelta(days=1), ) for metric in response.metrics: 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 7ee12be0a0be..727906c27494 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/_helpers.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/_helpers.py @@ -50,17 +50,23 @@ def order_results(request_order, responses): return ordered def construct_iso8601(start=None, end=None, duration=None): + if duration is not None: + duration = 'PT{}S'.format(duration.total_seconds()) iso_str = None if start is not None: start = Serializer.serialize_iso(start) + if end and duration: + raise ValueError("start_time can only be provided with duration or end_time, but not both.") if end is not None: end = Serializer.serialize_iso(end) iso_str = start + '/' + end elif duration is not None: iso_str = start + '/' + duration else: - raise ValueError("Start time must be provided aling with duration or end time.") + raise ValueError("Start time must be provided along with duration or end time.") elif end is not None: + if not duration: + raise ValueError("End time must be provided along with duration or start time.") end = Serializer.serialize_iso(end) iso_str = duration + '/' + end else: diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/_log_query_client.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/_log_query_client.py index 79f14744b53d..63f19cda2837 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/_log_query_client.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/_log_query_client.py @@ -5,7 +5,7 @@ # license information. # -------------------------------------------------------------------------- -from typing import TYPE_CHECKING, Any, Union, Sequence, Dict +from typing import TYPE_CHECKING, Any, Union, Sequence, Dict, Optional from azure.core.exceptions import HttpResponseError from ._generated._monitor_query_client import MonitorQueryClient @@ -16,6 +16,7 @@ if TYPE_CHECKING: from azure.core.credentials import TokenCredential + from datetime import timedelta class LogsQueryClient(object): @@ -49,7 +50,7 @@ def __init__(self, credential, **kwargs): self._query_op = self._client.query def query(self, workspace_id, query, duration=None, **kwargs): - # type: (str, str, str, Any) -> LogsQueryResults + # type: (str, str, Optional[timedelta], Any) -> LogsQueryResults """Execute an Analytics query. Executes an Analytics query for data. @@ -63,9 +64,9 @@ def query(self, workspace_id, query, duration=None, **kwargs): :param query: The Analytics query. Learn more about the `Analytics query syntax `_. :type query: str - :param str duration: The duration for which to query the data. This can also be accompanied + :param ~datetime.timedelta duration: The duration for which to query the data. This can also be accompanied with either start_time or end_time. If start_time or end_time is not provided, the current time is - taken as the end time. This should be provided in a ISO8601 string format like 'PT1H', 'P1Y2M10DT2H30M'. + taken as the end time. :keyword datetime start_time: The start time from which to query the data. This should be accompanied with either end_time or duration. :keyword datetime end_time: The end time till which to query the data. This should be accompanied 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 a72d926ab377..1d39b7adeb0f 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 @@ -7,7 +7,7 @@ # pylint: disable=anomalous-backslash-in-string -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional from ._generated._monitor_query_client import ( MonitorQueryClient, @@ -17,6 +17,7 @@ from ._helpers import get_metrics_authentication_policy, construct_iso8601 if TYPE_CHECKING: + from datetime import timedelta from azure.core.credentials import TokenCredential from azure.core.paging import ItemPaged @@ -53,7 +54,7 @@ def __init__(self, credential, **kwargs): self._definitions_op = self._client.metric_definitions def query(self, resource_uri, metric_names, duration=None, **kwargs): - # type: (str, list, str, Any) -> MetricsResult + # type: (str, list, Optional[timedelta], Any) -> MetricsResult """Lists the metric values for a resource. **Note**: Although the start_time, end_time, duration are optional parameters, it is highly @@ -63,9 +64,9 @@ def query(self, resource_uri, metric_names, duration=None, **kwargs): :type resource_uri: str :param metric_names: The names of the metrics to retrieve. :type metric_names: list[str] - :param str duration: The duration for which to query the data. This can also be accompanied + :param ~datetime.timedelta duration: The duration for which to query the data. This can also be accompanied with either start_time or end_time. If start_time or end_time is not provided, the current time is - taken as the end time. This should be provided in a ISO8601 string format like 'PT1H', 'P1Y2M10DT2H30M'. + taken as the end time. :keyword datetime start_time: The start time from which to query the data. This should be accompanied with either end_time or duration. :keyword datetime end_time: The end time till which to query the data. This should be accompanied 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 c477de7909c1..d584623754b9 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/_models.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/_models.py @@ -151,9 +151,9 @@ class LogsQueryRequest(InternalLogQueryRequest): :param query: The Analytics query. Learn more about the `Analytics query syntax `_. :type query: str - :param str duration: The duration for which to query the data. This can also be accompanied + :param ~datetime.timedelta duration: The duration for which to query the data. This can also be accompanied with either start_time or end_time. If start_time or end_time is not provided, the current time is - taken as the end time. This should be provided in a ISO8601 string format like 'PT1H', 'P1Y2M10DT2H30M'. + taken as the end time. :keyword datetime start_time: The start time from which to query the data. This should be accompanied with either end_time or duration. :keyword datetime end_time: The end time till which to query the data. This should be accompanied diff --git a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_log_query_client_async.py b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_log_query_client_async.py index c100c5791355..f840472815dd 100644 --- a/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_log_query_client_async.py +++ b/sdk/monitor/azure-monitor-query/azure/monitor/query/aio/_log_query_client_async.py @@ -5,7 +5,8 @@ # license information. # -------------------------------------------------------------------------- -from typing import Any, Union, Sequence, Dict, TYPE_CHECKING +from datetime import timedelta +from typing import Any, Union, Sequence, Dict, Optional, TYPE_CHECKING from azure.core.exceptions import HttpResponseError from .._generated.aio._monitor_query_client import MonitorQueryClient @@ -41,7 +42,7 @@ async def query( self, workspace_id: str, query: str, - duration: str = None, + duration: Optional[timedelta] = None, **kwargs: Any) -> LogsQueryResults: """Execute an Analytics query. @@ -56,9 +57,9 @@ async def query( :param query: The Analytics query. Learn more about the `Analytics query syntax `_. :type query: str - :param str duration: The duration for which to query the data. This can also be accompanied + :param ~datetime.timedelta duration: The duration for which to query the data. This can also be accompanied with either start_time or end_time. If start_time or end_time is not provided, the current time is - taken as the end time. This should be provided in a ISO8601 string format like 'PT1H', 'P1Y2M10DT2H30M'. + taken as the end time. :keyword datetime start_time: The start time from which to query the data. This should be accompanied with either end_time or duration. :keyword datetime end_time: The end time till which to query the data. This should be accompanied 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 7d6d3423d80e..28124ac7635a 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 @@ -7,7 +7,8 @@ # pylint: disable=anomalous-backslash-in-string -from typing import TYPE_CHECKING, Any, List +from datetime import timedelta +from typing import TYPE_CHECKING, Any, List, Optional from azure.core.async_paging import AsyncItemPaged @@ -42,7 +43,13 @@ def __init__(self, credential: "AsyncTokenCredential", **kwargs: Any) -> None: self._namespace_op = self._client.metric_namespaces self._definitions_op = self._client.metric_definitions - async def query(self, resource_uri: str, metric_names: List, duration: str = None, **kwargs: Any) -> MetricsResult: + async def query( + self, + resource_uri: str, + metric_names: List, + duration: Optional[timedelta] = None, + **kwargs: Any + ) -> MetricsResult: """Lists the metric values for a resource. **Note**: Although the start_time, end_time, duration are optional parameters, it is highly @@ -52,9 +59,9 @@ async def query(self, resource_uri: str, metric_names: List, duration: str = Non :type resource_uri: str :param metric_names: The names of the metrics to retrieve. :type metric_names: list - :param str duration: The duration for which to query the data. This can also be accompanied + :param ~datetime.timedelta duration: The duration for which to query the data. This can also be accompanied with either start_time or end_time. If start_time or end_time is not provided, the current time is - taken as the end time. This should be provided in a ISO8601 string format like 'PT1H', 'P1Y2M10DT2H30M'. + taken as the end time. :keyword datetime start_time: The start time from which to query the data. This should be accompanied with either end_time or duration. :keyword datetime end_time: The end time till which to query the data. This should be accompanied diff --git a/sdk/monitor/azure-monitor-query/samples/async_samples/sample_metrics_query_client_async.py b/sdk/monitor/azure-monitor-query/samples/async_samples/sample_metrics_query_client_async.py index 0b3e673caa61..ae1da2593ea3 100644 --- a/sdk/monitor/azure-monitor-query/samples/async_samples/sample_metrics_query_client_async.py +++ b/sdk/monitor/azure-monitor-query/samples/async_samples/sample_metrics_query_client_async.py @@ -3,7 +3,7 @@ import os import asyncio -from datetime import datetime +from datetime import datetime, timedelta import urllib3 from azure.monitor.query.aio import MetricsQueryClient from azure.identity.aio import ClientSecretCredential @@ -25,7 +25,7 @@ async def query_metrics(): metrics_uri, metric_names=["PublishSuccessCount"], start_time=datetime(2021, 5, 25), - duration='P1D' + duration=timedelta(days=1) ) for metric in response.metrics: 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 eff925ea8a44..50917ce33ab8 100644 --- a/sdk/monitor/azure-monitor-query/samples/sample_batch_query.py +++ b/sdk/monitor/azure-monitor-query/samples/sample_batch_query.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from datetime import datetime +from datetime import datetime, timedelta import os import pandas as pd from azure.monitor.query import LogsQueryClient, LogsQueryRequest @@ -20,13 +20,13 @@ requests = [ LogsQueryRequest( query="AzureActivity | summarize count()", - duration="PT1H", + duration=timedelta(hours=1), workspace_id= os.environ['LOG_WORKSPACE_ID'] ), LogsQueryRequest( query= """AppRequests | take 10 | summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId""", - duration="PT1H", + duration=timedelta(hours=1), start_time=datetime(2021, 6, 2), workspace_id= os.environ['LOG_WORKSPACE_ID'] ), 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 10a160f71bed..b3e978fe37d9 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 @@ -3,7 +3,7 @@ import os import pandas as pd -from datetime import datetime +from datetime import datetime, timedelta from msrest.serialization import UTC from azure.monitor.query import LogsQueryClient from azure.identity import ClientSecretCredential @@ -27,7 +27,7 @@ end_time = datetime.now(UTC()) # returns LogsQueryResults -response = client.query(os.environ['LOG_WORKSPACE_ID'], query, duration='PT1H', end_time=end_time) +response = client.query(os.environ['LOG_WORKSPACE_ID'], query, duration=timedelta(days=1), end_time=end_time) if not response.tables: print("No results for the query") diff --git a/sdk/monitor/azure-monitor-query/samples/sample_log_query_client_without_pandas.py b/sdk/monitor/azure-monitor-query/samples/sample_log_query_client_without_pandas.py index fa74b229e3ed..19a089db2159 100644 --- a/sdk/monitor/azure-monitor-query/samples/sample_log_query_client_without_pandas.py +++ b/sdk/monitor/azure-monitor-query/samples/sample_log_query_client_without_pandas.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. import os -from datetime import datetime +from datetime import datetime, timedelta from msrest.serialization import UTC from azure.monitor.query import LogsQueryClient from azure.identity import ClientSecretCredential @@ -23,7 +23,7 @@ end_time = datetime.now(UTC()) # returns LogsQueryResults -response = client.query(os.environ['LOG_WORKSPACE_ID'], query, duration='PT1H', end_time=end_time) +response = client.query(os.environ['LOG_WORKSPACE_ID'], query, duration=timedelta(hours=1), end_time=end_time) if not response.tables: print("No results for the query") diff --git a/sdk/monitor/azure-monitor-query/samples/sample_metrics_query_client.py b/sdk/monitor/azure-monitor-query/samples/sample_metrics_query_client.py index 2230ee736f1d..fd38bb66a355 100644 --- a/sdk/monitor/azure-monitor-query/samples/sample_metrics_query_client.py +++ b/sdk/monitor/azure-monitor-query/samples/sample_metrics_query_client.py @@ -25,7 +25,7 @@ metrics_uri, metric_names=["MatchedEventCount"], start_time=datetime(2021, 6, 21), - duration='P1D', + duration=timedelta(days=1), aggregation=['Count'] ) diff --git a/sdk/monitor/azure-monitor-query/tests/async/test_metrics_client_async.py b/sdk/monitor/azure-monitor-query/tests/async/test_metrics_client_async.py index 11e08aa4908b..b58d866ffae0 100644 --- a/sdk/monitor/azure-monitor-query/tests/async/test_metrics_client_async.py +++ b/sdk/monitor/azure-monitor-query/tests/async/test_metrics_client_async.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, time, timedelta import pytest import os from azure.identity.aio import ClientSecretCredential @@ -20,7 +20,7 @@ async def test_metrics_auth(): os.environ['METRICS_RESOURCE_URI'], metric_names=["MatchedEventCount"], start_time=datetime(2021, 6, 21), - duration='P1D', + duration=timedelta(days=1), aggregation=['Count'] ) assert response diff --git a/sdk/monitor/azure-monitor-query/tests/test_logs_timespans.py b/sdk/monitor/azure-monitor-query/tests/test_logs_timespans.py index 85af60795414..5a584edd93ac 100644 --- a/sdk/monitor/azure-monitor-query/tests/test_logs_timespans.py +++ b/sdk/monitor/azure-monitor-query/tests/test_logs_timespans.py @@ -1,4 +1,4 @@ -from datetime import datetime, time, timedelta +from datetime import datetime, timedelta import pytest import json import os @@ -8,6 +8,8 @@ from azure.core.exceptions import HttpResponseError from azure.monitor.query import LogsQueryClient, LogsQueryRequest +from azure.monitor.query._helpers import construct_iso8601 + def _credential(): credential = ClientSecretCredential( client_id = os.environ['AZURE_CLIENT_ID'], @@ -52,11 +54,11 @@ def test_query_duration_and_end_time(): query = "AppRequests | take 5" end_time = datetime.now(UTC()) - duration = 'P3D' + duration = timedelta(days=3) def callback(request): dic = json.loads(request.http_request.body) - assert 'P3D/' in dic.get('timespan') + assert 'PT259200.0S/' in dic.get('timespan') client.query(os.environ['LOG_WORKSPACE_ID'], query, duration=duration, end_time=end_time, raw_request_hook=callback) @@ -68,11 +70,11 @@ def test_query_duration_and_start_time(): end_time = datetime.now(UTC()) start_time = end_time - timedelta(days=3) - duration = 'P3D' + duration = timedelta(days=3) def callback(request): dic = json.loads(request.http_request.body) - assert '/P3D' in dic.get('timespan') + assert '/PT259200.0S' in dic.get('timespan') client.query(os.environ['LOG_WORKSPACE_ID'], query, duration=duration, start_time=start_time, raw_request_hook=callback) @@ -83,10 +85,36 @@ def test_query_duration_only(): client = LogsQueryClient(credential) query = "AppRequests | take 5" - duration = 'P3D' + duration = timedelta(days=3) def callback(request): dic = json.loads(request.http_request.body) - assert 'P3D' in dic.get('timespan') + assert 'PT259200.0S' in dic.get('timespan') client.query(os.environ['LOG_WORKSPACE_ID'], query, duration=duration, raw_request_hook=callback) + +def test_duration_to_iso8601(): + d1 = timedelta(days=1) + d2 = timedelta(weeks=1) + d3 = timedelta(weeks=3, days=4) + d4 = timedelta(seconds=10) + d5 = timedelta(microseconds=1000) + d6 = timedelta(milliseconds=100000) + d7 = timedelta(hours=24, days=1) + + assert construct_iso8601(duration=d1) == 'PT86400.0S' + assert construct_iso8601(duration=d2) == 'PT604800.0S' + assert construct_iso8601(duration=d3) == 'PT2160000.0S' + assert construct_iso8601(duration=d4) == 'PT10.0S' + assert construct_iso8601(duration=d5) == 'PT0.001S' + assert construct_iso8601(duration=d5) == 'PT0.001S' + assert construct_iso8601(duration=d7) == 'PT172800.0S' + + with pytest.raises(ValueError, match="End time must be provided along with duration or start time."): + construct_iso8601(end=datetime.now(UTC())) + + with pytest.raises(ValueError, match="Start time must be provided along with duration or end time."): + construct_iso8601(start=datetime.now(UTC())) + + with pytest.raises(ValueError, match="start_time can only be provided with duration or end_time, but not both."): + construct_iso8601(end=datetime.now(UTC()), start=datetime(2020, 10, 10), duration=d3) diff --git a/sdk/monitor/azure-monitor-query/tests/test_metrics_client.py b/sdk/monitor/azure-monitor-query/tests/test_metrics_client.py index 8f5a69ede392..d12e7aa28e9f 100644 --- a/sdk/monitor/azure-monitor-query/tests/test_metrics_client.py +++ b/sdk/monitor/azure-monitor-query/tests/test_metrics_client.py @@ -1,6 +1,6 @@ import pytest import os -from datetime import datetime +from datetime import datetime, timedelta from azure.identity import ClientSecretCredential from azure.monitor.query import MetricsQueryClient @@ -20,7 +20,7 @@ def test_metrics_auth(): os.environ['METRICS_RESOURCE_URI'], metric_names=["MatchedEventCount"], start_time=datetime(2021, 6, 21), - duration='P1D', + duration=timedelta(days=1), aggregation=['Count'] ) assert response