Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ The definition of the word exceptionless is: to be without exception. [Exception

## Using Exceptionless

Set the deployment environment once at startup, with an optional override on each event:

```csharp
client.Configuration.SetEnvironment("production");
client.CreateLog("Deployment complete").SetEnvironment("staging").Submit();
```

The hosting integration falls back to `IHostEnvironment.EnvironmentName`. Explicit configuration wins; `Exceptionless:Environment` and `Exceptionless__Environment` are also supported. .NET Framework applications can use the `Exceptionless:Environment` app setting or the `environment` attribute on the `<exceptionless>` configuration section. Names are trimmed and limited to 64 characters, preserving the supplied casing. The server filters case-insensitively and normalizes aggregation keys. Missing or invalid values remain unspecified. The top-level event `environment` is separate from machine diagnostics in `data.@environment`. Stacks and fixed versions remain shared across environments.

Refer to the Exceptionless documentation here: [Exceptionless Docs](https://exceptionless.com/docs/).

## Getting Started (Development)
Expand Down
2 changes: 1 addition & 1 deletion build/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.300" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.303" PrivateAssets="All"/>
<PackageReference Include="AsyncFixer" Version="2.1.0" PrivateAssets="All" />
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="All" />
</ItemGroup>
Expand Down
20 changes: 20 additions & 0 deletions src/Exceptionless/Configuration/ExceptionlessConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@

namespace Exceptionless {
public class ExceptionlessConfiguration {
private string _environment;

/// <summary>The default deployment environment for every event.</summary>
public string Environment {
get => _environment;
set {
_environment = Utility.DeploymentEnvironment.Normalize(value);
IsEnvironmentConfigured = value != null;
}
}

/// <summary>Whether an environment was explicitly configured, including an invalid value that remains unspecified.</summary>
public bool IsEnvironmentConfigured { get; private set; }

/// <summary>Applies a deployment environment fallback without replacing an explicitly configured value.</summary>
public void SetDefaultEnvironment(string environment) {
if (!IsEnvironmentConfigured)
_environment = Utility.DeploymentEnvironment.Normalize(environment);
}

private const string DEFAULT_SERVER_URL = "https://collector.exceptionless.io";
private const string DEFAULT_CONFIG_SERVER_URL = "https://config.exceptionless.io";
private const string DEFAULT_HEARTBEAT_SERVER_URL = "https://heartbeat.exceptionless.io";
Expand Down
3 changes: 3 additions & 0 deletions src/Exceptionless/Configuration/ExceptionlessSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ internal class ExceptionlessSection : ConfigurationSection {
[ConfigurationProperty("apiKey", IsRequired = true)]
public string ApiKey { get { return base["apiKey"] as string; } set { base["apiKey"] = value; } }

[ConfigurationProperty("environment")]
public string Environment { get { return base["environment"] as string; } set { base["environment"] = value; } }

[ConfigurationProperty("serverUrl")]
public string ServerUrl { get { return base["serverUrl"] as string; } set { base["serverUrl"] = value; } }

Expand Down
8 changes: 7 additions & 1 deletion src/Exceptionless/Extensions/EventBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

namespace Exceptionless {
public static class EventBuilderExtensions {
/// <summary>Overrides the deployment environment for this event.</summary>
public static EventBuilder SetEnvironment(this EventBuilder builder, string environment) {
builder.Target.Environment = environment;
return builder;
}

/// <summary>
/// Sets the user's identity (ie. email address, username, user id) that the event happened to.
/// </summary>
Expand Down Expand Up @@ -109,4 +115,4 @@ public static EventBuilder AddRecentTraceLogEntries(this EventBuilder builder, D
return builder;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@
#endif

#if NET45
using System.Collections.Specialized;
using System.Configuration;
using Exceptionless.Extensions;
using Exceptionless.Utility;
#endif

namespace Exceptionless {
public static class ExceptionlessConfigurationExtensions {
/// <summary>Sets the default deployment environment for every event.</summary>
public static void SetEnvironment(this ExceptionlessConfiguration config, string environment) {
config.Environment = environment;
}

private const string INSTALL_ID_KEY = "ExceptionlessInstallId";

/// <summary>
Expand Down Expand Up @@ -288,9 +294,16 @@ public static void ReadFromConfigSection(this ExceptionlessConfiguration config)
config.Resolver.GetLog().Error(typeof(ExceptionlessConfigurationExtensions), ex, String.Concat("Error retrieving configuration section: ", ex.Message));
}

config.ReadFromConfigSection(section);
}

internal static void ReadFromConfigSection(this ExceptionlessConfiguration config, ExceptionlessSection section) {
if (section == null)
return;

if (section.ElementInformation.Properties["environment"].ValueOrigin != PropertyValueOrigin.Default)
config.Environment = section.Environment;

if (!section.Enabled)
config.Enabled = false;

Expand Down Expand Up @@ -380,18 +393,25 @@ public static void ReadFromConfigSection(this ExceptionlessConfiguration config)
/// </summary>
/// <param name="config">The configuration object you want to apply the attribute settings to.</param>
public static void ReadFromAppSettings(this ExceptionlessConfiguration config) {
string apiKey = ConfigurationManager.AppSettings["Exceptionless:ApiKey"];
config.ReadFromAppSettings(ConfigurationManager.AppSettings);
}

internal static void ReadFromAppSettings(this ExceptionlessConfiguration config, NameValueCollection settings) {
if (settings["Exceptionless:Environment"] != null)
config.Environment = settings["Exceptionless:Environment"];

string apiKey = settings["Exceptionless:ApiKey"];
if (IsValidApiKey(apiKey))
config.ApiKey = apiKey;

if (Boolean.TryParse(ConfigurationManager.AppSettings["Exceptionless:Enabled"], out bool enabled) && !enabled)
if (Boolean.TryParse(settings["Exceptionless:Enabled"], out bool enabled) && !enabled)
config.Enabled = false;

string serverUrl = ConfigurationManager.AppSettings["Exceptionless:ServerUrl"];
string serverUrl = settings["Exceptionless:ServerUrl"];
if (!String.IsNullOrEmpty(serverUrl))
config.ServerUrl = serverUrl;

string defaultTags = ConfigurationManager.AppSettings["Exceptionless:DefaultTags"];
string defaultTags = settings["Exceptionless:DefaultTags"];
if (!String.IsNullOrEmpty(defaultTags))
foreach (var tag in defaultTags.SplitAndTrim(',').Where(tag => !String.IsNullOrEmpty(tag)))
config.DefaultTags.Add(tag);
Expand All @@ -412,6 +432,9 @@ public static void ReadFromConfiguration(this ExceptionlessConfiguration config,
throw new ArgumentNullException(nameof(settings));

var section = settings.GetSection("Exceptionless");
if (section["Environment"] != null) {
config.Environment = section["Environment"];
}
Comment thread
ejsmith marked this conversation as resolved.
if (Boolean.TryParse(section["Enabled"], out bool enabled) && !enabled)
config.Enabled = false;

Expand Down Expand Up @@ -483,6 +506,11 @@ public static void ReadFromConfiguration(this ExceptionlessConfiguration config,
/// </summary>
/// <param name="config">The configuration object you want to apply the attribute settings to.</param>
public static void ReadFromEnvironmentalVariables(this ExceptionlessConfiguration config) {
string environment = GetEnvironmentalVariable("Exceptionless:Environment") ?? GetEnvironmentalVariable("Exceptionless__Environment");
if (environment != null) {
config.Environment = environment;
}

string apiKey = GetEnvironmentalVariable("Exceptionless:ApiKey") ?? GetEnvironmentalVariable("Exceptionless__ApiKey");
if (IsValidApiKey(apiKey))
config.ApiKey = apiKey;
Expand Down Expand Up @@ -576,4 +604,4 @@ private static bool IsValidApiKey(string apiKey) {
return !String.IsNullOrEmpty(apiKey) && apiKey != "API_KEY_HERE";
}
}
}
}
21 changes: 19 additions & 2 deletions src/Exceptionless/Models/Client/Event.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@
namespace Exceptionless.Models {
[Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))]
public class Event : IData {
private string _environment;

/// <summary>The deployment environment, such as production or staging.</summary>
[Json.JsonProperty(NullValueHandling = Json.NullValueHandling.Ignore)]
public string Environment {
get => _environment;
set {
_environment = Utility.DeploymentEnvironment.Normalize(value);
HasEnvironmentOverride = value != null;
}
}

internal bool HasEnvironmentOverride { get; set; }

public Event() {
Tags = new TagSet();
Data = new DataDictionary();
Expand Down Expand Up @@ -60,7 +74,7 @@ public Event() {
public string ReferenceId { get; set; }

protected bool Equals(Event other) {
return string.Equals(Type, other.Type) && string.Equals(Source, other.Source) && Tags.CollectionEquals(other.Tags) && string.Equals(Message, other.Message) && string.Equals(Geo, other.Geo) && Value == other.Value && Equals(Data, other.Data);
return string.Equals(Environment, other.Environment) && string.Equals(Type, other.Type) && string.Equals(Source, other.Source) && Tags.CollectionEquals(other.Tags) && string.Equals(Message, other.Message) && string.Equals(Geo, other.Geo) && Value == other.Value && Equals(Data, other.Data);
}

public override bool Equals(object obj) {
Expand All @@ -84,6 +98,9 @@ public override int GetHashCode() {
hashCode = (hashCode * 397) ^ (Geo == null ? 0 : Geo.GetHashCode());
hashCode = (hashCode * 397) ^ Value.GetHashCode();
hashCode = (hashCode * 397) ^ (Data == null ? 0 : Data.GetCollectionHashCode(_exclusions));
if (Environment != null) {
hashCode = (hashCode * 397) ^ Environment.GetHashCode();
}
return hashCode;
}
}
Expand Down Expand Up @@ -115,4 +132,4 @@ public static class KnownDataKeys {
public const string ManualStackingInfo = "@stack";
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Exceptionless.Plugins.Default {
[Priority(1)]
public sealed class DeploymentEnvironmentPlugin : IEventPlugin {
public void Run(EventPluginContext context) {
if (!context.Event.HasEnvironmentOverride)
context.Event.Environment = context.Client.Configuration.Environment;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ public void Run(EventPluginContext context) {
ctx.SetException(ex);

var serializer = context.Resolver.GetJsonSerializer();
context.Client.SubmitEvent(serializer.Deserialize(serializer.Serialize(context.Event), typeof(Event)) as Event, ctx);
var child = serializer.Deserialize(serializer.Serialize(context.Event), typeof(Event)) as Event;
if (child != null)
child.HasEnvironmentOverride = context.Event.HasEnvironmentOverride;
context.Client.SubmitEvent(child, ctx);
}

context.Cancel = true;
Expand Down
1 change: 1 addition & 0 deletions src/Exceptionless/Plugins/EventPluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static void Run(EventPluginContext context) {
}

public static void AddDefaultPlugins(ExceptionlessConfiguration config) {
config.AddPlugin<DeploymentEnvironmentPlugin>();
config.AddPlugin<HandleAggregateExceptionsPlugin>();
config.AddPlugin<EventExclusionPlugin>();
config.AddPlugin<ConfigurationDefaultsPlugin>();
Expand Down
18 changes: 18 additions & 0 deletions src/Exceptionless/Utility/DeploymentEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace Exceptionless.Utility {
internal static class DeploymentEnvironment {
public static string Normalize(string value) {
string name = value?.Trim();
if (String.IsNullOrEmpty(name) || name.Length > 64)
return null;

foreach (char character in name) {
if (Char.IsControl(character))
return null;
}

return name;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static IHostApplicationBuilder UseExceptionless(this IHostApplicationBuil
/// Adds the given pre-configured <see cref="ExceptionlessClient"/> to the host builder and registers lifecycle hooks.
/// </summary>
public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuilder builder, ExceptionlessClient client) {
client.Configuration.SetDefaultEnvironment(builder.Environment.EnvironmentName);
builder.Services.AddExceptionless(client);
builder.Services.AddExceptionlessLifetimeService();
return builder;
Expand Down Expand Up @@ -90,6 +91,7 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic
client.Configuration.ReadFromEnvironmentalVariables();

configure?.Invoke(client.Configuration);
client.Configuration.SetDefaultEnvironment(sp.GetService<IHostEnvironment>()?.EnvironmentName);
Comment thread
ejsmith marked this conversation as resolved.

return client;
});
Expand All @@ -110,6 +112,7 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic
client.Configuration.ReadFromConfiguration(configuration);

configure?.Invoke(client.Configuration);
client.Configuration.SetDefaultEnvironment(sp.GetService<IHostEnvironment>()?.EnvironmentName);

return client;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ private Event CreateSimpleEvent() {
var ev= new Event {
Date = DateTime.Now,
Message = "Testing",
Environment = "production",
Type = Event.KnownTypes.Log,
Source = "StorageSerializer"
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#if NET45
using System.Collections.Specialized;
using System.Configuration;
using System.IO;
using Xunit;

namespace Exceptionless.Tests.Configuration {
public class DeploymentEnvironmentConfigurationTests {
[Fact]
public void ReadFromConfigSection_LoadsEnvironmentAttribute() {
string path = Path.GetTempFileName();
try {
File.WriteAllText(path, "<configuration><configSections><section name=\"exceptionless\" type=\"Exceptionless.ExceptionlessSection, Exceptionless\" /></configSections><exceptionless apiKey=\"test\" environment=\" Staging \" /></configuration>");
var mapped = ConfigurationManager.OpenMappedExeConfiguration(new ExeConfigurationFileMap { ExeConfigFilename = path }, ConfigurationUserLevel.None);
using var client = new ExceptionlessClient();
client.Configuration.ReadFromConfigSection((ExceptionlessSection)mapped.GetSection("exceptionless"));
Assert.Equal("Staging", client.Configuration.Environment);
} finally {
File.Delete(path);
}
}

[Fact]
public void ReadFromConfigSection_MissingEnvironment_PreservesConfiguredValue() {
using var client = new ExceptionlessClient();
client.Configuration.Environment = "staging";
client.Configuration.ReadFromConfigSection(new ExceptionlessSection());
Assert.Equal("staging", client.Configuration.Environment);
}

[Fact]
public void ReadFromAppSettings_LoadsEnvironmentAndPreservesMissingSetting() {
using var client = new ExceptionlessClient();
client.Configuration.Environment = "staging";
client.Configuration.ReadFromAppSettings(new NameValueCollection());
Assert.Equal("staging", client.Configuration.Environment);
client.Configuration.ReadFromAppSettings(new NameValueCollection { ["Exceptionless:Environment"] = " Production " });
Assert.Equal("Production", client.Configuration.Environment);
}
}
}
#endif
1 change: 1 addition & 0 deletions test/Exceptionless.Tests/Exceptionless.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@

<ItemGroup Condition=" '$(TargetFramework)' == 'net472' ">
<Reference Include="System.Threading.Tasks" />
<Reference Include="System.Configuration" />
<Reference Include="System.Runtime" />
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
Expand Down
Loading