Added Logentries to track down automatic upgrade issues

This commit is contained in:
Keivan Beigi 2014-12-16 15:35:42 -08:00
parent 427b102900
commit 493a3c9724
17 changed files with 3106 additions and 245 deletions

View file

@ -0,0 +1,648 @@
using System;
using System.Collections.Concurrent;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
namespace LogentriesCore
{
public class AsyncLogger
{
#region Constants
// Current version number.
protected const String Version = "2.6.0";
// Size of the internal event queue.
protected const int QueueSize = 32768;
// Minimal delay between attempts to reconnect in milliseconds.
protected const int MinDelay = 100;
// Maximal delay between attempts to reconnect in milliseconds.
protected const int MaxDelay = 10000;
// Appender signature - used for debugging messages.
protected const String LeSignature = "LE: ";
// Legacy Logentries configuration names.
protected const String LegacyConfigTokenName = "LOGENTRIES_TOKEN";
protected const String LegacyConfigAccountKeyName = "LOGENTRIES_ACCOUNT_KEY";
protected const String LegacyConfigLocationName = "LOGENTRIES_LOCATION";
// New Logentries configuration names.
protected const String ConfigTokenName = "Logentries.Token";
protected const String ConfigAccountKeyName = "Logentries.AccountKey";
protected const String ConfigLocationName = "Logentries.Location";
// Error message displayed when invalid token is detected.
protected const String InvalidTokenMessage = "\n\nIt appears your LOGENTRIES_TOKEN value is invalid or missing.\n\n";
// Error message displayed when invalid account_key or location parameters are detected.
protected const String InvalidHttpPutCredentialsMessage = "\n\nIt appears your LOGENTRIES_ACCOUNT_KEY or LOGENTRIES_LOCATION values are invalid or missing.\n\n";
// Error message deisplayed when queue overflow occurs.
protected const String QueueOverflowMessage = "\n\nLogentries buffer queue overflow. Message dropped.\n\n";
// Newline char to trim from message for formatting.
protected static char[] TrimChars = { '\r', '\n' };
/** Non-Unix and Unix Newline */
protected static string[] posix_newline = { "\r\n", "\n" };
/** Unicode line separator character */
protected static string line_separator = "\u2028";
// Restricted symbols that should not appear in host name.
// See http://support.microsoft.com/kb/228275/en-us for details.
private static Regex ForbiddenHostNameChars = new Regex(@"[/\\\[\]\""\:\;\|\<\>\+\=\,\?\* _]{1,}", RegexOptions.Compiled);
/** Regex used to validate GUID in .NET3.5 */
private static Regex isGuid = new Regex(@"^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$", RegexOptions.Compiled);
#endregion
#region Singletons
// UTF-8 output character set.
protected static readonly UTF8Encoding UTF8 = new UTF8Encoding();
// ASCII character set used by HTTP.
protected static readonly ASCIIEncoding ASCII = new ASCIIEncoding();
//static list of all the queues the le appender might be managing.
private static ConcurrentBag<BlockingCollection<string>> _allQueues = new ConcurrentBag<BlockingCollection<string>>();
/// <summary>
/// Determines if the queue is empty after waiting the specified waitTime.
/// Returns true or false if the underlying queues are empty.
/// </summary>
/// <param name="waitTime">The length of time the method should block before giving up waiting for it to empty.</param>
/// <returns>True if the queue is empty, false if there are still items waiting to be written.</returns>
public static bool AreAllQueuesEmpty(TimeSpan waitTime)
{
var start = DateTime.UtcNow;
var then = DateTime.UtcNow;
while (start.Add(waitTime) > then)
{
if (_allQueues.All(x => x.Count == 0))
return true;
Thread.Sleep(100);
then = DateTime.UtcNow;
}
return _allQueues.All(x => x.Count == 0);
}
#endregion
public AsyncLogger()
{
Queue = new BlockingCollection<string>(QueueSize);
_allQueues.Add(Queue);
WorkerThread = new Thread(Run);
WorkerThread.Name = "Logentries Log4net Appender";
WorkerThread.IsBackground = true;
}
#region Configuration properties
private String m_Token = "";
private String m_AccountKey = "";
private String m_Location = "";
private bool m_ImmediateFlush = false;
private bool m_Debug = false;
private bool m_UseHttpPut = false;
private bool m_UseSsl = false;
// Properties for defining location of DataHub instance if one is used.
private bool m_UseDataHub = false; // By default Logentries service is used instead of DataHub instance.
private String m_DataHubAddr = "";
private int m_DataHubPort = 0;
// Properties to define host name of user's machine and define user-specified log ID.
private bool m_UseHostName = false; // Defines whether to prefix log message with HostName or not.
private String m_HostName = ""; // User-defined or auto-defined host name (if not set in config. file)
private String m_LogID = ""; // User-defined log ID to be prefixed to the log message.
// Sets DataHub usage flag.
public void setIsUsingDataHub(bool useDataHub)
{
m_UseDataHub = useDataHub;
}
public bool getIsUsingDataHab()
{
return m_UseDataHub;
}
// Sets DataHub instance address.
public void setDataHubAddr(String dataHubAddr)
{
m_DataHubAddr = dataHubAddr;
}
public String getDataHubAddr()
{
return m_DataHubAddr;
}
// Sets the port on which DataHub instance is waiting for log messages.
public void setDataHubPort(int port)
{
m_DataHubPort = port;
}
public int getDataHubPort()
{
return m_DataHubPort;
}
public void setToken(String token)
{
m_Token = token;
}
public String getToken()
{
return m_Token;
}
public void setAccountKey(String accountKey)
{
m_AccountKey = accountKey;
}
public string getAccountKey()
{
return m_AccountKey;
}
public void setLocation(String location)
{
m_Location = location;
}
public String getLocation()
{
return m_Location;
}
public void setImmediateFlush(bool immediateFlush)
{
m_ImmediateFlush = immediateFlush;
}
public bool getImmediateFlush()
{
return m_ImmediateFlush;
}
public void setDebug(bool debug)
{
m_Debug = debug;
}
public bool getDebug()
{
return m_Debug;
}
public void setUseHttpPut(bool useHttpPut)
{
m_UseHttpPut = useHttpPut;
}
public bool getUseHttpPut()
{
return m_UseHttpPut;
}
public void setUseSsl(bool useSsl)
{
m_UseSsl = useSsl;
}
public bool getUseSsl()
{
return m_UseSsl;
}
public void setUseHostName(bool useHostName)
{
m_UseHostName = useHostName;
}
public bool getUseHostName()
{
return m_UseHostName;
}
public void setHostName(String hostName)
{
m_HostName = hostName;
}
public String getHostName()
{
return m_HostName;
}
public void setLogID(String logID)
{
m_LogID = logID;
}
public String getLogID()
{
return m_LogID;
}
#endregion
protected readonly BlockingCollection<string> Queue;
protected readonly Thread WorkerThread;
protected readonly Random Random = new Random();
private LeClient LeClient = null;
protected bool IsRunning = false;
#region Protected methods
protected virtual void Run()
{
try
{
// Open connection.
ReopenConnection();
string logMessagePrefix = String.Empty;
if (m_UseHostName)
{
// If LogHostName is set to "true", but HostName is not defined -
// try to get host name from Environment.
if (m_HostName == String.Empty)
{
try
{
WriteDebugMessages("HostName parameter is not defined - trying to get it from System.Environment.MachineName");
m_HostName = "HostName=" + System.Environment.MachineName + " ";
}
catch (InvalidOperationException ex)
{
// Cannot get host name automatically, so assume that HostName is not used
// and log message is sent without it.
m_UseHostName = false;
WriteDebugMessages("Failed to get HostName parameter using System.Environment.MachineName. Log messages will not be prefixed by HostName");
}
}
else
{
if (!CheckIfHostNameValid(m_HostName))
{
// If user-defined host name is incorrect - we cannot use it
// and log message is sent without it.
m_UseHostName = false;
WriteDebugMessages("HostName parameter contains prohibited characters. Log messages will not be prefixed by HostName");
}
else
{
m_HostName = "HostName=" + m_HostName + " ";
}
}
}
if (m_LogID != String.Empty)
{
logMessagePrefix = m_LogID + " ";
}
if (m_UseHostName)
{
logMessagePrefix += m_HostName;
}
// Flag that is set if logMessagePrefix is empty.
bool isPrefixEmpty = (logMessagePrefix == String.Empty);
// Send data in queue.
while (true)
{
// Take data from queue.
var line = Queue.Take();
// Replace newline chars with line separator to format multi-line events nicely.
foreach (String newline in posix_newline)
{
line = line.Replace(newline, line_separator);
}
// If m_UseDataHub == true (logs are sent to DataHub instance) then m_Token is not
// appended to the message.
string finalLine = ((!m_UseHttpPut && !m_UseDataHub) ? this.m_Token + line : line) + '\n';
// Add prefixes: LogID and HostName if they are defined.
if (!isPrefixEmpty)
{
finalLine = logMessagePrefix + finalLine;
}
byte[] data = UTF8.GetBytes(finalLine);
// Send data, reconnect if needed.
while (true)
{
try
{
this.LeClient.Write(data, 0, data.Length);
if (m_ImmediateFlush)
this.LeClient.Flush();
}
catch (IOException)
{
// Reopen the lost connection.
ReopenConnection();
continue;
}
break;
}
}
}
catch (ThreadInterruptedException ex)
{
WriteDebugMessages("Logentries asynchronous socket client was interrupted.", ex);
}
}
protected virtual void OpenConnection()
{
try
{
if (LeClient == null)
{
// Create LeClient instance providing all needed parameters. If DataHub-related properties
// have not been overridden by log4net or NLog configurators, then DataHub is not used,
// because m_UseDataHub == false by default.
LeClient = new LeClient(m_UseHttpPut, m_UseSsl, m_UseDataHub, m_DataHubAddr, m_DataHubPort);
}
LeClient.Connect();
if (m_UseHttpPut)
{
var header = String.Format("PUT /{0}/hosts/{1}/?realtime=1 HTTP/1.1\r\n\r\n", m_AccountKey, m_Location);
LeClient.Write(ASCII.GetBytes(header), 0, header.Length);
}
}
catch (Exception ex)
{
throw new IOException("An error occurred while opening the connection.", ex);
}
}
protected virtual void ReopenConnection()
{
CloseConnection();
var rootDelay = MinDelay;
while (true)
{
try
{
OpenConnection();
return;
}
catch (Exception ex)
{
if (m_Debug)
{
WriteDebugMessages("Unable to connect to Logentries API.", ex);
}
}
rootDelay *= 2;
if (rootDelay > MaxDelay)
rootDelay = MaxDelay;
var waitFor = rootDelay + Random.Next(rootDelay);
try
{
Thread.Sleep(waitFor);
}
catch
{
throw new ThreadInterruptedException();
}
}
}
protected virtual void CloseConnection()
{
if (LeClient != null)
LeClient.Close();
}
public static bool IsNullOrWhiteSpace(String value)
{
if (value == null) return true;
for (int i = 0; i < value.Length; i++)
{
if (!Char.IsWhiteSpace(value[i])) return false;
}
return true;
}
private string retrieveSetting(String name)
{
string value;
value = ConfigurationManager.AppSettings[name];
if (IsNullOrWhiteSpace(value))
{
try
{
value = Environment.GetEnvironmentVariable(name);
}
catch (SecurityException)
{
}
}
return value;
}
/*
* Use CloudConfigurationManager with .NET4.0 and fallback to System.Configuration for previous frameworks.
*
* NOTE: This is not entirely clear with regards to the above comment, but this block of code uses a compiler directive NET4_0
* which is not set by default anywhere, so most uses of this code will default back to the "pre-.Net4.0" code branch, even
* if you are using .Net4.0 or .Net4.5.
*
* The second issue is that there are two appsetting keys for each setting - the "legacy" key, such as "LOGENTRIES_TOKEN"
* and the "non-legacy" key, such as "Logentries.Token". Again, I'm not sure of the reasons behind this, so the code below checks
* both the legacy and non-legacy keys, defaulting to the legacy keys if they are found.
*
* It probably should be investigated whether the fallback to ConfigurationManager is needed at all, as CloudConfigurationManager
* will retrieve settings from appSettings in a non-Azure environment.
*/
protected virtual bool LoadCredentials()
{
if (!m_UseHttpPut)
{
if (GetIsValidGuid(m_Token))
return true;
var configToken = retrieveSetting(LegacyConfigTokenName) ?? retrieveSetting(ConfigTokenName);
if (!String.IsNullOrEmpty(configToken) && GetIsValidGuid(configToken))
{
m_Token = configToken;
return true;
}
WriteDebugMessages(InvalidTokenMessage);
return false;
}
if (m_AccountKey != "" && GetIsValidGuid(m_AccountKey) && m_Location != "")
return true;
var configAccountKey = ConfigurationManager.AppSettings[LegacyConfigAccountKeyName] ?? ConfigurationManager.AppSettings[ConfigAccountKeyName];
if (!String.IsNullOrEmpty(configAccountKey) && GetIsValidGuid(configAccountKey))
{
m_AccountKey = configAccountKey;
var configLocation = ConfigurationManager.AppSettings[LegacyConfigLocationName] ?? ConfigurationManager.AppSettings[ConfigLocationName];
if (!String.IsNullOrEmpty(configLocation))
{
m_Location = configLocation;
return true;
}
}
WriteDebugMessages(InvalidHttpPutCredentialsMessage);
return false;
}
private bool CheckIfHostNameValid(String hostName)
{
return !ForbiddenHostNameChars.IsMatch(hostName); // Returns false if reg.ex. matches any of forbidden chars.
}
static bool IsGuid(string candidate, out Guid output)
{
bool isValid = false;
output = Guid.Empty;
if (isGuid.IsMatch(candidate))
{
output = new Guid(candidate);
isValid = true;
}
return isValid;
}
protected virtual bool GetIsValidGuid(string guidString)
{
if (String.IsNullOrEmpty(guidString))
return false;
System.Guid newGuid = System.Guid.NewGuid();
return IsGuid(guidString, out newGuid);
}
protected virtual void WriteDebugMessages(string message, Exception ex)
{
if (!m_Debug)
return;
message = LeSignature + message;
string[] messages = { message, ex.ToString() };
foreach (var msg in messages)
{
// Use below line instead when compiling with log4net1.2.10.
//LogLog.Debug(msg);
//LogLog.Debug(typeof(LogentriesAppender), msg);
Debug.WriteLine(message);
}
}
protected virtual void WriteDebugMessages(string message)
{
if (!m_Debug)
return;
message = LeSignature + message;
// Use below line instead when compiling with log4net1.2.10.
//LogLog.Debug(message);
//LogLog.Debug(typeof(LogentriesAppender), message);
Debug.WriteLine(message);
}
#endregion
#region publicMethods
public virtual void AddLine(string line)
{
if (!IsRunning)
{
// We need to load user credentials only
// if the configuration does not state that DataHub is used;
// credentials needed only if logs are sent to LE service directly.
bool credentialsLoaded = false;
if(!m_UseDataHub)
{
credentialsLoaded = LoadCredentials();
}
// If in DataHub mode credentials are ignored.
if (credentialsLoaded || m_UseDataHub)
{
WriteDebugMessages("Starting Logentries asynchronous socket client.");
WorkerThread.Start();
IsRunning = true;
}
}
WriteDebugMessages("Queueing: " + line);
String trimmedEvent = line.TrimEnd(TrimChars);
// Try to append data to queue.
if (!Queue.TryAdd(trimmedEvent))
{
Queue.Take();
if (!Queue.TryAdd(trimmedEvent))
WriteDebugMessages(QueueOverflowMessage);
}
}
public void interruptWorker()
{
WorkerThread.Interrupt();
}
#endregion
}
}

