//******************************************************************************************************
// InfluxDBOutputAdapter.cs - Gbtc
//
// Copyright © 2010, 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://www.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:
// ----------------------------------------------------------------------------------------------------
// 07/02/2014 - J. Ritchie Carroll
// Generated original version of source code.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GSF;
using GSF.Diagnostics;
using GSF.Threading;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
namespace InfluxDBAdapters
{
///
/// Represents an output adapter that archives measurements to a local archive.
///
[Description("InfluxDB: Archives measurements to an InfluxDB instance")]
public class InfluxDBOutputAdapter : OutputAdapterBase
{
#region [ Members ]
// Constants
///
/// Default value for .
///
public const string DefaultUserName = "root";
///
/// Default value for .
///
public const string DefaultPassword = "root";
///
/// Default value for .
///
public const bool DefaultUseParallelPosting = true;
///
/// Default value for .
///
public const int DefaultValuesPerPost = 50;
// Fields
private readonly ShortSynchronizedOperation m_requestRestart;
private Uri m_requestUri; // Data posting URI for InfluxDB connection
private string m_serverUri; // Base server URI for InfluxDB connection
private string m_databaseName; // Database name forInfluxDB connection
private string m_userName; // Username for InfluxDB connection
private string m_password; // Password for InfluxDB connection
private bool m_useParallelPosting; // Enable parallel posting
private int m_valuesPerPost; // Maximum values to send per post (when using parallel posting)
private string m_connectionResponse; // Response from connection attempt
private long m_totalValues; // Total archived values
private long m_totalPosts; // Total post to the InfluxDB connection
private long m_totalParallelGroups; // Total measurement groups processed in parallel
private long m_ignoredErrors; // Total ignored 400 and 500 HTTP status code errors
#endregion
#region [ Constructors ]
///
/// Creates a new ;
///
public InfluxDBOutputAdapter()
{
m_requestRestart = new ShortSynchronizedOperation(() =>
{
if (Enabled)
Start();
});
}
#endregion
#region [ Properties ]
///
/// Gets or sets the base server URI for the InfluxDB connection.
///
[ConnectionStringParameter, Description("Defines the base server URI for the InfluxDB connection (e.g., http://localhost:8086).")]
public string ServerUri
{
get
{
return m_serverUri;
}
set
{
m_serverUri = value;
}
}
///
/// Gets or sets the database name for the InfluxDB connection.
///
[ConnectionStringParameter, Description("Defines the database name for the InfluxDB connection. Database will not be created, it is expected to already exist.")]
public string DatabaseName
{
get
{
return m_databaseName;
}
set
{
m_databaseName = value;
}
}
///
/// Gets or sets the user name for the InfluxDB connection.
///
[ConnectionStringParameter, Description("Defines the the user name for the InfluxDB connection."), DefaultValue(DefaultUserName)]
public string UserName
{
get
{
return m_userName;
}
set
{
m_userName = value;
}
}
///
/// Gets or sets the password for the InfluxDB connection.
///
[ConnectionStringParameter, Description("Defines the password for the InfluxDB connection."), DefaultValue(DefaultPassword)]
public string Password
{
get
{
return m_password;
}
set
{
m_password = value;
}
}
///
/// Gets or sets flag that determines if multiple posts to InfluxDB should be made in parallel.
///
[ConnectionStringParameter, Description("Defines flag that determines if multiple posts to InfluxDB should be made in parallel."), DefaultValue(DefaultUseParallelPosting)]
public bool UseParallelPosting
{
get
{
return m_useParallelPosting;
}
set
{
m_useParallelPosting = value;
}
}
///
/// Gets or sets the maximum values to send per post when is true for the InfluxDB connection.
///
[ConnectionStringParameter, Description("When parallel posting is enabled, defines the maximum values to send per post for the InfluxDB connection."), DefaultValue(DefaultValuesPerPost)]
public int ValuesPerPost
{
get
{
return m_valuesPerPost;
}
set
{
m_valuesPerPost = value;
}
}
///
/// Gets or sets whether or not to automatically place measurements back into the processing
/// queue if an exception occurs while processing. Defaults to false.
///
[ConnectionStringParameter, Description("Defines whether or not to automatically place measurements back into the processing queue if an exception occurs while processing. For InfluxDB adapter this defaults to true."), DefaultValue(true)]
public override bool RequeueOnException
{
get
{
return base.RequeueOnException;
}
set
{
base.RequeueOnException = value;
}
}
///
/// Returns a flag that determines if measurements sent to this are destined for archival.
///
public override bool OutputIsForArchive
{
get
{
return true;
}
}
///
/// Gets flag that determines if this uses an asynchronous connection.
///
protected override bool UseAsyncConnect
{
get
{
return false;
}
}
///
/// Gets a detailed status for this .
///
public override string Status
{
get
{
StringBuilder status = new StringBuilder();
status.Append(base.Status);
status.AppendFormat(" InfluxDB base URI: {0}", m_serverUri);
status.AppendLine();
status.AppendFormat(" Use parallel posting: {0}", m_useParallelPosting);
status.AppendLine();
if (m_useParallelPosting)
{
status.AppendFormat(" Maximum values per post: {0:N0}", m_valuesPerPost);
status.AppendLine();
status.AppendFormat(" Average parallel threads: {0:0.0}", m_totalParallelGroups / (double)InternalProcessQueue.TotalFunctionCalls);
status.AppendLine();
}
status.AppendFormat(" Total archived values: {0:N0}", m_totalValues);
status.AppendLine();
status.AppendFormat(" Total posts: {0:N0}", m_totalPosts);
status.AppendLine();
status.AppendFormat(" Average values per post: {0:R}", Math.Round(m_totalValues / (double)m_totalPosts, 2));
status.AppendLine();
status.AppendFormat(" Connection response: {0}", m_connectionResponse);
status.AppendLine();
status.AppendFormat("Ignored error status codes: {0:N0} total 400 or 500", m_ignoredErrors);
status.AppendLine();
return status.ToString();
}
}
#endregion
#region [ Methods ]
///
/// Returns a brief status of this
///
/// Maximum number of characters in the status string
/// Status
public override string GetShortStatus(int maxLength)
{
return string.Format("Archived {0:N0} measurements via {1:N0} posts to \"{2}\".", m_totalValues, m_totalPosts, m_databaseName).CenterText(maxLength);
}
///
/// Initializes .
///
public override void Initialize()
{
base.Initialize();
Dictionary settings = Settings;
string setting;
if (!settings.TryGetValue("serverUri", out m_serverUri))
throw new InvalidOperationException("Base server URI is a required setting for the InfluxDB connection. Please add a server URI setting in the format of \"serverUri=http://localhost:8086\" to the connection string.");
if (!settings.TryGetValue("databaseName", out m_databaseName))
throw new InvalidOperationException("Database name is a required setting for the InfluxDB connection. Please add a database name setting in the format of \"databaseName=MyDB\" to the connection string.");
if (settings.TryGetValue("userName", out setting))
m_userName = setting;
else
m_userName = DefaultUserName;
if (settings.TryGetValue("password", out setting))
m_password = setting;
else
m_password = DefaultPassword;
if (settings.TryGetValue("useParallelPosting", out setting))
m_useParallelPosting = setting.ParseBoolean();
else
m_useParallelPosting = DefaultUseParallelPosting;
if (!settings.TryGetValue("valuesPerPost", out setting) || !int.TryParse(setting, out m_valuesPerPost))
m_valuesPerPost = DefaultValuesPerPost;
// Since InfluxDB API is web based, you generally want to retry posts in case of issues
if (!settings.ContainsKey("requeueOnException"))
RequeueOnException = true;
// Define request URI
UriBuilder requestUri = new UriBuilder(m_serverUri);
requestUri.Path = string.Format("db/{0}/series", m_databaseName.UriEncode());
requestUri.Query = string.Format("u={0}&p={1}", m_userName.UriEncode(), m_password.UriEncode());
m_requestUri = requestUri.Uri;
}
///
/// Attempts to connect to InfluxDB database.
///
protected override void AttemptConnection()
{
try
{
// Setup an authenticate request
UriBuilder requestUri = new UriBuilder(m_serverUri);
requestUri.Path = string.Format("db/{0}/authenticate", m_databaseName.UriEncode());
requestUri.Query = string.Format("u={0}&p={1}", m_userName.UriEncode(), m_password.UriEncode());
// Create a web request for authentication that will at least make sure the server is available
HttpWebRequest request = WebRequest.Create(requestUri.Uri) as HttpWebRequest;
if ((object)request != null)
{
request.ContentType = "application/json";
// Attempt query - if this doesn't throw an exception, query succeeded
using (WebResponse response = request.GetResponse())
{
m_connectionResponse = ((HttpWebResponse)response).StatusDescription;
}
}
else
{
throw new InvalidOperationException("Failed to create an HTTP request from " + requestUri);
}
}
catch (Exception ex)
{
m_connectionResponse = ex.Message;
// Rethrow any captured exceptions, this will restart connection cycle in OutputAdapterBase
throw;
}
}
///
/// Attempts to disconnect from InfluxDB.
///
protected override void AttemptDisconnection()
{
// Connections are stateless, nothing to do here...
}
///
/// Serializes measurements to InfluxDB.
///
protected override void ProcessMeasurements(IMeasurement[] measurements)
{
if (measurements.Length == 0)
return;
if (m_useParallelPosting)
{
List measurementGroups = new List();
IMeasurement[] measurementGroup = measurements.Take(m_valuesPerPost).ToArray();
int skipCount = measurementGroup.Length;
while (measurementGroup.Length > 0)
{
measurementGroups.Add(measurementGroup);
measurementGroup = measurements.Skip(skipCount).Take(m_valuesPerPost).ToArray();
skipCount += measurementGroup.Length;
}
Parallel.ForEach(measurementGroups, PostMeasurementsToArchive);
m_totalParallelGroups += measurementGroups.Count;
}
else
{
PostMeasurementsToArchive(measurements);
}
}
private void PostMeasurementsToArchive(IMeasurement[] measurements)
{
const string PostFormat = "{{\"name\":\"{0}\",\"columns\":[\"time\",\"value\",\"quality\"],\"points\":[[{1},{2},\"{3}\"]]}}";
try
{
HttpWebRequest request = WebRequest.Create(m_requestUri) as HttpWebRequest;
if ((object)request == null)
throw new InvalidOperationException("Failed to create an HTTP request from " + m_requestUri);
request.Method = "POST";
request.KeepAlive = true;
request.SendChunked = true;
request.AllowWriteStreamBuffering = true;
request.ContentType = "application/json";
// Build a JSON post expression with measurement values to use as post data
StringBuilder jsonData = new StringBuilder();
foreach (IMeasurement measurement in measurements)
{
if (jsonData.Length > 0)
jsonData.Append(',');
else
jsonData.Append('[');
jsonData.AppendFormat(PostFormat, measurement.Key, GetEpochMilliseconds(measurement.Timestamp), measurement.AdjustedValue, measurement.StateFlags);
}
jsonData.Append(']');
// Encode JSON data as UTF8
byte[] postData = Encoding.UTF8.GetBytes(jsonData.ToString());
// Write data to request stream
request.ContentLength = postData.Length;
using (Stream requestStream = request.GetRequestStream())
{
requestStream.Write(postData, 0, postData.Length);
}
// Post data
WebResponse response = request.GetResponse();
response.Dispose();
Interlocked.Add(ref m_totalValues, measurements.Length);
Interlocked.Increment(ref m_totalPosts);
}
catch (Exception ex)
{
if (RequeueOnException)
InternalProcessQueue.InsertRange(0, measurements);
bool ignoreError = false;
// Under load, 400 and 500 status code errors seem to be occasionally reported - we ignore these
WebException webex = ex as WebException;
if ((object)webex != null)
{
HttpWebResponse webResponse = webex.Response as HttpWebResponse;
if ((object)webResponse != null)
{
HttpStatusCode statusCode = webResponse.StatusCode;
ignoreError = (statusCode == HttpStatusCode.BadRequest || statusCode == HttpStatusCode.InternalServerError);
}
}
if (!ignoreError)
{
OnProcessException(MessageLevel.Warning, ex);
// So long as user hasn't requested to stop, restart connection cycle when exceptions occur
m_requestRestart.RunOnceAsync();
}
else
{
Interlocked.Increment(ref m_ignoredErrors);
}
}
}
private long GetEpochMilliseconds(Ticks timestamp)
{
return (long)(timestamp - UnixTimeTag.BaseTicks).ToMilliseconds();
}
#endregion
}
}