//******************************************************************************************************
// AnnotationExtensions.cs - Gbtc
//
// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 09/22/2016 - J. Ritchie Carroll
// Generated original version of source code.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using GSF;
using GSF.Diagnostics;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
// ReSharper disable CompareOfFloatsByEqualityOperator
namespace GrafanaAdapters
{
///
/// Grafana extensions class.
///
public static class AnnotationExtensions
{
private static readonly LogPublisher s_log = Logger.CreatePublisher(typeof(AnnotationExtensions), MessageClass.Component);
private static readonly Regex s_aliasedTagExpression = new(@"^\s*(?[A-Z_][A-Z0-9_]*)\s*\=\s*(?.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
///
/// Gets table name for specified annotation .
///
/// Annotation type.
/// Table name for specified annotation .
public static string TableName(this AnnotationType type)
{
switch (type)
{
case AnnotationType.RaisedAlarms:
return "RaisedAlarms";
case AnnotationType.ClearedAlarms:
return "ClearedAlarms";
default:
return "Undefined";
}
}
private static string Translate(this string tableName)
{
// Source metadata for raised and cleared alarms is the same
if (tableName.Equals("RaisedAlarms", StringComparison.OrdinalIgnoreCase))
return "Alarms";
if (tableName.Equals("ClearedAlarms", StringComparison.OrdinalIgnoreCase))
return "Alarms";
return tableName;
}
///
/// Gets the target field name for Guid based point IDs for table used with specified annotation .
///
/// Annotation type.
/// Target field name for Guid based point IDs for specified annotation .
public static string TargetFieldName(this AnnotationType type)
{
switch (type)
{
case AnnotationType.RaisedAlarms:
case AnnotationType.ClearedAlarms:
return "AssociatedMeasurementID";
default:
throw new InvalidOperationException("Cannot extract target for specified annotation type.");
}
}
///
/// Determines if the data point is applicable for specified annotation .
///
/// Annotation type.
/// Time series values data point.
/// true if the data point is applicable for specified annotation ; otherwise, false.
public static bool IsApplicable(this AnnotationType type, double[] datapoint)
{
if (datapoint is null)
throw new ArgumentNullException(nameof(datapoint));
double value = datapoint[TimeSeriesValues.Value];
switch (type)
{
case AnnotationType.RaisedAlarms:
return value != 0.0D;
case AnnotationType.ClearedAlarms:
return value == 0.0D;
default:
throw new InvalidOperationException("Cannot determine data point applicability for specified annotation type.");
}
}
///
/// Populates an annotation response title, text and tags for specified annotation .
///
/// Annotation type.
/// Annotation response.
/// Target of annotation response.
/// Associated metadata definition for response.
/// Time series values data point for response.
/// Metadata of source definitions.
/// Populates an annotation response title, text and tags for specified annotation .
public static void PopulateResponse(this AnnotationType type, AnnotationResponse response, string target, DataRow definition, double[] datapoint, DataSet source)
{
if (response is null)
throw new ArgumentNullException(nameof(response));
if (target is null)
throw new ArgumentNullException(nameof(target));
if (definition is null)
throw new ArgumentNullException(nameof(definition));
if (datapoint is null)
throw new ArgumentNullException(nameof(datapoint));
switch (type)
{
case AnnotationType.RaisedAlarms:
case AnnotationType.ClearedAlarms:
DataRow metadata = GetTargetMetaData(source, definition["SignalID"]);
response.title = $"Alarm {(type == AnnotationType.RaisedAlarms ? "Raised" : "Cleared")}";
response.text = $"{definition["Description"]}
Condition: {GetAlarmCondition(definition)}
Severity: {definition["Severity"]}
[{metadata["ID"]}]: {metadata["SignalReference"]}";
response.tags = $"{metadata["PointTag"]},{target}";
break;
default:
throw new InvalidOperationException("Cannot populate response information for specified annotation type.");
}
}
///
/// Extracts a Grafana from an .
///
/// Annotation request.
/// List of desired targets.
/// Maximum points to return.
/// Grafana query request object from an annotation .
public static QueryRequest ExtractQueryRequest(this AnnotationRequest request, IEnumerable targets, int maxDataPoints)
{
if (targets is null)
throw new ArgumentNullException(nameof(targets));
// Create annotation query request for full resolution data using "Interval(0, {target})"
// function so that any encountered alarms not will not be down-sampled
return new()
{
range = request.range,
rangeRaw = request.rangeRaw,
interval = "*",
targets = targets.Select((target, index) => new Target { refId = $"ID{index}", target = $"Interval(0, {target})" }).ToList(),
format = "json",
maxDataPoints = maxDataPoints
};
}
///
/// Parses query expression from annotation for annotation type.
///
/// Grafana annotation.
/// Determines if query is using a filter expression.
/// Parsed annotation type for query expression from .
public static AnnotationType ParseQueryType(this Annotation annotation, out bool useFilterExpression)
{
if (annotation is null)
throw new ArgumentNullException(nameof(annotation));
string query = annotation.query ?? "";
Tuple result = TargetCache>.GetOrAdd(query, () =>
{
AnnotationType type = AnnotationType.Undefined;
bool parsedFilterExpression = false;
if (AdapterBase.ParseFilterExpression(query, out string tableName, out string _, out string _, out int _))
{
parsedFilterExpression = true;
switch (tableName.ToUpperInvariant())
{
case "RAISEDALARMS":
type = AnnotationType.RaisedAlarms;
break;
case "CLEAREDALARMS":
type = AnnotationType.ClearedAlarms;
break;
default:
throw new InvalidOperationException("Invalid FILTER table for annotation query expression.");
}
}
else if (query.StartsWith("#RaisedAlarms", StringComparison.OrdinalIgnoreCase))
{
type = AnnotationType.RaisedAlarms;
}
else if (query.StartsWith("#ClearedAlarms", StringComparison.OrdinalIgnoreCase))
{
type = AnnotationType.ClearedAlarms;
}
if (type == AnnotationType.Undefined)
throw new InvalidOperationException("Unrecognized type or syntax for annotation query expression.");
return new(type, parsedFilterExpression);
});
useFilterExpression = result.Item2;
return result.Item1;
}
///
/// Parses query expression from annotation request for annotation type.
///
/// Grafana annotation request.
/// Determines if query is using a filter expression.
/// Parsed annotation type for query expression from annotation .
public static AnnotationType ParseQueryType(this AnnotationRequest request, out bool useFilterExpression)
{
if (request is null)
throw new ArgumentNullException(nameof(request));
return request.annotation.ParseQueryType(out useFilterExpression);
}
///
/// Parses source definitions for an annotation query.
///
/// Grafana annotation.
/// Annotation type.
/// Metadata of source definitions.
/// Determines if query is using a filter expression.
/// Parsed source definitions from .
public static Dictionary ParseSourceDefinitions(this Annotation annotation, AnnotationType type, DataSet source, bool useFilterExpression)
{
if (annotation is null)
throw new ArgumentNullException(nameof(annotation));
if (source is null)
throw new ArgumentNullException(nameof(source));
if (type == AnnotationType.Undefined)
throw new InvalidOperationException("Unrecognized type or syntax for annotation query expression.");
string query = annotation.query ?? "";
return TargetCache>.GetOrAdd(query, () =>
{
DataRow[] rows;
if (useFilterExpression)
{
if (AdapterBase.ParseFilterExpression(query, out string tableName, out string expression, out string sortField, out int takeCount))
rows = source.Tables[tableName.Translate()].Select(expression, sortField).Take(takeCount).ToArray();
else
throw new InvalidOperationException("Invalid FILTER syntax for annotation query expression.");
}
else
{
// Assume all records if no filter expression was provided
rows = source.Tables[type.TableName().Translate()].Rows.Cast().ToArray();
}
Dictionary definitions = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in rows)
{
MeasurementKey key = GetTargetFromGuid(row[type.TargetFieldName()].ToString());
if (key != MeasurementKey.Undefined)
definitions[key.TagFromKey(source)] = row;
}
return definitions;
});
}
///
/// Parses source definitions for an annotation query.
///
/// Grafana annotation request.
/// Annotation type.
/// Metadata of source definitions.
/// Determines if query is using a filter expression.
/// Parsed source definitions from annotation .
public static Dictionary ParseSourceDefinitions(this AnnotationRequest request, AnnotationType type, DataSet source, bool useFilterExpression)
{
if (request is null)
throw new ArgumentNullException(nameof(request));
return request.annotation.ParseSourceDefinitions(type, source, useFilterExpression);
}
///
/// Looks up point tag from measurement value.
///
/// to lookup.
/// Source metadata.
/// Point tag name from source metadata.
///
/// This function uses the function which uses a linear
/// search algorithm that can be slow for large data sets, it is recommended that any results
/// for calls to this function be cached to improve performance.
///
internal static string TagFromKey(this MeasurementKey key, DataSet source)
{
DataRow record = GetMetaData(source, "ActiveMeasurements", $"ID = '{key}'");
return record is null ? key.ToString() : record["PointTag"].ToNonNullString(key.ToString());
}
///
/// Looks up measurement key from point tag.
///
/// Point tag to lookup.
/// Source metadata.
/// Table to search.
/// Measurement key from source metadata.
///
/// This function uses the function which uses a linear
/// search algorithm that can be slow for large data sets, it is recommended that any results
/// for calls to this function be cached to improve performance.
///
internal static MeasurementKey KeyFromTag(this string pointTag, DataSet source, string table = "ActiveMeasurements")
{
DataRow record = pointTag.MetadataRecordFromTag(source, table);
if (record is null)
return MeasurementKey.Undefined;
try
{
return MeasurementKey.LookUpOrCreate(record["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), record["ID"].ToString());
}
catch (Exception ex)
{
Logger.SwallowException(ex);
return MeasurementKey.Undefined;
}
}
///
/// Looks up measurement key from signal ID.
///
/// Signal ID to lookup.
/// Source metadata.
/// Table to search.
/// Measurement key from source metadata.
///
/// This function uses the function which uses a linear
/// search algorithm that can be slow for large data sets, it is recommended that any results
/// for calls to this function be cached to improve performance.
///
internal static Tuple KeyAndTagFromSignalID(this string signalID, DataSet source, string table = "ActiveMeasurements")
{
DataRow record = signalID.MetadataRecordFromSignalID(source, table);
string pointTag = "Undefined";
if (record is null)
return new(MeasurementKey.Undefined, pointTag);
try
{
MeasurementKey key = MeasurementKey.LookUpOrCreate(record["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), record["ID"].ToString());
pointTag = record["PointTag"].ToNonNullString(key.ToString());
return new(key, pointTag);
}
catch (Exception ex)
{
Logger.SwallowException(ex);
return new(MeasurementKey.Undefined, pointTag);
}
}
///
/// Splits any defined alias from a point tag expression.
///
/// Source point tag expression that can contain an alias.
/// Alias, if defined.
/// Point tag name without any alias.
internal static string SplitAlias(this string tagExpression, out string alias)
{
Match match = s_aliasedTagExpression.Match(tagExpression);
if (match.Success)
{
alias = match.Result("${Identifier}");
return match.Result("${Expression}").Trim();
}
alias = null;
return tagExpression;
}
///
/// Looks up metadata record from point tag.
///
/// Point tag to lookup.
/// Source metadata.
/// Table to search.
/// Metadata record from source metadata for provided point tag.
///
///
/// Use "table.pointTag" format to specify which table to pull point tag from.
///
///
/// This function uses the function which uses a linear
/// search algorithm that can be slow for large data sets, it is recommended that any results
/// for calls to this function be cached to improve performance.
///
///
internal static DataRow MetadataRecordFromTag(this string pointTag, DataSet source, string table) =>
GetMetaData(source, table, $"PointTag = '{SplitAlias(pointTag, out string _)}'");
///
/// Looks up metadata record from signal ID.
///
/// Signal ID to lookup.
/// Source metadata.
/// Table to search.
/// Metadata record from source metadata for provided point tag.
///
/// This function uses the function which uses a linear
/// search algorithm that can be slow for large data sets, it is recommended that any results
/// for calls to this function be cached to improve performance.
///
internal static DataRow MetadataRecordFromSignalID(this string signalID, DataSet source, string table) =>
GetMetaData(source, table, $"SignalID = '{signalID}'");
private static DataRow GetMetaData(DataSet source, string table, string expression)
{
try
{
DataRow[] filteredRows = source.Tables[table].Select(expression);
if (filteredRows.Length > 1)
s_log.Publish(MessageLevel.Warning, "Duplicate Tag Names", $"Grafana query for \"{expression}\" produced {filteredRows.Length:N0} records. Key values for meta-data are expected to be unique, invalid meta-data results may be returned.");
return filteredRows.Length > 0 ? filteredRows[0] : null;
}
catch (Exception ex)
{
Logger.SwallowException(ex);
return null;
}
}
private static MeasurementKey GetTargetFromGuid(string guidID) =>
MeasurementKey.LookUpBySignalID(Guid.Parse(guidID));
private static DataRow GetTargetMetaData(DataSet source, object value)
{
string target = value.ToNonNullNorWhiteSpace(Guid.Empty.ToString());
return TargetCache.GetOrAdd(target, () => GetMetaData(source, "ActiveMeasurements", $"ID = '{GetTargetFromGuid(target)}'"));
}
private static string GetAlarmCondition(DataRow defintion)
{
StringBuilder description;
description = new("value");
if (!Enum.TryParse(defintion["Operation"].ToNonNullNorWhiteSpace(AlarmOperation.Equal.ToString()), out AlarmOperation operation))
operation = AlarmOperation.Equal;
switch (operation)
{
case AlarmOperation.Equal:
description.Append(" = ");
break;
case AlarmOperation.NotEqual:
description.Append(" != ");
break;
case AlarmOperation.GreaterOrEqual:
description.Append(" >= ");
break;
case AlarmOperation.LessOrEqual:
description.Append(" <= ");
break;
case AlarmOperation.GreaterThan:
description.Append(" > ");
break;
case AlarmOperation.LessThan:
description.Append(" < ");
break;
case AlarmOperation.Flatline:
description.Append(" flat-lined for ");
description.Append(defintion["Delay"]);
description.Append(" seconds");
return description.ToString();
default:
description.Append(operation.GetDescription());
break;
}
string setPoint = defintion["SetPoint"].ToNonNullString();
description.Append(string.IsNullOrWhiteSpace(setPoint) ? "undefined" : setPoint);
return description.ToString();
}
}
}