diff --git a/eng/config.json b/eng/config.json index fd4ae27babee..d2eda38590e4 100644 --- a/eng/config.json +++ b/eng/config.json @@ -109,6 +109,10 @@ "Name": "monitor/azingest", "CoverageGoal": 0.75 }, + { + "Name": "monitor/query/azlogs", + "CoverageGoal": 0.85 + }, { "Name": "security/keyvault/azadmin", "CoverageGoal": 0.80 diff --git a/sdk/monitor/query/azlogs/CHANGELOG.md b/sdk/monitor/query/azlogs/CHANGELOG.md new file mode 100644 index 000000000000..41d5a6edc34c --- /dev/null +++ b/sdk/monitor/query/azlogs/CHANGELOG.md @@ -0,0 +1,5 @@ +# Release History + +## 0.1.0 (2024-03-07) + +* This is the initial release of the `azlogs` module \ No newline at end of file diff --git a/sdk/monitor/query/azlogs/LICENSE.txt b/sdk/monitor/query/azlogs/LICENSE.txt new file mode 100644 index 000000000000..4b3ba9df30d6 --- /dev/null +++ b/sdk/monitor/query/azlogs/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE \ No newline at end of file diff --git a/sdk/monitor/query/azlogs/README.md b/sdk/monitor/query/azlogs/README.md new file mode 100644 index 000000000000..18ad40944f5d --- /dev/null +++ b/sdk/monitor/query/azlogs/README.md @@ -0,0 +1,115 @@ +# Azure Monitor Query client module for Go + +The Azure Monitor Query client module is used to execute read-only queries against [Azure Monitor][azure_monitor_overview]'s log data platform. + +[Logs][logs_overview] collects and organizes log and performance data from monitored resources. Data from different sources such as platform logs from Azure services, log and performance data from virtual machines agents, and usage and performance data from apps can be consolidated into a single [Azure Log Analytics workspace][log_analytics_workspace]. The various data types can be analyzed together using the [Kusto Query Language][kusto_query_language]. See the [Kusto to SQL cheat sheet][kusto_to_sql] for more information. + +Source code | Package (pkg.go.dev) | [REST API documentation][monitor_rest_docs] | [Product documentation][monitor_docs] | Samples + +## Getting started + +### Prerequisites + +* Go, version 1.18 or higher - [Install Go](https://go.dev/doc/install) +* Azure subscription - [Create a free account][azure_sub] +* To query Logs, you need one of the following things: + * An [Azure Log Analytics workspace][log_analytics_workspace_create] + * The resource URI of an Azure resource (Storage Account, Key Vault, Cosmos DB, etc.) + +### Install the packages + +Install the `azlogs` and `azidentity` modules with `go get`: + +```bash +go get github.com/Azure/azure-sdk-for-go/sdk/monitor/query/azlogs +go get github.com/Azure/azure-sdk-for-go/sdk/azidentity +``` + +The [azidentity][azure_identity] module is used for Azure Active Directory authentication during client construction. + +### Authentication + +An authenticated client object is required to execute a query. The examples demonstrate using [azidentity.NewDefaultAzureCredential][default_cred_ref] to authenticate; however, the client accepts any [azidentity][azure_identity] credential. See the [azidentity][azure_identity] documentation for more information about other credential types. + +The clients default to the Azure public cloud. For other cloud configurations, see the [cloud][cloud_documentation] package documentation. + +#### Create a client + +Example client. + +## Key concepts + +### Timespan + +It's best practice to always query with a timespan (type `TimeInterval`) to prevent excessive queries of the entire logs data set. Log queries use the ISO8601 Time Interval Standard. All time should be represented in UTC. If the timespan is included in both the Kusto query string and `Timespan` field, the timespan is the intersection of the two values. + +Use the `NewTimeInterval()` method for easy creation. + +### Logs query rate limits and throttling + +The Log Analytics service applies throttling when the request rate is too high. Limits, such as the maximum number of rows returned, are also applied on the Kusto queries. For more information, see [Query API][service_limits]. + +If you're executing a batch logs query, a throttled request will return a `ErrorInfo` object. That object's `code` value will be `ThrottledError`. + +### Advanced logs queries + +#### Query multiple workspaces + +To run the same query against multiple Log Analytics workspaces, add the additional workspace ID strings to the `AdditionalWorkspaces` slice in the `QueryBody` struct. + +When multiple workspaces are included in the query, the logs in the result table are not grouped according to the workspace from which they were retrieved. + +#### Increase wait time, include statistics, include render (visualization) + +The `LogsQueryOptions` type is used for advanced logs options. + +* By default, your query will run for up to three minutes. To increase the default timeout, set `LogsQueryOptions.Wait` to the desired number of seconds. The maximum wait time is 10 minutes (600 seconds). + +* To get logs query execution statistics, such as CPU and memory consumption, set `LogsQueryOptions.Statistics` to `true`. + +* To get visualization data for logs queries, set `LogsQueryOptions.Visualization` to `true`. + +```go +azlogs.QueryWorkspaceOptions{ + Options: &azlogs.LogsQueryOptions{ + Statistics: to.Ptr(true), + Visualization: to.Ptr(true), + Wait: to.Ptr(600), + }, + } +``` + +## Examples + +Get started with our examples. + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a [Contributor License Agreement (CLA)][cla] declaring that you have the right to, and actually do, grant us the rights to use your contribution. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate +the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to +do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct][coc]. For more information, see +the [Code of Conduct FAQ][coc_faq] or contact [opencode@microsoft.com][coc_contact] with any additional questions or +comments. + + +[azure_identity]: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity +[azure_sub]: https://azure.microsoft.com/free/ +[azure_monitor_overview]: https://learn.microsoft.com/azure/azure-monitor/overview +[cloud_documentation]: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud +[default_cred_ref]: https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/azidentity#defaultazurecredential +[kusto_query_language]: https://learn.microsoft.com/azure/data-explorer/kusto/query/ +[kusto_to_sql]: https://learn.microsoft.com/azure/data-explorer/kusto/query/sqlcheatsheet +[log_analytics_workspace]: https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-workspace-overview +[log_analytics_workspace_create]: https://learn.microsoft.com/azure/azure-monitor/logs/quick-create-workspace +[logs_overview]: https://learn.microsoft.com/azure/azure-monitor/logs/data-platform-logs +[monitor_docs]: https://learn.microsoft.com/azure/azure-monitor/ +[monitor_rest_docs]: https://learn.microsoft.com/rest/api/monitor/ +[service_limits]: https://learn.microsoft.com/azure/azure-monitor/service-limits#la-query-api +[cla]: https://cla.microsoft.com +[coc]: https://opensource.microsoft.com/codeofconduct/ +[coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/ +[coc_contact]: mailto:opencode@microsoft.com \ No newline at end of file diff --git a/sdk/monitor/query/azlogs/assets.json b/sdk/monitor/query/azlogs/assets.json new file mode 100644 index 000000000000..195e0e237200 --- /dev/null +++ b/sdk/monitor/query/azlogs/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "go", + "TagPrefix": "go/monitor/query/azlogs", + "Tag": "go/monitor/query/azlogs_139fabf226" +} diff --git a/sdk/monitor/query/azlogs/autorest.md b/sdk/monitor/query/azlogs/autorest.md new file mode 100644 index 000000000000..8dc3236f193f --- /dev/null +++ b/sdk/monitor/query/azlogs/autorest.md @@ -0,0 +1,127 @@ +## Go + +``` yaml +title: Logs Query Client +clear-output-folder: false +go: true +input-file: https://github.com/Azure/azure-rest-api-specs/blob/0373f0edc4414fd402603fac51d0df93f1f70507/specification/operationalinsights/data-plane/Microsoft.OperationalInsights/stable/2022-10-27/OperationalInsights.json +license-header: MICROSOFT_MIT_NO_VERSION +module: github.com/Azure/azure-sdk-for-go/sdk/monitor/query/azlogs +openapi-type: "data-plane" +output-folder: ../azlogs +security: "AADToken" +use: "@autorest/go@4.0.0-preview.61" +inject-spans: true +version: "^3.0.0" + +directive: + # delete extra endpoints + - from: swagger-document + where: $["paths"] + transform: > + delete $["/workspaces/{workspaceId}/metadata"]; + - from: swagger-document + where: $["x-ms-paths"] + transform: > + delete $["/{resourceId}/query?disambiguation_dummy"]; + - from: swagger-document + where: $["paths"] + transform: > + delete $["/$batch"]; + + # delete extra operations + - remove-operation: Query_Get + - remove-operation: Query_ResourceGet + - remove-operation: Query_Batch + + # delete metadata and batch models + - remove-model: metadataResults + - remove-model: metadataCategory + - remove-model: metadataSolution + - remove-model: metadataResourceType + - remove-model: metadataTable + - remove-model: metadataFunction + - remove-model: metadataQuery + - remove-model: metadataApplication + - remove-model: metadataWorkspace + - remove-model: metadataResource + - remove-model: metadataPermissions + - remove-model: batchRequest + - remove-model: batchQueryRequest + - remove-model: batchResponse + - remove-model: batchQueryResponse + - remove-model: batchQueryResults + + # rename log operations to generate into a separate logs client + - rename-operation: + from: Query_Execute + to: Logs_QueryWorkspace + - rename-operation: + from: Query_ResourceExecute + to: Logs_QueryResource + + # rename Body.Workspaces to Body.AdditionalWorkspaces + - from: swagger-document + where: $.definitions.queryBody.properties.workspaces + transform: $["x-ms-client-name"] = "AdditionalWorkspaces" + + # rename Render to Visualization + - from: swagger-document + where: $.definitions.queryResults.properties.render + transform: $["x-ms-client-name"] = "Visualization" + + # rename Prefer to Options + - from: swagger-document + where: $.parameters.PreferHeaderParameter + transform: $["x-ms-client-name"] = "Options" + - from: options.go + where: $ + transform: return $.replace(/Options \*string/g, "Options *LogsQueryOptions"); + - from: client.go + where: $ + transform: return $.replace(/\*options\.Options/g, "options.Options.preferHeader()"); + + # add descriptions for models and constants that don't have them + - from: constants.go + where: $ + transform: return $.replace(/type ResultType string/, "//ResultType - Reduces the set of data collected. The syntax allowed depends on the operation. See the operation's description for details.\ntype ResultType string"); + + # delete unused error models + - from: models.go + where: $ + transform: return $.replace(/(?:\/\/.*\s)+type (?:ErrorResponse|ErrorResponseAutoGenerated|ErrorInfo|ErrorDetail).+\{(?:\s.+\s)+\}\s/g, ""); + - from: models_serde.go + where: $ + transform: return $.replace(/(?:\/\/.*\s)+func \(\w \*?(?:ErrorResponse|ErrorResponseAutoGenerated|ErrorInfo|ErrorDetail)\).*\{\s(?:.+\s)+\}\s/g, ""); + + # add host as field in client struct + - from: client.go + where: $ + transform: return $.replace(/host/g, "client.host"); + - from: client.go + where: $ + transform: return $.replace(/internal \*azcore.Client/g, "host string\n internal *azcore.Client"); + - from: constants.go + where: $ + transform: return $.replace(/const host = "(.*?)"/, ""); + + # change Table.Rows from type [][]byte to type []Row + - from: models.go + where: $ + transform: return $.replace(/Rows \[\]\[\]\[\]byte/g, "Rows []Row"); + + # change type of timespan from *string to *TimeInterval + - from: + - models.go + - options.go + where: $ + transform: return $.replace(/Timespan \*string/g, "Timespan *TimeInterval"); + + # delete client name prefix from method options and response types + - from: + - client.go + - options.go + - response_types.go + where: $ + transform: return $.replace(/Client(\w+)((?:Options|Response))/g, "$1$2"); +``` \ No newline at end of file diff --git a/sdk/monitor/query/azlogs/build.go b/sdk/monitor/query/azlogs/build.go new file mode 100644 index 000000000000..ccb9164a0bc6 --- /dev/null +++ b/sdk/monitor/query/azlogs/build.go @@ -0,0 +1,10 @@ +//go:build go1.18 +// +build go1.18 + +//go:generate autorest ./autorest.md --rawjson-as-bytes +//go:generate gofmt -w . + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azlogs diff --git a/sdk/monitor/query/azlogs/ci.yml b/sdk/monitor/query/azlogs/ci.yml new file mode 100644 index 000000000000..64495e5bca82 --- /dev/null +++ b/sdk/monitor/query/azlogs/ci.yml @@ -0,0 +1,36 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. +trigger: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/monitor/query/azlogs + +pr: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/monitor/query/azlogs + + +stages: +- template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml + parameters: + ServiceDirectory: 'monitor/query/azlogs' + RunLiveTests: true + UsePipelineProxy: false + SupportedClouds: 'Public,UsGov,China' + EnvVars: + AZURE_CLIENT_ID: $(AZLOGS_CLIENT_ID) + AZURE_TENANT_ID: $(AZLOGS_TENANT_ID) + AZURE_CLIENT_SECRET: $(AZLOGS_CLIENT_SECRET) + AZURE_SUBSCRIPTION_ID: $(AZLOGS_SUBSCRIPTION_ID) \ No newline at end of file diff --git a/sdk/monitor/query/azlogs/client.go b/sdk/monitor/query/azlogs/client.go new file mode 100644 index 000000000000..5babce5e2127 --- /dev/null +++ b/sdk/monitor/query/azlogs/client.go @@ -0,0 +1,141 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. DO NOT EDIT. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. + +package azlogs + +import ( + "context" + "errors" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "net/http" + "net/url" + "strings" +) + +// Client contains the methods for the Logs group. +// Don't use this type directly, use a constructor function instead. +type Client struct { + host string + internal *azcore.Client +} + +// QueryResource - Executes an Analytics query for data in the context of a resource. Here [https://learn.microsoft.com/azure/azure-monitor/logs/api/azure-resource-queries] +// is an example for using POST with an Analytics +// query. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2022-10-27 +// - resourceID - The identifier of the resource. +// - body - The Analytics query. Learn more about the Analytics query syntax [https://azure.microsoft.com/documentation/articles/app-insights-analytics-reference/] +// - options - QueryResourceOptions contains the optional parameters for the Client.QueryResource method. +func (client *Client) QueryResource(ctx context.Context, resourceID string, body QueryBody, options *QueryResourceOptions) (QueryResourceResponse, error) { + var err error + ctx, endSpan := runtime.StartSpan(ctx, "Client.QueryResource", client.internal.Tracer(), nil) + defer func() { endSpan(err) }() + req, err := client.queryResourceCreateRequest(ctx, resourceID, body, options) + if err != nil { + return QueryResourceResponse{}, err + } + httpResp, err := client.internal.Pipeline().Do(req) + if err != nil { + return QueryResourceResponse{}, err + } + if !runtime.HasStatusCode(httpResp, http.StatusOK) { + err = runtime.NewResponseError(httpResp) + return QueryResourceResponse{}, err + } + resp, err := client.queryResourceHandleResponse(httpResp) + return resp, err +} + +// queryResourceCreateRequest creates the QueryResource request. +func (client *Client) queryResourceCreateRequest(ctx context.Context, resourceID string, body QueryBody, options *QueryResourceOptions) (*policy.Request, error) { + urlPath := "/{resourceId}/query" + urlPath = strings.ReplaceAll(urlPath, "{resourceId}", resourceID) + req, err := runtime.NewRequest(ctx, http.MethodPost, runtime.JoinPaths(client.host, urlPath)) + if err != nil { + return nil, err + } + if options != nil && options.Options != nil { + req.Raw().Header["Prefer"] = []string{options.Options.preferHeader()} + } + req.Raw().Header["Accept"] = []string{"application/json"} + if err := runtime.MarshalAsJSON(req, body); err != nil { + return nil, err + } + return req, nil +} + +// queryResourceHandleResponse handles the QueryResource response. +func (client *Client) queryResourceHandleResponse(resp *http.Response) (QueryResourceResponse, error) { + result := QueryResourceResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.QueryResults); err != nil { + return QueryResourceResponse{}, err + } + return result, nil +} + +// QueryWorkspace - Executes an Analytics query for data. Here [https://learn.microsoft.com/azure/azure-monitor/logs/api/request-format] +// is an example for using POST with an Analytics query. +// If the operation fails it returns an *azcore.ResponseError type. +// +// Generated from API version 2022-10-27 +// - workspaceID - Primary Workspace ID of the query. This is the Workspace ID from the Properties blade in the Azure portal. +// - body - The Analytics query. Learn more about the Analytics query syntax [https://azure.microsoft.com/documentation/articles/app-insights-analytics-reference/] +// - options - QueryWorkspaceOptions contains the optional parameters for the Client.QueryWorkspace method. +func (client *Client) QueryWorkspace(ctx context.Context, workspaceID string, body QueryBody, options *QueryWorkspaceOptions) (QueryWorkspaceResponse, error) { + var err error + ctx, endSpan := runtime.StartSpan(ctx, "Client.QueryWorkspace", client.internal.Tracer(), nil) + defer func() { endSpan(err) }() + req, err := client.queryWorkspaceCreateRequest(ctx, workspaceID, body, options) + if err != nil { + return QueryWorkspaceResponse{}, err + } + httpResp, err := client.internal.Pipeline().Do(req) + if err != nil { + return QueryWorkspaceResponse{}, err + } + if !runtime.HasStatusCode(httpResp, http.StatusOK) { + err = runtime.NewResponseError(httpResp) + return QueryWorkspaceResponse{}, err + } + resp, err := client.queryWorkspaceHandleResponse(httpResp) + return resp, err +} + +// queryWorkspaceCreateRequest creates the QueryWorkspace request. +func (client *Client) queryWorkspaceCreateRequest(ctx context.Context, workspaceID string, body QueryBody, options *QueryWorkspaceOptions) (*policy.Request, error) { + urlPath := "/workspaces/{workspaceId}/query" + if workspaceID == "" { + return nil, errors.New("parameter workspaceID cannot be empty") + } + urlPath = strings.ReplaceAll(urlPath, "{workspaceId}", url.PathEscape(workspaceID)) + req, err := runtime.NewRequest(ctx, http.MethodPost, runtime.JoinPaths(client.host, urlPath)) + if err != nil { + return nil, err + } + if options != nil && options.Options != nil { + req.Raw().Header["Prefer"] = []string{options.Options.preferHeader()} + } + req.Raw().Header["Accept"] = []string{"application/json"} + if err := runtime.MarshalAsJSON(req, body); err != nil { + return nil, err + } + return req, nil +} + +// queryWorkspaceHandleResponse handles the QueryWorkspace response. +func (client *Client) queryWorkspaceHandleResponse(resp *http.Response) (QueryWorkspaceResponse, error) { + result := QueryWorkspaceResponse{} + if err := runtime.UnmarshalAsJSON(resp, &result.QueryResults); err != nil { + return QueryWorkspaceResponse{}, err + } + return result, nil +} diff --git a/sdk/monitor/query/azlogs/client_test.go b/sdk/monitor/query/azlogs/client_test.go new file mode 100644 index 000000000000..00b32c7c6277 --- /dev/null +++ b/sdk/monitor/query/azlogs/client_test.go @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azlogs_test + +import ( + "context" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/monitor/query/azlogs" + "github.com/stretchr/testify/require" +) + +var query string = "let dt = datatable (DateTime: datetime, Bool:bool, Guid: guid, Int: int, Long:long, Double: double, String: string, Timespan: timespan, Decimal: decimal, Dynamic: dynamic)\n" + "[datetime(2015-12-31 23:59:59.9), false, guid(74be27de-1e4e-49d9-b579-fe0b331d3642), 12345, 1, 12345.6789, 'string value', 10s, decimal(0.10101), dynamic({\"a\":123, \"b\":\"hello\", \"c\":[1,2,3], \"d\":{}})];" + "range x from 1 to 100 step 1 | extend y=1 | join kind=fullouter dt on $left.y == $right.Long" + +type queryTest struct { + Bool bool + Long int64 + String string +} + +func TestClient(t *testing.T) { + client, err := azlogs.NewClient(credential, nil) + require.NoError(t, err) + require.NotNil(t, client) + + c := cloud.Configuration{ + ActiveDirectoryAuthorityHost: "https://...", + Services: map[cloud.ServiceName]cloud.ServiceConfiguration{ + cloud.ResourceManager: { + Audience: "", + Endpoint: "", + }, + }, + } + opts := azcore.ClientOptions{Cloud: c} + cloudClient, err := azlogs.NewClient(credential, &azlogs.ClientOptions{ClientOptions: opts}) + require.Error(t, err) + require.Equal(t, err.Error(), "provided Cloud field is missing Azure Monitor Logs configuration") + require.Nil(t, cloudClient) +} + +func TestQueryWorkspace_BasicQuerySuccess(t *testing.T) { + client := startTest(t) + timespan := azlogs.NewTimeInterval(time.Date(2022, 12, 1, 0, 0, 0, 0, time.UTC), time.Date(2022, 12, 2, 0, 0, 0, 0, time.UTC)) + body := azlogs.QueryBody{ + Query: to.Ptr(query), + Timespan: to.Ptr(timespan), + } + testSerde(t, &body) + + res, err := client.QueryWorkspace(context.Background(), workspaceID, body, nil) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Nil(t, res.Visualization) + require.Nil(t, res.Statistics) + require.Len(t, res.Tables, 1) + require.Len(t, res.Tables[0].Rows, 100) + + var queryResults []queryTest + for _, table := range res.Tables { + queryResults = make([]queryTest, len(table.Rows)) + + for index, row := range table.Rows { + queryResults[index] = queryTest{ + Long: int64(row[6].(float64)), + String: row[8].(string), + Bool: row[3].(bool), + } + } + } + + require.Len(t, queryResults, 100) + require.False(t, queryResults[99].Bool) + require.Equal(t, queryResults[99].String, "string value") + require.Equal(t, queryResults[99].Long, int64(1)) + + testSerde(t, &res) +} + +func TestQueryWorkspace_BasicQueryFailure(t *testing.T) { + client := startTest(t) + + res, err := client.QueryWorkspace( + context.Background(), + workspaceID, + azlogs.QueryBody{ + Query: to.Ptr("not a valid query"), + Timespan: to.Ptr(azlogs.TimeInterval("PT2H")), + }, + nil, + ) + require.Error(t, err) + require.Nil(t, res.Error) + require.Nil(t, res.Tables) + + var httpErr *azcore.ResponseError + require.ErrorAs(t, err, &httpErr) + require.Equal(t, httpErr.ErrorCode, "BadArgumentError") + require.Equal(t, httpErr.StatusCode, 400) + + testSerde(t, &res) +} + +func TestQueryWorkspace_PartialError(t *testing.T) { + client := startTest(t) + query := "let Weight = 92233720368547758; range x from 1 to 3 step 1 | summarize percentilesw(x, Weight * 100, 50)" + + res, err := client.QueryWorkspace(context.Background(), workspaceID, azlogs.QueryBody{Query: &query}, nil) + require.NoError(t, err) + require.NotNil(t, res.Error) + require.Equal(t, res.Error.Code, "PartialError") + require.Contains(t, res.Error.Error(), "PartialError") + + testSerde(t, &res) +} + +// tests for special options: timeout, statistics, visualization +func TestQueryWorkspace_AdvancedQuerySuccess(t *testing.T) { + client := startTest(t) + + res, err := client.QueryWorkspace(context.Background(), workspaceID, azlogs.QueryBody{Query: &query}, + &azlogs.QueryWorkspaceOptions{Options: &azlogs.LogsQueryOptions{Statistics: to.Ptr(true), Visualization: to.Ptr(true), Wait: to.Ptr(600)}}) + require.NoError(t, err) + require.Nil(t, res.Error) + require.NotNil(t, res.Tables) + require.NotNil(t, res.Visualization) + require.NotNil(t, res.Statistics) + testSerde(t, &res) +} + +func TestQueryWorkspace_MultipleWorkspaces(t *testing.T) { + client := startTest(t) + workspaces := []*string{&workspaceID2} + body := azlogs.QueryBody{ + Query: &query, + AdditionalWorkspaces: workspaces, + } + testSerde(t, &body) + + res, err := client.QueryWorkspace(context.Background(), workspaceID, body, nil) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Tables[0].Rows, 100) +} + +func TestQueryResource(t *testing.T) { + client := startTest(t) + timespan := azlogs.NewTimeInterval(time.Date(2022, 12, 1, 0, 0, 0, 0, time.UTC), time.Date(2022, 12, 2, 0, 0, 0, 0, time.UTC)) + body := azlogs.QueryBody{ + Query: to.Ptr(query), + Timespan: to.Ptr(timespan), + } + testSerde(t, &body) + + res, err := client.QueryResource(context.Background(), resourceURI, body, nil) + require.NoError(t, err) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Nil(t, res.Visualization) + require.Nil(t, res.Statistics) + require.Len(t, res.Tables, 1) + require.Len(t, res.Tables[0].Rows, 100) + testSerde(t, &res) +} + +func TestQueryResource_Fail(t *testing.T) { + client := startTest(t) + + res, err := client.QueryResource( + context.Background(), + resourceURI, + azlogs.QueryBody{ + Query: to.Ptr("not a valid query"), + Timespan: to.Ptr(azlogs.TimeInterval("PT2H")), + }, + nil, + ) + require.Error(t, err) + require.Nil(t, res.Error) + require.Nil(t, res.Tables) + + var httpErr *azcore.ResponseError + require.ErrorAs(t, err, &httpErr) + require.Equal(t, httpErr.ErrorCode, "BadArgumentError") + require.Equal(t, httpErr.StatusCode, 400) + + testSerde(t, &res) +} + +func TestQueryResource_Advanced(t *testing.T) { + client := startTest(t) + + res, err := client.QueryResource(context.Background(), resourceURI, azlogs.QueryBody{Query: &query}, + &azlogs.QueryResourceOptions{Options: &azlogs.LogsQueryOptions{Statistics: to.Ptr(true), Visualization: to.Ptr(true), Wait: to.Ptr(600)}}) + require.NoError(t, err) + require.Nil(t, res.Error) + require.NotNil(t, res.Tables) + require.NotNil(t, res.Visualization) + require.NotNil(t, res.Statistics) + testSerde(t, &res) +} + +func TestTimeInterval(t *testing.T) { + timespan := azlogs.NewTimeInterval(time.Date(2022, 3, 2, 1, 2, 3, 0, time.UTC), time.Date(2022, 3, 3, 0, 0, 0, 0, time.UTC)) + require.Equal(t, timespan, azlogs.TimeInterval("2022-03-02T01:02:03Z/2022-03-03T00:00:00Z")) + + start, end, err := timespan.Values() + require.NoError(t, err) + require.Equal(t, start, time.Date(2022, 3, 2, 1, 2, 3, 0, time.UTC)) + require.Equal(t, end, time.Date(2022, 3, 3, 0, 0, 0, 0, time.UTC)) + + _, _, err = to.Ptr(azlogs.TimeInterval("hi")).Values() + require.Error(t, err) +} diff --git a/sdk/monitor/query/azlogs/cloud_config.go b/sdk/monitor/query/azlogs/cloud_config.go new file mode 100644 index 000000000000..deda5797f4bd --- /dev/null +++ b/sdk/monitor/query/azlogs/cloud_config.go @@ -0,0 +1,27 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azlogs + +import "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + +// Cloud Service Names for Monitor Query Logs, used to identify the respective cloud.ServiceConfiguration +const ServiceNameLogs cloud.ServiceName = "query/azlogs" + +func init() { + cloud.AzureChina.Services[ServiceNameLogs] = cloud.ServiceConfiguration{ + Audience: "https://api.loganalytics.azure.cn", + Endpoint: "https://api.loganalytics.azure.cn/v1", + } + cloud.AzureGovernment.Services[ServiceNameLogs] = cloud.ServiceConfiguration{ + Audience: "https://api.loganalytics.us", + Endpoint: "https://api.loganalytics.us/v1", + } + cloud.AzurePublic.Services[ServiceNameLogs] = cloud.ServiceConfiguration{ + Audience: "https://api.loganalytics.io", + Endpoint: "https://api.loganalytics.io/v1", + } +} diff --git a/sdk/monitor/query/azlogs/constants.go b/sdk/monitor/query/azlogs/constants.go new file mode 100644 index 000000000000..b9765dac9fc9 --- /dev/null +++ b/sdk/monitor/query/azlogs/constants.go @@ -0,0 +1,41 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. DO NOT EDIT. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. + +package azlogs + +// LogsColumnType - The data type of this column. +type LogsColumnType string + +const ( + LogsColumnTypeBool LogsColumnType = "bool" + LogsColumnTypeDatetime LogsColumnType = "datetime" + LogsColumnTypeDecimal LogsColumnType = "decimal" + LogsColumnTypeDynamic LogsColumnType = "dynamic" + LogsColumnTypeGUID LogsColumnType = "guid" + LogsColumnTypeInt LogsColumnType = "int" + LogsColumnTypeLong LogsColumnType = "long" + LogsColumnTypeReal LogsColumnType = "real" + LogsColumnTypeString LogsColumnType = "string" + LogsColumnTypeTimespan LogsColumnType = "timespan" +) + +// PossibleLogsColumnTypeValues returns the possible values for the LogsColumnType const type. +func PossibleLogsColumnTypeValues() []LogsColumnType { + return []LogsColumnType{ + LogsColumnTypeBool, + LogsColumnTypeDatetime, + LogsColumnTypeDecimal, + LogsColumnTypeDynamic, + LogsColumnTypeGUID, + LogsColumnTypeInt, + LogsColumnTypeLong, + LogsColumnTypeReal, + LogsColumnTypeString, + LogsColumnTypeTimespan, + } +} diff --git a/sdk/monitor/query/azlogs/custom_client.go b/sdk/monitor/query/azlogs/custom_client.go new file mode 100644 index 000000000000..7df3653fb934 --- /dev/null +++ b/sdk/monitor/query/azlogs/custom_client.go @@ -0,0 +1,143 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azlogs + +// this file contains handwritten additions to the generated code + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" +) + +// ClientOptions contains optional settings for Client. +type ClientOptions struct { + azcore.ClientOptions +} + +// NewClient creates a client that accesses Azure Monitor logs data. +func NewClient(credential azcore.TokenCredential, options *ClientOptions) (*Client, error) { + if options == nil { + options = &ClientOptions{} + } + if reflect.ValueOf(options.Cloud).IsZero() { + options.Cloud = cloud.AzurePublic + } + c, ok := options.Cloud.Services[ServiceNameLogs] + if !ok || c.Audience == "" || c.Endpoint == "" { + return nil, errors.New("provided Cloud field is missing Azure Monitor Logs configuration") + } + + authPolicy := runtime.NewBearerTokenPolicy(credential, []string{c.Audience + "/.default"}, nil) + azcoreClient, err := azcore.NewClient(moduleName, version, runtime.PipelineOptions{PerRetry: []policy.Policy{authPolicy}}, &options.ClientOptions) + if err != nil { + return nil, err + } + return &Client{host: c.Endpoint, internal: azcoreClient}, nil +} + +// ErrorInfo - The code and message for an error. +type ErrorInfo struct { + // REQUIRED; A machine readable error code. + Code string + + // full error message detailing why the operation failed. + data []byte +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type ErrorInfo. +func (e *ErrorInfo) UnmarshalJSON(data []byte) error { + e.data = data + ei := struct{ Code string }{} + if err := json.Unmarshal(data, &ei); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", e, err) + } + e.Code = ei.Code + + return nil +} + +// Error implements a custom error for type ErrorInfo. +func (e *ErrorInfo) Error() string { + return string(e.data) +} + +// Row of data in a table, types of data used by service specified in LogsColumnType +type Row []any + +// TimeInterval specifies the time range over which to query. +// Use NewTimeInterval() for help formatting. +// Follows the ISO8601 time interval standard with most common +// format being startISOTime/endISOTime. ISO8601 durations also supported (ex "PT2H" for last two hours). +// Use UTC for all times. +type TimeInterval string + +// NewTimeInterval creates a TimeInterval for use in a query. +// Use UTC for start and end times. +func NewTimeInterval(start time.Time, end time.Time) TimeInterval { + return TimeInterval(start.Format(time.RFC3339) + "/" + end.Format(time.RFC3339)) +} + +// Values returns the interval's start and end times if it's in the format startISOTime/endISOTime, else it will return an error. +func (i TimeInterval) Values() (time.Time, time.Time, error) { + // split into different start and end times + times := strings.Split(string(i), "/") + if len(times) != 2 { + return time.Time{}, time.Time{}, errors.New("time interval should be in format startISOTime/endISOTime") + } + start, err := time.Parse(time.RFC3339, times[0]) + if err != nil { + return time.Time{}, time.Time{}, errors.New("error parsing start time") + } + end, err := time.Parse(time.RFC3339, times[1]) + if err != nil { + return time.Time{}, time.Time{}, errors.New("error parsing end time") + } + // return times + return start, end, nil +} + +// LogsQueryOptions sets server timeout, query statistics and visualization information +type LogsQueryOptions struct { + // Set Statistics to true to get logs query execution statistics, + // such as CPU and memory consumption. Defaults to false. + Statistics *bool + + // Set Visualization to true to get visualization + // data for logs queries. Defaults to false. + Visualization *bool + + // By default, the Azure Monitor Query service will run your + // query for up to three minutes. To increase the default timeout, + // set Wait to desired number of seconds. + // Max wait time the service will allow is ten minutes (600 seconds). + Wait *int +} + +// preferHeader converts LogsQueryOptions from struct to properly formatted sting +// to be used in the request Prefer Header +func (l LogsQueryOptions) preferHeader() string { + var options []string + if l.Statistics != nil && *l.Statistics { + options = append(options, "include-statistics=true") + } + if l.Visualization != nil && *l.Visualization { + options = append(options, "include-render=true") + } + if l.Wait != nil { + options = append(options, fmt.Sprintf("wait=%d", *l.Wait)) + } + return strings.Join(options, ",") +} diff --git a/sdk/monitor/query/azlogs/examples_test.go b/sdk/monitor/query/azlogs/examples_test.go new file mode 100644 index 000000000000..f5cc223be2c5 --- /dev/null +++ b/sdk/monitor/query/azlogs/examples_test.go @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azlogs_test + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/monitor/query/azlogs" +) + +var client azlogs.Client + +type queryResult struct { + Bool bool + Long int64 + Double float64 + String string +} + +func ExampleNewClient() { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + //TODO: handle error + } + + client, err := azlogs.NewClient(cred, nil) + if err != nil { + //TODO: handle error + } + _ = client +} + +func ExampleClient_QueryWorkspace() { + // QueryWorkspace allows users to query log data. + + // A workspace ID is required to query logs. To find the workspace ID: + // 1. If not already made, create a Log Analytics workspace (https://learn.microsoft.com/azure/azure-monitor/logs/quick-create-workspace). + // 2. Navigate to your workspace's page in the Azure portal. + // 3. From the **Overview** blade, copy the value of the ***Workspace ID*** property. + + workspaceID := "g4d1e129-fb1e-4b0a-b234-250abc987ea65" // example Azure Log Analytics Workspace ID + + res, err := client.QueryWorkspace( + context.TODO(), + workspaceID, + azlogs.QueryBody{ + Query: to.Ptr("AzureActivity | top 10 by TimeGenerated"), // example Kusto query + Timespan: to.Ptr(azlogs.NewTimeInterval(time.Date(2022, 12, 25, 0, 0, 0, 0, time.UTC), time.Date(2022, 12, 25, 12, 0, 0, 0, time.UTC))), + }, + nil) + if err != nil { + //TODO: handle error + } + if res.Error != nil { + //TODO: handle partial error + } + + // Print Rows + for _, table := range res.Tables { + for _, row := range table.Rows { + fmt.Println(row) + } + } +} + +func ExampleClient_QueryWorkspace_second() { + // `QueryWorkspace` also has more advanced options, including querying multiple workspaces + // and LogsQueryOptions (including statistics and visualization information and increasing default timeout). + + // When multiple workspaces are included in the query, the logs in the result table are not grouped + // according to the workspace from which it was retrieved. + workspaceID1 := "g4d1e129-fb1e-4b0a-b234-250abc987ea65" // example Azure Log Analytics Workspace ID + workspaceID2 := "h4bc4471-2e8c-4b1c-8f47-12b9a4d5ac71" + additionalWorkspaces := []*string{to.Ptr(workspaceID2)} + + // Advanced query options + // Setting Statistics to true returns stats information in Results.Statistics + // Setting Visualization to true returns visualization information in Results.Visualization + options := &azlogs.QueryWorkspaceOptions{ + Options: &azlogs.LogsQueryOptions{ + Statistics: to.Ptr(true), + Visualization: to.Ptr(true), + Wait: to.Ptr(600), + }, + } + + res, err := client.QueryWorkspace( + context.TODO(), + workspaceID1, + azlogs.QueryBody{ + Query: to.Ptr(query), + Timespan: to.Ptr(azlogs.NewTimeInterval(time.Date(2022, 12, 25, 0, 0, 0, 0, time.UTC), time.Date(2022, 12, 25, 12, 0, 0, 0, time.UTC))), + AdditionalWorkspaces: additionalWorkspaces, + }, + options) + if err != nil { + //TODO: handle error + } + if res.Error != nil { + //TODO: handle partial error + } + + // Example of converting table data into a slice of structs. + // Query results are returned in Table Rows and are of type any. + // Type assertion is required to access the underlying value of each index in a Row. + var QueryResults []queryResult + for _, table := range res.Tables { + QueryResults = make([]queryResult, len(table.Rows)) + for index, row := range table.Rows { + QueryResults[index] = queryResult{ + Bool: row[0].(bool), + Long: int64(row[1].(float64)), + Double: float64(row[2].(float64)), + String: row[3].(string), + } + } + } + + fmt.Println(QueryResults) + + // Print out Statistics + fmt.Printf("Statistics: %s", string(res.Statistics)) + + // Print out Visualization information + fmt.Printf("Visualization: %s", string(res.Visualization)) + +} + +func ExampleClient_QueryResource() { + // Instead of requiring a Log Analytics workspace, + // QueryResource allows users to query logs directly from an Azure resource through a resource ID. + + // To find the resource ID: + // 1. Navigate to your resource's page in the Azure portal. + // 2. From the **Overview** blade, select the **JSON View** link. + // 3. In the resulting JSON, copy the value of the `id` property. + + resourceID := "/subscriptions/fajfkx93-c1d8-40ad-9cce-e49c10ca8qe6/resourceGroups/testgroup/providers/Microsoft.Storage/storageAccounts/mystorageacount" // example resource ID + + res, err := client.QueryResource( + context.TODO(), + resourceID, + azlogs.QueryBody{ + Query: to.Ptr("StorageBlobLogs | where TimeGenerated > ago(3d)"), // example Kusto query + Timespan: to.Ptr(azlogs.NewTimeInterval(time.Date(2022, 12, 25, 0, 0, 0, 0, time.UTC), time.Date(2022, 12, 25, 12, 0, 0, 0, time.UTC))), + }, + nil) + if err != nil { + //TODO: handle error + } + if res.Error != nil { + //TODO: handle partial error + } + + // Print Rows + for _, table := range res.Tables { + for _, row := range table.Rows { + fmt.Println(row) + } + } +} diff --git a/sdk/monitor/query/azlogs/go.mod b/sdk/monitor/query/azlogs/go.mod new file mode 100644 index 000000000000..f3564dee94f2 --- /dev/null +++ b/sdk/monitor/query/azlogs/go.mod @@ -0,0 +1,27 @@ +module github.com/Azure/azure-sdk-for-go/sdk/monitor/query/azlogs + +go 1.18 + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dnaeon/go-vcr v1.2.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sdk/monitor/query/azlogs/go.sum b/sdk/monitor/query/azlogs/go.sum new file mode 100644 index 000000000000..77b218cc6b8e --- /dev/null +++ b/sdk/monitor/query/azlogs/go.sum @@ -0,0 +1,41 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sdk/monitor/query/azlogs/models.go b/sdk/monitor/query/azlogs/models.go new file mode 100644 index 000000000000..8f38be4423c9 --- /dev/null +++ b/sdk/monitor/query/azlogs/models.go @@ -0,0 +1,58 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. DO NOT EDIT. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. + +package azlogs + +// Column - A column in a table. +type Column struct { + // The name of this column. + Name *string + + // The data type of this column. + Type *LogsColumnType +} + +// QueryBody - The Analytics query. Learn more about the Analytics query syntax [https://azure.microsoft.com/documentation/articles/app-insights-analytics-reference/] +type QueryBody struct { + // REQUIRED; The query to execute. + Query *string + + // A list of workspaces to query in addition to the primary workspace. + AdditionalWorkspaces []*string + + // Optional. The timespan over which to query data. This is an ISO8601 time period value. This timespan is applied in addition + // to any that are specified in the query expression. + Timespan *TimeInterval +} + +// QueryResults - Contains the tables, columns & rows resulting from a query. +type QueryResults struct { + // REQUIRED; The results of the query in tabular format. + Tables []*Table + + // The code and message for an error. + Error *ErrorInfo + + // Statistics represented in JSON format. + Statistics []byte + + // Visualization data in JSON format. + Visualization []byte +} + +// Table - Contains the columns and rows for one table in a query response. +type Table struct { + // REQUIRED; The list of columns in this table. + Columns []*Column + + // REQUIRED; The name of the table. + Name *string + + // REQUIRED; The resulting rows from this query. + Rows []Row +} diff --git a/sdk/monitor/query/azlogs/models_serde.go b/sdk/monitor/query/azlogs/models_serde.go new file mode 100644 index 000000000000..59ad3e79ab9b --- /dev/null +++ b/sdk/monitor/query/azlogs/models_serde.go @@ -0,0 +1,176 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. DO NOT EDIT. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. + +package azlogs + +import ( + "encoding/json" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "reflect" +) + +// MarshalJSON implements the json.Marshaller interface for type Column. +func (c Column) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "name", c.Name) + populate(objectMap, "type", c.Type) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type Column. +func (c *Column) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", c, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "name": + err = unpopulate(val, "Name", &c.Name) + delete(rawMsg, key) + case "type": + err = unpopulate(val, "Type", &c.Type) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", c, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type QueryBody. +func (q QueryBody) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "workspaces", q.AdditionalWorkspaces) + populate(objectMap, "query", q.Query) + populate(objectMap, "timespan", q.Timespan) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type QueryBody. +func (q *QueryBody) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", q, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "workspaces": + err = unpopulate(val, "AdditionalWorkspaces", &q.AdditionalWorkspaces) + delete(rawMsg, key) + case "query": + err = unpopulate(val, "Query", &q.Query) + delete(rawMsg, key) + case "timespan": + err = unpopulate(val, "Timespan", &q.Timespan) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", q, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type QueryResults. +func (q QueryResults) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "error", q.Error) + populate(objectMap, "statistics", json.RawMessage(q.Statistics)) + populate(objectMap, "tables", q.Tables) + populate(objectMap, "render", json.RawMessage(q.Visualization)) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type QueryResults. +func (q *QueryResults) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", q, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "error": + err = unpopulate(val, "Error", &q.Error) + delete(rawMsg, key) + case "statistics": + q.Statistics = val + delete(rawMsg, key) + case "tables": + err = unpopulate(val, "Tables", &q.Tables) + delete(rawMsg, key) + case "render": + q.Visualization = val + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", q, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type Table. +func (t Table) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "columns", t.Columns) + populate(objectMap, "name", t.Name) + populate(objectMap, "rows", t.Rows) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type Table. +func (t *Table) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "columns": + err = unpopulate(val, "Columns", &t.Columns) + delete(rawMsg, key) + case "name": + err = unpopulate(val, "Name", &t.Name) + delete(rawMsg, key) + case "rows": + err = unpopulate(val, "Rows", &t.Rows) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + } + return nil +} + +func populate(m map[string]any, k string, v any) { + if v == nil { + return + } else if azcore.IsNullValue(v) { + m[k] = nil + } else if !reflect.ValueOf(v).IsNil() { + m[k] = v + } +} + +func unpopulate(data json.RawMessage, fn string, v any) error { + if data == nil { + return nil + } + if err := json.Unmarshal(data, v); err != nil { + return fmt.Errorf("struct field %s: %v", fn, err) + } + return nil +} diff --git a/sdk/monitor/query/azlogs/options.go b/sdk/monitor/query/azlogs/options.go new file mode 100644 index 000000000000..024f9df9613f --- /dev/null +++ b/sdk/monitor/query/azlogs/options.go @@ -0,0 +1,21 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. DO NOT EDIT. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. + +package azlogs + +// QueryResourceOptions contains the optional parameters for the Client.QueryResource method. +type QueryResourceOptions struct { + // Optional. The prefer header to set server timeout, query statistics and visualization information. + Options *LogsQueryOptions +} + +// QueryWorkspaceOptions contains the optional parameters for the Client.QueryWorkspace method. +type QueryWorkspaceOptions struct { + // Optional. The prefer header to set server timeout, query statistics and visualization information. + Options *LogsQueryOptions +} diff --git a/sdk/monitor/query/azlogs/response_types.go b/sdk/monitor/query/azlogs/response_types.go new file mode 100644 index 000000000000..f3a28f0bd627 --- /dev/null +++ b/sdk/monitor/query/azlogs/response_types.go @@ -0,0 +1,21 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) AutoRest Code Generator. DO NOT EDIT. +// Changes may cause incorrect behavior and will be lost if the code is regenerated. + +package azlogs + +// QueryResourceResponse contains the response from method Client.QueryResource. +type QueryResourceResponse struct { + // Contains the tables, columns & rows resulting from a query. + QueryResults +} + +// QueryWorkspaceResponse contains the response from method Client.QueryWorkspace. +type QueryWorkspaceResponse struct { + // Contains the tables, columns & rows resulting from a query. + QueryResults +} diff --git a/sdk/monitor/query/azlogs/test-resources.bicep b/sdk/monitor/query/azlogs/test-resources.bicep new file mode 100644 index 000000000000..240b023e074a --- /dev/null +++ b/sdk/monitor/query/azlogs/test-resources.bicep @@ -0,0 +1,50 @@ +param baseName string +param sku string = 'pergb2018' +param appSku string = 'standard' +param retentionInDays int = 30 +param resourcePermissions bool = false +param location string = resourceGroup().location + +resource log_analytics1 'Microsoft.OperationalInsights/workspaces@2020-08-01' = { + name: '${baseName}1' + location: location + properties: { + sku: { + name: sku + } + retentionInDays: retentionInDays + features: { + searchVersion: 1 + legacy: 0 + enableLogAccessUsingOnlyResourcePermissions: resourcePermissions + } + } +} + +resource log_analytics2 'Microsoft.OperationalInsights/workspaces@2020-08-01' = { + name: '${baseName}2' + location: location + properties: { + sku: { + name: sku + } + retentionInDays: retentionInDays + features: { + searchVersion: 1 + legacy: 0 + enableLogAccessUsingOnlyResourcePermissions: resourcePermissions + } + } +} + +resource app_config 'Microsoft.AppConfiguration/configurationStores@2022-05-01' = { + name: baseName + location: location + sku: { + name: appSku + } +} + +output WORKSPACE_ID string = log_analytics1.properties.customerId +output WORKSPACE_ID2 string = log_analytics2.properties.customerId +output RESOURCE_URI string = app_config.id diff --git a/sdk/monitor/query/azlogs/utils_test.go b/sdk/monitor/query/azlogs/utils_test.go new file mode 100644 index 000000000000..c14a5ea58386 --- /dev/null +++ b/sdk/monitor/query/azlogs/utils_test.go @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azlogs_test + +import ( + "context" + "encoding/json" + "os" + "regexp" + "strings" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/internal/recording" + "github.com/Azure/azure-sdk-for-go/sdk/monitor/query/azlogs" + "github.com/stretchr/testify/require" +) + +const ( + recordingDirectory = "sdk/monitor/query/azlogs/testdata" + fakeWorkspaceID = "32d1e136-gg81-4b0a-b647-260cdc471f68" + fakeWorkspaceID2 = "asdjkfj8k20-gg81-4b0a-9fu2-260c09fn1f68" + fakeResourceURI = "/subscriptions/faa080af-c1d8-40ad-9cce-e1a451va7b87/resourceGroups/rg-example/providers/Microsoft.AppConfiguration/configurationStores/example" + fakeSubscrtiptionID = "faa080af-c1d8-40ad-9cce-e1a451va7b87" + fakeRegion = "westus2" +) + +var ( + credential azcore.TokenCredential + workspaceID string + workspaceID2 string + resourceURI string + subscriptionID string + region string + clientCloud cloud.Configuration +) + +func TestMain(m *testing.M) { + code := run(m) + os.Exit(code) +} + +func run(m *testing.M) int { + if recording.GetRecordMode() == recording.PlaybackMode || recording.GetRecordMode() == recording.RecordingMode { + proxy, err := recording.StartTestProxy(recordingDirectory, nil) + if err != nil { + panic(err) + } + defer func() { + err := recording.StopTestProxy(proxy) + if err != nil { + panic(err) + } + }() + } + + if recording.GetRecordMode() == recording.PlaybackMode { + credential = &FakeCredential{} + } else { + // tenantID := getEnvVar("AZLOGS_TENANT_ID", "") + // clientID := getEnvVar("AZLOGS_CLIENT_ID", "") + // secret := getEnvVar("AZLOGS_CLIENT_SECRET", "") + // var err error + // credential, err = azidentity.NewClientSecretCredential(tenantID, clientID, secret, nil) + var err error + credential, err = azidentity.NewDefaultAzureCredential(nil) + if err != nil { + panic(err) + } + + if cloudEnv, ok := os.LookupEnv("AZLOGS_ENVIRONMENT"); ok { + if strings.EqualFold(cloudEnv, "AzureUSGovernment") { + clientCloud = cloud.AzureGovernment + } + if strings.EqualFold(cloudEnv, "AzureChinaCloud") { + clientCloud = cloud.AzureChina + } + } + + } + workspaceID = getEnvVar("WORKSPACE_ID", fakeWorkspaceID) + workspaceID2 = getEnvVar("WORKSPACE_ID2", fakeWorkspaceID2) + resourceURI = getEnvVar("RESOURCE_URI", fakeResourceURI) + subscriptionID = getEnvVar("AZLOGS_SUBSCRIPTION_ID", fakeSubscrtiptionID) + region = getEnvVar("AZLOGS_LOCATION", fakeRegion) + + return m.Run() +} + +func startRecording(t *testing.T) { + err := recording.Start(t, recordingDirectory, nil) + require.NoError(t, err) + t.Cleanup(func() { + err := recording.Stop(t, nil) + require.NoError(t, err) + }) +} + +func startTest(t *testing.T) *azlogs.Client { + startRecording(t) + transport, err := recording.NewRecordingHTTPClient(t, nil) + require.NoError(t, err) + opts := &azlogs.ClientOptions{ClientOptions: azcore.ClientOptions{Transport: transport, Cloud: clientCloud}} + client, err := azlogs.NewClient(credential, opts) + if err != nil { + panic(err) + } + return client +} + +func getEnvVar(envVar string, fakeValue string) string { + // get value + value := fakeValue + if recording.GetRecordMode() == recording.LiveMode || recording.GetRecordMode() == recording.RecordingMode { + value = os.Getenv(envVar) + if value == "" { + panic("no value for " + envVar) + } + } + + // sanitize value + if fakeValue != "" && recording.GetRecordMode() == recording.RecordingMode { + err := recording.AddGeneralRegexSanitizer(fakeValue, value, nil) + if err != nil { + panic(err) + } + } + + return value +} + +type FakeCredential struct{} + +func (f *FakeCredential) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) { + return azcore.AccessToken{Token: "faketoken", ExpiresOn: time.Now().Add(time.Hour).UTC()}, nil +} + +type serdeModel interface { + json.Marshaler + json.Unmarshaler +} + +func testSerde[T serdeModel](t *testing.T, model T) { + data, err := model.MarshalJSON() + require.NoError(t, err) + err = model.UnmarshalJSON(data) + require.NoError(t, err) + + // testing unmarshal error scenarios + var data2 []byte + err = model.UnmarshalJSON(data2) + require.Error(t, err) + + m := regexp.MustCompile(":.*$") + modifiedData := m.ReplaceAllString(string(data), ":false}") + if !strings.Contains(modifiedData, "render") && modifiedData != "{}" { + data3 := []byte(modifiedData) + err = model.UnmarshalJSON(data3) + require.Error(t, err) + } +} diff --git a/sdk/monitor/query/azlogs/version.go b/sdk/monitor/query/azlogs/version.go new file mode 100644 index 000000000000..bacf01c9f977 --- /dev/null +++ b/sdk/monitor/query/azlogs/version.go @@ -0,0 +1,12 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azlogs + +const ( + moduleName = "github.com/Azure/azure-sdk-for-go/sdk/monitor/query/azlogs" + version = "v0.1.0" +)