diff --git a/docs/samples/Microsoft.ML.Samples/Dynamic/Transforms/TimeSeries/LocalizeRootCauseMultidimension.cs b/docs/samples/Microsoft.ML.Samples/Dynamic/Transforms/TimeSeries/LocalizeRootCauseMultidimension.cs new file mode 100644 index 0000000000..16ad1fb2e6 --- /dev/null +++ b/docs/samples/Microsoft.ML.Samples/Dynamic/Transforms/TimeSeries/LocalizeRootCauseMultidimension.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.ML; +using Microsoft.ML.TimeSeries; + +namespace Samples.Dynamic +{ + public static class LocalizeRootCauseMultipleDimensions + { + private static string AGG_SYMBOL = "##SUM##"; + + public static void Example() + { + // Create a new ML context, for ML.NET operations. It can be used for + // exception tracking and logging, as well as the source of randomness. + var mlContext = new MLContext(); + + // Create an root cause localization input instance. + DateTime timestamp = GetTimestamp(); + var data = new RootCauseLocalizationInput(timestamp, GetAnomalyDimension(), new List() { new MetricSlice(timestamp, GetTimeSeriesPoints()) }, AggregateType.Sum, AGG_SYMBOL); + + // Get the root cause localization result. + List prediction = mlContext.AnomalyDetection.LocalizeRootCauses(data); + + // Print the localization results. + int count = 0; + foreach (RootCause cause in prediction) + { + count++; + foreach (RootCauseItem item in cause.Items) + { + Console.WriteLine($"Prepared cause #{count} ..."); + Console.WriteLine($"Score: {item.Score}, Path: {String.Join(" ", item.Path)}, Direction: {item.Direction}, Dimension:{String.Join(" ", item.Dimension)}"); + } + } + + //Prepared cause #1 ... + //Score: 0.26670448876705927, Path: DataCenter, Direction: Up, Dimension:[Country, UK] [DeviceType, ##SUM##] [DataCenter, DC1] + //Prepared cause #2 ... + //Score: 0.254746585094852, Path: DeviceType, Direction: Up, Dimension:[Country, UK] [DeviceType, Laptop] [DataCenter, ##SUM##] + } + + private static List GetTimeSeriesPoints() + { + List TimeSeriesPoints = new List(); + + Dictionary dic1 = new Dictionary + { + { "Country", "UK" }, + { "DeviceType", "Laptop" }, + { "DataCenter", "DC1" } + }; + TimeSeriesPoints.Add(new TimeSeriesPoint(200, 100, true, dic1)); + + Dictionary dic2 = new Dictionary(); + dic2.Add("Country", "UK"); + dic2.Add("DeviceType", "Mobile"); + dic2.Add("DataCenter", "DC1"); + TimeSeriesPoints.Add(new TimeSeriesPoint(1000, 100, true, dic2)); + + Dictionary dic3 = new Dictionary(); + dic3.Add("Country", "UK"); + dic3.Add("DeviceType", AGG_SYMBOL); + dic3.Add("DataCenter", "DC1"); + TimeSeriesPoints.Add(new TimeSeriesPoint(1200, 200, true, dic3)); + + Dictionary dic4 = new Dictionary(); + dic4.Add("Country", "UK"); + dic4.Add("DeviceType", "Laptop"); + dic4.Add("DataCenter", "DC2"); + TimeSeriesPoints.Add(new TimeSeriesPoint(100, 100, false, dic4)); + + Dictionary dic5 = new Dictionary(); + dic5.Add("Country", "UK"); + dic5.Add("DeviceType", "Mobile"); + dic5.Add("DataCenter", "DC2"); + TimeSeriesPoints.Add(new TimeSeriesPoint(200, 200, false, dic5)); + + Dictionary dic6 = new Dictionary(); + dic6.Add("Country", "UK"); + dic6.Add("DeviceType", AGG_SYMBOL); + dic6.Add("DataCenter", "DC2"); + TimeSeriesPoints.Add(new TimeSeriesPoint(300, 300, false, dic6)); + + Dictionary dic7 = new Dictionary(); + dic7.Add("Country", "UK"); + dic7.Add("DeviceType", AGG_SYMBOL); + dic7.Add("DataCenter", AGG_SYMBOL); + TimeSeriesPoints.Add(new TimeSeriesPoint(1800, 750, true, dic7)); + + Dictionary dic8 = new Dictionary(); + dic8.Add("Country", "UK"); + dic8.Add("DeviceType", "Laptop"); + dic8.Add("DataCenter", AGG_SYMBOL); + TimeSeriesPoints.Add(new TimeSeriesPoint(1500, 450, true, dic8)); + + Dictionary dic9 = new Dictionary(); + dic9.Add("Country", "UK"); + dic9.Add("DeviceType", "Mobile"); + dic9.Add("DataCenter", AGG_SYMBOL); + TimeSeriesPoints.Add(new TimeSeriesPoint(600, 550, false, dic9)); + + Dictionary dic10 = new Dictionary(); + dic10.Add("Country", "UK"); + dic10.Add("DeviceType", "Mobile"); + dic10.Add("DataCenter", "DC3"); + TimeSeriesPoints.Add(new TimeSeriesPoint(100, 100, false, dic10)); + + Dictionary dic11 = new Dictionary(); + dic11.Add("Country", "UK"); + dic11.Add("DeviceType", "Laptop"); + dic11.Add("DataCenter", "DC3"); + TimeSeriesPoints.Add(new TimeSeriesPoint(200, 250, false, dic11)); + + Dictionary dic12 = new Dictionary(); + dic12.Add("Country", "UK"); + dic12.Add("DeviceType", AGG_SYMBOL); + dic12.Add("DataCenter", "DC3"); + TimeSeriesPoints.Add(new TimeSeriesPoint(300, 350, false, dic12)); + + return TimeSeriesPoints; + } + + private static Dictionary GetAnomalyDimension() + { + Dictionary dim = new Dictionary(); + dim.Add("Country", "UK"); + dim.Add("DeviceType", AGG_SYMBOL); + dim.Add("DataCenter", AGG_SYMBOL); + + return dim; + } + + private static DateTime GetTimestamp() + { + return new DateTime(2020, 3, 23, 0, 0, 0); + } + } +} \ No newline at end of file diff --git a/docs/samples/Microsoft.ML.Samples/Program.cs b/docs/samples/Microsoft.ML.Samples/Program.cs index 4c46399421..b30e20d7cf 100644 --- a/docs/samples/Microsoft.ML.Samples/Program.cs +++ b/docs/samples/Microsoft.ML.Samples/Program.cs @@ -6,20 +6,23 @@ namespace Microsoft.ML.Samples { public static class Program { - public static void Main(string[] args) => RunAll(); + public static void Main(string[] args) => RunAll(args == null || args.Length == 0 ? null : args[0]); - internal static void RunAll() + internal static void RunAll(string name = null) { int samples = 0; foreach (var type in Assembly.GetExecutingAssembly().GetTypes()) { - var sample = type.GetMethod("Example", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); - - if (sample != null) + if (name == null || name.Equals(type.Name)) { - Console.WriteLine(type.Name); - sample.Invoke(null, null); - samples++; + var sample = type.GetMethod("Example", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); + + if (sample != null) + { + Console.WriteLine(type.Name); + sample.Invoke(null, null); + samples++; + } } } diff --git a/src/Microsoft.ML.TimeSeries/ExtensionsCatalog.cs b/src/Microsoft.ML.TimeSeries/ExtensionsCatalog.cs index f709352893..350a244f58 100644 --- a/src/Microsoft.ML.TimeSeries/ExtensionsCatalog.cs +++ b/src/Microsoft.ML.TimeSeries/ExtensionsCatalog.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using Microsoft.ML.Data; using Microsoft.ML.Runtime; using Microsoft.ML.TimeSeries; @@ -197,6 +198,35 @@ public static IDataView DetectEntireAnomalyBySrCnn(this AnomalyDetectionCatalog /// /// public static RootCause LocalizeRootCause(this AnomalyDetectionCatalog catalog, RootCauseLocalizationInput src, double beta = 0.3, double rootCauseThreshold = 0.95) + { + List causes = LocalizeRootCauses(catalog, src, beta, rootCauseThreshold); + if (causes?.Count > 0) + { + return causes[0]; + } + else + { + return null; + } + + } + + /// + /// Outputs an ordered list of s. The order corresponds to which prepared cause is most likely to be the root cause. + /// + /// The anomaly detection catalog. + /// Root cause's input. The data is an instance of . + /// Beta is a weight parameter for user to choose. It is used when score is calculated for each root cause item. The range of beta should be in [0,1]. For a larger beta, root cause point which has a large difference between value and expected value will get a high score. On the contrary, for a small beta, root cause items which has a high relative change will get a high score. + /// A threshold to determine whether the point should be root cause. The range of this threshold should be in [0,1]. + /// If the point's delta is equal to or larger than rootCauseThreshold multiplied by anomaly dimension point's delta, this point is treated as a root cause. Different threshold will turn out different results. Users can choose the delta according to their data and requirments. + /// + /// + /// + /// + /// + public static List LocalizeRootCauses(this AnomalyDetectionCatalog catalog, RootCauseLocalizationInput src, double beta = 0.5, double rootCauseThreshold = 0.95) { IHostEnvironment host = CatalogUtils.GetEnvironment(catalog); @@ -205,12 +235,11 @@ public static RootCause LocalizeRootCause(this AnomalyDetectionCatalog catalog, //check parameters host.CheckUserArg(beta >= 0 && beta <= 1, nameof(beta), "Must be in [0,1]"); - host.CheckUserArg(rootCauseThreshold >= 0 && rootCauseThreshold <= 1, nameof(beta), "Must be in [0,1]"); + host.CheckUserArg(rootCauseThreshold >= 0 && rootCauseThreshold <= 1, nameof(rootCauseThreshold), "Must be in [0,1]"); - //find out the root cause + //find out the possible causes RootCauseAnalyzer analyzer = new RootCauseAnalyzer(src, beta, rootCauseThreshold); - RootCause dst = analyzer.Analyze(); - return dst; + return analyzer.AnalyzePossibleCauses(); } /// diff --git a/src/Microsoft.ML.TimeSeries/RootCauseAnalyzer.cs b/src/Microsoft.ML.TimeSeries/RootCauseAnalyzer.cs index bc18c0b248..748dbeb607 100644 --- a/src/Microsoft.ML.TimeSeries/RootCauseAnalyzer.cs +++ b/src/Microsoft.ML.TimeSeries/RootCauseAnalyzer.cs @@ -10,7 +10,7 @@ namespace Microsoft.ML.TimeSeries { - public class RootCauseAnalyzer + internal class RootCauseAnalyzer { private static double _anomalyRatioThreshold = 0.5; private static double _anomalyPreDeltaThreshold = 2; @@ -18,27 +18,32 @@ public class RootCauseAnalyzer private RootCauseLocalizationInput _src; private double _beta; private double _rootCauseThreshold; + private List _preparedCauses; public RootCauseAnalyzer(RootCauseLocalizationInput src, double beta, double rootCauseThreshold) { _src = src; _beta = beta; _rootCauseThreshold = rootCauseThreshold; + _preparedCauses = new List(); } public RootCause Analyze() + { + return AnalyzeOneLayer(_src).FirstOrDefault(); + } + + public List AnalyzePossibleCauses() { return AnalyzeOneLayer(_src); } /// - /// This is a function for analyze one layer for root cause, we select one dimension with values who contributes the most to the anomaly. + /// This is a function for analyzing one layer for root cause. We rank dimensions according to their likelihood of containing the root case. + /// For each dimension, we select one dimension with values who contributes the most to the anomaly. /// - private RootCause AnalyzeOneLayer(RootCauseLocalizationInput src) + private List AnalyzeOneLayer(RootCauseLocalizationInput src) { - RootCause dst = new RootCause(); - dst.Items = new List(); - DimensionInfo dimensionInfo = SeparateDimension(src.AnomalyDimension, src.AggregateSymbol); Tuple, TimeSeriesPoint>> pointInfo = GetPointsInfo(src, dimensionInfo); PointTree pointTree = pointInfo.Item1; @@ -48,13 +53,17 @@ private RootCause AnalyzeOneLayer(RootCauseLocalizationInput src) //which means there is no anomaly point with the anomaly dimension or no point under anomaly dimension if (anomalyTree.ParentNode == null || dimPointMapping.Count == 0) { - return dst; + _preparedCauses.Add(new RootCause() { Items = new List() }); + return _preparedCauses; } - dst.Items.AddRange(LocalizeRootCauseByDimension(anomalyTree, pointTree, src.AnomalyDimension, dimensionInfo.AggDims)); - GetRootCauseDirectionAndScore(dimPointMapping, src.AnomalyDimension, dst, _beta, pointTree, src.AggregateType, src.AggregateSymbol); + LocalizeRootCausesByDimension(anomalyTree, pointTree, src.AnomalyDimension, dimensionInfo.AggDims); + foreach (var dst in _preparedCauses) + { + GetRootCauseDirectionAndScore(dimPointMapping, src.AnomalyDimension, dst, _beta, pointTree, src.AggregateType, src.AggregateSymbol); + } - return dst; + return _preparedCauses; } protected List GetTotalPointsForAnomalyTimestamp(RootCauseLocalizationInput src) @@ -121,51 +130,59 @@ protected Dictionary GetSubDim(Dictionary dimens return new Dictionary(keyList.Select(dim => new KeyValuePair(dim, dimension[dim])).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); } - private List LocalizeRootCauseByDimension(PointTree anomalyTree, PointTree pointTree, Dictionary anomalyDimension, List aggDims) + private void LocalizeRootCausesByDimension(PointTree anomalyTree, PointTree pointTree, Dictionary anomalyDimension, List aggDims) { - BestDimension best = null; + IEnumerable best; if (anomalyTree.ChildrenNodes.Count == 0) { - //has no children node information, should use the leaves node(whose point has no aggrgated dimensions) information - best = SelectBestDimension(pointTree.Leaves, anomalyTree.Leaves, aggDims); + //has no children node information, should use the leaves node (whose point has no aggregated dimensions) information + best = SelectOrderedDimension(pointTree.Leaves, anomalyTree.Leaves, aggDims); } else { //has no leaves information, should calculate the entropy information according to the children nodes - best = SelectBestDimension(pointTree.ChildrenNodes, anomalyTree.ChildrenNodes, aggDims); + best = SelectOrderedDimensions(pointTree.ChildrenNodes, anomalyTree.ChildrenNodes, aggDims); } if (best == null) { - return new List() { new RootCauseItem(anomalyDimension) }; + _preparedCauses.Append(new RootCause { Items = new List() { new RootCauseItem(anomalyDimension) } }); } - List children = null; - if (anomalyTree.ChildrenNodes.ContainsKey(best.DimensionKey)) + bool rootAsAnomaly = false; + foreach (var dimension in best) { - //Use children node information to get top anomalies - children = GetTopAnomaly(anomalyTree.ChildrenNodes[best.DimensionKey], anomalyTree.ParentNode, pointTree.ChildrenNodes[best.DimensionKey].Count > 0 ? pointTree.ChildrenNodes[best.DimensionKey] : pointTree.Leaves, best.DimensionKey, !(pointTree.ChildrenNodes[best.DimensionKey].Count > 0)); - } - else - { - //Use leaves node informatin to get top anomalies - children = GetTopAnomaly(anomalyTree.Leaves, anomalyTree.ParentNode, pointTree.Leaves, best.DimensionKey, true); - } + RootCause rootCause = new RootCause { Items = new List() }; - if (children == null) - { - //As the cause couldn't be found, the root cause should be itself - return new List() { new RootCauseItem(anomalyDimension) }; - } - else - { - List causes = new List(); - // For the found causes, we return the result - foreach (TimeSeriesPoint anomaly in children) + rootCause.GainRatio = dimension.GainRatio; + List children = null; + if (anomalyTree.ChildrenNodes.ContainsKey(dimension.DimensionKey)) + { + //Use children node information to get top anomalies + children = GetTopAnomaly(anomalyTree.ChildrenNodes[dimension.DimensionKey], anomalyTree.ParentNode, pointTree.ChildrenNodes[dimension.DimensionKey].Count > 0 ? pointTree.ChildrenNodes[dimension.DimensionKey] : pointTree.Leaves, dimension.DimensionKey, !(pointTree.ChildrenNodes[dimension.DimensionKey].Count > 0)); + } + else + { + //Use leaves node informatin to get top anomalies + children = GetTopAnomaly(anomalyTree.Leaves, anomalyTree.ParentNode, pointTree.Leaves, dimension.DimensionKey, true); + } + + if (children == null) + { + //As the cause couldn't be found, the root cause should be itself + if (!rootAsAnomaly) + { + rootAsAnomaly = true; + rootCause.Items.Add(new RootCauseItem(anomalyDimension)); + } + } + else { - causes.Add(new RootCauseItem(UpdateDimensionValue(anomalyDimension, best.DimensionKey, anomaly.Dimension[best.DimensionKey]), new List() { best.DimensionKey })); + rootCause.Items.AddRange(children.Select(anomaly => + new RootCauseItem(UpdateDimensionValue(anomalyDimension, dimension.DimensionKey, anomaly.Dimension[dimension.DimensionKey]), new List() { dimension.DimensionKey }))); } - return causes; + + _preparedCauses.Add(rootCause); } } @@ -225,12 +242,12 @@ protected List GetTopAnomaly(List anomalyPoint } /// - /// Use leaves point information to select best dimension + /// Use leaves point information to select ordered dimensions /// - protected BestDimension SelectBestDimension(List totalPoints, List anomalyPoints, List aggDim) + protected IEnumerable SelectOrderedDimension(List totalPoints, List anomalyPoints, List aggDim) { double totalEntropy = GetEntropy(totalPoints.Count, anomalyPoints.Count); - SortedDictionary entroyGainMap = new SortedDictionary(); + SortedDictionary entropyGainMap = new SortedDictionary(); Dictionary entroyGainRatioMap = new Dictionary(); double sumGain = 0; @@ -248,7 +265,8 @@ protected BestDimension SelectBestDimension(List totalPoints, L { gain = 0; } - entroyGainMap.Add(dimension, gain); + entropyGainMap.Add(dimension, gain); + dimension.Gain = gain; double gainRatio = gain / GetDimensionIntrinsicValue(dimension.PointDis); if (Double.IsInfinity(gainRatio)) @@ -256,20 +274,20 @@ protected BestDimension SelectBestDimension(List totalPoints, L gainRatio = 0; } entroyGainRatioMap.Add(dimension, gainRatio); + dimension.GainRatio = gainRatio; sumGain += gain; } double meanGain = sumGain / aggDim.Count(); - BestDimension best = FindBestDimension(entroyGainMap, entroyGainRatioMap, meanGain); - return best; + return OrderDimensions(entropyGainMap, entroyGainRatioMap, meanGain); } /// - /// Use children point information to select best dimension + /// Use children point information to select ordered dimensions /// - private BestDimension SelectBestDimension(Dictionary> pointChildren, Dictionary> anomalyChildren, List aggDim) + private IEnumerable SelectOrderedDimensions(Dictionary> pointChildren, Dictionary> anomalyChildren, List aggDim) { SortedDictionary entropyMap = new SortedDictionary(); Dictionary entropyRatioMap = new Dictionary(); @@ -303,14 +321,14 @@ private BestDimension SelectBestDimension(Dictionary aggDims, TimeSeriesPoint poi } } - private BestDimension FindBestDimension(SortedDictionary valueMap, Dictionary valueRatioMap, double meanGain, bool isLeavesLevel = true) + private IEnumerable OrderDimensions(SortedDictionary valueMap, Dictionary valueRatioMap, double meanGain, bool isLeavesLevel = true) { - BestDimension best = null; - foreach (KeyValuePair dimension in valueMap) - { - if (dimension.Key.AnomalyDis.Count == 1 || (isLeavesLevel ? dimension.Value >= meanGain : dimension.Value <= meanGain)) + List> valueMapAsList = valueMap.ToList(); + List ordered = new List(); + + BestDimension best; + do { + best = null; + + foreach (KeyValuePair dimension in valueMapAsList) { - if (best == null) - { - best = dimension.Key; - } - else + if (dimension.Key.AnomalyDis.Count == 1 || (isLeavesLevel ? dimension.Value >= meanGain : dimension.Value <= meanGain)) { - bool isRatioNan = Double.IsNaN(valueRatioMap[best]); - if (dimension.Key.AnomalyDis.Count > 1) + if (best == null) { - if (best.AnomalyDis.Count != 1 && !isRatioNan && (isLeavesLevel ? valueRatioMap[best].CompareTo(dimension.Value) <= 0 : valueRatioMap[best].CompareTo(dimension.Value) >= 0)) - { - best = GetBestDimension(best, dimension, valueRatioMap); - } + best = dimension.Key; } else if (dimension.Key.AnomalyDis.Count == 1) { - - if (best.AnomalyDis.Count > 1) + bool isRatioNan = Double.IsNaN(valueRatioMap[best]); + if (dimension.Key.AnomalyDis.Count > 1) { - best = dimension.Key; + if (best.AnomalyDis.Count != 1 && !isRatioNan && (isLeavesLevel ? valueRatioMap[best].CompareTo(dimension.Value) <= 0 : valueRatioMap[best].CompareTo(dimension.Value) >= 0)) + { + best = GetBestDimension(best, dimension, valueRatioMap); + } } - else if (best.AnomalyDis.Count == 1) + else if (dimension.Key.AnomalyDis.Count == 1) { - if (!isRatioNan && (isLeavesLevel ? valueRatioMap[best].CompareTo(dimension.Value) <= 0 : valueRatioMap[best].CompareTo(dimension.Value) >= 0)) + + if (best.AnomalyDis.Count > 1) { best = GetBestDimension(best, dimension, valueRatioMap); } + else if (best.AnomalyDis.Count == 1) + { + if (!isRatioNan && (isLeavesLevel ? valueRatioMap[best].CompareTo(dimension.Value) <= 0 : valueRatioMap[best].CompareTo(dimension.Value) >= 0)) + { + best = GetBestDimension(best, dimension, valueRatioMap); + } + } } } } } - } - return best; + if (best != null) + { + valueMapAsList.RemoveAll(kv => kv.Key == best); + ordered.Add(best); + } + } while (best != null); + + return ordered; } private BestDimension GetBestDimension(BestDimension best, KeyValuePair dimension, Dictionary valueRatioMap) @@ -731,11 +762,13 @@ public PointTree() } } - public class BestDimension : IComparable + internal class BestDimension : IComparable { internal string DimensionKey; internal Dictionary AnomalyDis; internal Dictionary PointDis; + internal double Gain; + internal double GainRatio; public BestDimension() { diff --git a/src/Microsoft.ML.TimeSeries/RootCauseLocalizationType.cs b/src/Microsoft.ML.TimeSeries/RootCauseLocalizationType.cs index f3ce5ea8c1..d0b247c146 100644 --- a/src/Microsoft.ML.TimeSeries/RootCauseLocalizationType.cs +++ b/src/Microsoft.ML.TimeSeries/RootCauseLocalizationType.cs @@ -14,6 +14,17 @@ public sealed class RootCause /// A List for root cause item. Instance of the item should be . /// public List Items { get; set; } + + /// + /// The gain for the potential root cause + /// + public double Gain { get; set; } + + /// + /// The gain ratio for the potential root cause + /// + public double GainRatio { get; set; } + public RootCause() { Items = new List(); diff --git a/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs index 471b135ac3..613f71299a 100644 --- a/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs +++ b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs @@ -689,6 +689,51 @@ public void RootCauseLocalization() } } + [Fact] + public void MultiDimensionalRootCauseLocalization() + { + // Create an root cause localizatiom input + var rootCauseLocalizationInput = new RootCauseLocalizationInput(GetRootCauseTimestamp(), GetRootCauseAnomalyDimension(), new List() { new MetricSlice(GetRootCauseTimestamp(), GetRootCauseLocalizationPoints()) }, AggregateType.Sum, _rootCauseAggSymbol); + + var ml = new MLContext(1); + List preparedCauses = ml.AnomalyDetection.LocalizeRootCauses(rootCauseLocalizationInput); + + Assert.NotNull(preparedCauses); + Assert.Equal(2, preparedCauses.Count); + + Assert.Equal(1, (int)preparedCauses[0].Items.Count); + Assert.Equal(3, (int)preparedCauses[0].Items[0].Dimension.Count); + Assert.Equal(AnomalyDirection.Up, preparedCauses[0].Items[0].Direction); + Assert.Equal(1, (int)preparedCauses[0].Items[0].Path.Count); + Assert.Equal("DataCenter", preparedCauses[0].Items[0].Path[0]); + + Dictionary expectedDim = new Dictionary(); + expectedDim.Add("Country", "UK"); + expectedDim.Add("DeviceType", _rootCauseAggSymbol); + expectedDim.Add("DataCenter", "DC1"); + + foreach (KeyValuePair pair in preparedCauses[0].Items[0].Dimension) + { + Assert.Equal(expectedDim[pair.Key], pair.Value); + } + + Assert.Equal(1, (int)preparedCauses[1].Items.Count); + Assert.Equal(3, (int)preparedCauses[1].Items[0].Dimension.Count); + Assert.Equal(AnomalyDirection.Up, preparedCauses[1].Items[0].Direction); + Assert.Equal(1, (int)preparedCauses[1].Items[0].Path.Count); + Assert.Equal("DeviceType", preparedCauses[1].Items[0].Path[0]); + + expectedDim = new Dictionary(); + expectedDim.Add("Country", "UK"); + expectedDim.Add("DeviceType", "Laptop"); + expectedDim.Add("DataCenter", _rootCauseAggSymbol); + + foreach (KeyValuePair pair in preparedCauses[1].Items[0].Dimension) + { + Assert.Equal(expectedDim[pair.Key], pair.Value); + } + } + [Theory] [InlineData(-1, 6)] [InlineData(60, 6)] @@ -756,19 +801,37 @@ private static List GetRootCauseLocalizationPoints() dic7.Add("Country", "UK"); dic7.Add("DeviceType", _rootCauseAggSymbol); dic7.Add("DataCenter", _rootCauseAggSymbol); - points.Add(new TimeSeriesPoint(1500, 500, true, dic7)); + points.Add(new TimeSeriesPoint(1800, 750, true, dic7)); Dictionary dic8 = new Dictionary(); dic8.Add("Country", "UK"); dic8.Add("DeviceType", "Laptop"); dic8.Add("DataCenter", _rootCauseAggSymbol); - points.Add(new TimeSeriesPoint(300, 200, true, dic8)); + points.Add(new TimeSeriesPoint(1500, 450, true, dic8)); Dictionary dic9 = new Dictionary(); dic9.Add("Country", "UK"); dic9.Add("DeviceType", "Mobile"); dic9.Add("DataCenter", _rootCauseAggSymbol); - points.Add(new TimeSeriesPoint(1200, 300, true, dic9)); + points.Add(new TimeSeriesPoint(600, 550, false, dic9)); + + Dictionary dic10 = new Dictionary(); + dic10.Add("Country", "UK"); + dic10.Add("DeviceType", "Mobile"); + dic10.Add("DataCenter", "DC3"); + points.Add(new TimeSeriesPoint(100, 100, false, dic10)); + + Dictionary dic11 = new Dictionary(); + dic11.Add("Country", "UK"); + dic11.Add("DeviceType", "Laptop"); + dic11.Add("DataCenter", "DC3"); + points.Add(new TimeSeriesPoint(200, 250, false, dic11)); + + Dictionary dic12 = new Dictionary(); + dic12.Add("Country", "UK"); + dic12.Add("DeviceType", _rootCauseAggSymbol); + dic12.Add("DataCenter", "DC3"); + points.Add(new TimeSeriesPoint(300, 350, false, dic12)); return points; }