View file

@ -0,0 +1,103 @@
using System;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
namespace LogentriesCore
{
class LeClient
{
// Logentries API server address.
protected const String LeApiUrl = "api.logentries.com";
// Port number for token logging on Logentries API server.
protected const int LeApiTokenPort = 10000;
// Port number for TLS encrypted token logging on Logentries API server
protected const int LeApiTokenTlsPort = 20000;
// Port number for HTTP PUT logging on Logentries API server.
protected const int LeApiHttpPort = 80;
// Port number for SSL HTTP PUT logging on Logentries API server.
protected const int LeApiHttpsPort = 443;
// Creates LeClient instance. If do not define useServerUrl and/or useOverrideProt during call
// LeClient will be configured to work with api.logentries.com server; otherwise - with
// defined server on defined port.
public LeClient(bool useHttpPut, bool useSsl, bool useDataHub, String serverAddr, int port)
{
// Override port number and server address to send logs to DataHub instance.
if (useDataHub)
{
m_UseSsl = false; // DataHub does not support receiving log messages over SSL for now.
m_TcpPort = port;
m_ServerAddr = serverAddr;
}
else
{
m_UseSsl = useSsl;
if (!m_UseSsl)
m_TcpPort = useHttpPut ? LeApiHttpPort : LeApiTokenPort;
else
m_TcpPort = useHttpPut ? LeApiHttpsPort : LeApiTokenTlsPort;
}
}
private bool m_UseSsl = false;
private int m_TcpPort;
private TcpClient m_Client = null;
private Stream m_Stream = null;
private SslStream m_SslStream = null;
private String m_ServerAddr = LeApiUrl; // By default m_ServerAddr points to api.logentries.com if useDataHub is not set to true.
private Stream ActiveStream
{
get
{
return m_UseSsl ? m_SslStream : m_Stream;
}
}
public void Connect()
{
m_Client = new TcpClient(m_ServerAddr, m_TcpPort);
m_Client.NoDelay = true;
m_Stream = m_Client.GetStream();
if (m_UseSsl)
{
m_SslStream = new SslStream(m_Stream);
m_SslStream.AuthenticateAsClient(m_ServerAddr);
}
}
public void Write(byte[] buffer, int offset, int count)
{
ActiveStream.Write(buffer, offset, count);
}
public void Flush()
{
ActiveStream.Flush();
}
public void Close()
{
if (m_Client != null)
{
try
{
m_Client.Close();
}
catch
{
}
}
}
}
}

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{90D6E9FC-7B88-4E1B-B018-8FA742274558}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LogentriesCore</RootNamespace>
<AssemblyName>LogentriesCore</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<RestorePackages>true</RestorePackages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="AsyncLogger.cs" />
<Compile Include="LeClient.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>if $(ConfigurationName) == Release (
COPY "$(TargetPath)" "%25NUGET_PROJECTS%25$(ProjectName)\2.6.0\lib\net40\" /Y
nuget pack %25NUGET_PROJECTS%25$(ProjectName)\2.6.0\logentries.core.nuspec /o %25NUGET_PROJECTS%25$(ProjectName)\2.6.0\
)</PostBuildEvent>
</PropertyGroup>
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View file

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("LogentriesCore")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LogentriesCore")]
[assembly: AssemblyCopyright("Copyright © 2013")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("14055980-6937-4745-9449-dabf47c1d892")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("2.6.0.0")]
[assembly: AssemblyFileVersion("2.6.0.0")]

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.WindowsAzure.ConfigurationManager" version="2.0.1.0" targetFramework="net40" />
</packages>