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();
```

You can also set `Exceptionless:Environment` in .NET configuration or the `Exceptionless__Environment` environment variable. A value set in code takes precedence; configuration and environment variables apply only when code has not set one. The hosting integration uses `IHostEnvironment.EnvironmentName` only when no environment is configured; a configured empty or invalid value leaves events unspecified instead. .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
31 changes: 31 additions & 0 deletions src/Exceptionless/Configuration/ExceptionlessConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,37 @@

namespace Exceptionless {
public class ExceptionlessConfiguration {
private string _environment;
private bool _isEnvironmentSetInCode;

/// <summary>The default deployment environment for every event. A value set here takes precedence over configuration files and environment variables.</summary>
public string Environment {
get => _environment;
set {
_environment = Utility.DeploymentEnvironment.Normalize(value);
IsEnvironmentConfigured = value != null;
_isEnvironmentSetInCode = 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 an environment from a configuration source unless one was set in code. Later sources replace earlier ones.</summary>
internal void SetConfiguredEnvironment(string environment) {
if (environment == null || _isEnvironmentSetInCode)
return;

_environment = Utility.DeploymentEnvironment.Normalize(environment);
IsEnvironmentConfigured = true;
}

/// <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.SetConfiguredEnvironment(section.Environment);

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

Expand Down Expand Up @@ -380,18 +393,24 @@ 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) {
config.SetConfiguredEnvironment(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 +431,7 @@ public static void ReadFromConfiguration(this ExceptionlessConfiguration config,
throw new ArgumentNullException(nameof(settings));

var section = settings.GetSection("Exceptionless");
config.SetConfiguredEnvironment(section["Environment"]);
if (Boolean.TryParse(section["Enabled"], out bool enabled) && !enabled)
config.Enabled = false;

Expand Down Expand Up @@ -483,6 +503,8 @@ 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) {
config.SetConfiguredEnvironment(GetEnvironmentalVariable("Exceptionless:Environment") ?? GetEnvironmentalVariable("Exceptionless__Environment"));

string apiKey = GetEnvironmentalVariable("Exceptionless:ApiKey") ?? GetEnvironmentalVariable("Exceptionless__ApiKey");
if (IsValidApiKey(apiKey))
config.ApiKey = apiKey;
Expand Down Expand Up @@ -576,4 +598,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,58 @@
#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() {
// Arrange
using var client = new ExceptionlessClient();

// Act
client.Configuration.ReadFromAppSettings(new NameValueCollection { ["Exceptionless:Environment"] = " Production " });
client.Configuration.ReadFromAppSettings(new NameValueCollection());

// Assert
Assert.Equal("Production", client.Configuration.Environment);
}

[Fact]
public void ReadFromAppSettings_EnvironmentSetInCode_KeepsCodeValue() {
// Arrange
using var client = new ExceptionlessClient();
client.Configuration.Environment = "staging";

// Act
client.Configuration.ReadFromAppSettings(new NameValueCollection { ["Exceptionless:Environment"] = "production" });

// Assert
Assert.Equal("staging", 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
Loading