diff --git a/README.md b/README.md index 63d5f844..11361029 100644 --- a/README.md +++ b/README.md @@ -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 `` 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) diff --git a/build/common.props b/build/common.props index 904a13ee..49ec682b 100644 --- a/build/common.props +++ b/build/common.props @@ -41,7 +41,7 @@ - + diff --git a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs index 348ddae2..27e5e0e5 100644 --- a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs +++ b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs @@ -13,6 +13,26 @@ namespace Exceptionless { public class ExceptionlessConfiguration { + private string _environment; + + /// The default deployment environment for every event. + public string Environment { + get => _environment; + set { + _environment = Utility.DeploymentEnvironment.Normalize(value); + IsEnvironmentConfigured = value != null; + } + } + + /// Whether an environment was explicitly configured, including an invalid value that remains unspecified. + public bool IsEnvironmentConfigured { get; private set; } + + /// Applies a deployment environment fallback without replacing an explicitly configured value. + 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"; diff --git a/src/Exceptionless/Configuration/ExceptionlessSection.cs b/src/Exceptionless/Configuration/ExceptionlessSection.cs index 9209a1ff..86e0b74f 100644 --- a/src/Exceptionless/Configuration/ExceptionlessSection.cs +++ b/src/Exceptionless/Configuration/ExceptionlessSection.cs @@ -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; } } diff --git a/src/Exceptionless/Extensions/EventBuilderExtensions.cs b/src/Exceptionless/Extensions/EventBuilderExtensions.cs index 58323a38..559ba4c4 100644 --- a/src/Exceptionless/Extensions/EventBuilderExtensions.cs +++ b/src/Exceptionless/Extensions/EventBuilderExtensions.cs @@ -4,6 +4,12 @@ namespace Exceptionless { public static class EventBuilderExtensions { + /// Overrides the deployment environment for this event. + public static EventBuilder SetEnvironment(this EventBuilder builder, string environment) { + builder.Target.Environment = environment; + return builder; + } + /// /// Sets the user's identity (ie. email address, username, user id) that the event happened to. /// @@ -109,4 +115,4 @@ public static EventBuilder AddRecentTraceLogEntries(this EventBuilder builder, D return builder; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs b/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs index 6050d569..46f76699 100644 --- a/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs +++ b/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs @@ -20,6 +20,7 @@ #endif #if NET45 +using System.Collections.Specialized; using System.Configuration; using Exceptionless.Extensions; using Exceptionless.Utility; @@ -27,6 +28,11 @@ namespace Exceptionless { public static class ExceptionlessConfigurationExtensions { + /// Sets the default deployment environment for every event. + public static void SetEnvironment(this ExceptionlessConfiguration config, string environment) { + config.Environment = environment; + } + private const string INSTALL_ID_KEY = "ExceptionlessInstallId"; /// @@ -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; @@ -380,18 +393,25 @@ public static void ReadFromConfigSection(this ExceptionlessConfiguration config) /// /// The configuration object you want to apply the attribute settings to. 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); @@ -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"]; + } if (Boolean.TryParse(section["Enabled"], out bool enabled) && !enabled) config.Enabled = false; @@ -483,6 +506,11 @@ public static void ReadFromConfiguration(this ExceptionlessConfiguration config, /// /// The configuration object you want to apply the attribute settings to. 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; @@ -576,4 +604,4 @@ private static bool IsValidApiKey(string apiKey) { return !String.IsNullOrEmpty(apiKey) && apiKey != "API_KEY_HERE"; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless/Models/Client/Event.cs b/src/Exceptionless/Models/Client/Event.cs index 31d233d9..23c3e77d 100644 --- a/src/Exceptionless/Models/Client/Event.cs +++ b/src/Exceptionless/Models/Client/Event.cs @@ -4,6 +4,20 @@ namespace Exceptionless.Models { [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class Event : IData { + private string _environment; + + /// The deployment environment, such as production or staging. + [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(); @@ -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) { @@ -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; } } @@ -115,4 +132,4 @@ public static class KnownDataKeys { public const string ManualStackingInfo = "@stack"; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless/Plugins/Default/001_DeploymentEnvironmentPlugin.cs b/src/Exceptionless/Plugins/Default/001_DeploymentEnvironmentPlugin.cs new file mode 100644 index 00000000..4c16f622 --- /dev/null +++ b/src/Exceptionless/Plugins/Default/001_DeploymentEnvironmentPlugin.cs @@ -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; + } + } +} diff --git a/src/Exceptionless/Plugins/Default/005_HandleAggregateExceptionsPlugin.cs b/src/Exceptionless/Plugins/Default/005_HandleAggregateExceptionsPlugin.cs index 245dc1a3..d3faae5e 100644 --- a/src/Exceptionless/Plugins/Default/005_HandleAggregateExceptionsPlugin.cs +++ b/src/Exceptionless/Plugins/Default/005_HandleAggregateExceptionsPlugin.cs @@ -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; diff --git a/src/Exceptionless/Plugins/EventPluginManager.cs b/src/Exceptionless/Plugins/EventPluginManager.cs index 3f9dfd1b..b9c36f7d 100644 --- a/src/Exceptionless/Plugins/EventPluginManager.cs +++ b/src/Exceptionless/Plugins/EventPluginManager.cs @@ -24,6 +24,7 @@ public static void Run(EventPluginContext context) { } public static void AddDefaultPlugins(ExceptionlessConfiguration config) { + config.AddPlugin(); config.AddPlugin(); config.AddPlugin(); config.AddPlugin(); diff --git a/src/Exceptionless/Utility/DeploymentEnvironment.cs b/src/Exceptionless/Utility/DeploymentEnvironment.cs new file mode 100644 index 00000000..743a1e39 --- /dev/null +++ b/src/Exceptionless/Utility/DeploymentEnvironment.cs @@ -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; + } + } +} diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs index b224111f..5ba0b6b9 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs @@ -30,6 +30,7 @@ public static IHostApplicationBuilder UseExceptionless(this IHostApplicationBuil /// Adds the given pre-configured to the host builder and registers lifecycle hooks. /// public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuilder builder, ExceptionlessClient client) { + client.Configuration.SetDefaultEnvironment(builder.Environment.EnvironmentName); builder.Services.AddExceptionless(client); builder.Services.AddExceptionlessLifetimeService(); return builder; @@ -90,6 +91,7 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic client.Configuration.ReadFromEnvironmentalVariables(); configure?.Invoke(client.Configuration); + client.Configuration.SetDefaultEnvironment(sp.GetService()?.EnvironmentName); return client; }); @@ -110,6 +112,7 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic client.Configuration.ReadFromConfiguration(configuration); configure?.Invoke(client.Configuration); + client.Configuration.SetDefaultEnvironment(sp.GetService()?.EnvironmentName); return client; }); diff --git a/test/Exceptionless.TestHarness/Serializer/StorageSerializerTestBase.cs b/test/Exceptionless.TestHarness/Serializer/StorageSerializerTestBase.cs index 39bb0db0..9044adc1 100644 --- a/test/Exceptionless.TestHarness/Serializer/StorageSerializerTestBase.cs +++ b/test/Exceptionless.TestHarness/Serializer/StorageSerializerTestBase.cs @@ -27,6 +27,7 @@ private Event CreateSimpleEvent() { var ev= new Event { Date = DateTime.Now, Message = "Testing", + Environment = "production", Type = Event.KnownTypes.Log, Source = "StorageSerializer" }; diff --git a/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs b/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs new file mode 100644 index 00000000..5ab8dce5 --- /dev/null +++ b/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs @@ -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, "
"); + 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 diff --git a/test/Exceptionless.Tests/Exceptionless.Tests.csproj b/test/Exceptionless.Tests/Exceptionless.Tests.csproj index e1c0134d..97f0ccb9 100644 --- a/test/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/test/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -51,6 +51,7 @@ + diff --git a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs index 6a783446..3db62467 100644 --- a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs +++ b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs @@ -1,12 +1,60 @@ #if NET10_0_OR_GREATER using System.Linq; +using Exceptionless.Configuration; +using Exceptionless.Dependency; using Exceptionless.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Moq; using Xunit; namespace Exceptionless.Tests.Platforms { public class HostingExtensionsTests { + [Theory] + [InlineData(null, "Staging")] + [InlineData("production", "production")] + [InlineData("", null)] + [InlineData(" ", null)] + [InlineData("prod\ninvalid", null)] + [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", null)] + public void AddExceptionless_DeploymentEnvironment_UsesHostAsFallback(string? configuredEnvironment, string? expectedEnvironment) { + var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { EnvironmentName = "Staging" }); + var client = new ExceptionlessClient(); + client.Configuration.Environment = configuredEnvironment; + builder.AddExceptionless(client); + + using var services = builder.Services.BuildServiceProvider(); + Assert.Equal(expectedEnvironment, services.GetRequiredService().Configuration.Environment); + } + + [Fact] + public void AddExceptionless_HostFallback_DoesNotBecomeExplicitConfiguration() { + using var client = new ExceptionlessClient(); + var staging = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { EnvironmentName = "Staging" }); + staging.AddExceptionless(client); + Assert.Equal("Staging", client.Configuration.Environment); + Assert.False(client.Configuration.IsEnvironmentConfigured); + + var production = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { EnvironmentName = "Production" }); + production.AddExceptionless(client); + Assert.Equal("Production", client.Configuration.Environment); + Assert.False(client.Configuration.IsEnvironmentConfigured); + } + + [Fact] + public void AddExceptionless_ProvidedClient_RemainsOwnedByCaller() { + var resolver = new Mock(); + using var client = new ExceptionlessClient(new ExceptionlessConfiguration(resolver.Object)); + var builder = Host.CreateApplicationBuilder(); + builder.AddExceptionless(client); + + using (var services = builder.Services.BuildServiceProvider()) { + Assert.Same(client, services.GetRequiredService()); + } + + resolver.Verify(r => r.Dispose(), Times.Never); + } + [Fact] public void AddExceptionless_WhenCalled_RegistersClientAndLifetimeService() { // Arrange diff --git a/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs b/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs new file mode 100644 index 00000000..491a6ffa --- /dev/null +++ b/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs @@ -0,0 +1,60 @@ +using Exceptionless.Models; +using Exceptionless.Plugins; +using Exceptionless.Plugins.Default; +using Xunit; + +namespace Exceptionless.Tests.Plugins { + public class DeploymentEnvironmentPluginTests : PluginTestBase { + public DeploymentEnvironmentPluginTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Run_DeploymentEnvironment_UsesDefaultAndPreservesEventOverride() { + var client = CreateClient(); + client.Configuration.SetEnvironment(" Production "); + var plugin = new DeploymentEnvironmentPlugin(); + var context = new EventPluginContext(client, new Event()); + plugin.Run(context); + Assert.Equal("Production", context.Event.Environment); + + var builder = client.CreateLog("test", "message").SetEnvironment(" Staging "); + var overridden = new EventPluginContext(client, builder.Target); + plugin.Run(overridden); + Assert.Equal("Staging", overridden.Event.Environment); + Assert.Empty(overridden.Event.Data); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("prod\ninvalid")] + [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] + public void Run_InvalidDeploymentEnvironment_DoesNotUseDefault(string environment) { + var client = CreateClient(); + client.Configuration.SetEnvironment("production"); + var builder = client.CreateLog("test", "message").SetEnvironment(environment); + var context = new EventPluginContext(client, builder.Target); + new DeploymentEnvironmentPlugin().Run(context); + Assert.Null(context.Event.Environment); + } + + [Theory] + [InlineData(null, "development", true)] + [InlineData("production", "production", false)] + [InlineData("", null, false)] + public void Pipeline_ExclusionCallbacks_SeeEffectiveEnvironment(string? environment, string? expectedEnvironment, bool cancelled) { + using var client = CreateClient(); + client.Configuration.Environment = "development"; + string? observedEnvironment = null; + client.Configuration.AddEventExclusion(ev => { + observedEnvironment = ev.Environment; + return ev.Environment != "development"; + }); + var context = new EventPluginContext(client, new Event { Type = Event.KnownTypes.FeatureUsage, Environment = environment }); + + EventPluginManager.Run(context); + + Assert.Equal(expectedEnvironment, observedEnvironment); + Assert.Equal(cancelled, context.Cancel); + } + } +} diff --git a/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs b/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs index 0f05689c..f6b42b24 100644 --- a/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs @@ -37,23 +37,30 @@ public void SingleInnerException() { Assert.True(context.Cancel); } - [Fact] - public async Task MultipleInnerException() { + [Theory] + [InlineData(null, "production")] + [InlineData("Staging", "Staging")] + [InlineData("", null)] + [InlineData("prod\ninvalid", null)] + [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", null)] + public async Task MultipleInnerException(string? environment, string? expectedEnvironment) { var submissionClient = new InMemorySubmissionClient(); - var client = new ExceptionlessClient("LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"); + using var client = new ExceptionlessClient("LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"); client.Configuration.Resolver.Register(submissionClient); + client.Configuration.Environment = "production"; var plugin = new HandleAggregateExceptionsPlugin(); var exceptionOne = new Exception("one"); var exceptionTwo = new Exception("two"); - var context = new EventPluginContext(client, new Event()); + var context = new EventPluginContext(client, new Event { Environment = environment }); context.ContextData.SetException(new AggregateException(exceptionOne, exceptionTwo)); plugin.Run(context); Assert.True(context.Cancel); await client.ProcessQueueAsync(); Assert.Equal(2, submissionClient.Events.Count); + Assert.All(submissionClient.Events, ev => Assert.Equal(expectedEnvironment, ev.Environment)); } } } diff --git a/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs b/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs index ef479fc0..35a73735 100644 --- a/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs @@ -14,6 +14,21 @@ namespace Exceptionless.Tests.Plugins { public class DuplicateCheckerPluginTests : PluginTestBase { public DuplicateCheckerPluginTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Run_SameEventAcrossEnvironments_OnlyMergesWithinEnvironment() { + var client = CreateClient(); + using (var plugin = new DuplicateCheckerPlugin(TimeSpan.FromMinutes(1))) { + foreach (bool duplicate in new[] { false, true }) { + foreach (string environment in new[] { "Production", "production", "staging", null }) { + var builder = client.CreateLog("Environment test").SetEnvironment(environment); + var context = new EventPluginContext(client, builder.Target, builder.PluginContextData); + plugin.Run(context); + Assert.Equal(duplicate, context.Cancel); + } + } + } + } [Fact] public void CanRemoveDuplicateExceptions() { diff --git a/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs b/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs index 3d20f279..ed05b026 100644 --- a/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs +++ b/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs @@ -6,6 +6,21 @@ namespace Exceptionless.Tests.Serializer.Models { public class EventSerializerTests : SerializerTestBase { + [Fact] + public void Serialize_DeploymentEnvironment_RoundTripsSeparatelyFromRuntimeMetadata() { + var model = new Event { Environment = " Production " }; + model.Data[Event.KnownDataKeys.EnvironmentInfo] = new EnvironmentInfo { MachineName = "worker-1" }; + string json = Serialize(model); + Assert.Contains("\"environment\":\"Production\"", json); + var result = Deserialize(json); + Assert.Equal("Production", result.Environment); + Assert.Equal("worker-1", result.GetEnvironmentInfo().MachineName); + Assert.Null(new Event { Environment = new string('x', 65) }.Environment); + Assert.Null(new Event { Environment = " " }.Environment); + Assert.NotEqual(new Event { Environment = "production" }, new Event { Environment = "staging" }); + Assert.NotEqual(new Event { Environment = "Production" }, new Event { Environment = "production" }); + } + /* lang=json */ private const string MinimalJson = """{"type":"log","source":"app","date":"0001-01-01T00:00:00+00:00","tags":[],"message":null,"geo":null,"value":null,"count":null,"data":{},"reference_id":null}"""; /* lang=json */