diff --git a/sdk/monitor/azure-monitor-query/README.md b/sdk/monitor/azure-monitor-query/README.md index 42d0c7564c71a..42edb93e036a9 100644 --- a/sdk/monitor/azure-monitor-query/README.md +++ b/sdk/monitor/azure-monitor-query/README.md @@ -90,7 +90,127 @@ LogsAsyncClient logsAsyncClient = new LogsClientBuilder() + "; value = " + logsTableCell.getValueAsString())); } } -} + +``` +### Get logs for a query and read the response as a model type + +```java + + LogsQueryResult queryResults = logsClient + .queryLogs("d2d0e126-fa1e-4b0a-b647-250cdd471e68", "AppRequests", null); + + // Sample to use a model type to read the results + for (LogsTable table : queryResults.getLogsTables()) { + for (LogsTableRow row : table.getTableRows()) { + CustomModel model = row.getRowAsObject(CustomModel.class); + System.out.println("Time generated " + model.getTimeGenerated() + "; success = " + model.getSuccess() + + "; operation name = " + model.getOperationName()); + } + } + + + public class CustomModel { + private OffsetDateTime timeGenerated; + private String tenantId; + private String id; + private String source; + private Boolean success; + private Double durationMs; + private Object properties; + private Object measurements; + private String operationName; + private String operationId; + private Object operationLinks; + + + public OffsetDateTime getTimeGenerated() { + return timeGenerated; + } + + public void setTimeGenerated(OffsetDateTime timeGenerated) { + this.timeGenerated = timeGenerated; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public Boolean getSuccess() { + return success; + } + + public void setSuccess(Boolean success) { + this.success = success; + } + + public Double getDurationMs() { + return durationMs; + } + + public void setDurationMs(Double durationMs) { + this.durationMs = durationMs; + } + + public Object getProperties() { + return properties; + } + + public void setProperties(Object properties) { + this.properties = properties; + } + + public Object getMeasurements() { + return measurements; + } + + public void setMeasurements(Object measurements) { + this.measurements = measurements; + } + + public String getOperationName() { + return operationName; + } + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public String getOperationId() { + return operationId; + } + + public void setOperationId(String operationId) { + this.operationId = operationId; + } + + public Object getOperationLinks() { + return operationLinks; + } + + public void setOperationLinks(Object operationLinks) { + this.operationLinks = operationLinks; + } + } ``` ### Get logs for a batch of queries @@ -272,7 +392,7 @@ client library to use the Netty HTTP client. Configuring or changing the HTTP cl All client libraries, by default, use the Tomcat-native Boring SSL library to enable native-level performance for SSL operations. The Boring SSL library is an uber jar containing native libraries for Linux / macOS / Windows, and provides -better performance compared to the default SSL implementation within the JDK. For more information, including how to +better performance compared to the default SSL com.azure.monitor.collect.metrics.implementation within the JDK. For more information, including how to reduce the dependency size, refer to the [performance tuning][performance_tuning] section of the wiki. ## Next steps diff --git a/sdk/monitor/azure-monitor-query/pom.xml b/sdk/monitor/azure-monitor-query/pom.xml index 2f4074c25aa8e..9d0be9049e487 100644 --- a/sdk/monitor/azure-monitor-query/pom.xml +++ b/sdk/monitor/azure-monitor-query/pom.xml @@ -73,6 +73,12 @@ 1.6.2 test + + com.azure + azure-core-serializer-json-jackson + 1.2.3 + test + diff --git a/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/LogsAsyncClient.java b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/LogsAsyncClient.java index 2024ba22a76ef..a845e17a7e196 100644 --- a/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/LogsAsyncClient.java +++ b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/LogsAsyncClient.java @@ -13,6 +13,7 @@ import com.azure.monitor.query.log.implementation.models.BatchRequest; import com.azure.monitor.query.log.implementation.models.BatchResponse; import com.azure.monitor.query.log.implementation.models.ErrorInfo; +import com.azure.monitor.query.log.implementation.models.ErrorResponseException; import com.azure.monitor.query.log.implementation.models.LogQueryRequest; import com.azure.monitor.query.log.implementation.models.LogQueryResponse; import com.azure.monitor.query.log.implementation.models.LogQueryResult; @@ -22,7 +23,9 @@ import com.azure.monitor.query.models.LogsQueryBatch; import com.azure.monitor.query.models.LogsQueryBatchResult; import com.azure.monitor.query.models.LogsQueryBatchResultCollection; +import com.azure.monitor.query.models.LogsQueryError; import com.azure.monitor.query.models.LogsQueryErrorDetails; +import com.azure.monitor.query.models.LogsQueryException; import com.azure.monitor.query.models.LogsQueryOptions; import com.azure.monitor.query.models.LogsQueryResult; import com.azure.monitor.query.models.LogsTable; @@ -33,6 +36,7 @@ import reactor.core.publisher.Mono; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -128,6 +132,14 @@ Mono> queryLogsBatchWithResponse(LogsQu batchRequest.setRequests(requests); return innerClient.getQueries().batchWithResponseAsync(batchRequest, context) + .onErrorMap(ex -> { + if (ex instanceof ErrorResponseException) { + ErrorResponseException error = (ErrorResponseException) ex; + ErrorInfo errorInfo = error.getValue().getError(); + return new LogsQueryException(error.getResponse(), mapLogsQueryError(errorInfo)); + } + return ex; + }) .map(this::convertToLogQueryBatchResult); } @@ -139,17 +151,39 @@ private Response convertToLogQueryBatchResult(Re for (LogQueryResponse singleQueryResponse : batchResponse.getResponses()) { LogsQueryBatchResult logsQueryBatchResult = new LogsQueryBatchResult(singleQueryResponse.getId(), singleQueryResponse.getStatus(), getLogsQueryResult(singleQueryResponse.getBody()), - mapLogsQueryBatchError(singleQueryResponse.getBody().getError())); + mapLogsQueryError(singleQueryResponse.getBody().getError())); batchResults.add(logsQueryBatchResult); } batchResults.sort(Comparator.comparingInt(o -> Integer.parseInt(o.getId()))); return new SimpleResponse<>(response.getRequest(), response.getStatusCode(), response.getHeaders(), logsQueryBatchResultCollection); } - private LogsQueryErrorDetails mapLogsQueryBatchError(ErrorInfo errors) { + private LogsQueryErrorDetails mapLogsQueryError(ErrorInfo errors) { if (errors != null) { - return new LogsQueryErrorDetails(errors.getMessage(), errors.getCode(), - errors.getDetails().get(0).getTarget()); + List errorDetails = Collections.emptyList(); + if (errors.getDetails() != null) { + errorDetails = errors.getDetails() + .stream() + .map(errorDetail -> new LogsQueryError(errorDetail.getCode(), + errorDetail.getMessage(), + errorDetail.getTarget(), + errorDetail.getValue(), + errorDetail.getResources(), + errorDetail.getAdditionalProperties())) + .collect(Collectors.toList()); + } + + ErrorInfo innerError = errors.getInnererror(); + ErrorInfo currentError = errors.getInnererror(); + while (currentError != null) { + innerError = errors.getInnererror(); + currentError = errors.getInnererror(); + } + String code = errors.getCode(); + if (!errors.getCode().equals(innerError.getCode())) { + code = innerError.getCode(); + } + return new LogsQueryErrorDetails(errors.getMessage(), code, errorDetails); } return null; } @@ -186,6 +220,14 @@ Mono> queryLogsWithResponse(LogsQueryOptions options, queryBody, preferHeader, context) + .onErrorMap(ex -> { + if (ex instanceof ErrorResponseException) { + ErrorResponseException error = (ErrorResponseException) ex; + ErrorInfo errorInfo = error.getValue().getError(); + return new LogsQueryException(error.getResponse(), mapLogsQueryError(errorInfo)); + } + return ex; + }) .map(this::convertToLogQueryResult); } diff --git a/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryError.java b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryError.java new file mode 100644 index 0000000000000..61d65c40006f2 --- /dev/null +++ b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryError.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.query.models; + +import java.util.List; + +/** + * Error details of a failed log query. + */ + +public final class LogsQueryError { + /* + * The error's code. + */ + private final String code; + + /* + * A human readable error message. + */ + private final String message; + + /* + * Indicates which property in the request is responsible for the error. + */ + private final String target; + + /* + * Indicates which value in 'target' is responsible for the error. + */ + private final String value; + + /* + * Indicates resources which were responsible for the error. + */ + private final List resources; + + /* + * Additional properties that can be provided on the error details object + */ + private final Object additionalProperties; + + /** + * Creates an instance of ErrorDetail class. + * @param code the code value to set. + * @param message the message value to set. + * @param target indicates which property in the request is responsible for the error. + * @param value indicates which value in 'target' is responsible for the error. + * @param resources indicates resources which were responsible for the error. + * @param additionalProperties additional properties that can be provided on the error details object + */ + public LogsQueryError( + String code, + String message, + String target, + String value, + List resources, + Object additionalProperties) { + this.code = code; + this.message = message; + this.target = target; + this.value = value; + this.resources = resources; + this.additionalProperties = additionalProperties; + } + + /** + * Get the code property: The error's code. + * @return the code value. + */ + public String getCode() { + return this.code; + } + + /** + * Get the message property: A human readable error message. + * @return the message value. + */ + public String getMessage() { + return this.message; + } + + /** + * Get the target property: Indicates which property in the request is responsible for the error. + * @return the target value. + */ + public String getTarget() { + return this.target; + } + + /** + * Get the value property: Indicates which value in 'target' is responsible for the error. + * @return the value value. + */ + public String getValue() { + return this.value; + } + + /** + * Get the resources property: Indicates resources which were responsible for the error. + * @return the resources value. + */ + public List getResources() { + return this.resources; + } + + /** + * Get the additionalProperties property: Additional properties that can be provided on the error details object. + * @return the additionalProperties value. + */ + public Object getAdditionalProperties() { + return this.additionalProperties; + } +} diff --git a/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryErrorDetails.java b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryErrorDetails.java index 90f3d434dd58d..86da66d534907 100644 --- a/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryErrorDetails.java +++ b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryErrorDetails.java @@ -5,6 +5,8 @@ import com.azure.core.annotation.Immutable; +import java.util.List; + /** * The error details of a failed log query. */ @@ -12,19 +14,18 @@ public final class LogsQueryErrorDetails { private final String message; private final String code; - private final String target; + private final List errors; /** * Creates an instance of {@link LogsQueryErrorDetails} with the failure code and target. * @param message The error message. * @param code The error code indicating the reason for the error. - * @param target Indicates which property in the request is responsible for the error. + * @param errors The list of additional error details. */ - public LogsQueryErrorDetails(String message, String code, String target) { - + public LogsQueryErrorDetails(String message, String code, List errors) { this.message = message; this.code = code; - this.target = target; + this.errors = errors; } /** @@ -44,10 +45,10 @@ public String getCode() { } /** - * Indicates which property in the request is responsible for the error. - * @return The property in the request that is responsible for the error. + * Returns the list of additional error details. + * @return the list of additional error details. */ - public String getTarget() { - return target; + public List getErrors() { + return errors; } } diff --git a/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryException.java b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryException.java new file mode 100644 index 0000000000000..9916ba59af887 --- /dev/null +++ b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsQueryException.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.query.models; + +import com.azure.core.exception.HttpResponseException; +import com.azure.core.http.HttpResponse; + +/** + * Exception thrown when a query to retrieve logs fails. + */ +public final class LogsQueryException extends HttpResponseException { + private final transient LogsQueryErrorDetails error; + + /** + * Creates a new instance of this exception with the {@link HttpResponse} and {@link LogsQueryError error} + * information. + * @param response The {@link HttpResponse}. + * @param error The {@link LogsQueryError error} details. + */ + public LogsQueryException(HttpResponse response, LogsQueryErrorDetails error) { + super("Failed to executed logs query", response, error); + this.error = error; + } + + @Override + public LogsQueryErrorDetails getValue() { + return this.error; + } +} diff --git a/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsTableRow.java b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsTableRow.java index 600433356f173..f34b6ed096862 100644 --- a/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsTableRow.java +++ b/sdk/monitor/azure-monitor-query/src/main/java/com/azure/monitor/query/models/LogsTableRow.java @@ -4,9 +4,16 @@ package com.azure.monitor.query.models; import com.azure.core.annotation.Immutable; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.serializer.TypeReference; +import java.lang.reflect.Field; +import java.util.Arrays; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; /** * Represents a row in a {@link LogsTable} of a logs query. @@ -15,6 +22,7 @@ public final class LogsTableRow { private final int rowIndex; private final List tableRow; + private final ClientLogger logger = new ClientLogger(LogsTableRow.class); /** * Creates a row in a {@link LogsTable} of a logs query. @@ -46,12 +54,69 @@ public List getTableRow() { * Returns the value associated with the given column name. If the column name is not found * {@link Optional#isPresent()} evaluates to {@code false}. * @param columnName the column name for which the value is returned. - * * @return The value associated with the given column name. */ public Optional getColumnValue(String columnName) { return tableRow.stream() - .filter(cell -> cell.getColumnName().equals(columnName)) - .findFirst(); + .filter(cell -> cell.getColumnName().equals(columnName)) + .findFirst(); + } + + /** + * Returns the table row as an object of the given {@code type}. The field names on the type should be + * reflectively accessible and the names should match the column names. If the table row contains columns that + * are not available on the object, they are ignored and similarly if the fields in the object are not found in + * the table columns, they will be null. + * @param type The type of the object to be returned + * @param The class type. + * @return The object that this table row is mapped to. + * @throws IllegalArgumentException if an instance of the object cannot be created. + */ + public T getRowAsObject(Class type) { + try { + T t = type.newInstance(); + + Map declaredFieldMapping = Arrays.stream(type.getDeclaredFields()) + .collect(Collectors.toMap(field -> field.getName().toLowerCase(Locale.ROOT), field -> field)); + + tableRow.stream() + .forEach(tableCell -> { + String columnName = tableCell.getColumnName(); + try { + Field field = declaredFieldMapping.get(columnName.toLowerCase(Locale.ROOT)); + if (field == null) { + return; + } + field.setAccessible(true); + if (tableCell.getColumnType() == ColumnDataType.BOOL) { + field.set(t, tableCell.getValueAsBoolean()); + } else if (tableCell.getColumnType() == ColumnDataType.DATETIME) { + field.set(t, tableCell.getValueAsDateTime()); + } else if (tableCell.getColumnType() == ColumnDataType.DYNAMIC) { + if (tableCell.getValueAsDynamic() != null) { + field.set(t, + tableCell.getValueAsDynamic() + .toObject(TypeReference.createInstance(field.getType()))); + } + } else if (tableCell.getColumnType() == ColumnDataType.INT) { + field.set(t, tableCell.getValueAsInteger()); + } else if (tableCell.getColumnType() == ColumnDataType.LONG) { + field.set(t, tableCell.getValueAsLong()); + } else if (tableCell.getColumnType() == ColumnDataType.REAL) { + field.set(t, tableCell.getValueAsDouble()); + } else if (tableCell.getColumnType() == ColumnDataType.STRING) { + field.set(t, tableCell.getValueAsString()); + } + field.setAccessible(false); + } catch (IllegalAccessException ex) { + throw logger.logExceptionAsError( + new IllegalArgumentException("Failed to set column value for " + columnName, ex)); + } + }); + return t; + } catch (InstantiationException | IllegalAccessException ex) { + throw logger.logExceptionAsError( + new IllegalArgumentException("Cannot create an instance of class " + type.getName(), ex)); + } } } diff --git a/sdk/monitor/azure-monitor-query/src/samples/java/com/azure/monitor/query/LogsQueryWithModels.java b/sdk/monitor/azure-monitor-query/src/samples/java/com/azure/monitor/query/LogsQueryWithModels.java new file mode 100644 index 0000000000000..9757dc91d2ace --- /dev/null +++ b/sdk/monitor/azure-monitor-query/src/samples/java/com/azure/monitor/query/LogsQueryWithModels.java @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.query; + +import com.azure.core.util.Configuration; +import com.azure.identity.ClientSecretCredential; +import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.monitor.query.models.LogsQueryResult; +import com.azure.monitor.query.models.LogsTable; +import com.azure.monitor.query.models.LogsTableRow; + +import java.time.OffsetDateTime; + +/** + * Sample to demonstrate using a custom model to read the results of a logs query. + */ +public class LogsQueryWithModels { + + /** + * The main method to run the sample. + * @param args ignored args + */ + public static void main(String[] args) { + ClientSecretCredential tokenCredential = new ClientSecretCredentialBuilder() + .clientId(Configuration.getGlobalConfiguration().get("AZURE_MONITOR_CLIENT_ID")) + .clientSecret(Configuration.getGlobalConfiguration().get("AZURE_MONITOR_CLIENT_SECRET")) + .tenantId(Configuration.getGlobalConfiguration().get("AZURE_TENANT_ID")) + .build(); + + LogsClient logsClient = new LogsClientBuilder() + .credential(tokenCredential) + .buildClient(); + + LogsQueryResult queryResults = logsClient + .queryLogs("{workspace-id}", "AppRequests", null); + + // Sample to use a model type to read the results + for (LogsTable table : queryResults.getLogsTables()) { + for (LogsTableRow row : table.getTableRows()) { + CustomModel model = row.getRowAsObject(CustomModel.class); + System.out.println("Time generated " + model.getTimeGenerated() + "; success = " + model.getSuccess() + + "; operation name = " + model.getOperationName()); + } + } + } + + /** + * A custom model to read the logs query result. + */ + private static class CustomModel { + private OffsetDateTime timeGenerated; + private String tenantId; + private String id; + private String source; + private Boolean success; + private Double durationMs; + private Object properties; + private String operationName; + private String operationId; + + + /** + * Returns the time the log event was generated. + * @return the time the log event was generated. + */ + public OffsetDateTime getTimeGenerated() { + return timeGenerated; + } + + /** + * Returns the tenant id of the resource for which this log was recorded. + * @return the tenant id of the resource for which this log was recorded. + */ + public String getTenantId() { + return tenantId; + } + + /** + * Returns the unique identifier of this log. + * @return the unique identifier of this log. + */ + public String getId() { + return id; + } + + /** + * Returns the source of this log. + * @return the source of this log. + */ + public String getSource() { + return source; + } + + /** + * Returns {@code true} if the logged request returned a successful response. + * @return {@code true} if the logged request returned a successful response. + */ + public Boolean getSuccess() { + return success; + } + + /** + * Returns the time duration the service took to process the request. + * @return the time duration the service took to process the request. + */ + public Double getDurationMs() { + return durationMs; + } + + /** + * Returns additional properties of the request. + * @return additional properties of the request. + */ + public Object getProperties() { + return properties; + } + + /** + * Returns the name of the operation. + * @return the name of the operation. + */ + public String getOperationName() { + return operationName; + } + } + +}