Skip to content

Commit

Permalink
Support CreateQuery in AMC (Azure#21862)
Browse files Browse the repository at this point in the history
  • Loading branch information
pakrym authored Jun 15, 2021
1 parent 449fbbc commit 6b17054
Show file tree
Hide file tree
Showing 51 changed files with 3,177 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public LogsQueryClient(Azure.Core.TokenCredential credential) { }
public LogsQueryClient(Azure.Core.TokenCredential credential, Azure.Monitor.Query.LogsQueryClientOptions options) { }
public LogsQueryClient(System.Uri endpoint, Azure.Core.TokenCredential credential) { }
public LogsQueryClient(System.Uri endpoint, Azure.Core.TokenCredential credential, Azure.Monitor.Query.LogsQueryClientOptions options) { }
public static string CreateQuery(System.FormattableString filter) { throw null; }
public virtual Azure.Response<Azure.Monitor.Query.Models.LogsQueryResult> Query(string workspace, string query, Azure.Core.DateTimeRange timeRange, Azure.Monitor.Query.LogsQueryOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response<Azure.Monitor.Query.Models.LogsQueryResult>> QueryAsync(string workspace, string query, Azure.Core.DateTimeRange timeRange, Azure.Monitor.Query.LogsQueryOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response<System.Collections.Generic.IReadOnlyList<T>>> QueryAsync<T>(string workspace, string query, Azure.Core.DateTimeRange timeRange, Azure.Monitor.Query.LogsQueryOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
Expand Down
107 changes: 107 additions & 0 deletions sdk/monitor/Azure.Monitor.Query/src/LogsQueryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Azure.Core;
using Azure.Core.Pipeline;
using Azure.Monitor.Query.Models;
Expand Down Expand Up @@ -208,6 +211,110 @@ public virtual async Task<Response<LogsBatchQueryResult>> QueryBatchAsync(LogsBa
throw;
}
}

/// <summary>
/// Create a Kusto query from an interpolated string. The interpolated values will be quoted and escaped as necessary.
/// </summary>
/// <param name="filter">An interpolated query string.</param>
/// <returns>A valid Kusto query.</returns>
public static string CreateQuery(FormattableString filter)
{
if (filter == null) { return null; }

string[] args = new string[filter.ArgumentCount];
for (int i = 0; i < filter.ArgumentCount; i++)
{
args[i] = filter.GetArgument(i) switch
{
// Null
null => throw new ArgumentException(
$"Unable to convert argument {i} to an Kusto literal. " +
$"Unable to format an untyped null value, please use typed-null expression " +
$"(bool(null), datetime(null), dynamic(null), guid(null), int(null), long(null), real(null), double(null), time(null))"),

// Boolean
true => "true",
false => "false",

// Numeric
sbyte x => $"int({x.ToString(CultureInfo.InvariantCulture)})",
byte x => $"int({x.ToString(CultureInfo.InvariantCulture)})",
short x => $"int({x.ToString(CultureInfo.InvariantCulture)})",
ushort x => $"int({x.ToString(CultureInfo.InvariantCulture)})",
int x => $"int({x.ToString(CultureInfo.InvariantCulture)})",
uint x => $"int({x.ToString(CultureInfo.InvariantCulture)})",

float x => $"real({x.ToString(CultureInfo.InvariantCulture)})",
double x => $"real({x.ToString(CultureInfo.InvariantCulture)})",

// Int64
long x => $"long({x.ToString(CultureInfo.InvariantCulture)})",
ulong x => $"long({x.ToString(CultureInfo.InvariantCulture)})",

decimal x => $"decimal({x.ToString(CultureInfo.InvariantCulture)})",

// Guid
Guid x => $"guid({x.ToString("D", CultureInfo.InvariantCulture)})",

// Dates as 8601 with a time zone
DateTimeOffset x => $"datetime({x.UtcDateTime.ToString("O", CultureInfo.InvariantCulture)})",
DateTime x => $"datetime({x.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture)})",
TimeSpan x => $"time({x.ToString("c", CultureInfo.InvariantCulture)})",

// Text
string x => EscapeStringValue(x),
char x => EscapeStringValue(x),

// Everything else
object x => throw new ArgumentException(
$"Unable to convert argument {i} from type {x.GetType()} to an Kusto literal.")
};
}

return string.Format(CultureInfo.InvariantCulture, filter.Format, args);
}

private static string EscapeStringValue(string s)
{
StringBuilder escaped = new();
escaped.Append('"');

foreach (char c in s)
{
switch (c)
{
case '"':
escaped.Append("\\\"");
break;
case '\\':
escaped.Append("\\\\");
break;
case '\r':
escaped.Append("\\r");
break;
case '\n':
escaped.Append("\\n");
break;
case '\t':
escaped.Append("\\t");
break;
default:
escaped.Append(c);
break;
}
}

escaped.Append('"');
return escaped.ToString();
}

private static string EscapeStringValue(char s) =>
s switch
{
_ when s == '"' => "'\"'",
_ => $"\"{s}\""
};

internal static QueryBody CreateQueryBody(string query, DateTimeRange timeRange, LogsQueryOptions options, out string prefer)
{
var queryBody = new QueryBody(query);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
Expand Down Expand Up @@ -570,6 +571,70 @@ public async Task CanSetServiceTimeout()
}
}

[RecordedTest]
[TestCaseSource(nameof(Queries))]
public async Task CanQueryWithFormattedQuery(FormattableStringWrapper query)
{
var client = CreateClient();

var response = await client.QueryAsync<bool>(TestEnvironment.WorkspaceId, LogsQueryClient.CreateQuery(query.Value), _logsTestData.DataTimeRange);
Assert.True(response.Value.Single());
}

public static IEnumerable<FormattableStringWrapper> Queries
{
get
{
yield return new($"print {true} == true");
yield return new($"print {false} == false");
yield return new($"print {(byte)1} == int(1)");
yield return new($"print {(sbyte)2} == int(2)");
yield return new($"print {(ushort)3} == int(3)");
yield return new($"print {(short)4} == int(4)");
yield return new($"print {(uint)5} == int(5)");
yield return new($"print {(int)6} == int(6)");

yield return new($"print {1000000000} == int(1000000000)");
yield return new($"print {1.1F} == real(1.1)");
yield return new($"print {1.2D} == real(1.2)");
yield return new($"print {1.3M} == decimal(1.3)");
yield return new($"print {1000000000000000000L} == long(1000000000000000000)");
yield return new($"print {1000000000000000001UL} == long(1000000000000000001)");

yield return new($"print {Guid.Parse("74be27de-1e4e-49d9-b579-fe0b331d3642")} == guid(74be27de-1e4e-49d9-b579-fe0b331d3642)");

yield return new($"print {DateTimeOffset.Parse("2015-12-31 23:59:59.9+00:00", null, DateTimeStyles.RoundtripKind)} == datetime(2015-12-31 23:59:59.9)");
yield return new($"print {DateTime.Parse("2015-12-31 23:59:59.9+00:00", null, DateTimeStyles.RoundtripKind)} == datetime(2015-12-31 23:59:59.9)");

yield return new($"print {TimeSpan.FromSeconds(10)} == 10s");

yield return new($"print {"hello world"} == \"hello world\"");
yield return new($"print {"hello \" world"} == \"hello \\\" world\"");
yield return new($"print {"\\\""} == \"\\\\\\\"\"");

yield return new($"print {"\r\n\t"} == \"\\r\\n\\t\"");

yield return new($"print {'"'} == \"\\\"\"");
yield return new($"print {'\''} == \"'\"");
}
}

// To fix recording names
public readonly struct FormattableStringWrapper
{
public FormattableString Value { get; }

public FormattableStringWrapper(FormattableString value)
{
Value = value;
}

public override string ToString()
{
return string.Format(Value.Format, Value.GetArguments().Select(a => a?.GetType().Name).ToArray());
}
}

private record TestModel
{
public string Name { get; set; }
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 6b17054

Please sign in to comment.