diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/checkstyle-suppressions.xml b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/checkstyle-suppressions.xml index 766caf41d4093..227fa9311cd16 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/checkstyle-suppressions.xml +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/checkstyle-suppressions.xml @@ -235,6 +235,17 @@ + + + + + + + + + + + @@ -276,6 +287,7 @@ + @@ -339,7 +351,15 @@ + + + + + + + + diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/spotbugs-exclude.xml b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/spotbugs-exclude.xml index 5d6697f82d1b8..ab19b1b35f78a 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/spotbugs-exclude.xml +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/spotbugs-exclude.xml @@ -37,6 +37,7 @@ + @@ -98,5 +99,9 @@ + + + + diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/QuickPulseDataCollector.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/QuickPulseDataCollector.java index 39d69289f0f87..5121c7259581c 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/QuickPulseDataCollector.java +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/QuickPulseDataCollector.java @@ -163,6 +163,7 @@ private void addException(TelemetryExceptionData exceptionData, int itemCount) { counters.exceptions.addAndGet(itemCount); List exceptionList = exceptionData.getExceptions(); + // Exception is a class from live metrics swagger that represents a document for an exception Exception exceptionDoc = new Exception(); if (exceptionList != null && !exceptionList.isEmpty()) { exceptionDoc.setExceptionMessage(exceptionList.get(0).getMessage()); diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/CustomDimensions.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/CustomDimensions.java new file mode 100644 index 0000000000000..30fdeb0dd896c --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/CustomDimensions.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +import java.util.HashMap; +import java.util.Map; + +public class CustomDimensions { + private Map customDimensions; + + public CustomDimensions() { + this.customDimensions = new HashMap(); + } + + public void setCustomDimensions(Map customDimensions, Map customMeasurements) { + Map resultMap = new HashMap<>(); + if (customDimensions != null) { + resultMap.putAll(customDimensions); + } + if (customMeasurements != null) { + for (Map.Entry cmEntry : customMeasurements.entrySet()) { + resultMap.put(cmEntry.getKey(), cmEntry.getValue().toString()); + } + } + this.customDimensions = resultMap; + } + + public Map getCustomDimensions() { + return this.customDimensions; + } +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/DependencyDataColumns.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/DependencyDataColumns.java new file mode 100644 index 0000000000000..b8cea69356744 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/DependencyDataColumns.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.RemoteDependencyData; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.FormattedDuration; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Casing of private fields is to match the names of fields passed down via filtering configuration +public class DependencyDataColumns implements TelemetryColumns { + private final CustomDimensions customDims; + private final Map mapping = new HashMap<>(); + + public DependencyDataColumns(RemoteDependencyData rdData) { + customDims = new CustomDimensions(); + customDims.setCustomDimensions(rdData.getProperties(), rdData.getMeasurements()); + mapping.put(KnownDependencyColumns.TARGET, rdData.getTarget()); + mapping.put(KnownDependencyColumns.DURATION, + FormattedDuration.getDurationFromTelemetryItemDurationString(rdData.getDuration())); + mapping.put(KnownDependencyColumns.SUCCESS, rdData.isSuccess()); + mapping.put(KnownDependencyColumns.NAME, rdData.getName()); + int resultCode; + try { + resultCode = Integer.parseInt(rdData.getResultCode()); + } catch (NumberFormatException e) { + resultCode = -1; + } + mapping.put(KnownDependencyColumns.RESULT_CODE, resultCode); + mapping.put(KnownDependencyColumns.TYPE, rdData.getType()); + mapping.put(KnownDependencyColumns.DATA, rdData.getData()); + } + + // To be used for tests only + public DependencyDataColumns(String target, long duration, boolean success, String name, int resultCode, + String type, String data, Map dims, Map measurements) { + customDims = new CustomDimensions(); + customDims.setCustomDimensions(dims, measurements); + mapping.put(KnownDependencyColumns.TARGET, target); + mapping.put(KnownDependencyColumns.DURATION, duration); + mapping.put(KnownDependencyColumns.SUCCESS, success); + mapping.put(KnownDependencyColumns.NAME, name); + mapping.put(KnownDependencyColumns.RESULT_CODE, resultCode); + mapping.put(KnownDependencyColumns.TYPE, type); + mapping.put(KnownDependencyColumns.DATA, data); + } + + public T getFieldValue(String fieldName, Class type) { + return type.cast(mapping.get(fieldName)); + } + + public Map getCustomDimensions() { + return this.customDims.getCustomDimensions(); + } + + public List getAllFieldValuesAsString() { + List result = new ArrayList<>(); + for (Object value : mapping.values()) { + if (value instanceof String) { + result.add((String) value); + } else if (value instanceof Integer) { + result.add(((Integer) value).toString()); + } else if (value instanceof Long) { + result.add(((Long) value).toString()); + } else { // boolean + result.add(((Boolean) value).toString()); + } + } + return result; + } + +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/ExceptionDataColumns.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/ExceptionDataColumns.java new file mode 100644 index 0000000000000..8ecbaf00e7714 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/ExceptionDataColumns.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.TelemetryExceptionData; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.TelemetryExceptionDetails; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Casing of private fields is to match the names of fields passed down via filtering configuration +public class ExceptionDataColumns implements TelemetryColumns { + + private final CustomDimensions customDims; + + private final Map mapping = new HashMap<>(); + + public ExceptionDataColumns(TelemetryExceptionData exceptionData) { + customDims = new CustomDimensions(); + customDims.setCustomDimensions(exceptionData.getProperties(), exceptionData.getMeasurements()); + List details = exceptionData.getExceptions(); + mapping.put(KnownExceptionColumns.MESSAGE, + details != null && !details.isEmpty() ? details.get(0).getMessage() : ""); + mapping.put(KnownExceptionColumns.STACK, + details != null && !details.isEmpty() ? details.get(0).getStack() : ""); + } + + // To be used in tests only + public ExceptionDataColumns(String message, String stackTrace, Map dims, + Map measurements) { + customDims = new CustomDimensions(); + customDims.setCustomDimensions(dims, measurements); + mapping.put(KnownExceptionColumns.MESSAGE, message); + mapping.put(KnownExceptionColumns.STACK, stackTrace); + } + + public Map getCustomDimensions() { + return this.customDims.getCustomDimensions(); + } + + public T getFieldValue(String fieldName, Class type) { + return type.cast(mapping.get(fieldName)); + } + + public List getAllFieldValuesAsString() { + List result = new ArrayList<>(); + for (Object value : mapping.values()) { + result.add((String) value); + } + return result; + } +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/Filter.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/Filter.java new file mode 100644 index 0000000000000..fd3c2cf454842 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/Filter.java @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.swagger.models.DerivedMetricInfo; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.swagger.models.FilterConjunctionGroupInfo; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.swagger.models.FilterInfo; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.swagger.models.PredicateType; + +import java.util.List; +import java.util.Map; + +public class Filter { + public static final String CUSTOM_DIM_FIELDNAME_PREFIX = "CustomDimensions."; + public static final String ANY_FIELD = "*"; + + // To be used when checking telemetry against metric charts filters + public static boolean checkMetricFilters(DerivedMetricInfo derivedMetricInfo, TelemetryColumns data) { + if (derivedMetricInfo.getFilterGroups().isEmpty()) { + // This should never happen - even when a user does not add filter pills to the derived metric, + // the filterGroups array should have one filter group with an empty array of filters. + return true; + } + + // Haven't yet seen any case where there is more than one filter group in a derived metric info. + // Just to be safe, handling the multiple filter conjunction group case as an or operation. + boolean matched = false; + for (FilterConjunctionGroupInfo filterGroup : derivedMetricInfo.getFilterGroups()) { + matched = matched || checkFilterConjunctionGroup(filterGroup, data); + } + return matched; + } + + // To be used when checking telemetry against document filters. This also gets reused in the logic for checking metrics + // charts filters. + public static boolean checkFilterConjunctionGroup(FilterConjunctionGroupInfo filterConjunctionGroupInfo, + TelemetryColumns data) { + // All of the filters need to match for this to return true (and operation). + for (FilterInfo filter : filterConjunctionGroupInfo.getFilters()) { + if (!checkFilter(filter, data)) { + return false; + } + } + return true; + } + + private static boolean checkFilter(FilterInfo filter, TelemetryColumns data) { + if (ANY_FIELD.equals(filter.getFieldName())) { + return checkAnyFieldFilter(filter, data); + } else if (filter.getFieldName().startsWith(CUSTOM_DIM_FIELDNAME_PREFIX)) { + return checkCustomDimFilter(filter, data); + } else { + String fieldName = filter.getFieldName(); + + if (filter.getFieldName().equals(KnownRequestColumns.SUCCESS)) { + boolean fieldValueBoolean = data.getFieldValue(filter.getFieldName(), Boolean.class); + boolean comparand = Boolean.parseBoolean(filter.getComparand().toLowerCase()); + if (filter.getPredicate().equals(PredicateType.EQUAL)) { + return fieldValueBoolean == comparand; + } else if (filter.getPredicate().equals(PredicateType.NOT_EQUAL)) { + return fieldValueBoolean != comparand; + } + } else if (filter.getFieldName().equals(KnownDependencyColumns.DURATION)) { + long comparand = getMicroSecondsFromFilterTimestampString(filter.getComparand()); + long fieldValueLong = data.getFieldValue(KnownRequestColumns.DURATION, Long.class); + return numericCompare(fieldValueLong, comparand, filter.getPredicate()); + } else if (filter.getFieldName().equals(KnownDependencyColumns.RESULT_CODE) + || filter.getFieldName().equals(KnownRequestColumns.RESPONSE_CODE)) { + int comparand = Integer.parseInt(filter.getComparand()); + PredicateType predicate = filter.getPredicate(); + int fieldValueInt = data.getFieldValue(fieldName, Integer.class); + return numericCompare(fieldValueInt, comparand, predicate); + } else { + // string fields + String fieldValueString = data.getFieldValue(fieldName, String.class); + return stringCompare(fieldValueString, filter.getComparand(), filter.getPredicate()); + } + } + return false; + } + + private static boolean checkAnyFieldFilter(FilterInfo filter, TelemetryColumns data) { + + List values = data.getAllFieldValuesAsString(); + for (String value : values) { + if (stringCompare(value, filter.getComparand(), filter.getPredicate())) { + return true; + } + } + Map customDimensions = data.getCustomDimensions(); + for (String value : customDimensions.values()) { + if (stringCompare(value, filter.getComparand(), filter.getPredicate())) { + return true; + } + } + return false; + } + + private static boolean checkCustomDimFilter(FilterInfo filter, TelemetryColumns data) { + Map customDimensions = data.getCustomDimensions(); + String fieldName = filter.getFieldName().replace(CUSTOM_DIM_FIELDNAME_PREFIX, ""); + if (customDimensions.containsKey(fieldName)) { + String value = customDimensions.get(fieldName); + return stringCompare(value, filter.getComparand(), filter.getPredicate()); + } else { + return false; // the asked for field is not present in the custom dimensions + } + } + + private static boolean stringCompare(String fieldValue, String comparand, PredicateType predicate) { + if (predicate.equals(PredicateType.EQUAL)) { + return fieldValue != null && fieldValue.equals(comparand); + } else if (predicate.equals(PredicateType.NOT_EQUAL)) { + return fieldValue != null && !fieldValue.equals(comparand); + } else if (predicate.equals(PredicateType.CONTAINS)) { + return fieldValue != null && fieldValue.toLowerCase().contains(comparand.toLowerCase()); + } else if (predicate.equals(PredicateType.DOES_NOT_CONTAIN)) { + return fieldValue != null && !fieldValue.toLowerCase().contains(comparand.toLowerCase()); + } + return false; + } + + private static boolean numericCompare(long fieldValue, long comparand, PredicateType predicate) { + if (predicate.equals(PredicateType.EQUAL)) { + return fieldValue == comparand; + } else if (predicate.equals(PredicateType.NOT_EQUAL)) { + return fieldValue != comparand; + } else if (predicate.equals(PredicateType.GREATER_THAN)) { + return fieldValue > comparand; + } else if (predicate.equals(PredicateType.GREATER_THAN_OR_EQUAL)) { + return fieldValue >= comparand; + } else if (predicate.equals(PredicateType.LESS_THAN)) { + return fieldValue < comparand; + } else if (predicate.equals(PredicateType.LESS_THAN_OR_EQUAL)) { + return fieldValue <= comparand; + } else { + return false; + } + } + + public static long getMicroSecondsFromFilterTimestampString(String timestamp) { + // The service side will return a timestamp in the following format: + // [days].[hours]:[minutes]:[seconds] + // the seconds may be a whole number or something like 7.89. 7.89 seconds translates to 7890000 microseconds. + // examples: "14.6:56:7.89" = 1234567890000 microseconds, "0.0:0:0.2" = 200000 microseconds + + // Split the timestamp by ":" + String[] parts = timestamp.split(":"); + if (parts.length != 3) { + return Long.MIN_VALUE; // Return a special value to indicate an error + } + + // Parse seconds and minutes + long microseconds = (long) (Double.parseDouble(parts[2]) * 1000000L); + int minutes = parseInt(parts[1]); + + // Split the first part by "." to get days and hours + String[] firstPart = parts[0].split("\\."); + if (firstPart.length != 2) { + return Long.MIN_VALUE; // Return a special value to indicate an error + } + + int hours = parseInt(firstPart[1]); + int days = parseInt(firstPart[0]); + + if (minutes == Integer.MIN_VALUE || hours == Integer.MIN_VALUE || days == Integer.MIN_VALUE) { + return Long.MIN_VALUE; // Return a special value to indicate an error + } + + // Calculate the total microseconds + return microseconds + (minutes * 60L * 1000000L) + (hours * 60L * 60L * 1000000L) + + (days * 24L * 60L * 60L * 1000000L); + + } + + private static int parseInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return Integer.MIN_VALUE; + } + } + +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownDependencyColumns.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownDependencyColumns.java new file mode 100644 index 0000000000000..1cbe55d84e10b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownDependencyColumns.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +public class KnownDependencyColumns { + public static final String TARGET = "Target"; + public static final String DURATION = "Duration"; + public static final String RESULT_CODE = "ResultCode"; + public static final String SUCCESS = "Success"; + public static final String TYPE = "Type"; + public static final String DATA = "Data"; + public static final String NAME = "Name"; +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownExceptionColumns.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownExceptionColumns.java new file mode 100644 index 0000000000000..f8baa6dc6f7e9 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownExceptionColumns.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +public class KnownExceptionColumns { + public static final String MESSAGE = "Exception.Message"; + public static final String STACK = "Exception.StackTrace"; +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownRequestColumns.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownRequestColumns.java new file mode 100644 index 0000000000000..63a6c9f627a14 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownRequestColumns.java @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +public class KnownRequestColumns { + public static final String URL = "Url"; + public static final String DURATION = "Duration"; + public static final String RESPONSE_CODE = "ResponseCode"; + public static final String SUCCESS = "Success"; + public static final String NAME = "Name"; + +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownTraceColumns.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownTraceColumns.java new file mode 100644 index 0000000000000..f5c5556dcdfe8 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/KnownTraceColumns.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +public class KnownTraceColumns { + public static final String MESSAGE = "Message"; +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/RequestDataColumns.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/RequestDataColumns.java new file mode 100644 index 0000000000000..2b265bcf299f8 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/RequestDataColumns.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.RequestData; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.FormattedDuration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +// Casing of private fields is to match the names of fields passed down via filtering configuration +public class RequestDataColumns implements TelemetryColumns { + private final Map mapping = new HashMap<>(); + private final CustomDimensions customDims; + + public RequestDataColumns(RequestData requestData) { + customDims = new CustomDimensions(); + customDims.setCustomDimensions(requestData.getProperties(), requestData.getMeasurements()); + mapping.put(KnownRequestColumns.URL, requestData.getUrl()); + mapping.put(KnownRequestColumns.SUCCESS, requestData.isSuccess()); + mapping.put(KnownRequestColumns.DURATION, + FormattedDuration.getDurationFromTelemetryItemDurationString(requestData.getDuration())); + mapping.put(KnownRequestColumns.NAME, requestData.getName()); + int responseCode; + try { + responseCode = Integer.parseInt(requestData.getResponseCode()); + } catch (NumberFormatException e) { + responseCode = -1; + } + mapping.put(KnownRequestColumns.RESPONSE_CODE, responseCode); + } + + // To be used in tests only + public RequestDataColumns(String url, long duration, int responseCode, boolean success, String name, + Map dims, Map measurements) { + customDims = new CustomDimensions(); + customDims.setCustomDimensions(dims, measurements); + mapping.put(KnownRequestColumns.URL, url); + mapping.put(KnownRequestColumns.SUCCESS, success); + mapping.put(KnownRequestColumns.DURATION, duration); + mapping.put(KnownRequestColumns.NAME, name); + mapping.put(KnownRequestColumns.RESPONSE_CODE, responseCode); + } + + public T getFieldValue(String fieldName, Class type) { + return type.cast(mapping.get(fieldName)); + } + + public List getAllFieldValuesAsString() { + List result = new ArrayList<>(); + for (Object value : mapping.values()) { + if (value instanceof String) { + result.add((String) value); + } else if (value instanceof Integer) { + result.add(((Integer) value).toString()); + } else if (value instanceof Long) { + result.add(((Long) value).toString()); + } else { // boolean + result.add(((Boolean) value).toString()); + } + } + return result; + } + + public Map getCustomDimensions() { + return this.customDims.getCustomDimensions(); + } + +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/TelemetryColumns.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/TelemetryColumns.java new file mode 100644 index 0000000000000..6a31bebaec053 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/TelemetryColumns.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +import java.util.List; +import java.util.Map; + +public interface TelemetryColumns { + T getFieldValue(String fieldName, Class type); + + List getAllFieldValuesAsString(); + + Map getCustomDimensions(); + +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/TraceDataColumns.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/TraceDataColumns.java new file mode 100644 index 0000000000000..ed07aa577e6fc --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filtering/TraceDataColumns.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering; + +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.MessageData; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TraceDataColumns implements TelemetryColumns { + private final Map mapping = new HashMap<>(); + + private final CustomDimensions customDims; + + public TraceDataColumns(MessageData traceData) { + customDims = new CustomDimensions(); + customDims.setCustomDimensions(traceData.getProperties(), traceData.getMeasurements()); + mapping.put(KnownTraceColumns.MESSAGE, traceData.getMessage()); + } + + // to be used in tests only + public TraceDataColumns(String message, Map dims, Map measurements) { + customDims = new CustomDimensions(); + customDims.setCustomDimensions(dims, measurements); + mapping.put(KnownTraceColumns.MESSAGE, message); + } + + public Map getCustomDimensions() { + return this.customDims.getCustomDimensions(); + } + + public T getFieldValue(String fieldName, Class type) { + return type.cast(mapping.get(fieldName)); + } + + public List getAllFieldValuesAsString() { + List result = new ArrayList<>(); + result.add((String) mapping.get(KnownTraceColumns.MESSAGE)); + return result; + } +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/utils/FormattedDuration.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/utils/FormattedDuration.java index 41aa192483284..8d9e7a1bd604d 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/utils/FormattedDuration.java +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/main/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/utils/FormattedDuration.java @@ -44,6 +44,33 @@ public static String fromNanos(long durationNanos) { return sb.toString(); } + // Returns duration in microseconds + public static long getDurationFromTelemetryItemDurationString(String duration) { + // duration in format: DD.HH:MM:SS.MMMMMM. Must be less than 1000 days. + try { + String[] parts = duration.split("\\."); + int days = 0; + String hms; + long microseconds; + if (parts.length == 3) { + days = Integer.parseInt(parts[0]); + hms = parts[1]; + microseconds = Integer.parseInt(parts[2]); + } else { //length 2 + hms = parts[0]; + microseconds = Integer.parseInt(parts[1]); + } + String[] hmsParts = hms.split(":"); + int hours = Integer.parseInt(hmsParts[0]); + int minutes = Integer.parseInt(hmsParts[1]); + int seconds = Integer.parseInt(hmsParts[2]); + return (days * 24L * 60L * 60L * 1000000L) + (hours * 60L * 60L * 1000000L) + (minutes * 60L * 1000000L) + + (seconds * 1000000L) + microseconds; + } catch (NumberFormatException e) { + return -1; + } + } + private static void appendDaysHoursMinutesSeconds(StringBuilder sb, long days, long hours, long minutes, long seconds) { if (days > 0) { diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filteringTest/FilterTest.java b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filteringTest/FilterTest.java new file mode 100644 index 0000000000000..5175f59b7c25c --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/src/test/java/com/azure/monitor/opentelemetry/autoconfigure/implementation/quickpulse/filteringTest/FilterTest.java @@ -0,0 +1,485 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filteringTest; + +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.MessageData; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.TelemetryExceptionData; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.TelemetryExceptionDetails; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.TelemetryItem; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.MonitorDomain; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.RequestData; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.RemoteDependencyData; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.QuickPulseTestBase; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.filtering.*; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.swagger.models.AggregationType; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.swagger.models.DerivedMetricInfo; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.swagger.models.FilterInfo; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.swagger.models.PredicateType; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.swagger.models.FilterConjunctionGroupInfo; + +import io.vertx.core.cli.annotations.Description; +import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; +import java.util.HashMap; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; + +class FilterTest { + + private FilterInfo createFilterInfoWithParams(String fieldName, PredicateType predicate, String comparand) { + FilterInfo result = new FilterInfo(); + result.setFieldName(fieldName); + result.setPredicate(predicate); + result.setComparand(comparand); + return result; + } + + private DerivedMetricInfo createDerivedMetricInfo(String id, String telemetryType, AggregationType agg, + AggregationType backendAgg, String projection, List filterGroups) { + DerivedMetricInfo result = new DerivedMetricInfo(); + result.setId(id); + result.setTelemetryType(telemetryType); + result.setAggregation(agg); + result.setBackEndAggregation(backendAgg); + result.setProjection(projection); + result.setFilterGroups(filterGroups); + return result; + } + + private List createListWithOneFilterConjunctionGroupAndOneFilter(FilterInfo filter) { + List result = new ArrayList<>(); + FilterConjunctionGroupInfo group = new FilterConjunctionGroupInfo(); + List filters = new ArrayList<>(); + filters.add(filter); + group.setFilters(filters); + result.add(group); + return result; + } + + @Test + @Description("This tests if the any field (*) filter can filter telemetry correctly, with various combos of predicates & column types") + void testAnyFieldFilter() { + FilterInfo anyFieldContainsHi = createFilterInfoWithParams(Filter.ANY_FIELD, PredicateType.CONTAINS, "hi"); + FilterInfo anyFieldNotContains + = createFilterInfoWithParams(Filter.ANY_FIELD, PredicateType.DOES_NOT_CONTAIN, "hi"); + FilterInfo anyFieldContainsCool = createFilterInfoWithParams(Filter.ANY_FIELD, PredicateType.CONTAINS, "cool"); + FilterInfo anyFieldForNumeric = createFilterInfoWithParams(Filter.ANY_FIELD, PredicateType.CONTAINS, "200"); + FilterInfo anyFieldForBoolean = createFilterInfoWithParams(Filter.ANY_FIELD, PredicateType.CONTAINS, "true"); + + RequestDataColumns request1 + = new RequestDataColumns("https://test.com/hiThere", 200, 200, true, "GET /hiThere", null, null); + Map customDims = new HashMap<>(); + customDims.put("property", "cool"); + RequestDataColumns request2 + = new RequestDataColumns("https://test.com/bye", 200, 200, true, "GET /bye", customDims, null); + + List filterGroups + = createListWithOneFilterConjunctionGroupAndOneFilter(anyFieldContainsHi); + DerivedMetricInfo derivedMetricInfo = createDerivedMetricInfo("random-id", "Request", AggregationType.SUM, + AggregationType.SUM, "Count()", filterGroups); + + FilterConjunctionGroupInfo filterGroup = filterGroups.get(0); + + // request contains "hi" in multiple fields & filter is contains hi + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, request1)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, request1)); + + // request does not contain "hi" in any field & filter is contains hi + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, request2)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, request2)); + + // request does not contain "hi" in any field & filter is does not contain hi + filterGroup.setFilters(asList(anyFieldNotContains)); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, request2)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, request2)); + + // request contains "cool" in custom dimensions & filter is contains cool + filterGroup.setFilters(asList(anyFieldContainsCool)); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, request2)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, request2)); + + // request contains 200 in duration & filter is contains "200". + // fields are expected to be treated as string + filterGroup.setFilters(asList(anyFieldForNumeric)); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, request1)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, request1)); + + // request contains true in Success & filter is contains "true". + // fields are expected to be treated as string + filterGroup.setFilters(asList(anyFieldForBoolean)); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, request1)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, request1)); + + } + + @Test + @Description("This tests if the custom dimension filter works correctly, with various predicates") + void testCustomDimensionFilter() { + FilterInfo customDimFilter + = createFilterInfoWithParams(Filter.CUSTOM_DIM_FIELDNAME_PREFIX + "hi", PredicateType.EQUAL, "hi"); + List filterGroups + = createListWithOneFilterConjunctionGroupAndOneFilter(customDimFilter); + DerivedMetricInfo derivedMetricInfo = createDerivedMetricInfo("random-id", "Request", AggregationType.SUM, + AggregationType.SUM, "Count()", filterGroups); + + Map customDims1 = new HashMap<>(); + customDims1.put("property", "hi"); + + Map customDimsHiBye = new HashMap<>(); + customDimsHiBye.put("hi", "bye"); + + Map customDimsHi = new HashMap<>(); + customDimsHi.put("hi", "hi"); + + Map customDimsHiThere = new HashMap<>(); + customDimsHiThere.put("hi", "hi there"); + + RequestDataColumns request1 + = new RequestDataColumns("https://test.com/hiThere", 200, 200, true, "GET /hiThere", customDims1, null); + RequestDataColumns requestHiBye + = new RequestDataColumns("https://test.com/hiThere", 200, 200, true, "GET /hiThere", customDimsHiBye, null); + RequestDataColumns requestHi + = new RequestDataColumns("https://test.com/hiThere", 200, 200, true, "GET /hiThere", customDimsHi, null); + RequestDataColumns requestHiThere = new RequestDataColumns("https://test.com/hiThere", 200, 200, true, + "GET /hiThere", customDimsHiThere, null); + + FilterConjunctionGroupInfo filterGroup = filterGroups.get(0); + + // the asked for field is not in the custom dimensions so return false + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, request1)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, request1)); + + // the asked for field is in the custom dimensions but value does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, requestHiBye)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, requestHiBye)); + + // the asked for field is in the custom dimensions and value matches + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestHi)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestHi)); + + // testing not equal predicate. The CustomDimensions.hi value != hi so return true. + customDimFilter.setPredicate(PredicateType.NOT_EQUAL); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestHiBye)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestHiBye)); + + // testing does not contain predicate. The CustomDimensions.hi value does not contain hi so return true. + customDimFilter.setPredicate(PredicateType.DOES_NOT_CONTAIN); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestHiBye)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestHiBye)); + + // testing contains predicate. The CustomDimensions.hi value contains hi so return true. + customDimFilter.setPredicate(PredicateType.CONTAINS); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestHiThere)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestHiThere)); + } + + @Test + @Description("This tests if filters on known boolean columns (Success) work correctly, with various predicates and telemetry types") + void testBooleanFilter() { + FilterInfo booleanFilter = createFilterInfoWithParams(KnownRequestColumns.SUCCESS, PredicateType.EQUAL, "true"); + List filterGroups + = createListWithOneFilterConjunctionGroupAndOneFilter(booleanFilter); + DerivedMetricInfo derivedMetricInfo = createDerivedMetricInfo("random-id", "Request", AggregationType.SUM, + AggregationType.SUM, "Count()", filterGroups); + RequestDataColumns requestTrue + = new RequestDataColumns("https://test.com/hiThere", 200, 200, true, "GET /hiThere", null, null); + RequestDataColumns requestFalse + = new RequestDataColumns("https://test.com/hiThere", 200, 0, false, "GET /hiThere", null, null); + DependencyDataColumns dependencyTrue = new DependencyDataColumns("test.com", 200, true, "GET /hiThere", 200, + "HTTP", "https://test.com/hiThere?x=y", null, null); + DependencyDataColumns dependencyFalse = new DependencyDataColumns("test.com", 200, false, "GET /hiThere", 0, + "HTTP", "https://test.com/hiThere?x=y", null, null); + FilterConjunctionGroupInfo filterGroup = filterGroups.get(0); + + // Request Success filter matches + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestTrue)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestTrue)); + + // Request Success filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, requestFalse)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, requestFalse)); + + // Request Success filter matches for != predicate + booleanFilter.setPredicate(PredicateType.NOT_EQUAL); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestFalse)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestFalse)); + + // Dependency Success filter matches + derivedMetricInfo.setTelemetryType("Dependency"); + booleanFilter.setPredicate(PredicateType.EQUAL); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, dependencyTrue)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, dependencyTrue)); + + // Dependency Success filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, dependencyFalse)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, dependencyFalse)); + + // Dependency Success filter matches for != predicate + booleanFilter.setPredicate(PredicateType.NOT_EQUAL); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, dependencyFalse)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, dependencyFalse)); + } + + @Test + @Description("This tests if filtering works on known numeric columns, with various predicates and telemetry types") + void testNumericFilters() { + FilterInfo numericFilter + = createFilterInfoWithParams(KnownRequestColumns.RESPONSE_CODE, PredicateType.EQUAL, "200"); + List filterGroups + = createListWithOneFilterConjunctionGroupAndOneFilter(numericFilter); + DerivedMetricInfo derivedMetricInfo = createDerivedMetricInfo("random-id", "Request", AggregationType.SUM, + AggregationType.SUM, "Count()", filterGroups); + RequestDataColumns requestSuccessful + = new RequestDataColumns("https://test.com/hiThere", 1234567890000L, 200, true, "GET /hiThere", null, null); + RequestDataColumns requestFail = new RequestDataColumns("https://test.com/hiThere", 1234567890000L, 404, false, + "GET /hiThere", null, null); + RequestDataColumns requestShortDuration + = new RequestDataColumns("https://test.com/hiThere", 400, 200, true, "GET /hiThere", null, null); + DependencyDataColumns dependencySuccessful = new DependencyDataColumns("test.com", 1234567890000L, true, + "GET /hiThere", 200, "HTTP", "https://test.com/hiThere?x=y", null, null); + DependencyDataColumns dependencyFail = new DependencyDataColumns("test.com", 1234567890000L, false, + "GET /hiThere", 0, "HTTP", "https://test.com/hiThere?x=y", null, null); + DependencyDataColumns dependencyShortDuration = new DependencyDataColumns("test.com", 400, true, "GET /hiThere", + 200, "HTTP", "https://test.com/hiThere?x=y", null, null); + FilterConjunctionGroupInfo filterGroup = filterGroups.get(0); + + // Request ResponseCode filter matches + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestSuccessful)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestSuccessful)); + + // Request ResponseCode filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, requestFail)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, requestFail)); + + // Dependency ResultCode filter matches + derivedMetricInfo.setTelemetryType("Dependency"); + numericFilter.setFieldName(KnownDependencyColumns.RESULT_CODE); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, dependencySuccessful)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, dependencySuccessful)); + + // Dependency ResultCode filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, dependencyFail)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, dependencyFail)); + + // Dependency Duration filter matches + numericFilter.setFieldName(KnownDependencyColumns.DURATION); + // 14 days, 6 hrs, 56 miutes, 7.89 seconds (1234567890 ms is the matching value for what the user would put in filtering UI) + // the UI sends the following string down to SDK + numericFilter.setComparand("14.6:56:7.89"); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, dependencyFail)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, dependencyFail)); + + // Dependency duration filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, dependencyShortDuration)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, dependencyShortDuration)); + + // Request Duration filter matches + numericFilter.setFieldName(KnownRequestColumns.DURATION); + derivedMetricInfo.setTelemetryType("Request"); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestSuccessful)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestSuccessful)); + + // Dependency duration filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, requestShortDuration)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, requestShortDuration)); + + // != predicate + numericFilter.setPredicate(PredicateType.NOT_EQUAL); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestShortDuration)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestShortDuration)); + + // < predicate + numericFilter.setPredicate(PredicateType.LESS_THAN); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestShortDuration)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestShortDuration)); + + // <= predicate + numericFilter.setPredicate(PredicateType.LESS_THAN_OR_EQUAL); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestShortDuration)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestShortDuration)); + + // > predicate + numericFilter.setPredicate(PredicateType.GREATER_THAN); + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, requestShortDuration)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, requestShortDuration)); + + // >= predicate + numericFilter.setPredicate(PredicateType.GREATER_THAN_OR_EQUAL); + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, requestShortDuration)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, requestShortDuration)); + } + + @Test + @Description("This tests if filtering works on a known string column from each telemetry type, with various predicates") + void testStringFilters() { + FilterInfo stringFilter = createFilterInfoWithParams(KnownRequestColumns.URL, PredicateType.CONTAINS, "hi"); + List filterGroups + = createListWithOneFilterConjunctionGroupAndOneFilter(stringFilter); + DerivedMetricInfo derivedMetricInfo = createDerivedMetricInfo("random-id", "Request", AggregationType.SUM, + AggregationType.SUM, "Count()", filterGroups); + RequestDataColumns requestHi + = new RequestDataColumns("https://test.com/hiThere", 200, 200, true, "GET /hiThere", null, null); + RequestDataColumns requestBye + = new RequestDataColumns("https://test.com/bye", 200, 200, true, "GET /bye", null, null); + DependencyDataColumns dependencyHi = new DependencyDataColumns("test.com", 200, true, "GET /hiThere", 200, + "HTTP", "https://test.com/hiThere?x=y", null, null); + DependencyDataColumns dependencyBye = new DependencyDataColumns("test.com", 200, true, "GET /bye", 200, "HTTP", + "https://test.com/bye", null, null); + TraceDataColumns traceHi = new TraceDataColumns("hi there", null, null); + TraceDataColumns traceBye = new TraceDataColumns("bye", null, null); + ExceptionDataColumns exceptionHi = new ExceptionDataColumns("Exception Message hi", "Stack Trace", null, null); + ExceptionDataColumns exception = new ExceptionDataColumns("Exception Message", "Stack Trace", null, null); + FilterConjunctionGroupInfo filterGroup = filterGroups.get(0); + + // Request Url filter matches + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestHi)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestHi)); + + // Request Url filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, requestBye)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, requestBye)); + + // Dependency Data filter matches + derivedMetricInfo.setTelemetryType("Dependency"); + stringFilter.setFieldName(KnownDependencyColumns.DATA); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, dependencyHi)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, dependencyHi)); + + // Dependency Data filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, dependencyBye)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, dependencyBye)); + + // Trace Message filter matches + derivedMetricInfo.setTelemetryType("Trace"); + stringFilter.setFieldName(KnownTraceColumns.MESSAGE); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, traceHi)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, traceHi)); + + // Trace Message filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, traceBye)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, traceBye)); + + // Exception.Message filter matches. + derivedMetricInfo.setTelemetryType("Exception"); + stringFilter.setFieldName(KnownExceptionColumns.MESSAGE); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, exceptionHi)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, exceptionHi)); + + // Exception Message filter does not match + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, exception)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, exception)); + + // != predicate + stringFilter.setPredicate(PredicateType.NOT_EQUAL); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, exception)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, exception)); + + // not contains + stringFilter.setPredicate(PredicateType.DOES_NOT_CONTAIN); + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, exception)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, exception)); + + // equal + stringFilter.setPredicate(PredicateType.EQUAL); + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, exception)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, exception)); + } + + @Test + @Description("If a FilterConjunctionGroupInfo has an empty list of filters, telemetry should match") + void testEmptyFilterConjunctionGroupInfo() { + // create empty filter list + FilterConjunctionGroupInfo filterGroup = new FilterConjunctionGroupInfo(); + filterGroup.setFilters(new ArrayList()); + List filterGroups = asList(filterGroup); + + DerivedMetricInfo derivedMetricInfo = createDerivedMetricInfo("random-id", "Request", AggregationType.SUM, + AggregationType.SUM, "Count()", filterGroups); + RequestDataColumns request + = new RequestDataColumns("https://test.com/hiThere", 200, 200, true, "GET /hiThere", null, null); + + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, request)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, request)); + } + + @Test + @Description("If there are multiple filters in a FilterConjunctionGroupInfo, then telemetry should only match if all conditions satisfied") + void testMultipleFiltersInGroup() { + FilterInfo filter1 = createFilterInfoWithParams(KnownRequestColumns.URL, PredicateType.CONTAINS, "hi"); + FilterInfo filter2 = createFilterInfoWithParams(KnownRequestColumns.RESPONSE_CODE, PredicateType.EQUAL, "200"); + FilterConjunctionGroupInfo filterGroup = new FilterConjunctionGroupInfo(); + filterGroup.setFilters(asList(filter1, filter2)); + List filterGroups = asList(filterGroup); + + DerivedMetricInfo derivedMetricInfo = createDerivedMetricInfo("random-id", "Request", AggregationType.SUM, + AggregationType.SUM, "Count()", filterGroups); + RequestDataColumns requestHi + = new RequestDataColumns("https://test.com/hiThere", 200, 200, true, "GET /hiThere", null, null); + RequestDataColumns requestBye + = new RequestDataColumns("https://test.com/bye", 200, 200, true, "GET /bye", null, null); + + // matches both filters + assertTrue(Filter.checkFilterConjunctionGroup(filterGroup, requestHi)); + assertTrue(Filter.checkMetricFilters(derivedMetricInfo, requestHi)); + + // only one filter matches, the entire conjunction group should return false + assertFalse(Filter.checkFilterConjunctionGroup(filterGroup, requestBye)); + assertFalse(Filter.checkMetricFilters(derivedMetricInfo, requestBye)); + } + + @Test + @Description("Test the constructors for child classes of TelemetryColumns") + void testConstructors() { + TelemetryItem requestItem + = QuickPulseTestBase.createRequestTelemetry("GET /hiThere", new Date(), 1234567890L, "200", true); + TelemetryItem dependencyItem = QuickPulseTestBase.createRemoteDependencyTelemetry("GET /hiThere", + "https://test.com/hiThere?x=y", 400, true); + + TelemetryExceptionData exceptionItem = new TelemetryExceptionData(); + TelemetryExceptionDetails details = new TelemetryExceptionDetails(); + details.setMessage("A message"); + details.setStack("A stack trace"); + exceptionItem.setExceptions(asList(details)); + + MessageData traceItem = new MessageData(); + traceItem.setMessage("A message"); + + MonitorDomain requestData = requestItem.getData().getBaseData(); + RequestDataColumns requestDataColumns = new RequestDataColumns((RequestData) requestData); + + MonitorDomain dependencyData = dependencyItem.getData().getBaseData(); + ((RemoteDependencyData) dependencyData).setType("HTTP"); + ((RemoteDependencyData) dependencyData).setTarget("test.com"); + ((RemoteDependencyData) dependencyData).setResultCode("200"); + DependencyDataColumns dependencyDataColumns = new DependencyDataColumns((RemoteDependencyData) dependencyData); + + ExceptionDataColumns exceptionData = new ExceptionDataColumns(exceptionItem); + TraceDataColumns traceDataColumns = new TraceDataColumns(traceItem); + + assertTrue(requestDataColumns.getFieldValue(KnownRequestColumns.SUCCESS, Boolean.class)); + assertEquals(requestDataColumns.getFieldValue(KnownRequestColumns.DURATION, Long.class), 1234567890000L); + assertEquals(requestDataColumns.getFieldValue(KnownRequestColumns.RESPONSE_CODE, Integer.class), 200); + assertEquals(requestDataColumns.getFieldValue(KnownRequestColumns.NAME, String.class), "GET /hiThere"); + assertEquals(requestDataColumns.getFieldValue(KnownRequestColumns.URL, String.class), "foo"); + + assertTrue(dependencyDataColumns.getFieldValue(KnownRequestColumns.SUCCESS, Boolean.class)); + assertEquals(dependencyDataColumns.getFieldValue(KnownDependencyColumns.DATA, String.class), + "https://test.com/hiThere?x=y"); + assertEquals(dependencyDataColumns.getFieldValue(KnownRequestColumns.NAME, String.class), "GET /hiThere"); + assertEquals(dependencyDataColumns.getFieldValue(KnownDependencyColumns.TYPE, String.class), "HTTP"); + assertEquals(dependencyDataColumns.getFieldValue(KnownRequestColumns.DURATION, Long.class), 400000L); + assertEquals(dependencyDataColumns.getFieldValue(KnownDependencyColumns.RESULT_CODE, Integer.class), 200); + assertEquals(dependencyDataColumns.getFieldValue(KnownDependencyColumns.TARGET, String.class), "test.com"); + + assertEquals(exceptionData.getFieldValue(KnownExceptionColumns.MESSAGE, String.class), "A message"); + assertEquals(exceptionData.getFieldValue(KnownExceptionColumns.STACK, String.class), "A stack trace"); + + assertEquals(traceDataColumns.getFieldValue(KnownTraceColumns.MESSAGE, String.class), "A message"); + } + +}