diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/404.html b/404.html new file mode 100644 index 0000000000..cecc877bd6 --- /dev/null +++ b/404.html @@ -0,0 +1,587 @@ + + + + + + + + + + 404 Error | elmah.io + + + + + + + + + + + + + + +
+
+ +
+
+

404 Seems like this page is missing

+

See if you can find what you were looking for in the left column.

+
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 0000000000..c0fe98de9b --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +docs.elmah.io \ No newline at end of file diff --git a/IntegrateWith/HipChat/index.html b/IntegrateWith/HipChat/index.html new file mode 100644 index 0000000000..85ed677b1b --- /dev/null +++ b/IntegrateWith/HipChat/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/IntegrateWith/Slack/index.html b/IntegrateWith/Slack/index.html new file mode 100644 index 0000000000..ffe82275ad --- /dev/null +++ b/IntegrateWith/Slack/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/IntegrateWith/Zapier/index.html b/IntegrateWith/Zapier/index.html new file mode 100644 index 0000000000..cc37a0b555 --- /dev/null +++ b/IntegrateWith/Zapier/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/adding-version-information/index.html b/adding-version-information/index.html new file mode 100644 index 0000000000..0a4786c1cd --- /dev/null +++ b/adding-version-information/index.html @@ -0,0 +1,783 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Adding Version Information + + + + + + + + + + + + + + + + + + +
+
+ + Documentation menu +
+
+ +

Adding Version Information

+ +

Almost every piece of software has some sort of version. Whether it's a nice-looking SemVer string or a simple timestamp, being able to distinguish one version from the other is important. elmah.io supports sending version information from your application in every message logged in two ways:

+
    +
  1. By adding the version manually (as explained in this document).
  2. +
  3. By using the Deployment Tracking feature (as explained in Set Up Deployment Tracking).
  4. +
+

Version Numbers on the UI

+

Let's start by looking at how version numbers are represented in the elmah.io UI. Every message contains a version property as illustrated below:

+

Error Details with Version Number

+

The error is logged by an application with version number 1.0.0. This way, you will be able to see which version of your software logged each message.

+

Having the version number on the message opens up some interesting search possibilities. Imagine that you want to search for every message logged by 1.0.* versions of your software, including release candidates, etc. Simply search in the search box like this:

+

Search for Versions

+

The example above finds every message logged from 1.0.0, 1.0.0-rc1, 1.0.1, etc.

+

Adding Version Numbers

+

How you choose to represent version numbers in your system is really up to you. elmah.io doesn't require SemVer, even though we strongly recommend you use it. Version is a simple string in our API, which means that you can send anything from SemVer to a stringified timestamp.

+

Adding a version number to every message logged in elmah.io is easy as 1-2-3. If you're using our API, there's a property named version where you can put the version of your application. Chances are that you are using one of the integrations for ASP.NET Core, log4net, or Serilog. There are multiple ways to send a version number to elmah.io.

+

ASP.NET Core

+

Adding version information to all errors logged from Elmah.Io.AspNetCore can be achieved using the OnMessage action when initializing logging to elmah.io:

+

builder.Services.AddElmahIo(options =>
+{
+    // ...
+
+    options.OnMessage = msg =>
+    {
+        msg.Version = "1.2.3";
+    };
+});
+

+

ASP.NET

+

You probably want to attach the same version number on every message logged in elmah.io. The easiest way to achieve that is to create a global event handler for the OnMessage event, which is triggered every time the elmah.io client logs a message to elmah.io:

+

Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client
+var logger = ErrorLog.Client;
+logger.OnMessage += (sender, args) =>
+{
+    args.Message.Version = "1.2.3";
+}
+

+

In the example, the message send off to elmah.io is decorated with the version number 1.2.3 You will need to replace this with the value of an app setting, the assembly info, or whatever strategy you've used to make the version number available to your code.

+

If you're logging errors to elmah.io in catch blocks, logging the version number can be done using a similar approach to the above:

+

try
+{
+    CallSomeBusinessLogic(inputValue);
+}
+catch (Exception e)
+{
+    e.Data.Add("X-ELMAHIO-VERSION", "1.2.3");
+    ErrorSignal.FromCurrentContext().Raise(e);
+}
+

+

In this case, the code at this point doesn't know anything about elmah.io. Luckily, there's an alternative to the Version property, by putting a custom element in the Data dictionary on Exception. The exact name of the key must be X-ELMAHIO-VERSION for elmah.io to interpret this as the version number.

+

Microsoft.Extensions.Logging

+

When using the Elmah.Io.Extensions.Logging package there are multiple ways of enriching log messages with version information. If you want the same version number on all log messages you can use the OnMessage action:

+

builder.Logging.AddElmahIo(options =>
+{
+    // ...
+
+    options.OnMessage = msg =>
+    {
+        msg.Version = "1.2.3";
+    };
+});
+

+

As an alternative, you can push a version property on individual log messages using either a structured property:

+

logger.LogInformation("Message from {version}", "1.2.3");
+

+

Or using scopes:

+

using (logger.BeginScope(new { Version = "1.2.3" }))
+{
+    logger.LogInformation("A message inside a logging scope");
+}
+

+

log4net

+

log4net supports the concept of customer properties in various ways. Since log4net properties are converted to custom properties in elmah.io, the easiest way to add a version number of every message logged through log4net is by configuring a global property somewhere in your initialization code:

+

log4net.GlobalContext.Properties["version"] = "1.2.3";
+

+

log4net supports custom properties in the context of a log call as well. To do that, put the version property in the ThreadContext before logging to log4net:

+

log4net.ThreadContext.Properties["version"] = "1.2.3";
+log4net.Error("This is an error message");
+

+

NLog

+

NLog supports structured properties as well as various context objects. To set a version number on all log messages you can include the following code after initializing NLog:

+

GlobalDiagnosticsContext.Set("version", "1.2.3");
+

+

If the elmah.io NLog target is initialized from C# code you can also use the OnMessage action:

+

var elmahIoTarget = new ElmahIoTarget();
+// ...
+elmahIoTarget.OnMessage = msg =>
+{
+    msg.Version = "1.2.3";
+};
+

+

To set a version number on a single log message, you can include it as a property:

+

log.Info("Message from {version}", "1.2.3");
+

+

Serilog

+

Serilog can decorate all log messages using enrichers:

+

var logger =
+    new LoggerConfiguration()
+        .Enrich.WithProperty("version", "1.2.3")
+        .Enrich.FromLogContext()
+        .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID")))
+        .CreateLogger();
+

+

You can also enrich a single log message with a version number using a structured property:

+

Log.Information("Meesage from {version}", "1.2.3");
+

+

Or using the LogContext:

+

using (LogContext.PushProperty("version", "1.2.3"))
+{
+    Log.Error("An error message");
+}
+

+ +
+

This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

+

+ See how we can help you monitor your website for crashes + Monitor your website +

+
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/allowing-elmah-io-uptime-agents/index.html b/allowing-elmah-io-uptime-agents/index.html new file mode 100644 index 0000000000..2cbf74d34f --- /dev/null +++ b/allowing-elmah-io-uptime-agents/index.html @@ -0,0 +1,687 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Allowing elmah.io uptime agents + + + + + + + + + + + + + + + + + + +
+
+ + Documentation menu +
+
+ +

Allowing elmah.io uptime agents

+

elmah.io Uptime Monitoring runs in 9 different regions. Depending on which regions you enabled on each uptime check, your network infrastructure/firewall, or security settings, you may need to allow requests from elmah.io uptime agents.

+

You can allow requests by allowing all requests with the following user-agent:

+

User-Agent: elmahio-uptimebot/2.0
+

+

Allowing requests by possible inbound IP addresses is also an option. Since we run on Azure these changes over time, but here's a list by region as of time of writing this article:

+

🇺🇸 United States West

+

23.100.40.244,23.100.40.246,23.100.40.2,23.100.32.186,40.83.253.229,138.91.92.9,138.91.172.82,138.91.230.36,138.91.228.104,23.100.46.198,40.112.243.45,20.190.0.233,20.190.1.8,20.190.1.21,20.190.1.32,20.190.1.42,20.190.1.61,20.190.1.72,20.190.0.33,20.190.1.162,20.190.1.177,20.190.1.187,20.190.1.191,52.149.26.109,52.149.26.223,52.149.26.248,52.149.27.21,52.149.27.137,52.149.28.241,51.143.61.29,52.137.93.170,52.149.29.95,52.149.29.238,52.149.30.76,52.149.30.96,20.190.1.217,20.190.2.43,20.190.2.66,52.143.80.96,52.156.145.74,52.156.145.106
+

+

🇺🇸 United States Central

+

13.86.38.119,13.89.106.62,40.89.243.166,52.143.244.58,52.154.168.87,52.154.169.9,52.154.169.57,52.154.169.86,52.154.169.106,52.154.169.204,52.154.170.25,52.154.170.83,52.154.170.158,52.154.171.81,52.154.171.88,52.154.173.21,40.89.244.137,52.154.173.79,52.154.173.101,52.154.173.180,52.154.174.243,52.154.175.58,52.154.240.147,52.154.240.170,52.154.241.97,52.154.241.132,52.154.241.246,52.154.43.251,52.154.44.21,52.154.44.95
+

+

🇺🇸 United States East

+

13.92.137.62,13.92.138.72,13.92.137.49,13.92.143.167,13.68.182.254,13.82.23.172,13.82.23.182,13.82.18.16,13.82.18.62,13.92.139.214,40.71.11.179,52.247.85.151,52.247.85.207,52.247.85.217,52.247.85.218,52.247.85.228,52.247.85.242,52.247.74.244,52.247.75.195,52.247.76.11,52.247.76.21,52.247.76.72,52.247.76.155,52.247.85.252,52.247.86.14,52.247.86.22,52.167.77.53,52.177.247.146,52.184.204.69,20.49.97.1
+

+

🇬🇧 United Kingdom South

+

51.140.180.76,51.140.191.115,51.140.176.159,51.140.176.11,51.140.185.39,51.140.177.87,51.140.185.119,51.140.188.39,51.140.188.241,51.140.183.68,51.140.184.173
+

+

🇬🇧 United Kingdom West

+

51.140.210.96,51.140.252.30,51.140.251.198,51.141.123.138,51.140.248.129,51.140.252.164,51.140.248.213,52.142.164.59,51.140.248.195,51.140.250.153
+

+

🇦🇺 Australia South East

+

191.239.187.197,191.239.187.149,191.239.184.74,191.239.188.25,52.189.210.149,52.189.209.158,52.189.215.220,52.189.208.40,40.127.95.32,40.115.76.80,40.115.76.83,40.115.76.93,40.115.76.94,40.115.76.101,40.115.76.109,40.115.76.129,40.115.76.143,191.239.188.11,13.77.50.103
+

+

🇪🇺 Europe North

+

40.127.186.114,40.127.183.46,40.127.189.87,40.127.183.64,40.115.126.151,137.135.216.255,40.85.101.214,40.85.101.216,40.85.101.219,40.127.139.252,13.69.228.38
+

+

🇪🇺 Europe West

+

20.71.75.100,20.71.75.133,20.71.75.198,20.71.75.237,20.71.76.1,20.71.76.23,20.71.76.57,20.71.76.171,20.71.76.196,20.71.77.10,20.71.77.83,20.71.77.134,20.71.77.205,20.71.78.20,20.71.78.208,20.71.78.243,20.71.79.77,20.71.79.165,20.71.79.228,20.73.232.14,20.73.232.51,20.73.232.121,20.73.232.130,20.73.232.163,20.73.232.186,20.73.232.217,20.73.232.241,20.73.132.133,20.73.132.167,20.73.133.173
+

+

🇧🇷 Brazil South

+

191.232.176.16,191.232.180.88,191.232.177.130,191.232.180.187,191.232.179.236,191.232.177.40,191.232.180.122
+

+ +
+

This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

+

+ See how we can help you monitor your website for crashes + Monitor your website +

+
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/asp-net-core-troubleshooting/index.html b/asp-net-core-troubleshooting/index.html new file mode 100644 index 0000000000..3a85bbc6dc --- /dev/null +++ b/asp-net-core-troubleshooting/index.html @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + ASP.NET Core Troubleshooting + + + + + + + + + + + + + + + + + + +
+
+ + Documentation menu +
+
+ +

ASP.NET Core Troubleshooting

+

So, your ASP.NET Core application doesn't log errors to elmah.io? Here is a list of things to try out:

+
    +
  • Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation.
  • +
  • Make sure to reference the most recent version of the Elmah.Io.AspNetCore NuGet package.
  • +
  • Make sure that the Elmah.Io.Client NuGet package is installed and that the major version matches that of Elmah.Io.AspNetCore.
  • +
  • Make sure that you are calling both the AddElmahIo- and UseElmahIo-methods in the Program.cs file (or Startup.cs for older applications), as described on Logging to elmah.io from ASP.NET Core.
  • +
  • Make sure that you call the UseElmahIo-method after invoking other Use* methods that in any way inspect exceptions (like UseDeveloperExceptionPage and UseExceptionHandler).
  • +
  • Make sure that you call the UseElmahIo-method before invoking UseMvc, UseEndpoints, and similar.
  • +
  • Make sure that your server has an outgoing internet connection and that it can communicate with api.elmah.io on port 443. The integration for ASP.NET Core support setting up an HTTP proxy if your server doesn't allow outgoing traffic. Check out Logging through a proxy for details.
  • +
  • Make sure that you didn't enable any Ignore filters or set up any Rules with an ignore action on the log in question.
  • +
  • Make sure that you don't have any code catching all exceptions happening in your system and ignoring them (could be a logging filter, a piece of middleware, or similar).
  • +
  • Make sure that you haven't reached the message limit included in your current plan. Your current usage can be viewed on the Subscription tab on organization settings.
  • +
+
+
+
+
+
+
Some of the bullets above have been implemented as Roslyn analyzers. Check out Roslyn analyzers for elmah.io and ASP.NET Core for details.
+
+
+ +

Common problems and how to fix them

+

Here you will a list of common exceptions and how to solve them.

+

InvalidOperationException

+

Exception

+

[InvalidOperationException: Unable to resolve service for type 'Elmah.Io.AspNetCore.IBackgroundTaskQueue' while attempting to activate 'Elmah.Io.AspNetCore.ElmahIoMiddleware'.]
+   Microsoft.Extensions.Internal.ActivatorUtilities+ConstructorMatcher.CreateInstance(IServiceProvider provider)
+   Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass4_0.<UseMiddleware>b__0(RequestDelegate next)
+   Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder.Build()
+   Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
+

+

Solution

+

You forgot to call the AddElmahIo-method in the Program.cs file:

+

builder.Services.AddElmahIo(o =>
+{
+    // ...
+});
+

+

ArgumentException

+

Exception

+

[ArgumentException: Input an API key Parameter name: apiKey]
+   Elmah.Io.AspNetCore.Extensions.StringExtensions.AssertApiKey(string apiKey)
+   Elmah.Io.AspNetCore.ElmahIoMiddleware..ctor(RequestDelegate next, IBackgroundTaskQueue queue, IOptions<ElmahIoOptions> options)
+   Microsoft.Extensions.Internal.ActivatorUtilities+ConstructorMatcher.CreateInstance(IServiceProvider provider)
+   Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
+   Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass4_0.<UseMiddleware>b__0(RequestDelegate next)
+   Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder.Build()
+   Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
+

+

Solution

+

You forgot to call the AddElmahIo-method in the Program.cs file:

+

builder.Services.AddElmahIo(o =>
+{
+    // ...
+});
+

+

or you called AddElmahIo without options and didn't provide these options elsewhere:

+

builder.Services.AddElmahIo();
+

+

Even though you configure elmah.io through appsettings.json you still need to call AddElmahIo. In this case, you can register ElmahIoOptions manually and use the empty AddElmahIo overload:

+

builder.Services.Configure<ElmahIoOptions>(Configuration.GetSection("ElmahIo"));
+builder.Services.AddElmahIo();
+

+

An error occurred while starting the application

+

If you see the error An error occurred while starting the application and the exception isn't logged to elmah.io, the error probably happens before hitting the elmah.io middleware. To help find out what is going on, add the following lines to your Program.cs file:

+

builder.WebHost.UseSetting(WebHostDefaults.DetailedErrorsKey, "true");
+builder.WebHost.CaptureStartupErrors(true);
+

+

URL missing when using Map

+

When handling requests with the Map method, ASP.NET Core will remove the path from HttpRequest.Path. In this case, Elmah.Io.AspNetCore will look for an URL in the HttpRequest.PathBase property. This is not already enough and won't always return the right URL. Consider using the MapWhen method instead.

+

Thread pool thread or asynchronous tasks blocked on a synchronous call

+

Azure and other systems with runtime diagnostics and validation may complain with the error Thread pool thread or asynchronous tasks blocked on a synchronous call in the Elmah.Io.AspNetCore package. This can be caused by our implementation of the package using a background worker for processing batches of messages. This background worker runs in a single thread and will never cause thread starvation as suggested by the error. We may want to move the internal implementation from BlockingCollection to Channel at some point.

+ +
+

This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

+

+ See how we can help you monitor your website for crashes + Monitor your website +

+
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/asp-net-troubleshooting/index.html b/asp-net-troubleshooting/index.html new file mode 100644 index 0000000000..aad26acae7 --- /dev/null +++ b/asp-net-troubleshooting/index.html @@ -0,0 +1,745 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + ASP.NET Troubleshooting + + + + + + + + + + + + + + + + + + +
+
+ + Documentation menu +
+
+ +

ASP.NET Troubleshooting

+

You are probably here because your application doesn't log errors to elmah.io, even though you installed the integration. Before contacting support, there are some things you can try out yourself.

+
    +
  • Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation.
  • +
  • Make sure that you are referencing one of the following NuGet packages: Elmah.Io, Elmah.Io.AspNet, Elmah.Io.Mvc or Elmah.Io.WebApi.
  • +
  • Make sure that the Elmah.Io.Client NuGet package is installed and that the major version matches that of Elmah.Io, Elmah.Io.AspNet, Elmah.Io.Mvc or Elmah.Io.WebApi.
  • +
  • Make sure that your project reference the following assemblies: Elmah, Elmah.Io, and Elmah.Io.Client.
  • +
  • Make sure that your web.config file contains valid config as described here. You can validate your web.config file using this Web.config Validator. When installing the Elmah.Io NuGet package, config is automatically added to your web.config file, as long as your Visual Studio allows for running PowerShell scripts as part of the installation. To check if you have the correct execution policy, go to the Package Manager Console and verify that the result of the following statement is RemoteSigned: Get-ExecutionPolicy
  • +
  • Make sure that your server has an outgoing internet connection and that it can communicate with api.elmah.io on port 443. Most of our integrations support setting up an HTTP proxy if your server doesn't allow outgoing traffic.
  • +
  • Make sure that you didn't enable any Ignore filters or set up any Rules with an ignore action on the log in question.
  • +
  • Make sure that you don't have any code catching all exceptions happening in your system and ignoring them (could be a logging filter or similar).
  • +
  • If you are using custom errors, make sure to configure it correctly. For more details, check out the following posts: Web.config customErrors element with ASP.NET explained and Demystifying ASP.NET MVC 5 Error Pages and Error Logging.
  • +
  • +
+

Common errors and how to fix them

+

Here you will a list of common errors/exceptions and how to solve them.

+

TypeLoadException

+

Exception

+

[TypeLoadException: Inheritance security rules violated by type: 'System.Net.Http.WebRequestHandler'. Derived types must either match the security accessibility of the base type or be less accessible.]
+   Microsoft.Rest.ServiceClient`1.CreateRootHandler() +0
+   Microsoft.Rest.ServiceClient`1..ctor(DelegatingHandler[] handlers) +59
+   Elmah.Io.Client.ElmahioAPI..ctor(DelegatingHandler[] handlers) +96
+   Elmah.Io.Client.ElmahioAPI..ctor(ServiceClientCredentials credentials, DelegatingHandler[] handlers) +70
+   Elmah.Io.Client.ElmahioAPI.Create(String apiKey, ElmahIoOptions options) +146
+   Elmah.Io.Client.ElmahioAPI.Create(String apiKey) +91
+   Elmah.Io.ErrorLog..ctor(IDictionary config) +109
+

+

Solution

+

This is most likely caused by a problem with the System.Net.Http NuGet package. Make sure to upgrade to the newest version (4.3.4 as of writing this). The default template for creating a new web application installs version 4.3.0 which is seriously flawed.

+

AmbiguousMatchException

+

Exception

+

[AmbiguousMatchException: Multiple custom attributes of the same type found.]
+   System.Attribute.GetCustomAttribute(Assembly element, Type attributeType, Boolean inherit) +119
+   System.Reflection.CustomAttributeExtensions.GetCustomAttribute(Assembly element, Type attributeType) +16
+   Microsoft.Rest.ServiceClient`1.get_FrameworkVersion() +226
+   Microsoft.Rest.ServiceClient`1.SetDefaultAgentInfo() +93
+   Microsoft.Rest.ServiceClient`1.InitializeHttpClient(HttpClient httpClient, HttpClientHandler httpClientHandler, DelegatingHandler[] handlers) +386
+   Microsoft.Rest.ServiceClient`1..ctor(HttpClient serviceHttpClient, HttpClientHandler rootHandler, Boolean disposeHttpClient, DelegatingHandler[] delHandlers) +82
+   Microsoft.Rest.ServiceClient`1..ctor(HttpClientHandler rootHandler, DelegatingHandler[] handlers) +66
+   Elmah.Io.Client.ElmahioAPI..ctor(HttpClientHandler rootHandler, DelegatingHandler[] handlers) +104
+   Elmah.Io.Client.ElmahioAPI..ctor(ServiceClientCredentials credentials, HttpClientHandler rootHandler, DelegatingHandler[] handlers) +78
+   Elmah.Io.ErrorLog..ctor(IDictionary config) +225
+
+[TargetInvocationException: Exception has been thrown by the target of an invocation.]
+   System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor) +0
+   System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) +359
+   System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes, StackCrawlMark& stackMark) +1485
+   System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes) +298
+   System.Activator.CreateInstance(Type type, Object[] args) +34
+   Elmah.ErrorLog.GetDefaultImpl(HttpContext context) +178
+   Elmah.ServiceCenter.GetService(Object context, Type serviceType) +17
+   Elmah.ErrorLog.GetDefault(HttpContext context) +34
+   Elmah.ErrorPageBase.get_ErrorLog() +39
+   Elmah.ErrorLogPage.OnLoad(EventArgs e) +400
+   System.Web.UI.Control.LoadRecursive() +154
+   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +4082
+

+

Solution

+

This is most likely caused by some other software installed on the machine hosting your website. Applications like Microsoft Monitoring Agent (Application Insights) are known for creating problems for other software running on the same machine. Pause or stop any other monitoring service to see if the problem goes away.

+

Exceptions aren't logged to elmah.io when adding the HandleError attribute

+

Much like custom errors, the HandleError attribute can swallow exceptions from your website. This means that ASP.NET MVC catches any exceptions and shows the Error.cshtml view. To log exceptions with this setup, you will need to extend your Error.cshtml file:

+

@model System.Web.Mvc.HandleErrorInfo
+
+@{
+    if (Model.Exception != null)
+    {
+        Elmah.ErrorLog.GetDefault(HttpContext.Current).Log(new Elmah.Error(Model.Exception, HttpContext.Current));
+    }
+}
+
+<div>
+    Your error page content goes here 
+</div> 
+

+

Errors are not logged when using update panels

+

ASP.NET Update Panels are great at many things. Unfortunately, one of those things is causing problems when trying to log server-side errors. If errors aren't logged as part of a call made from an update panel, you can try to add the following code to Global.asax.cs:

+

protected void Application_Error(object sender, EventArgs e)
+{
+    var ex = Server.GetLastError();
+    var error = new Elmah.Error(ex);
+    error.ServerVariables.Add("URL", HttpContext.Current?.Request?.Url?.AbsolutePath);
+    error.ServerVariables.Add("HTTP_USER_AGENT", HttpContext.Current?.Request?.UserAgent);
+    error.ServerVariables.Add("REMOTE_ADDR", HttpContext.Current?.Request?.UserHostAddress);
+    Elmah.ErrorLog.GetDefault(HttpContext.Current).Log(error);
+}
+

+

If you start experiencing errors logged multiple times, you can remove the ELMAH module from web.config:

+

<modules>
+    <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
+</modules>
+

+ +
+

This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

+

+ See how we can help you monitor your website for crashes + Monitor your website +

+
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/css/bootstrap-grid.css b/assets/css/bootstrap-grid.css new file mode 100644 index 0000000000..9cfa07ac50 --- /dev/null +++ b/assets/css/bootstrap-grid.css @@ -0,0 +1,3872 @@ +/*! + * Bootstrap Grid v4.5.3 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +html { + box-sizing: border-box; + -ms-overflow-style: scrollbar; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +.container, +.container-fluid, +.container-sm, +.container-md, +.container-lg, +.container-xl { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container, .container-sm { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container, .container-sm, .container-md { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container, .container-sm, .container-md, .container-lg { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container, .container-sm, .container-md, .container-lg, .container-xl { + max-width: 1140px; + } +} + +.row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.no-gutters { + margin-right: 0; + margin-left: 0; +} + +.no-gutters > .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, +.col-xl-auto { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; +} + +.row-cols-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.row-cols-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.row-cols-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.row-cols-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.row-cols-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; +} + +.row-cols-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; +} + +.col-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; +} + +.col-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.col-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; +} + +.col-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; +} + +.col-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; +} + +.col-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; +} + +.col-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; +} + +.col-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.order-first { + -ms-flex-order: -1; + order: -1; +} + +.order-last { + -ms-flex-order: 13; + order: 13; +} + +.order-0 { + -ms-flex-order: 0; + order: 0; +} + +.order-1 { + -ms-flex-order: 1; + order: 1; +} + +.order-2 { + -ms-flex-order: 2; + order: 2; +} + +.order-3 { + -ms-flex-order: 3; + order: 3; +} + +.order-4 { + -ms-flex-order: 4; + order: 4; +} + +.order-5 { + -ms-flex-order: 5; + order: 5; +} + +.order-6 { + -ms-flex-order: 6; + order: 6; +} + +.order-7 { + -ms-flex-order: 7; + order: 7; +} + +.order-8 { + -ms-flex-order: 8; + order: 8; +} + +.order-9 { + -ms-flex-order: 9; + order: 9; +} + +.order-10 { + -ms-flex-order: 10; + order: 10; +} + +.order-11 { + -ms-flex-order: 11; + order: 11; +} + +.order-12 { + -ms-flex-order: 12; + order: 12; +} + +.offset-1 { + margin-left: 8.333333%; +} + +.offset-2 { + margin-left: 16.666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.333333%; +} + +.offset-5 { + margin-left: 41.666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.333333%; +} + +.offset-8 { + margin-left: 66.666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.333333%; +} + +.offset-11 { + margin-left: 91.666667%; +} + +@media (min-width: 576px) { + .col-sm { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-sm-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-sm-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-sm-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-sm-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-sm-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-sm-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-sm-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-sm-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-sm-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-sm-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-sm-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-sm-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-sm-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-sm-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-first { + -ms-flex-order: -1; + order: -1; + } + .order-sm-last { + -ms-flex-order: 13; + order: 13; + } + .order-sm-0 { + -ms-flex-order: 0; + order: 0; + } + .order-sm-1 { + -ms-flex-order: 1; + order: 1; + } + .order-sm-2 { + -ms-flex-order: 2; + order: 2; + } + .order-sm-3 { + -ms-flex-order: 3; + order: 3; + } + .order-sm-4 { + -ms-flex-order: 4; + order: 4; + } + .order-sm-5 { + -ms-flex-order: 5; + order: 5; + } + .order-sm-6 { + -ms-flex-order: 6; + order: 6; + } + .order-sm-7 { + -ms-flex-order: 7; + order: 7; + } + .order-sm-8 { + -ms-flex-order: 8; + order: 8; + } + .order-sm-9 { + -ms-flex-order: 9; + order: 9; + } + .order-sm-10 { + -ms-flex-order: 10; + order: 10; + } + .order-sm-11 { + -ms-flex-order: 11; + order: 11; + } + .order-sm-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.333333%; + } + .offset-sm-2 { + margin-left: 16.666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.333333%; + } + .offset-sm-5 { + margin-left: 41.666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.333333%; + } + .offset-sm-8 { + margin-left: 66.666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.333333%; + } + .offset-sm-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 768px) { + .col-md { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-md-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-md-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-md-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-md-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-md-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-md-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-md-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-md-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-md-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-md-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-md-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-md-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-md-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-md-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-md-first { + -ms-flex-order: -1; + order: -1; + } + .order-md-last { + -ms-flex-order: 13; + order: 13; + } + .order-md-0 { + -ms-flex-order: 0; + order: 0; + } + .order-md-1 { + -ms-flex-order: 1; + order: 1; + } + .order-md-2 { + -ms-flex-order: 2; + order: 2; + } + .order-md-3 { + -ms-flex-order: 3; + order: 3; + } + .order-md-4 { + -ms-flex-order: 4; + order: 4; + } + .order-md-5 { + -ms-flex-order: 5; + order: 5; + } + .order-md-6 { + -ms-flex-order: 6; + order: 6; + } + .order-md-7 { + -ms-flex-order: 7; + order: 7; + } + .order-md-8 { + -ms-flex-order: 8; + order: 8; + } + .order-md-9 { + -ms-flex-order: 9; + order: 9; + } + .order-md-10 { + -ms-flex-order: 10; + order: 10; + } + .order-md-11 { + -ms-flex-order: 11; + order: 11; + } + .order-md-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.333333%; + } + .offset-md-2 { + margin-left: 16.666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.333333%; + } + .offset-md-5 { + margin-left: 41.666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.333333%; + } + .offset-md-8 { + margin-left: 66.666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.333333%; + } + .offset-md-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 992px) { + .col-lg { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-lg-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-lg-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-lg-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-lg-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-lg-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-lg-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-lg-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-lg-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-lg-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-lg-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-lg-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-lg-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-lg-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-lg-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-lg-first { + -ms-flex-order: -1; + order: -1; + } + .order-lg-last { + -ms-flex-order: 13; + order: 13; + } + .order-lg-0 { + -ms-flex-order: 0; + order: 0; + } + .order-lg-1 { + -ms-flex-order: 1; + order: 1; + } + .order-lg-2 { + -ms-flex-order: 2; + order: 2; + } + .order-lg-3 { + -ms-flex-order: 3; + order: 3; + } + .order-lg-4 { + -ms-flex-order: 4; + order: 4; + } + .order-lg-5 { + -ms-flex-order: 5; + order: 5; + } + .order-lg-6 { + -ms-flex-order: 6; + order: 6; + } + .order-lg-7 { + -ms-flex-order: 7; + order: 7; + } + .order-lg-8 { + -ms-flex-order: 8; + order: 8; + } + .order-lg-9 { + -ms-flex-order: 9; + order: 9; + } + .order-lg-10 { + -ms-flex-order: 10; + order: 10; + } + .order-lg-11 { + -ms-flex-order: 11; + order: 11; + } + .order-lg-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.333333%; + } + .offset-lg-2 { + margin-left: 16.666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.333333%; + } + .offset-lg-5 { + margin-left: 41.666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.333333%; + } + .offset-lg-8 { + margin-left: 66.666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.333333%; + } + .offset-lg-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-xl-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-xl-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-xl-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-xl-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-xl-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-xl-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-xl-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-xl-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-xl-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-xl-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-xl-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-xl-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-xl-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-xl-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-xl-first { + -ms-flex-order: -1; + order: -1; + } + .order-xl-last { + -ms-flex-order: 13; + order: 13; + } + .order-xl-0 { + -ms-flex-order: 0; + order: 0; + } + .order-xl-1 { + -ms-flex-order: 1; + order: 1; + } + .order-xl-2 { + -ms-flex-order: 2; + order: 2; + } + .order-xl-3 { + -ms-flex-order: 3; + order: 3; + } + .order-xl-4 { + -ms-flex-order: 4; + order: 4; + } + .order-xl-5 { + -ms-flex-order: 5; + order: 5; + } + .order-xl-6 { + -ms-flex-order: 6; + order: 6; + } + .order-xl-7 { + -ms-flex-order: 7; + order: 7; + } + .order-xl-8 { + -ms-flex-order: 8; + order: 8; + } + .order-xl-9 { + -ms-flex-order: 9; + order: 9; + } + .order-xl-10 { + -ms-flex-order: 10; + order: 10; + } + .order-xl-11 { + -ms-flex-order: 11; + order: 11; + } + .order-xl-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.333333%; + } + .offset-xl-2 { + margin-left: 16.666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.333333%; + } + .offset-xl-5 { + margin-left: 41.666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.333333%; + } + .offset-xl-8 { + margin-left: 66.666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.333333%; + } + .offset-xl-11 { + margin-left: 91.666667%; + } +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: -ms-flexbox !important; + display: flex !important; +} + +.d-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-sm-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-md-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-lg-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-xl-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media print { + .d-print-none { + display: none !important; + } + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-print-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +.flex-row { + -ms-flex-direction: row !important; + flex-direction: row !important; +} + +.flex-column { + -ms-flex-direction: column !important; + flex-direction: column !important; +} + +.flex-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; +} + +.flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; +} + +.flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; +} + +.flex-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; +} + +.flex-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; +} + +.flex-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; +} + +.flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; +} + +.justify-content-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; +} + +.justify-content-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; +} + +.justify-content-center { + -ms-flex-pack: center !important; + justify-content: center !important; +} + +.justify-content-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; +} + +.justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; +} + +.align-items-start { + -ms-flex-align: start !important; + align-items: flex-start !important; +} + +.align-items-end { + -ms-flex-align: end !important; + align-items: flex-end !important; +} + +.align-items-center { + -ms-flex-align: center !important; + align-items: center !important; +} + +.align-items-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; +} + +.align-items-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; +} + +.align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; +} + +.align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; +} + +.align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; +} + +.align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; +} + +.align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; +} + +.align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; +} + +.align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; +} + +.align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; +} + +.align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; +} + +.align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; +} + +.align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; +} + +.align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-sm-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-sm-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-sm-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-sm-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-sm-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-sm-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-sm-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-sm-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-sm-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-sm-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-sm-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-sm-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-sm-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-sm-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-sm-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-sm-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-sm-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-sm-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-sm-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-sm-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-sm-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-sm-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-sm-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-sm-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-sm-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-sm-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-sm-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 768px) { + .flex-md-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-md-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-md-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-md-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-md-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-md-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-md-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-md-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-md-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-md-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-md-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-md-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-md-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-md-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-md-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 992px) { + .flex-lg-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-lg-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-lg-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-lg-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-lg-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-lg-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-lg-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-lg-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-lg-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-lg-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-lg-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-lg-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-lg-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-lg-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 1200px) { + .flex-xl-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-xl-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-xl-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-xl-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-xl-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-xl-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-xl-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-xl-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-xl-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-xl-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-xl-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-xl-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-xl-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-xl-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-n1 { + margin: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.5rem !important; +} + +.m-n3 { + margin: -1rem !important; +} + +.mt-n3, +.my-n3 { + margin-top: -1rem !important; +} + +.mr-n3, +.mx-n3 { + margin-right: -1rem !important; +} + +.mb-n3, +.my-n3 { + margin-bottom: -1rem !important; +} + +.ml-n3, +.mx-n3 { + margin-left: -1rem !important; +} + +.m-n4 { + margin: -1.5rem !important; +} + +.mt-n4, +.my-n4 { + margin-top: -1.5rem !important; +} + +.mr-n4, +.mx-n4 { + margin-right: -1.5rem !important; +} + +.mb-n4, +.my-n4 { + margin-bottom: -1.5rem !important; +} + +.ml-n4, +.mx-n4 { + margin-left: -1.5rem !important; +} + +.m-n5 { + margin: -3rem !important; +} + +.mt-n5, +.my-n5 { + margin-top: -3rem !important; +} + +.mr-n5, +.mx-n5 { + margin-right: -3rem !important; +} + +.mb-n5, +.my-n5 { + margin-bottom: -3rem !important; +} + +.ml-n5, +.mx-n5 { + margin-left: -3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + .mt-sm-0, + .my-sm-0 { + margin-top: 0 !important; + } + .mr-sm-0, + .mx-sm-0 { + margin-right: 0 !important; + } + .mb-sm-0, + .my-sm-0 { + margin-bottom: 0 !important; + } + .ml-sm-0, + .mx-sm-0 { + margin-left: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .mt-sm-3, + .my-sm-3 { + margin-top: 1rem !important; + } + .mr-sm-3, + .mx-sm-3 { + margin-right: 1rem !important; + } + .mb-sm-3, + .my-sm-3 { + margin-bottom: 1rem !important; + } + .ml-sm-3, + .mx-sm-3 { + margin-left: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .mt-sm-4, + .my-sm-4 { + margin-top: 1.5rem !important; + } + .mr-sm-4, + .mx-sm-4 { + margin-right: 1.5rem !important; + } + .mb-sm-4, + .my-sm-4 { + margin-bottom: 1.5rem !important; + } + .ml-sm-4, + .mx-sm-4 { + margin-left: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .mt-sm-5, + .my-sm-5 { + margin-top: 3rem !important; + } + .mr-sm-5, + .mx-sm-5 { + margin-right: 3rem !important; + } + .mb-sm-5, + .my-sm-5 { + margin-bottom: 3rem !important; + } + .ml-sm-5, + .mx-sm-5 { + margin-left: 3rem !important; + } + .p-sm-0 { + padding: 0 !important; + } + .pt-sm-0, + .py-sm-0 { + padding-top: 0 !important; + } + .pr-sm-0, + .px-sm-0 { + padding-right: 0 !important; + } + .pb-sm-0, + .py-sm-0 { + padding-bottom: 0 !important; + } + .pl-sm-0, + .px-sm-0 { + padding-left: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .pt-sm-3, + .py-sm-3 { + padding-top: 1rem !important; + } + .pr-sm-3, + .px-sm-3 { + padding-right: 1rem !important; + } + .pb-sm-3, + .py-sm-3 { + padding-bottom: 1rem !important; + } + .pl-sm-3, + .px-sm-3 { + padding-left: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .pt-sm-4, + .py-sm-4 { + padding-top: 1.5rem !important; + } + .pr-sm-4, + .px-sm-4 { + padding-right: 1.5rem !important; + } + .pb-sm-4, + .py-sm-4 { + padding-bottom: 1.5rem !important; + } + .pl-sm-4, + .px-sm-4 { + padding-left: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .pt-sm-5, + .py-sm-5 { + padding-top: 3rem !important; + } + .pr-sm-5, + .px-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-5, + .py-sm-5 { + padding-bottom: 3rem !important; + } + .pl-sm-5, + .px-sm-5 { + padding-left: 3rem !important; + } + .m-sm-n1 { + margin: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.5rem !important; + } + .m-sm-n3 { + margin: -1rem !important; + } + .mt-sm-n3, + .my-sm-n3 { + margin-top: -1rem !important; + } + .mr-sm-n3, + .mx-sm-n3 { + margin-right: -1rem !important; + } + .mb-sm-n3, + .my-sm-n3 { + margin-bottom: -1rem !important; + } + .ml-sm-n3, + .mx-sm-n3 { + margin-left: -1rem !important; + } + .m-sm-n4 { + margin: -1.5rem !important; + } + .mt-sm-n4, + .my-sm-n4 { + margin-top: -1.5rem !important; + } + .mr-sm-n4, + .mx-sm-n4 { + margin-right: -1.5rem !important; + } + .mb-sm-n4, + .my-sm-n4 { + margin-bottom: -1.5rem !important; + } + .ml-sm-n4, + .mx-sm-n4 { + margin-left: -1.5rem !important; + } + .m-sm-n5 { + margin: -3rem !important; + } + .mt-sm-n5, + .my-sm-n5 { + margin-top: -3rem !important; + } + .mr-sm-n5, + .mx-sm-n5 { + margin-right: -3rem !important; + } + .mb-sm-n5, + .my-sm-n5 { + margin-bottom: -3rem !important; + } + .ml-sm-n5, + .mx-sm-n5 { + margin-left: -3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mt-sm-auto, + .my-sm-auto { + margin-top: auto !important; + } + .mr-sm-auto, + .mx-sm-auto { + margin-right: auto !important; + } + .mb-sm-auto, + .my-sm-auto { + margin-bottom: auto !important; + } + .ml-sm-auto, + .mx-sm-auto { + margin-left: auto !important; + } +} + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + .mt-md-0, + .my-md-0 { + margin-top: 0 !important; + } + .mr-md-0, + .mx-md-0 { + margin-right: 0 !important; + } + .mb-md-0, + .my-md-0 { + margin-bottom: 0 !important; + } + .ml-md-0, + .mx-md-0 { + margin-left: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .mt-md-3, + .my-md-3 { + margin-top: 1rem !important; + } + .mr-md-3, + .mx-md-3 { + margin-right: 1rem !important; + } + .mb-md-3, + .my-md-3 { + margin-bottom: 1rem !important; + } + .ml-md-3, + .mx-md-3 { + margin-left: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .mt-md-4, + .my-md-4 { + margin-top: 1.5rem !important; + } + .mr-md-4, + .mx-md-4 { + margin-right: 1.5rem !important; + } + .mb-md-4, + .my-md-4 { + margin-bottom: 1.5rem !important; + } + .ml-md-4, + .mx-md-4 { + margin-left: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .mt-md-5, + .my-md-5 { + margin-top: 3rem !important; + } + .mr-md-5, + .mx-md-5 { + margin-right: 3rem !important; + } + .mb-md-5, + .my-md-5 { + margin-bottom: 3rem !important; + } + .ml-md-5, + .mx-md-5 { + margin-left: 3rem !important; + } + .p-md-0 { + padding: 0 !important; + } + .pt-md-0, + .py-md-0 { + padding-top: 0 !important; + } + .pr-md-0, + .px-md-0 { + padding-right: 0 !important; + } + .pb-md-0, + .py-md-0 { + padding-bottom: 0 !important; + } + .pl-md-0, + .px-md-0 { + padding-left: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .pt-md-3, + .py-md-3 { + padding-top: 1rem !important; + } + .pr-md-3, + .px-md-3 { + padding-right: 1rem !important; + } + .pb-md-3, + .py-md-3 { + padding-bottom: 1rem !important; + } + .pl-md-3, + .px-md-3 { + padding-left: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .pt-md-4, + .py-md-4 { + padding-top: 1.5rem !important; + } + .pr-md-4, + .px-md-4 { + padding-right: 1.5rem !important; + } + .pb-md-4, + .py-md-4 { + padding-bottom: 1.5rem !important; + } + .pl-md-4, + .px-md-4 { + padding-left: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .pt-md-5, + .py-md-5 { + padding-top: 3rem !important; + } + .pr-md-5, + .px-md-5 { + padding-right: 3rem !important; + } + .pb-md-5, + .py-md-5 { + padding-bottom: 3rem !important; + } + .pl-md-5, + .px-md-5 { + padding-left: 3rem !important; + } + .m-md-n1 { + margin: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.5rem !important; + } + .m-md-n3 { + margin: -1rem !important; + } + .mt-md-n3, + .my-md-n3 { + margin-top: -1rem !important; + } + .mr-md-n3, + .mx-md-n3 { + margin-right: -1rem !important; + } + .mb-md-n3, + .my-md-n3 { + margin-bottom: -1rem !important; + } + .ml-md-n3, + .mx-md-n3 { + margin-left: -1rem !important; + } + .m-md-n4 { + margin: -1.5rem !important; + } + .mt-md-n4, + .my-md-n4 { + margin-top: -1.5rem !important; + } + .mr-md-n4, + .mx-md-n4 { + margin-right: -1.5rem !important; + } + .mb-md-n4, + .my-md-n4 { + margin-bottom: -1.5rem !important; + } + .ml-md-n4, + .mx-md-n4 { + margin-left: -1.5rem !important; + } + .m-md-n5 { + margin: -3rem !important; + } + .mt-md-n5, + .my-md-n5 { + margin-top: -3rem !important; + } + .mr-md-n5, + .mx-md-n5 { + margin-right: -3rem !important; + } + .mb-md-n5, + .my-md-n5 { + margin-bottom: -3rem !important; + } + .ml-md-n5, + .mx-md-n5 { + margin-left: -3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mt-md-auto, + .my-md-auto { + margin-top: auto !important; + } + .mr-md-auto, + .mx-md-auto { + margin-right: auto !important; + } + .mb-md-auto, + .my-md-auto { + margin-bottom: auto !important; + } + .ml-md-auto, + .mx-md-auto { + margin-left: auto !important; + } +} + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + .mt-lg-0, + .my-lg-0 { + margin-top: 0 !important; + } + .mr-lg-0, + .mx-lg-0 { + margin-right: 0 !important; + } + .mb-lg-0, + .my-lg-0 { + margin-bottom: 0 !important; + } + .ml-lg-0, + .mx-lg-0 { + margin-left: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .mt-lg-3, + .my-lg-3 { + margin-top: 1rem !important; + } + .mr-lg-3, + .mx-lg-3 { + margin-right: 1rem !important; + } + .mb-lg-3, + .my-lg-3 { + margin-bottom: 1rem !important; + } + .ml-lg-3, + .mx-lg-3 { + margin-left: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .mt-lg-4, + .my-lg-4 { + margin-top: 1.5rem !important; + } + .mr-lg-4, + .mx-lg-4 { + margin-right: 1.5rem !important; + } + .mb-lg-4, + .my-lg-4 { + margin-bottom: 1.5rem !important; + } + .ml-lg-4, + .mx-lg-4 { + margin-left: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .mt-lg-5, + .my-lg-5 { + margin-top: 3rem !important; + } + .mr-lg-5, + .mx-lg-5 { + margin-right: 3rem !important; + } + .mb-lg-5, + .my-lg-5 { + margin-bottom: 3rem !important; + } + .ml-lg-5, + .mx-lg-5 { + margin-left: 3rem !important; + } + .p-lg-0 { + padding: 0 !important; + } + .pt-lg-0, + .py-lg-0 { + padding-top: 0 !important; + } + .pr-lg-0, + .px-lg-0 { + padding-right: 0 !important; + } + .pb-lg-0, + .py-lg-0 { + padding-bottom: 0 !important; + } + .pl-lg-0, + .px-lg-0 { + padding-left: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .pt-lg-3, + .py-lg-3 { + padding-top: 1rem !important; + } + .pr-lg-3, + .px-lg-3 { + padding-right: 1rem !important; + } + .pb-lg-3, + .py-lg-3 { + padding-bottom: 1rem !important; + } + .pl-lg-3, + .px-lg-3 { + padding-left: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .pt-lg-4, + .py-lg-4 { + padding-top: 1.5rem !important; + } + .pr-lg-4, + .px-lg-4 { + padding-right: 1.5rem !important; + } + .pb-lg-4, + .py-lg-4 { + padding-bottom: 1.5rem !important; + } + .pl-lg-4, + .px-lg-4 { + padding-left: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .pt-lg-5, + .py-lg-5 { + padding-top: 3rem !important; + } + .pr-lg-5, + .px-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-5, + .py-lg-5 { + padding-bottom: 3rem !important; + } + .pl-lg-5, + .px-lg-5 { + padding-left: 3rem !important; + } + .m-lg-n1 { + margin: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.5rem !important; + } + .m-lg-n3 { + margin: -1rem !important; + } + .mt-lg-n3, + .my-lg-n3 { + margin-top: -1rem !important; + } + .mr-lg-n3, + .mx-lg-n3 { + margin-right: -1rem !important; + } + .mb-lg-n3, + .my-lg-n3 { + margin-bottom: -1rem !important; + } + .ml-lg-n3, + .mx-lg-n3 { + margin-left: -1rem !important; + } + .m-lg-n4 { + margin: -1.5rem !important; + } + .mt-lg-n4, + .my-lg-n4 { + margin-top: -1.5rem !important; + } + .mr-lg-n4, + .mx-lg-n4 { + margin-right: -1.5rem !important; + } + .mb-lg-n4, + .my-lg-n4 { + margin-bottom: -1.5rem !important; + } + .ml-lg-n4, + .mx-lg-n4 { + margin-left: -1.5rem !important; + } + .m-lg-n5 { + margin: -3rem !important; + } + .mt-lg-n5, + .my-lg-n5 { + margin-top: -3rem !important; + } + .mr-lg-n5, + .mx-lg-n5 { + margin-right: -3rem !important; + } + .mb-lg-n5, + .my-lg-n5 { + margin-bottom: -3rem !important; + } + .ml-lg-n5, + .mx-lg-n5 { + margin-left: -3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mt-lg-auto, + .my-lg-auto { + margin-top: auto !important; + } + .mr-lg-auto, + .mx-lg-auto { + margin-right: auto !important; + } + .mb-lg-auto, + .my-lg-auto { + margin-bottom: auto !important; + } + .ml-lg-auto, + .mx-lg-auto { + margin-left: auto !important; + } +} + +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + .mt-xl-0, + .my-xl-0 { + margin-top: 0 !important; + } + .mr-xl-0, + .mx-xl-0 { + margin-right: 0 !important; + } + .mb-xl-0, + .my-xl-0 { + margin-bottom: 0 !important; + } + .ml-xl-0, + .mx-xl-0 { + margin-left: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .mt-xl-3, + .my-xl-3 { + margin-top: 1rem !important; + } + .mr-xl-3, + .mx-xl-3 { + margin-right: 1rem !important; + } + .mb-xl-3, + .my-xl-3 { + margin-bottom: 1rem !important; + } + .ml-xl-3, + .mx-xl-3 { + margin-left: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .mt-xl-4, + .my-xl-4 { + margin-top: 1.5rem !important; + } + .mr-xl-4, + .mx-xl-4 { + margin-right: 1.5rem !important; + } + .mb-xl-4, + .my-xl-4 { + margin-bottom: 1.5rem !important; + } + .ml-xl-4, + .mx-xl-4 { + margin-left: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .mt-xl-5, + .my-xl-5 { + margin-top: 3rem !important; + } + .mr-xl-5, + .mx-xl-5 { + margin-right: 3rem !important; + } + .mb-xl-5, + .my-xl-5 { + margin-bottom: 3rem !important; + } + .ml-xl-5, + .mx-xl-5 { + margin-left: 3rem !important; + } + .p-xl-0 { + padding: 0 !important; + } + .pt-xl-0, + .py-xl-0 { + padding-top: 0 !important; + } + .pr-xl-0, + .px-xl-0 { + padding-right: 0 !important; + } + .pb-xl-0, + .py-xl-0 { + padding-bottom: 0 !important; + } + .pl-xl-0, + .px-xl-0 { + padding-left: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .pt-xl-3, + .py-xl-3 { + padding-top: 1rem !important; + } + .pr-xl-3, + .px-xl-3 { + padding-right: 1rem !important; + } + .pb-xl-3, + .py-xl-3 { + padding-bottom: 1rem !important; + } + .pl-xl-3, + .px-xl-3 { + padding-left: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .pt-xl-4, + .py-xl-4 { + padding-top: 1.5rem !important; + } + .pr-xl-4, + .px-xl-4 { + padding-right: 1.5rem !important; + } + .pb-xl-4, + .py-xl-4 { + padding-bottom: 1.5rem !important; + } + .pl-xl-4, + .px-xl-4 { + padding-left: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .pt-xl-5, + .py-xl-5 { + padding-top: 3rem !important; + } + .pr-xl-5, + .px-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-5, + .py-xl-5 { + padding-bottom: 3rem !important; + } + .pl-xl-5, + .px-xl-5 { + padding-left: 3rem !important; + } + .m-xl-n1 { + margin: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.5rem !important; + } + .m-xl-n3 { + margin: -1rem !important; + } + .mt-xl-n3, + .my-xl-n3 { + margin-top: -1rem !important; + } + .mr-xl-n3, + .mx-xl-n3 { + margin-right: -1rem !important; + } + .mb-xl-n3, + .my-xl-n3 { + margin-bottom: -1rem !important; + } + .ml-xl-n3, + .mx-xl-n3 { + margin-left: -1rem !important; + } + .m-xl-n4 { + margin: -1.5rem !important; + } + .mt-xl-n4, + .my-xl-n4 { + margin-top: -1.5rem !important; + } + .mr-xl-n4, + .mx-xl-n4 { + margin-right: -1.5rem !important; + } + .mb-xl-n4, + .my-xl-n4 { + margin-bottom: -1.5rem !important; + } + .ml-xl-n4, + .mx-xl-n4 { + margin-left: -1.5rem !important; + } + .m-xl-n5 { + margin: -3rem !important; + } + .mt-xl-n5, + .my-xl-n5 { + margin-top: -3rem !important; + } + .mr-xl-n5, + .mx-xl-n5 { + margin-right: -3rem !important; + } + .mb-xl-n5, + .my-xl-n5 { + margin-bottom: -3rem !important; + } + .ml-xl-n5, + .mx-xl-n5 { + margin-left: -3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mt-xl-auto, + .my-xl-auto { + margin-top: auto !important; + } + .mr-xl-auto, + .mx-xl-auto { + margin-right: auto !important; + } + .mb-xl-auto, + .my-xl-auto { + margin-bottom: auto !important; + } + .ml-xl-auto, + .mx-xl-auto { + margin-left: auto !important; + } +} +/*# sourceMappingURL=bootstrap-grid.css.map */ \ No newline at end of file diff --git a/assets/css/bootstrap-grid.css.map b/assets/css/bootstrap-grid.css.map new file mode 100644 index 0000000000..a664f9803a --- /dev/null +++ b/assets/css/bootstrap-grid.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","bootstrap-grid.css","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/_variables.scss","../../scss/mixins/_grid-framework.scss","../../scss/utilities/_display.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_spacing.scss"],"names":[],"mappings":"AAAA;;;;;ECKE;ADEF;EACE,sBAAsB;EACtB,6BAA6B;ACA/B;;ADGA;;;EAGE,mBAAmB;ACArB;;ACTE;;;;;;ECDA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;AFmBnB;;AGgCI;EFzCE;IACE,gBG+LG;EJlLT;AACF;;AG0BI;EFzCE;IACE,gBGgMG;EJ7KT;AACF;;AGoBI;EFzCE;IACE,gBGiMG;EJxKT;AACF;;AGcI;EFzCE;IACE,iBGkMI;EJnKV;AACF;;ACJE;ECnCA,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,mBAA0B;EAC1B,kBAAyB;AF2C3B;;ACLE;EACE,eAAe;EACf,cAAc;ADQlB;;ACVE;;EAMI,gBAAgB;EAChB,eAAe;ADSrB;;AK/DE;;;;;;EACE,kBAAkB;EAClB,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;ALuE7B;;AKjDM;EACE,0BAAa;EAAb,aAAa;EACb,oBAAY;EAAZ,YAAY;EACZ,eAAe;ALoDvB;;AK/CU;EHwBN,kBAAuB;EAAvB,cAAuB;EACvB,eAAwB;AF2B5B;;AKpDU;EHwBN,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AFgC5B;;AKzDU;EHwBN,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;AFqC5B;;AK9DU;EHwBN,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AF0C5B;;AKnEU;EHwBN,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AF+C5B;;AKxEU;EHwBN,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;AFoD5B;;AKvEM;EHCJ,kBAAc;EAAd,cAAc;EACd,WAAW;EACX,eAAe;AF0EjB;;AKvEU;EHbR,uBAAsC;EAAtC,mBAAsC;EAItC,oBAAuC;AFqFzC;;AK5EU;EHbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AF0FzC;;AKjFU;EHbR,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AF+FzC;;AKtFU;EHbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFoGzC;;AK3FU;EHbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFyGzC;;AKhGU;EHbR,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AF8GzC;;AKrGU;EHbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFmHzC;;AK1GU;EHbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFwHzC;;AK/GU;EHbR,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AF6HzC;;AKpHU;EHbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFkIzC;;AKzHU;EHbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AFuIzC;;AK9HU;EHbR,kBAAsC;EAAtC,cAAsC;EAItC,eAAuC;AF4IzC;;AK7HM;EAAwB,kBAAS;EAAT,SAAS;ALiIvC;;AK/HM;EAAuB,kBDmKG;ECnKH,SDmKG;AJhChC;;AKhIQ;EAAwB,iBADZ;EACY,QADZ;ALqIpB;;AKpIQ;EAAwB,iBADZ;EACY,QADZ;ALyIpB;;AKxIQ;EAAwB,iBADZ;EACY,QADZ;AL6IpB;;AK5IQ;EAAwB,iBADZ;EACY,QADZ;ALiJpB;;AKhJQ;EAAwB,iBADZ;EACY,QADZ;ALqJpB;;AKpJQ;EAAwB,iBADZ;EACY,QADZ;ALyJpB;;AKxJQ;EAAwB,iBADZ;EACY,QADZ;AL6JpB;;AK5JQ;EAAwB,iBADZ;EACY,QADZ;ALiKpB;;AKhKQ;EAAwB,iBADZ;EACY,QADZ;ALqKpB;;AKpKQ;EAAwB,iBADZ;EACY,QADZ;ALyKpB;;AKxKQ;EAAwB,kBADZ;EACY,SADZ;AL6KpB;;AK5KQ;EAAwB,kBADZ;EACY,SADZ;ALiLpB;;AKhLQ;EAAwB,kBADZ;EACY,SADZ;ALqLpB;;AK7KY;EHhBV,sBAA8C;AFiMhD;;AKjLY;EHhBV,uBAA8C;AFqMhD;;AKrLY;EHhBV,gBAA8C;AFyMhD;;AKzLY;EHhBV,uBAA8C;AF6MhD;;AK7LY;EHhBV,uBAA8C;AFiNhD;;AKjMY;EHhBV,gBAA8C;AFqNhD;;AKrMY;EHhBV,uBAA8C;AFyNhD;;AKzMY;EHhBV,uBAA8C;AF6NhD;;AK7MY;EHhBV,gBAA8C;AFiOhD;;AKjNY;EHhBV,uBAA8C;AFqOhD;;AKrNY;EHhBV,uBAA8C;AFyOhD;;AGpOI;EE3BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;ELmQrB;EK9PQ;IHwBN,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EFyO1B;EKlQQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF6O1B;EKtQQ;IHwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFiP1B;EK1QQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFqP1B;EK9QQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFyP1B;EKlRQ;IHwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF6P1B;EKhRI;IHCJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EFkRf;EK/QQ;IHbR,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EF4RvC;EKnRQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFgSvC;EKvRQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFoSvC;EK3RQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFwSvC;EK/RQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF4SvC;EKnSQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFgTvC;EKvSQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFoTvC;EK3SQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFwTvC;EK/SQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF4TvC;EKnTQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFgUvC;EKvTQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFoUvC;EK3TQ;IHbR,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EFwUvC;EKzTI;IAAwB,kBAAS;IAAT,SAAS;EL4TrC;EK1TI;IAAuB,kBDmKG;ICnKH,SDmKG;EJ0J9B;EK1TM;IAAwB,iBADZ;IACY,QADZ;EL8TlB;EK7TM;IAAwB,iBADZ;IACY,QADZ;ELiUlB;EKhUM;IAAwB,iBADZ;IACY,QADZ;ELoUlB;EKnUM;IAAwB,iBADZ;IACY,QADZ;ELuUlB;EKtUM;IAAwB,iBADZ;IACY,QADZ;EL0UlB;EKzUM;IAAwB,iBADZ;IACY,QADZ;EL6UlB;EK5UM;IAAwB,iBADZ;IACY,QADZ;ELgVlB;EK/UM;IAAwB,iBADZ;IACY,QADZ;ELmVlB;EKlVM;IAAwB,iBADZ;IACY,QADZ;ELsVlB;EKrVM;IAAwB,iBADZ;IACY,QADZ;ELyVlB;EKxVM;IAAwB,kBADZ;IACY,SADZ;EL4VlB;EK3VM;IAAwB,kBADZ;IACY,SADZ;EL+VlB;EK9VM;IAAwB,kBADZ;IACY,SADZ;ELkWlB;EK1VU;IHhBV,cAA4B;EF6W5B;EK7VU;IHhBV,sBAA8C;EFgX9C;EKhWU;IHhBV,uBAA8C;EFmX9C;EKnWU;IHhBV,gBAA8C;EFsX9C;EKtWU;IHhBV,uBAA8C;EFyX9C;EKzWU;IHhBV,uBAA8C;EF4X9C;EK5WU;IHhBV,gBAA8C;EF+X9C;EK/WU;IHhBV,uBAA8C;EFkY9C;EKlXU;IHhBV,uBAA8C;EFqY9C;EKrXU;IHhBV,gBAA8C;EFwY9C;EKxXU;IHhBV,uBAA8C;EF2Y9C;EK3XU;IHhBV,uBAA8C;EF8Y9C;AACF;;AG1YI;EE3BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;ELyarB;EKpaQ;IHwBN,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EF+Y1B;EKxaQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFmZ1B;EK5aQ;IHwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFuZ1B;EKhbQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF2Z1B;EKpbQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF+Z1B;EKxbQ;IHwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFma1B;EKtbI;IHCJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EFwbf;EKrbQ;IHbR,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EFkcvC;EKzbQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFscvC;EK7bQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF0cvC;EKjcQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF8cvC;EKrcQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFkdvC;EKzcQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFsdvC;EK7cQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF0dvC;EKjdQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF8dvC;EKrdQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFkevC;EKzdQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFsevC;EK7dQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF0evC;EKjeQ;IHbR,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EF8evC;EK/dI;IAAwB,kBAAS;IAAT,SAAS;ELkerC;EKheI;IAAuB,kBDmKG;ICnKH,SDmKG;EJgU9B;EKheM;IAAwB,iBADZ;IACY,QADZ;ELoelB;EKneM;IAAwB,iBADZ;IACY,QADZ;ELuelB;EKteM;IAAwB,iBADZ;IACY,QADZ;EL0elB;EKzeM;IAAwB,iBADZ;IACY,QADZ;EL6elB;EK5eM;IAAwB,iBADZ;IACY,QADZ;ELgflB;EK/eM;IAAwB,iBADZ;IACY,QADZ;ELmflB;EKlfM;IAAwB,iBADZ;IACY,QADZ;ELsflB;EKrfM;IAAwB,iBADZ;IACY,QADZ;ELyflB;EKxfM;IAAwB,iBADZ;IACY,QADZ;EL4flB;EK3fM;IAAwB,iBADZ;IACY,QADZ;EL+flB;EK9fM;IAAwB,kBADZ;IACY,SADZ;ELkgBlB;EKjgBM;IAAwB,kBADZ;IACY,SADZ;ELqgBlB;EKpgBM;IAAwB,kBADZ;IACY,SADZ;ELwgBlB;EKhgBU;IHhBV,cAA4B;EFmhB5B;EKngBU;IHhBV,sBAA8C;EFshB9C;EKtgBU;IHhBV,uBAA8C;EFyhB9C;EKzgBU;IHhBV,gBAA8C;EF4hB9C;EK5gBU;IHhBV,uBAA8C;EF+hB9C;EK/gBU;IHhBV,uBAA8C;EFkiB9C;EKlhBU;IHhBV,gBAA8C;EFqiB9C;EKrhBU;IHhBV,uBAA8C;EFwiB9C;EKxhBU;IHhBV,uBAA8C;EF2iB9C;EK3hBU;IHhBV,gBAA8C;EF8iB9C;EK9hBU;IHhBV,uBAA8C;EFijB9C;EKjiBU;IHhBV,uBAA8C;EFojB9C;AACF;;AGhjBI;EE3BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;EL+kBrB;EK1kBQ;IHwBN,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EFqjB1B;EK9kBQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFyjB1B;EKllBQ;IHwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF6jB1B;EKtlBQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFikB1B;EK1lBQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFqkB1B;EK9lBQ;IHwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFykB1B;EK5lBI;IHCJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EF8lBf;EK3lBQ;IHbR,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EFwmBvC;EK/lBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF4mBvC;EKnmBQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFgnBvC;EKvmBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFonBvC;EK3mBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFwnBvC;EK/mBQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF4nBvC;EKnnBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFgoBvC;EKvnBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFooBvC;EK3nBQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFwoBvC;EK/nBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF4oBvC;EKnoBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFgpBvC;EKvoBQ;IHbR,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EFopBvC;EKroBI;IAAwB,kBAAS;IAAT,SAAS;ELwoBrC;EKtoBI;IAAuB,kBDmKG;ICnKH,SDmKG;EJse9B;EKtoBM;IAAwB,iBADZ;IACY,QADZ;EL0oBlB;EKzoBM;IAAwB,iBADZ;IACY,QADZ;EL6oBlB;EK5oBM;IAAwB,iBADZ;IACY,QADZ;ELgpBlB;EK/oBM;IAAwB,iBADZ;IACY,QADZ;ELmpBlB;EKlpBM;IAAwB,iBADZ;IACY,QADZ;ELspBlB;EKrpBM;IAAwB,iBADZ;IACY,QADZ;ELypBlB;EKxpBM;IAAwB,iBADZ;IACY,QADZ;EL4pBlB;EK3pBM;IAAwB,iBADZ;IACY,QADZ;EL+pBlB;EK9pBM;IAAwB,iBADZ;IACY,QADZ;ELkqBlB;EKjqBM;IAAwB,iBADZ;IACY,QADZ;ELqqBlB;EKpqBM;IAAwB,kBADZ;IACY,SADZ;ELwqBlB;EKvqBM;IAAwB,kBADZ;IACY,SADZ;EL2qBlB;EK1qBM;IAAwB,kBADZ;IACY,SADZ;EL8qBlB;EKtqBU;IHhBV,cAA4B;EFyrB5B;EKzqBU;IHhBV,sBAA8C;EF4rB9C;EK5qBU;IHhBV,uBAA8C;EF+rB9C;EK/qBU;IHhBV,gBAA8C;EFksB9C;EKlrBU;IHhBV,uBAA8C;EFqsB9C;EKrrBU;IHhBV,uBAA8C;EFwsB9C;EKxrBU;IHhBV,gBAA8C;EF2sB9C;EK3rBU;IHhBV,uBAA8C;EF8sB9C;EK9rBU;IHhBV,uBAA8C;EFitB9C;EKjsBU;IHhBV,gBAA8C;EFotB9C;EKpsBU;IHhBV,uBAA8C;EFutB9C;EKvsBU;IHhBV,uBAA8C;EF0tB9C;AACF;;AGttBI;EE3BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;ELqvBrB;EKhvBQ;IHwBN,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EF2tB1B;EKpvBQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF+tB1B;EKxvBQ;IHwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EFmuB1B;EK5vBQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EFuuB1B;EKhwBQ;IHwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EF2uB1B;EKpwBQ;IHwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EF+uB1B;EKlwBI;IHCJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EFowBf;EKjwBQ;IHbR,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EF8wBvC;EKrwBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFkxBvC;EKzwBQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFsxBvC;EK7wBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF0xBvC;EKjxBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF8xBvC;EKrxBQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EFkyBvC;EKzxBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFsyBvC;EK7xBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EF0yBvC;EKjyBQ;IHbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EF8yBvC;EKryBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFkzBvC;EKzyBQ;IHbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EFszBvC;EK7yBQ;IHbR,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;EF0zBvC;EK3yBI;IAAwB,kBAAS;IAAT,SAAS;EL8yBrC;EK5yBI;IAAuB,kBDmKG;ICnKH,SDmKG;EJ4oB9B;EK5yBM;IAAwB,iBADZ;IACY,QADZ;ELgzBlB;EK/yBM;IAAwB,iBADZ;IACY,QADZ;ELmzBlB;EKlzBM;IAAwB,iBADZ;IACY,QADZ;ELszBlB;EKrzBM;IAAwB,iBADZ;IACY,QADZ;ELyzBlB;EKxzBM;IAAwB,iBADZ;IACY,QADZ;EL4zBlB;EK3zBM;IAAwB,iBADZ;IACY,QADZ;EL+zBlB;EK9zBM;IAAwB,iBADZ;IACY,QADZ;ELk0BlB;EKj0BM;IAAwB,iBADZ;IACY,QADZ;ELq0BlB;EKp0BM;IAAwB,iBADZ;IACY,QADZ;ELw0BlB;EKv0BM;IAAwB,iBADZ;IACY,QADZ;EL20BlB;EK10BM;IAAwB,kBADZ;IACY,SADZ;EL80BlB;EK70BM;IAAwB,kBADZ;IACY,SADZ;ELi1BlB;EKh1BM;IAAwB,kBADZ;IACY,SADZ;ELo1BlB;EK50BU;IHhBV,cAA4B;EF+1B5B;EK/0BU;IHhBV,sBAA8C;EFk2B9C;EKl1BU;IHhBV,uBAA8C;EFq2B9C;EKr1BU;IHhBV,gBAA8C;EFw2B9C;EKx1BU;IHhBV,uBAA8C;EF22B9C;EK31BU;IHhBV,uBAA8C;EF82B9C;EK91BU;IHhBV,gBAA8C;EFi3B9C;EKj2BU;IHhBV,uBAA8C;EFo3B9C;EKp2BU;IHhBV,uBAA8C;EFu3B9C;EKv2BU;IHhBV,gBAA8C;EF03B9C;EK12BU;IHhBV,uBAA8C;EF63B9C;EK72BU;IHhBV,uBAA8C;EFg4B9C;AACF;;AM76BM;EAAwB,wBAA0B;ANi7BxD;;AMj7BM;EAAwB,0BAA0B;ANq7BxD;;AMr7BM;EAAwB,gCAA0B;ANy7BxD;;AMz7BM;EAAwB,yBAA0B;AN67BxD;;AM77BM;EAAwB,yBAA0B;ANi8BxD;;AMj8BM;EAAwB,6BAA0B;ANq8BxD;;AMr8BM;EAAwB,8BAA0B;ANy8BxD;;AMz8BM;EAAwB,+BAA0B;EAA1B,wBAA0B;AN68BxD;;AM78BM;EAAwB,sCAA0B;EAA1B,+BAA0B;ANi9BxD;;AGh6BI;EGjDE;IAAwB,wBAA0B;ENs9BtD;EMt9BI;IAAwB,0BAA0B;ENy9BtD;EMz9BI;IAAwB,gCAA0B;EN49BtD;EM59BI;IAAwB,yBAA0B;EN+9BtD;EM/9BI;IAAwB,yBAA0B;ENk+BtD;EMl+BI;IAAwB,6BAA0B;ENq+BtD;EMr+BI;IAAwB,8BAA0B;ENw+BtD;EMx+BI;IAAwB,+BAA0B;IAA1B,wBAA0B;EN2+BtD;EM3+BI;IAAwB,sCAA0B;IAA1B,+BAA0B;EN8+BtD;AACF;;AG97BI;EGjDE;IAAwB,wBAA0B;ENo/BtD;EMp/BI;IAAwB,0BAA0B;ENu/BtD;EMv/BI;IAAwB,gCAA0B;EN0/BtD;EM1/BI;IAAwB,yBAA0B;EN6/BtD;EM7/BI;IAAwB,yBAA0B;ENggCtD;EMhgCI;IAAwB,6BAA0B;ENmgCtD;EMngCI;IAAwB,8BAA0B;ENsgCtD;EMtgCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENygCtD;EMzgCI;IAAwB,sCAA0B;IAA1B,+BAA0B;EN4gCtD;AACF;;AG59BI;EGjDE;IAAwB,wBAA0B;ENkhCtD;EMlhCI;IAAwB,0BAA0B;ENqhCtD;EMrhCI;IAAwB,gCAA0B;ENwhCtD;EMxhCI;IAAwB,yBAA0B;EN2hCtD;EM3hCI;IAAwB,yBAA0B;EN8hCtD;EM9hCI;IAAwB,6BAA0B;ENiiCtD;EMjiCI;IAAwB,8BAA0B;ENoiCtD;EMpiCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENuiCtD;EMviCI;IAAwB,sCAA0B;IAA1B,+BAA0B;EN0iCtD;AACF;;AG1/BI;EGjDE;IAAwB,wBAA0B;ENgjCtD;EMhjCI;IAAwB,0BAA0B;ENmjCtD;EMnjCI;IAAwB,gCAA0B;ENsjCtD;EMtjCI;IAAwB,yBAA0B;ENyjCtD;EMzjCI;IAAwB,yBAA0B;EN4jCtD;EM5jCI;IAAwB,6BAA0B;EN+jCtD;EM/jCI;IAAwB,8BAA0B;ENkkCtD;EMlkCI;IAAwB,+BAA0B;IAA1B,wBAA0B;ENqkCtD;EMrkCI;IAAwB,sCAA0B;IAA1B,+BAA0B;ENwkCtD;AACF;;AM/jCA;EAEI;IAAqB,wBAA0B;ENkkCjD;EMlkCE;IAAqB,0BAA0B;ENqkCjD;EMrkCE;IAAqB,gCAA0B;ENwkCjD;EMxkCE;IAAqB,yBAA0B;EN2kCjD;EM3kCE;IAAqB,yBAA0B;EN8kCjD;EM9kCE;IAAqB,6BAA0B;ENilCjD;EMjlCE;IAAqB,8BAA0B;ENolCjD;EMplCE;IAAqB,+BAA0B;IAA1B,wBAA0B;ENulCjD;EMvlCE;IAAqB,sCAA0B;IAA1B,+BAA0B;EN0lCjD;AACF;;AOxmCI;EAAgC,kCAA8B;EAA9B,8BAA8B;AP4mClE;;AO3mCI;EAAgC,qCAAiC;EAAjC,iCAAiC;AP+mCrE;;AO9mCI;EAAgC,0CAAsC;EAAtC,sCAAsC;APknC1E;;AOjnCI;EAAgC,6CAAyC;EAAzC,yCAAyC;APqnC7E;;AOnnCI;EAA8B,8BAA0B;EAA1B,0BAA0B;APunC5D;;AOtnCI;EAA8B,gCAA4B;EAA5B,4BAA4B;AP0nC9D;;AOznCI;EAA8B,sCAAkC;EAAlC,kCAAkC;AP6nCpE;;AO5nCI;EAA8B,6BAAyB;EAAzB,yBAAyB;APgoC3D;;AO/nCI;EAA8B,+BAAuB;EAAvB,uBAAuB;APmoCzD;;AOloCI;EAA8B,+BAAuB;EAAvB,uBAAuB;APsoCzD;;AOroCI;EAA8B,+BAAyB;EAAzB,yBAAyB;APyoC3D;;AOxoCI;EAA8B,+BAAyB;EAAzB,yBAAyB;AP4oC3D;;AO1oCI;EAAoC,+BAAsC;EAAtC,sCAAsC;AP8oC9E;;AO7oCI;EAAoC,6BAAoC;EAApC,oCAAoC;APipC5E;;AOhpCI;EAAoC,gCAAkC;EAAlC,kCAAkC;APopC1E;;AOnpCI;EAAoC,iCAAyC;EAAzC,yCAAyC;APupCjF;;AOtpCI;EAAoC,oCAAwC;EAAxC,wCAAwC;AP0pChF;;AOxpCI;EAAiC,gCAAkC;EAAlC,kCAAkC;AP4pCvE;;AO3pCI;EAAiC,8BAAgC;EAAhC,gCAAgC;AP+pCrE;;AO9pCI;EAAiC,iCAA8B;EAA9B,8BAA8B;APkqCnE;;AOjqCI;EAAiC,mCAAgC;EAAhC,gCAAgC;APqqCrE;;AOpqCI;EAAiC,kCAA+B;EAA/B,+BAA+B;APwqCpE;;AOtqCI;EAAkC,oCAAoC;EAApC,oCAAoC;AP0qC1E;;AOzqCI;EAAkC,kCAAkC;EAAlC,kCAAkC;AP6qCxE;;AO5qCI;EAAkC,qCAAgC;EAAhC,gCAAgC;APgrCtE;;AO/qCI;EAAkC,sCAAuC;EAAvC,uCAAuC;APmrC7E;;AOlrCI;EAAkC,yCAAsC;EAAtC,sCAAsC;APsrC5E;;AOrrCI;EAAkC,sCAAiC;EAAjC,iCAAiC;APyrCvE;;AOvrCI;EAAgC,oCAA2B;EAA3B,2BAA2B;AP2rC/D;;AO1rCI;EAAgC,qCAAiC;EAAjC,iCAAiC;AP8rCrE;;AO7rCI;EAAgC,mCAA+B;EAA/B,+BAA+B;APisCnE;;AOhsCI;EAAgC,sCAA6B;EAA7B,6BAA6B;APosCjE;;AOnsCI;EAAgC,wCAA+B;EAA/B,+BAA+B;APusCnE;;AOtsCI;EAAgC,uCAA8B;EAA9B,8BAA8B;AP0sClE;;AG9rCI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EPqvChE;EOpvCE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPuvCnE;EOtvCE;IAAgC,0CAAsC;IAAtC,sCAAsC;EPyvCxE;EOxvCE;IAAgC,6CAAyC;IAAzC,yCAAyC;EP2vC3E;EOzvCE;IAA8B,8BAA0B;IAA1B,0BAA0B;EP4vC1D;EO3vCE;IAA8B,gCAA4B;IAA5B,4BAA4B;EP8vC5D;EO7vCE;IAA8B,sCAAkC;IAAlC,kCAAkC;EPgwClE;EO/vCE;IAA8B,6BAAyB;IAAzB,yBAAyB;EPkwCzD;EOjwCE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPowCvD;EOnwCE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPswCvD;EOrwCE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPwwCzD;EOvwCE;IAA8B,+BAAyB;IAAzB,yBAAyB;EP0wCzD;EOxwCE;IAAoC,+BAAsC;IAAtC,sCAAsC;EP2wC5E;EO1wCE;IAAoC,6BAAoC;IAApC,oCAAoC;EP6wC1E;EO5wCE;IAAoC,gCAAkC;IAAlC,kCAAkC;EP+wCxE;EO9wCE;IAAoC,iCAAyC;IAAzC,yCAAyC;EPixC/E;EOhxCE;IAAoC,oCAAwC;IAAxC,wCAAwC;EPmxC9E;EOjxCE;IAAiC,gCAAkC;IAAlC,kCAAkC;EPoxCrE;EOnxCE;IAAiC,8BAAgC;IAAhC,gCAAgC;EPsxCnE;EOrxCE;IAAiC,iCAA8B;IAA9B,8BAA8B;EPwxCjE;EOvxCE;IAAiC,mCAAgC;IAAhC,gCAAgC;EP0xCnE;EOzxCE;IAAiC,kCAA+B;IAA/B,+BAA+B;EP4xClE;EO1xCE;IAAkC,oCAAoC;IAApC,oCAAoC;EP6xCxE;EO5xCE;IAAkC,kCAAkC;IAAlC,kCAAkC;EP+xCtE;EO9xCE;IAAkC,qCAAgC;IAAhC,gCAAgC;EPiyCpE;EOhyCE;IAAkC,sCAAuC;IAAvC,uCAAuC;EPmyC3E;EOlyCE;IAAkC,yCAAsC;IAAtC,sCAAsC;EPqyC1E;EOpyCE;IAAkC,sCAAiC;IAAjC,iCAAiC;EPuyCrE;EOryCE;IAAgC,oCAA2B;IAA3B,2BAA2B;EPwyC7D;EOvyCE;IAAgC,qCAAiC;IAAjC,iCAAiC;EP0yCnE;EOzyCE;IAAgC,mCAA+B;IAA/B,+BAA+B;EP4yCjE;EO3yCE;IAAgC,sCAA6B;IAA7B,6BAA6B;EP8yC/D;EO7yCE;IAAgC,wCAA+B;IAA/B,+BAA+B;EPgzCjE;EO/yCE;IAAgC,uCAA8B;IAA9B,8BAA8B;EPkzChE;AACF;;AGvyCI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EP81ChE;EO71CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPg2CnE;EO/1CE;IAAgC,0CAAsC;IAAtC,sCAAsC;EPk2CxE;EOj2CE;IAAgC,6CAAyC;IAAzC,yCAAyC;EPo2C3E;EOl2CE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPq2C1D;EOp2CE;IAA8B,gCAA4B;IAA5B,4BAA4B;EPu2C5D;EOt2CE;IAA8B,sCAAkC;IAAlC,kCAAkC;EPy2ClE;EOx2CE;IAA8B,6BAAyB;IAAzB,yBAAyB;EP22CzD;EO12CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP62CvD;EO52CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP+2CvD;EO92CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPi3CzD;EOh3CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPm3CzD;EOj3CE;IAAoC,+BAAsC;IAAtC,sCAAsC;EPo3C5E;EOn3CE;IAAoC,6BAAoC;IAApC,oCAAoC;EPs3C1E;EOr3CE;IAAoC,gCAAkC;IAAlC,kCAAkC;EPw3CxE;EOv3CE;IAAoC,iCAAyC;IAAzC,yCAAyC;EP03C/E;EOz3CE;IAAoC,oCAAwC;IAAxC,wCAAwC;EP43C9E;EO13CE;IAAiC,gCAAkC;IAAlC,kCAAkC;EP63CrE;EO53CE;IAAiC,8BAAgC;IAAhC,gCAAgC;EP+3CnE;EO93CE;IAAiC,iCAA8B;IAA9B,8BAA8B;EPi4CjE;EOh4CE;IAAiC,mCAAgC;IAAhC,gCAAgC;EPm4CnE;EOl4CE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPq4ClE;EOn4CE;IAAkC,oCAAoC;IAApC,oCAAoC;EPs4CxE;EOr4CE;IAAkC,kCAAkC;IAAlC,kCAAkC;EPw4CtE;EOv4CE;IAAkC,qCAAgC;IAAhC,gCAAgC;EP04CpE;EOz4CE;IAAkC,sCAAuC;IAAvC,uCAAuC;EP44C3E;EO34CE;IAAkC,yCAAsC;IAAtC,sCAAsC;EP84C1E;EO74CE;IAAkC,sCAAiC;IAAjC,iCAAiC;EPg5CrE;EO94CE;IAAgC,oCAA2B;IAA3B,2BAA2B;EPi5C7D;EOh5CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPm5CnE;EOl5CE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPq5CjE;EOp5CE;IAAgC,sCAA6B;IAA7B,6BAA6B;EPu5C/D;EOt5CE;IAAgC,wCAA+B;IAA/B,+BAA+B;EPy5CjE;EOx5CE;IAAgC,uCAA8B;IAA9B,8BAA8B;EP25ChE;AACF;;AGh5CI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EPu8ChE;EOt8CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPy8CnE;EOx8CE;IAAgC,0CAAsC;IAAtC,sCAAsC;EP28CxE;EO18CE;IAAgC,6CAAyC;IAAzC,yCAAyC;EP68C3E;EO38CE;IAA8B,8BAA0B;IAA1B,0BAA0B;EP88C1D;EO78CE;IAA8B,gCAA4B;IAA5B,4BAA4B;EPg9C5D;EO/8CE;IAA8B,sCAAkC;IAAlC,kCAAkC;EPk9ClE;EOj9CE;IAA8B,6BAAyB;IAAzB,yBAAyB;EPo9CzD;EOn9CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPs9CvD;EOr9CE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPw9CvD;EOv9CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EP09CzD;EOz9CE;IAA8B,+BAAyB;IAAzB,yBAAyB;EP49CzD;EO19CE;IAAoC,+BAAsC;IAAtC,sCAAsC;EP69C5E;EO59CE;IAAoC,6BAAoC;IAApC,oCAAoC;EP+9C1E;EO99CE;IAAoC,gCAAkC;IAAlC,kCAAkC;EPi+CxE;EOh+CE;IAAoC,iCAAyC;IAAzC,yCAAyC;EPm+C/E;EOl+CE;IAAoC,oCAAwC;IAAxC,wCAAwC;EPq+C9E;EOn+CE;IAAiC,gCAAkC;IAAlC,kCAAkC;EPs+CrE;EOr+CE;IAAiC,8BAAgC;IAAhC,gCAAgC;EPw+CnE;EOv+CE;IAAiC,iCAA8B;IAA9B,8BAA8B;EP0+CjE;EOz+CE;IAAiC,mCAAgC;IAAhC,gCAAgC;EP4+CnE;EO3+CE;IAAiC,kCAA+B;IAA/B,+BAA+B;EP8+ClE;EO5+CE;IAAkC,oCAAoC;IAApC,oCAAoC;EP++CxE;EO9+CE;IAAkC,kCAAkC;IAAlC,kCAAkC;EPi/CtE;EOh/CE;IAAkC,qCAAgC;IAAhC,gCAAgC;EPm/CpE;EOl/CE;IAAkC,sCAAuC;IAAvC,uCAAuC;EPq/C3E;EOp/CE;IAAkC,yCAAsC;IAAtC,sCAAsC;EPu/C1E;EOt/CE;IAAkC,sCAAiC;IAAjC,iCAAiC;EPy/CrE;EOv/CE;IAAgC,oCAA2B;IAA3B,2BAA2B;EP0/C7D;EOz/CE;IAAgC,qCAAiC;IAAjC,iCAAiC;EP4/CnE;EO3/CE;IAAgC,mCAA+B;IAA/B,+BAA+B;EP8/CjE;EO7/CE;IAAgC,sCAA6B;IAA7B,6BAA6B;EPggD/D;EO//CE;IAAgC,wCAA+B;IAA/B,+BAA+B;EPkgDjE;EOjgDE;IAAgC,uCAA8B;IAA9B,8BAA8B;EPogDhE;AACF;;AGz/CI;EIlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;EPgjDhE;EO/iDE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPkjDnE;EOjjDE;IAAgC,0CAAsC;IAAtC,sCAAsC;EPojDxE;EOnjDE;IAAgC,6CAAyC;IAAzC,yCAAyC;EPsjD3E;EOpjDE;IAA8B,8BAA0B;IAA1B,0BAA0B;EPujD1D;EOtjDE;IAA8B,gCAA4B;IAA5B,4BAA4B;EPyjD5D;EOxjDE;IAA8B,sCAAkC;IAAlC,kCAAkC;EP2jDlE;EO1jDE;IAA8B,6BAAyB;IAAzB,yBAAyB;EP6jDzD;EO5jDE;IAA8B,+BAAuB;IAAvB,uBAAuB;EP+jDvD;EO9jDE;IAA8B,+BAAuB;IAAvB,uBAAuB;EPikDvD;EOhkDE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPmkDzD;EOlkDE;IAA8B,+BAAyB;IAAzB,yBAAyB;EPqkDzD;EOnkDE;IAAoC,+BAAsC;IAAtC,sCAAsC;EPskD5E;EOrkDE;IAAoC,6BAAoC;IAApC,oCAAoC;EPwkD1E;EOvkDE;IAAoC,gCAAkC;IAAlC,kCAAkC;EP0kDxE;EOzkDE;IAAoC,iCAAyC;IAAzC,yCAAyC;EP4kD/E;EO3kDE;IAAoC,oCAAwC;IAAxC,wCAAwC;EP8kD9E;EO5kDE;IAAiC,gCAAkC;IAAlC,kCAAkC;EP+kDrE;EO9kDE;IAAiC,8BAAgC;IAAhC,gCAAgC;EPilDnE;EOhlDE;IAAiC,iCAA8B;IAA9B,8BAA8B;EPmlDjE;EOllDE;IAAiC,mCAAgC;IAAhC,gCAAgC;EPqlDnE;EOplDE;IAAiC,kCAA+B;IAA/B,+BAA+B;EPulDlE;EOrlDE;IAAkC,oCAAoC;IAApC,oCAAoC;EPwlDxE;EOvlDE;IAAkC,kCAAkC;IAAlC,kCAAkC;EP0lDtE;EOzlDE;IAAkC,qCAAgC;IAAhC,gCAAgC;EP4lDpE;EO3lDE;IAAkC,sCAAuC;IAAvC,uCAAuC;EP8lD3E;EO7lDE;IAAkC,yCAAsC;IAAtC,sCAAsC;EPgmD1E;EO/lDE;IAAkC,sCAAiC;IAAjC,iCAAiC;EPkmDrE;EOhmDE;IAAgC,oCAA2B;IAA3B,2BAA2B;EPmmD7D;EOlmDE;IAAgC,qCAAiC;IAAjC,iCAAiC;EPqmDnE;EOpmDE;IAAgC,mCAA+B;IAA/B,+BAA+B;EPumDjE;EOtmDE;IAAgC,sCAA6B;IAA7B,6BAA6B;EPymD/D;EOxmDE;IAAgC,wCAA+B;IAA/B,+BAA+B;EP2mDjE;EO1mDE;IAAgC,uCAA8B;IAA9B,8BAA8B;EP6mDhE;AACF;;AQppDQ;EAAgC,oBAA4B;ARwpDpE;;AQvpDQ;;EAEE,wBAAoC;AR0pD9C;;AQxpDQ;;EAEE,0BAAwC;AR2pDlD;;AQzpDQ;;EAEE,2BAA0C;AR4pDpD;;AQ1pDQ;;EAEE,yBAAsC;AR6pDhD;;AQ5qDQ;EAAgC,0BAA4B;ARgrDpE;;AQ/qDQ;;EAEE,8BAAoC;ARkrD9C;;AQhrDQ;;EAEE,gCAAwC;ARmrDlD;;AQjrDQ;;EAEE,iCAA0C;ARorDpD;;AQlrDQ;;EAEE,+BAAsC;ARqrDhD;;AQpsDQ;EAAgC,yBAA4B;ARwsDpE;;AQvsDQ;;EAEE,6BAAoC;AR0sD9C;;AQxsDQ;;EAEE,+BAAwC;AR2sDlD;;AQzsDQ;;EAEE,gCAA0C;AR4sDpD;;AQ1sDQ;;EAEE,8BAAsC;AR6sDhD;;AQ5tDQ;EAAgC,uBAA4B;ARguDpE;;AQ/tDQ;;EAEE,2BAAoC;ARkuD9C;;AQhuDQ;;EAEE,6BAAwC;ARmuDlD;;AQjuDQ;;EAEE,8BAA0C;ARouDpD;;AQluDQ;;EAEE,4BAAsC;ARquDhD;;AQpvDQ;EAAgC,yBAA4B;ARwvDpE;;AQvvDQ;;EAEE,6BAAoC;AR0vD9C;;AQxvDQ;;EAEE,+BAAwC;AR2vDlD;;AQzvDQ;;EAEE,gCAA0C;AR4vDpD;;AQ1vDQ;;EAEE,8BAAsC;AR6vDhD;;AQ5wDQ;EAAgC,uBAA4B;ARgxDpE;;AQ/wDQ;;EAEE,2BAAoC;ARkxD9C;;AQhxDQ;;EAEE,6BAAwC;ARmxDlD;;AQjxDQ;;EAEE,8BAA0C;ARoxDpD;;AQlxDQ;;EAEE,4BAAsC;ARqxDhD;;AQpyDQ;EAAgC,qBAA4B;ARwyDpE;;AQvyDQ;;EAEE,yBAAoC;AR0yD9C;;AQxyDQ;;EAEE,2BAAwC;AR2yDlD;;AQzyDQ;;EAEE,4BAA0C;AR4yDpD;;AQ1yDQ;;EAEE,0BAAsC;AR6yDhD;;AQ5zDQ;EAAgC,2BAA4B;ARg0DpE;;AQ/zDQ;;EAEE,+BAAoC;ARk0D9C;;AQh0DQ;;EAEE,iCAAwC;ARm0DlD;;AQj0DQ;;EAEE,kCAA0C;ARo0DpD;;AQl0DQ;;EAEE,gCAAsC;ARq0DhD;;AQp1DQ;EAAgC,0BAA4B;ARw1DpE;;AQv1DQ;;EAEE,8BAAoC;AR01D9C;;AQx1DQ;;EAEE,gCAAwC;AR21DlD;;AQz1DQ;;EAEE,iCAA0C;AR41DpD;;AQ11DQ;;EAEE,+BAAsC;AR61DhD;;AQ52DQ;EAAgC,wBAA4B;ARg3DpE;;AQ/2DQ;;EAEE,4BAAoC;ARk3D9C;;AQh3DQ;;EAEE,8BAAwC;ARm3DlD;;AQj3DQ;;EAEE,+BAA0C;ARo3DpD;;AQl3DQ;;EAEE,6BAAsC;ARq3DhD;;AQp4DQ;EAAgC,0BAA4B;ARw4DpE;;AQv4DQ;;EAEE,8BAAoC;AR04D9C;;AQx4DQ;;EAEE,gCAAwC;AR24DlD;;AQz4DQ;;EAEE,iCAA0C;AR44DpD;;AQ14DQ;;EAEE,+BAAsC;AR64DhD;;AQ55DQ;EAAgC,wBAA4B;ARg6DpE;;AQ/5DQ;;EAEE,4BAAoC;ARk6D9C;;AQh6DQ;;EAEE,8BAAwC;ARm6DlD;;AQj6DQ;;EAEE,+BAA0C;ARo6DpD;;AQl6DQ;;EAEE,6BAAsC;ARq6DhD;;AQ75DQ;EAAwB,2BAA2B;ARi6D3D;;AQh6DQ;;EAEE,+BAA+B;ARm6DzC;;AQj6DQ;;EAEE,iCAAiC;ARo6D3C;;AQl6DQ;;EAEE,kCAAkC;ARq6D5C;;AQn6DQ;;EAEE,gCAAgC;ARs6D1C;;AQr7DQ;EAAwB,0BAA2B;ARy7D3D;;AQx7DQ;;EAEE,8BAA+B;AR27DzC;;AQz7DQ;;EAEE,gCAAiC;AR47D3C;;AQ17DQ;;EAEE,iCAAkC;AR67D5C;;AQ37DQ;;EAEE,+BAAgC;AR87D1C;;AQ78DQ;EAAwB,wBAA2B;ARi9D3D;;AQh9DQ;;EAEE,4BAA+B;ARm9DzC;;AQj9DQ;;EAEE,8BAAiC;ARo9D3C;;AQl9DQ;;EAEE,+BAAkC;ARq9D5C;;AQn9DQ;;EAEE,6BAAgC;ARs9D1C;;AQr+DQ;EAAwB,0BAA2B;ARy+D3D;;AQx+DQ;;EAEE,8BAA+B;AR2+DzC;;AQz+DQ;;EAEE,gCAAiC;AR4+D3C;;AQ1+DQ;;EAEE,iCAAkC;AR6+D5C;;AQ3+DQ;;EAEE,+BAAgC;AR8+D1C;;AQ7/DQ;EAAwB,wBAA2B;ARigE3D;;AQhgEQ;;EAEE,4BAA+B;ARmgEzC;;AQjgEQ;;EAEE,8BAAiC;ARogE3C;;AQlgEQ;;EAEE,+BAAkC;ARqgE5C;;AQngEQ;;EAEE,6BAAgC;ARsgE1C;;AQhgEI;EAAmB,uBAAuB;ARogE9C;;AQngEI;;EAEE,2BAA2B;ARsgEjC;;AQpgEI;;EAEE,6BAA6B;ARugEnC;;AQrgEI;;EAEE,8BAA8B;ARwgEpC;;AQtgEI;;EAEE,4BAA4B;ARygElC;;AGlhEI;EKlDI;IAAgC,oBAA4B;ERykElE;EQxkEM;;IAEE,wBAAoC;ER0kE5C;EQxkEM;;IAEE,0BAAwC;ER0kEhD;EQxkEM;;IAEE,2BAA0C;ER0kElD;EQxkEM;;IAEE,yBAAsC;ER0kE9C;EQzlEM;IAAgC,0BAA4B;ER4lElE;EQ3lEM;;IAEE,8BAAoC;ER6lE5C;EQ3lEM;;IAEE,gCAAwC;ER6lEhD;EQ3lEM;;IAEE,iCAA0C;ER6lElD;EQ3lEM;;IAEE,+BAAsC;ER6lE9C;EQ5mEM;IAAgC,yBAA4B;ER+mElE;EQ9mEM;;IAEE,6BAAoC;ERgnE5C;EQ9mEM;;IAEE,+BAAwC;ERgnEhD;EQ9mEM;;IAEE,gCAA0C;ERgnElD;EQ9mEM;;IAEE,8BAAsC;ERgnE9C;EQ/nEM;IAAgC,uBAA4B;ERkoElE;EQjoEM;;IAEE,2BAAoC;ERmoE5C;EQjoEM;;IAEE,6BAAwC;ERmoEhD;EQjoEM;;IAEE,8BAA0C;ERmoElD;EQjoEM;;IAEE,4BAAsC;ERmoE9C;EQlpEM;IAAgC,yBAA4B;ERqpElE;EQppEM;;IAEE,6BAAoC;ERspE5C;EQppEM;;IAEE,+BAAwC;ERspEhD;EQppEM;;IAEE,gCAA0C;ERspElD;EQppEM;;IAEE,8BAAsC;ERspE9C;EQrqEM;IAAgC,uBAA4B;ERwqElE;EQvqEM;;IAEE,2BAAoC;ERyqE5C;EQvqEM;;IAEE,6BAAwC;ERyqEhD;EQvqEM;;IAEE,8BAA0C;ERyqElD;EQvqEM;;IAEE,4BAAsC;ERyqE9C;EQxrEM;IAAgC,qBAA4B;ER2rElE;EQ1rEM;;IAEE,yBAAoC;ER4rE5C;EQ1rEM;;IAEE,2BAAwC;ER4rEhD;EQ1rEM;;IAEE,4BAA0C;ER4rElD;EQ1rEM;;IAEE,0BAAsC;ER4rE9C;EQ3sEM;IAAgC,2BAA4B;ER8sElE;EQ7sEM;;IAEE,+BAAoC;ER+sE5C;EQ7sEM;;IAEE,iCAAwC;ER+sEhD;EQ7sEM;;IAEE,kCAA0C;ER+sElD;EQ7sEM;;IAEE,gCAAsC;ER+sE9C;EQ9tEM;IAAgC,0BAA4B;ERiuElE;EQhuEM;;IAEE,8BAAoC;ERkuE5C;EQhuEM;;IAEE,gCAAwC;ERkuEhD;EQhuEM;;IAEE,iCAA0C;ERkuElD;EQhuEM;;IAEE,+BAAsC;ERkuE9C;EQjvEM;IAAgC,wBAA4B;ERovElE;EQnvEM;;IAEE,4BAAoC;ERqvE5C;EQnvEM;;IAEE,8BAAwC;ERqvEhD;EQnvEM;;IAEE,+BAA0C;ERqvElD;EQnvEM;;IAEE,6BAAsC;ERqvE9C;EQpwEM;IAAgC,0BAA4B;ERuwElE;EQtwEM;;IAEE,8BAAoC;ERwwE5C;EQtwEM;;IAEE,gCAAwC;ERwwEhD;EQtwEM;;IAEE,iCAA0C;ERwwElD;EQtwEM;;IAEE,+BAAsC;ERwwE9C;EQvxEM;IAAgC,wBAA4B;ER0xElE;EQzxEM;;IAEE,4BAAoC;ER2xE5C;EQzxEM;;IAEE,8BAAwC;ER2xEhD;EQzxEM;;IAEE,+BAA0C;ER2xElD;EQzxEM;;IAEE,6BAAsC;ER2xE9C;EQnxEM;IAAwB,2BAA2B;ERsxEzD;EQrxEM;;IAEE,+BAA+B;ERuxEvC;EQrxEM;;IAEE,iCAAiC;ERuxEzC;EQrxEM;;IAEE,kCAAkC;ERuxE1C;EQrxEM;;IAEE,gCAAgC;ERuxExC;EQtyEM;IAAwB,0BAA2B;ERyyEzD;EQxyEM;;IAEE,8BAA+B;ER0yEvC;EQxyEM;;IAEE,gCAAiC;ER0yEzC;EQxyEM;;IAEE,iCAAkC;ER0yE1C;EQxyEM;;IAEE,+BAAgC;ER0yExC;EQzzEM;IAAwB,wBAA2B;ER4zEzD;EQ3zEM;;IAEE,4BAA+B;ER6zEvC;EQ3zEM;;IAEE,8BAAiC;ER6zEzC;EQ3zEM;;IAEE,+BAAkC;ER6zE1C;EQ3zEM;;IAEE,6BAAgC;ER6zExC;EQ50EM;IAAwB,0BAA2B;ER+0EzD;EQ90EM;;IAEE,8BAA+B;ERg1EvC;EQ90EM;;IAEE,gCAAiC;ERg1EzC;EQ90EM;;IAEE,iCAAkC;ERg1E1C;EQ90EM;;IAEE,+BAAgC;ERg1ExC;EQ/1EM;IAAwB,wBAA2B;ERk2EzD;EQj2EM;;IAEE,4BAA+B;ERm2EvC;EQj2EM;;IAEE,8BAAiC;ERm2EzC;EQj2EM;;IAEE,+BAAkC;ERm2E1C;EQj2EM;;IAEE,6BAAgC;ERm2ExC;EQ71EE;IAAmB,uBAAuB;ERg2E5C;EQ/1EE;;IAEE,2BAA2B;ERi2E/B;EQ/1EE;;IAEE,6BAA6B;ERi2EjC;EQ/1EE;;IAEE,8BAA8B;ERi2ElC;EQ/1EE;;IAEE,4BAA4B;ERi2EhC;AACF;;AG32EI;EKlDI;IAAgC,oBAA4B;ERk6ElE;EQj6EM;;IAEE,wBAAoC;ERm6E5C;EQj6EM;;IAEE,0BAAwC;ERm6EhD;EQj6EM;;IAEE,2BAA0C;ERm6ElD;EQj6EM;;IAEE,yBAAsC;ERm6E9C;EQl7EM;IAAgC,0BAA4B;ERq7ElE;EQp7EM;;IAEE,8BAAoC;ERs7E5C;EQp7EM;;IAEE,gCAAwC;ERs7EhD;EQp7EM;;IAEE,iCAA0C;ERs7ElD;EQp7EM;;IAEE,+BAAsC;ERs7E9C;EQr8EM;IAAgC,yBAA4B;ERw8ElE;EQv8EM;;IAEE,6BAAoC;ERy8E5C;EQv8EM;;IAEE,+BAAwC;ERy8EhD;EQv8EM;;IAEE,gCAA0C;ERy8ElD;EQv8EM;;IAEE,8BAAsC;ERy8E9C;EQx9EM;IAAgC,uBAA4B;ER29ElE;EQ19EM;;IAEE,2BAAoC;ER49E5C;EQ19EM;;IAEE,6BAAwC;ER49EhD;EQ19EM;;IAEE,8BAA0C;ER49ElD;EQ19EM;;IAEE,4BAAsC;ER49E9C;EQ3+EM;IAAgC,yBAA4B;ER8+ElE;EQ7+EM;;IAEE,6BAAoC;ER++E5C;EQ7+EM;;IAEE,+BAAwC;ER++EhD;EQ7+EM;;IAEE,gCAA0C;ER++ElD;EQ7+EM;;IAEE,8BAAsC;ER++E9C;EQ9/EM;IAAgC,uBAA4B;ERigFlE;EQhgFM;;IAEE,2BAAoC;ERkgF5C;EQhgFM;;IAEE,6BAAwC;ERkgFhD;EQhgFM;;IAEE,8BAA0C;ERkgFlD;EQhgFM;;IAEE,4BAAsC;ERkgF9C;EQjhFM;IAAgC,qBAA4B;ERohFlE;EQnhFM;;IAEE,yBAAoC;ERqhF5C;EQnhFM;;IAEE,2BAAwC;ERqhFhD;EQnhFM;;IAEE,4BAA0C;ERqhFlD;EQnhFM;;IAEE,0BAAsC;ERqhF9C;EQpiFM;IAAgC,2BAA4B;ERuiFlE;EQtiFM;;IAEE,+BAAoC;ERwiF5C;EQtiFM;;IAEE,iCAAwC;ERwiFhD;EQtiFM;;IAEE,kCAA0C;ERwiFlD;EQtiFM;;IAEE,gCAAsC;ERwiF9C;EQvjFM;IAAgC,0BAA4B;ER0jFlE;EQzjFM;;IAEE,8BAAoC;ER2jF5C;EQzjFM;;IAEE,gCAAwC;ER2jFhD;EQzjFM;;IAEE,iCAA0C;ER2jFlD;EQzjFM;;IAEE,+BAAsC;ER2jF9C;EQ1kFM;IAAgC,wBAA4B;ER6kFlE;EQ5kFM;;IAEE,4BAAoC;ER8kF5C;EQ5kFM;;IAEE,8BAAwC;ER8kFhD;EQ5kFM;;IAEE,+BAA0C;ER8kFlD;EQ5kFM;;IAEE,6BAAsC;ER8kF9C;EQ7lFM;IAAgC,0BAA4B;ERgmFlE;EQ/lFM;;IAEE,8BAAoC;ERimF5C;EQ/lFM;;IAEE,gCAAwC;ERimFhD;EQ/lFM;;IAEE,iCAA0C;ERimFlD;EQ/lFM;;IAEE,+BAAsC;ERimF9C;EQhnFM;IAAgC,wBAA4B;ERmnFlE;EQlnFM;;IAEE,4BAAoC;ERonF5C;EQlnFM;;IAEE,8BAAwC;ERonFhD;EQlnFM;;IAEE,+BAA0C;ERonFlD;EQlnFM;;IAEE,6BAAsC;ERonF9C;EQ5mFM;IAAwB,2BAA2B;ER+mFzD;EQ9mFM;;IAEE,+BAA+B;ERgnFvC;EQ9mFM;;IAEE,iCAAiC;ERgnFzC;EQ9mFM;;IAEE,kCAAkC;ERgnF1C;EQ9mFM;;IAEE,gCAAgC;ERgnFxC;EQ/nFM;IAAwB,0BAA2B;ERkoFzD;EQjoFM;;IAEE,8BAA+B;ERmoFvC;EQjoFM;;IAEE,gCAAiC;ERmoFzC;EQjoFM;;IAEE,iCAAkC;ERmoF1C;EQjoFM;;IAEE,+BAAgC;ERmoFxC;EQlpFM;IAAwB,wBAA2B;ERqpFzD;EQppFM;;IAEE,4BAA+B;ERspFvC;EQppFM;;IAEE,8BAAiC;ERspFzC;EQppFM;;IAEE,+BAAkC;ERspF1C;EQppFM;;IAEE,6BAAgC;ERspFxC;EQrqFM;IAAwB,0BAA2B;ERwqFzD;EQvqFM;;IAEE,8BAA+B;ERyqFvC;EQvqFM;;IAEE,gCAAiC;ERyqFzC;EQvqFM;;IAEE,iCAAkC;ERyqF1C;EQvqFM;;IAEE,+BAAgC;ERyqFxC;EQxrFM;IAAwB,wBAA2B;ER2rFzD;EQ1rFM;;IAEE,4BAA+B;ER4rFvC;EQ1rFM;;IAEE,8BAAiC;ER4rFzC;EQ1rFM;;IAEE,+BAAkC;ER4rF1C;EQ1rFM;;IAEE,6BAAgC;ER4rFxC;EQtrFE;IAAmB,uBAAuB;ERyrF5C;EQxrFE;;IAEE,2BAA2B;ER0rF/B;EQxrFE;;IAEE,6BAA6B;ER0rFjC;EQxrFE;;IAEE,8BAA8B;ER0rFlC;EQxrFE;;IAEE,4BAA4B;ER0rFhC;AACF;;AGpsFI;EKlDI;IAAgC,oBAA4B;ER2vFlE;EQ1vFM;;IAEE,wBAAoC;ER4vF5C;EQ1vFM;;IAEE,0BAAwC;ER4vFhD;EQ1vFM;;IAEE,2BAA0C;ER4vFlD;EQ1vFM;;IAEE,yBAAsC;ER4vF9C;EQ3wFM;IAAgC,0BAA4B;ER8wFlE;EQ7wFM;;IAEE,8BAAoC;ER+wF5C;EQ7wFM;;IAEE,gCAAwC;ER+wFhD;EQ7wFM;;IAEE,iCAA0C;ER+wFlD;EQ7wFM;;IAEE,+BAAsC;ER+wF9C;EQ9xFM;IAAgC,yBAA4B;ERiyFlE;EQhyFM;;IAEE,6BAAoC;ERkyF5C;EQhyFM;;IAEE,+BAAwC;ERkyFhD;EQhyFM;;IAEE,gCAA0C;ERkyFlD;EQhyFM;;IAEE,8BAAsC;ERkyF9C;EQjzFM;IAAgC,uBAA4B;ERozFlE;EQnzFM;;IAEE,2BAAoC;ERqzF5C;EQnzFM;;IAEE,6BAAwC;ERqzFhD;EQnzFM;;IAEE,8BAA0C;ERqzFlD;EQnzFM;;IAEE,4BAAsC;ERqzF9C;EQp0FM;IAAgC,yBAA4B;ERu0FlE;EQt0FM;;IAEE,6BAAoC;ERw0F5C;EQt0FM;;IAEE,+BAAwC;ERw0FhD;EQt0FM;;IAEE,gCAA0C;ERw0FlD;EQt0FM;;IAEE,8BAAsC;ERw0F9C;EQv1FM;IAAgC,uBAA4B;ER01FlE;EQz1FM;;IAEE,2BAAoC;ER21F5C;EQz1FM;;IAEE,6BAAwC;ER21FhD;EQz1FM;;IAEE,8BAA0C;ER21FlD;EQz1FM;;IAEE,4BAAsC;ER21F9C;EQ12FM;IAAgC,qBAA4B;ER62FlE;EQ52FM;;IAEE,yBAAoC;ER82F5C;EQ52FM;;IAEE,2BAAwC;ER82FhD;EQ52FM;;IAEE,4BAA0C;ER82FlD;EQ52FM;;IAEE,0BAAsC;ER82F9C;EQ73FM;IAAgC,2BAA4B;ERg4FlE;EQ/3FM;;IAEE,+BAAoC;ERi4F5C;EQ/3FM;;IAEE,iCAAwC;ERi4FhD;EQ/3FM;;IAEE,kCAA0C;ERi4FlD;EQ/3FM;;IAEE,gCAAsC;ERi4F9C;EQh5FM;IAAgC,0BAA4B;ERm5FlE;EQl5FM;;IAEE,8BAAoC;ERo5F5C;EQl5FM;;IAEE,gCAAwC;ERo5FhD;EQl5FM;;IAEE,iCAA0C;ERo5FlD;EQl5FM;;IAEE,+BAAsC;ERo5F9C;EQn6FM;IAAgC,wBAA4B;ERs6FlE;EQr6FM;;IAEE,4BAAoC;ERu6F5C;EQr6FM;;IAEE,8BAAwC;ERu6FhD;EQr6FM;;IAEE,+BAA0C;ERu6FlD;EQr6FM;;IAEE,6BAAsC;ERu6F9C;EQt7FM;IAAgC,0BAA4B;ERy7FlE;EQx7FM;;IAEE,8BAAoC;ER07F5C;EQx7FM;;IAEE,gCAAwC;ER07FhD;EQx7FM;;IAEE,iCAA0C;ER07FlD;EQx7FM;;IAEE,+BAAsC;ER07F9C;EQz8FM;IAAgC,wBAA4B;ER48FlE;EQ38FM;;IAEE,4BAAoC;ER68F5C;EQ38FM;;IAEE,8BAAwC;ER68FhD;EQ38FM;;IAEE,+BAA0C;ER68FlD;EQ38FM;;IAEE,6BAAsC;ER68F9C;EQr8FM;IAAwB,2BAA2B;ERw8FzD;EQv8FM;;IAEE,+BAA+B;ERy8FvC;EQv8FM;;IAEE,iCAAiC;ERy8FzC;EQv8FM;;IAEE,kCAAkC;ERy8F1C;EQv8FM;;IAEE,gCAAgC;ERy8FxC;EQx9FM;IAAwB,0BAA2B;ER29FzD;EQ19FM;;IAEE,8BAA+B;ER49FvC;EQ19FM;;IAEE,gCAAiC;ER49FzC;EQ19FM;;IAEE,iCAAkC;ER49F1C;EQ19FM;;IAEE,+BAAgC;ER49FxC;EQ3+FM;IAAwB,wBAA2B;ER8+FzD;EQ7+FM;;IAEE,4BAA+B;ER++FvC;EQ7+FM;;IAEE,8BAAiC;ER++FzC;EQ7+FM;;IAEE,+BAAkC;ER++F1C;EQ7+FM;;IAEE,6BAAgC;ER++FxC;EQ9/FM;IAAwB,0BAA2B;ERigGzD;EQhgGM;;IAEE,8BAA+B;ERkgGvC;EQhgGM;;IAEE,gCAAiC;ERkgGzC;EQhgGM;;IAEE,iCAAkC;ERkgG1C;EQhgGM;;IAEE,+BAAgC;ERkgGxC;EQjhGM;IAAwB,wBAA2B;ERohGzD;EQnhGM;;IAEE,4BAA+B;ERqhGvC;EQnhGM;;IAEE,8BAAiC;ERqhGzC;EQnhGM;;IAEE,+BAAkC;ERqhG1C;EQnhGM;;IAEE,6BAAgC;ERqhGxC;EQ/gGE;IAAmB,uBAAuB;ERkhG5C;EQjhGE;;IAEE,2BAA2B;ERmhG/B;EQjhGE;;IAEE,6BAA6B;ERmhGjC;EQjhGE;;IAEE,8BAA8B;ERmhGlC;EQjhGE;;IAEE,4BAA4B;ERmhGhC;AACF;;AG7hGI;EKlDI;IAAgC,oBAA4B;ERolGlE;EQnlGM;;IAEE,wBAAoC;ERqlG5C;EQnlGM;;IAEE,0BAAwC;ERqlGhD;EQnlGM;;IAEE,2BAA0C;ERqlGlD;EQnlGM;;IAEE,yBAAsC;ERqlG9C;EQpmGM;IAAgC,0BAA4B;ERumGlE;EQtmGM;;IAEE,8BAAoC;ERwmG5C;EQtmGM;;IAEE,gCAAwC;ERwmGhD;EQtmGM;;IAEE,iCAA0C;ERwmGlD;EQtmGM;;IAEE,+BAAsC;ERwmG9C;EQvnGM;IAAgC,yBAA4B;ER0nGlE;EQznGM;;IAEE,6BAAoC;ER2nG5C;EQznGM;;IAEE,+BAAwC;ER2nGhD;EQznGM;;IAEE,gCAA0C;ER2nGlD;EQznGM;;IAEE,8BAAsC;ER2nG9C;EQ1oGM;IAAgC,uBAA4B;ER6oGlE;EQ5oGM;;IAEE,2BAAoC;ER8oG5C;EQ5oGM;;IAEE,6BAAwC;ER8oGhD;EQ5oGM;;IAEE,8BAA0C;ER8oGlD;EQ5oGM;;IAEE,4BAAsC;ER8oG9C;EQ7pGM;IAAgC,yBAA4B;ERgqGlE;EQ/pGM;;IAEE,6BAAoC;ERiqG5C;EQ/pGM;;IAEE,+BAAwC;ERiqGhD;EQ/pGM;;IAEE,gCAA0C;ERiqGlD;EQ/pGM;;IAEE,8BAAsC;ERiqG9C;EQhrGM;IAAgC,uBAA4B;ERmrGlE;EQlrGM;;IAEE,2BAAoC;ERorG5C;EQlrGM;;IAEE,6BAAwC;ERorGhD;EQlrGM;;IAEE,8BAA0C;ERorGlD;EQlrGM;;IAEE,4BAAsC;ERorG9C;EQnsGM;IAAgC,qBAA4B;ERssGlE;EQrsGM;;IAEE,yBAAoC;ERusG5C;EQrsGM;;IAEE,2BAAwC;ERusGhD;EQrsGM;;IAEE,4BAA0C;ERusGlD;EQrsGM;;IAEE,0BAAsC;ERusG9C;EQttGM;IAAgC,2BAA4B;ERytGlE;EQxtGM;;IAEE,+BAAoC;ER0tG5C;EQxtGM;;IAEE,iCAAwC;ER0tGhD;EQxtGM;;IAEE,kCAA0C;ER0tGlD;EQxtGM;;IAEE,gCAAsC;ER0tG9C;EQzuGM;IAAgC,0BAA4B;ER4uGlE;EQ3uGM;;IAEE,8BAAoC;ER6uG5C;EQ3uGM;;IAEE,gCAAwC;ER6uGhD;EQ3uGM;;IAEE,iCAA0C;ER6uGlD;EQ3uGM;;IAEE,+BAAsC;ER6uG9C;EQ5vGM;IAAgC,wBAA4B;ER+vGlE;EQ9vGM;;IAEE,4BAAoC;ERgwG5C;EQ9vGM;;IAEE,8BAAwC;ERgwGhD;EQ9vGM;;IAEE,+BAA0C;ERgwGlD;EQ9vGM;;IAEE,6BAAsC;ERgwG9C;EQ/wGM;IAAgC,0BAA4B;ERkxGlE;EQjxGM;;IAEE,8BAAoC;ERmxG5C;EQjxGM;;IAEE,gCAAwC;ERmxGhD;EQjxGM;;IAEE,iCAA0C;ERmxGlD;EQjxGM;;IAEE,+BAAsC;ERmxG9C;EQlyGM;IAAgC,wBAA4B;ERqyGlE;EQpyGM;;IAEE,4BAAoC;ERsyG5C;EQpyGM;;IAEE,8BAAwC;ERsyGhD;EQpyGM;;IAEE,+BAA0C;ERsyGlD;EQpyGM;;IAEE,6BAAsC;ERsyG9C;EQ9xGM;IAAwB,2BAA2B;ERiyGzD;EQhyGM;;IAEE,+BAA+B;ERkyGvC;EQhyGM;;IAEE,iCAAiC;ERkyGzC;EQhyGM;;IAEE,kCAAkC;ERkyG1C;EQhyGM;;IAEE,gCAAgC;ERkyGxC;EQjzGM;IAAwB,0BAA2B;ERozGzD;EQnzGM;;IAEE,8BAA+B;ERqzGvC;EQnzGM;;IAEE,gCAAiC;ERqzGzC;EQnzGM;;IAEE,iCAAkC;ERqzG1C;EQnzGM;;IAEE,+BAAgC;ERqzGxC;EQp0GM;IAAwB,wBAA2B;ERu0GzD;EQt0GM;;IAEE,4BAA+B;ERw0GvC;EQt0GM;;IAEE,8BAAiC;ERw0GzC;EQt0GM;;IAEE,+BAAkC;ERw0G1C;EQt0GM;;IAEE,6BAAgC;ERw0GxC;EQv1GM;IAAwB,0BAA2B;ER01GzD;EQz1GM;;IAEE,8BAA+B;ER21GvC;EQz1GM;;IAEE,gCAAiC;ER21GzC;EQz1GM;;IAEE,iCAAkC;ER21G1C;EQz1GM;;IAEE,+BAAgC;ER21GxC;EQ12GM;IAAwB,wBAA2B;ER62GzD;EQ52GM;;IAEE,4BAA+B;ER82GvC;EQ52GM;;IAEE,8BAAiC;ER82GzC;EQ52GM;;IAEE,+BAAkC;ER82G1C;EQ52GM;;IAEE,6BAAgC;ER82GxC;EQx2GE;IAAmB,uBAAuB;ER22G5C;EQ12GE;;IAEE,2BAA2B;ER42G/B;EQ12GE;;IAEE,6BAA6B;ER42GjC;EQ12GE;;IAEE,8BAA8B;ER42GlC;EQ12GE;;IAEE,4BAA4B;ER42GhC;AACF","file":"bootstrap-grid.css","sourcesContent":["/*!\n * Bootstrap Grid v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/breakpoints\";\n@import \"mixins/grid-framework\";\n@import \"mixins/grid\";\n\n@import \"grid\";\n@import \"utilities/display\";\n@import \"utilities/flex\";\n@import \"utilities/spacing\";\n","/*!\n * Bootstrap Grid v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n.container,\n.container-fluid,\n.container-sm,\n.container-md,\n.container-lg,\n.container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n @include deprecate(\"The `make-container-max-widths` mixin\", \"v4.5.2\", \"v5\");\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\", \"%3c\"),\n (\">\", \"%3e\"),\n (\"#\", \"%23\"),\n (\"(\", \"%28\"),\n (\")\", \"%29\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n$table-th-font-weight: null !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: null !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: null !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: null !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n\n$form-validation-states: () !default;\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-x: 0 !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n$dropdown-header-padding: $dropdown-padding-y $dropdown-item-padding-x !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n$user-selects: all, auto, none !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @if $columns > 0 {\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n }\n\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n @if $columns > 0 {\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n"]} \ No newline at end of file diff --git a/assets/css/bootstrap-grid.min.css b/assets/css/bootstrap-grid.min.css new file mode 100644 index 0000000000..d323f93fd0 --- /dev/null +++ b/assets/css/bootstrap-grid.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap Grid v4.5.3 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,::after,::before{box-sizing:inherit}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}} +/*# sourceMappingURL=bootstrap-grid.min.css.map */ \ No newline at end of file diff --git a/assets/css/bootstrap-grid.min.css.map b/assets/css/bootstrap-grid.min.css.map new file mode 100644 index 0000000000..9c96ff302e --- /dev/null +++ b/assets/css/bootstrap-grid.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","dist/css/bootstrap-grid.css","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/utilities/_display.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_spacing.scss"],"names":[],"mappings":"AAAA;;;;;AAOA,KACE,WAAA,WACA,mBAAA,UAGF,ECCA,QADA,SDGE,WAAA,QETA,WDYF,iBAGA,cADA,cADA,cAGA,cEjBE,MAAA,KACA,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KCmDE,yBFzCE,WAAA,cACE,UAAA,OEwCJ,yBFzCE,WAAA,cAAA,cACE,UAAA,OEwCJ,yBFzCE,WAAA,cAAA,cAAA,cACE,UAAA,OEwCJ,0BFzCE,WAAA,cAAA,cAAA,cAAA,cACE,UAAA,QA4BN,KCnCA,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,aAAA,MACA,YAAA,MDsCA,YACE,aAAA,EACA,YAAA,EAFF,iBDeF,0BCTM,cAAA,EACA,aAAA,EGtDJ,KAAA,OAAA,QAAA,QAAA,QAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OJoEF,UAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFkJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACnG,aAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aIvEI,SAAA,SACA,MAAA,KACA,cAAA,KACA,aAAA,KAsBE,KACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,cFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,cFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,cFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,cFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,cFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,cFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,UFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,OFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,QFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,QFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,QFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,aAAwB,eAAA,GAAA,MAAA,GAExB,YAAuB,eAAA,GAAA,MAAA,GAGrB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAOpB,UFhBV,YAAA,UEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,IEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,IEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,IEgBU,WFhBV,YAAA,WEgBU,WFhBV,YAAA,WCKE,yBC3BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,iBFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,aFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,UFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAOpB,aFhBV,YAAA,EEgBU,aFhBV,YAAA,UEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,cFhBV,YAAA,WEgBU,cFhBV,YAAA,YCKE,yBC3BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,iBFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,aFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,UFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAOpB,aFhBV,YAAA,EEgBU,aFhBV,YAAA,UEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,cFhBV,YAAA,WEgBU,cFhBV,YAAA,YCKE,yBC3BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,iBFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,aFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,UFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAOpB,aFhBV,YAAA,EEgBU,aFhBV,YAAA,UEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,cFhBV,YAAA,WEgBU,cFhBV,YAAA,YCKE,0BC3BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,iBFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,aFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,UFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAOpB,aFhBV,YAAA,EEgBU,aFhBV,YAAA,UEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,cFhBV,YAAA,WEgBU,cFhBV,YAAA,YG5CI,QAAwB,QAAA,eAAxB,UAAwB,QAAA,iBAAxB,gBAAwB,QAAA,uBAAxB,SAAwB,QAAA,gBAAxB,SAAwB,QAAA,gBAAxB,aAAwB,QAAA,oBAAxB,cAAwB,QAAA,qBAAxB,QAAwB,QAAA,sBAAA,QAAA,eAAxB,eAAwB,QAAA,6BAAA,QAAA,sBFiD1B,yBEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBFiD1B,yBEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBFiD1B,yBEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBFiD1B,0BEjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBAU9B,aAEI,cAAqB,QAAA,eAArB,gBAAqB,QAAA,iBAArB,sBAAqB,QAAA,uBAArB,eAAqB,QAAA,gBAArB,eAAqB,QAAA,gBAArB,mBAAqB,QAAA,oBAArB,oBAAqB,QAAA,qBAArB,cAAqB,QAAA,sBAAA,QAAA,eAArB,qBAAqB,QAAA,6BAAA,QAAA,uBCbrB,UAAgC,mBAAA,cAAA,eAAA,cAChC,aAAgC,mBAAA,iBAAA,eAAA,iBAChC,kBAAgC,mBAAA,sBAAA,eAAA,sBAChC,qBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,WAA8B,cAAA,eAAA,UAAA,eAC9B,aAA8B,cAAA,iBAAA,UAAA,iBAC9B,mBAA8B,cAAA,uBAAA,UAAA,uBAC9B,WAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAE9B,uBAAoC,cAAA,gBAAA,gBAAA,qBACpC,qBAAoC,cAAA,cAAA,gBAAA,mBACpC,wBAAoC,cAAA,iBAAA,gBAAA,iBACpC,yBAAoC,cAAA,kBAAA,gBAAA,wBACpC,wBAAoC,cAAA,qBAAA,gBAAA,uBAEpC,mBAAiC,eAAA,gBAAA,YAAA,qBACjC,iBAAiC,eAAA,cAAA,YAAA,mBACjC,oBAAiC,eAAA,iBAAA,YAAA,iBACjC,sBAAiC,eAAA,mBAAA,YAAA,mBACjC,qBAAiC,eAAA,kBAAA,YAAA,kBAEjC,qBAAkC,mBAAA,gBAAA,cAAA,qBAClC,mBAAkC,mBAAA,cAAA,cAAA,mBAClC,sBAAkC,mBAAA,iBAAA,cAAA,iBAClC,uBAAkC,mBAAA,kBAAA,cAAA,wBAClC,sBAAkC,mBAAA,qBAAA,cAAA,uBAClC,uBAAkC,mBAAA,kBAAA,cAAA,kBAElC,iBAAgC,oBAAA,eAAA,WAAA,eAChC,kBAAgC,oBAAA,gBAAA,WAAA,qBAChC,gBAAgC,oBAAA,cAAA,WAAA,mBAChC,mBAAgC,oBAAA,iBAAA,WAAA,iBAChC,qBAAgC,oBAAA,mBAAA,WAAA,mBAChC,oBAAgC,oBAAA,kBAAA,WAAA,kBHYhC,yBGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBHYhC,yBGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBHYhC,yBGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBHYhC,0BGlDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBCtC5B,KAAgC,OAAA,YAChC,MPsgER,MOpgEU,WAAA,YAEF,MPugER,MOrgEU,aAAA,YAEF,MPwgER,MOtgEU,cAAA,YAEF,MPygER,MOvgEU,YAAA,YAfF,KAAgC,OAAA,iBAChC,MP8hER,MO5hEU,WAAA,iBAEF,MP+hER,MO7hEU,aAAA,iBAEF,MPgiER,MO9hEU,cAAA,iBAEF,MPiiER,MO/hEU,YAAA,iBAfF,KAAgC,OAAA,gBAChC,MPsjER,MOpjEU,WAAA,gBAEF,MPujER,MOrjEU,aAAA,gBAEF,MPwjER,MOtjEU,cAAA,gBAEF,MPyjER,MOvjEU,YAAA,gBAfF,KAAgC,OAAA,eAChC,MP8kER,MO5kEU,WAAA,eAEF,MP+kER,MO7kEU,aAAA,eAEF,MPglER,MO9kEU,cAAA,eAEF,MPilER,MO/kEU,YAAA,eAfF,KAAgC,OAAA,iBAChC,MPsmER,MOpmEU,WAAA,iBAEF,MPumER,MOrmEU,aAAA,iBAEF,MPwmER,MOtmEU,cAAA,iBAEF,MPymER,MOvmEU,YAAA,iBAfF,KAAgC,OAAA,eAChC,MP8nER,MO5nEU,WAAA,eAEF,MP+nER,MO7nEU,aAAA,eAEF,MPgoER,MO9nEU,cAAA,eAEF,MPioER,MO/nEU,YAAA,eAfF,KAAgC,QAAA,YAChC,MPspER,MOppEU,YAAA,YAEF,MPupER,MOrpEU,cAAA,YAEF,MPwpER,MOtpEU,eAAA,YAEF,MPypER,MOvpEU,aAAA,YAfF,KAAgC,QAAA,iBAChC,MP8qER,MO5qEU,YAAA,iBAEF,MP+qER,MO7qEU,cAAA,iBAEF,MPgrER,MO9qEU,eAAA,iBAEF,MPirER,MO/qEU,aAAA,iBAfF,KAAgC,QAAA,gBAChC,MPssER,MOpsEU,YAAA,gBAEF,MPusER,MOrsEU,cAAA,gBAEF,MPwsER,MOtsEU,eAAA,gBAEF,MPysER,MOvsEU,aAAA,gBAfF,KAAgC,QAAA,eAChC,MP8tER,MO5tEU,YAAA,eAEF,MP+tER,MO7tEU,cAAA,eAEF,MPguER,MO9tEU,eAAA,eAEF,MPiuER,MO/tEU,aAAA,eAfF,KAAgC,QAAA,iBAChC,MPsvER,MOpvEU,YAAA,iBAEF,MPuvER,MOrvEU,cAAA,iBAEF,MPwvER,MOtvEU,eAAA,iBAEF,MPyvER,MOvvEU,aAAA,iBAfF,KAAgC,QAAA,eAChC,MP8wER,MO5wEU,YAAA,eAEF,MP+wER,MO7wEU,cAAA,eAEF,MPgxER,MO9wEU,eAAA,eAEF,MPixER,MO/wEU,aAAA,eAQF,MAAwB,OAAA,kBACxB,OP+wER,OO7wEU,WAAA,kBAEF,OPgxER,OO9wEU,aAAA,kBAEF,OPixER,OO/wEU,cAAA,kBAEF,OPkxER,OOhxEU,YAAA,kBAfF,MAAwB,OAAA,iBACxB,OPuyER,OOryEU,WAAA,iBAEF,OPwyER,OOtyEU,aAAA,iBAEF,OPyyER,OOvyEU,cAAA,iBAEF,OP0yER,OOxyEU,YAAA,iBAfF,MAAwB,OAAA,gBACxB,OP+zER,OO7zEU,WAAA,gBAEF,OPg0ER,OO9zEU,aAAA,gBAEF,OPi0ER,OO/zEU,cAAA,gBAEF,OPk0ER,OOh0EU,YAAA,gBAfF,MAAwB,OAAA,kBACxB,OPu1ER,OOr1EU,WAAA,kBAEF,OPw1ER,OOt1EU,aAAA,kBAEF,OPy1ER,OOv1EU,cAAA,kBAEF,OP01ER,OOx1EU,YAAA,kBAfF,MAAwB,OAAA,gBACxB,OP+2ER,OO72EU,WAAA,gBAEF,OPg3ER,OO92EU,aAAA,gBAEF,OPi3ER,OO/2EU,cAAA,gBAEF,OPk3ER,OOh3EU,YAAA,gBAMN,QAAmB,OAAA,eACnB,SPk3EJ,SOh3EM,WAAA,eAEF,SPm3EJ,SOj3EM,aAAA,eAEF,SPo3EJ,SOl3EM,cAAA,eAEF,SPq3EJ,SOn3EM,YAAA,eJTF,yBIlDI,QAAgC,OAAA,YAChC,SPs7EN,SOp7EQ,WAAA,YAEF,SPs7EN,SOp7EQ,aAAA,YAEF,SPs7EN,SOp7EQ,cAAA,YAEF,SPs7EN,SOp7EQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SPy8EN,SOv8EQ,WAAA,iBAEF,SPy8EN,SOv8EQ,aAAA,iBAEF,SPy8EN,SOv8EQ,cAAA,iBAEF,SPy8EN,SOv8EQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SP49EN,SO19EQ,WAAA,gBAEF,SP49EN,SO19EQ,aAAA,gBAEF,SP49EN,SO19EQ,cAAA,gBAEF,SP49EN,SO19EQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SP++EN,SO7+EQ,WAAA,eAEF,SP++EN,SO7+EQ,aAAA,eAEF,SP++EN,SO7+EQ,cAAA,eAEF,SP++EN,SO7+EQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SPkgFN,SOhgFQ,WAAA,iBAEF,SPkgFN,SOhgFQ,aAAA,iBAEF,SPkgFN,SOhgFQ,cAAA,iBAEF,SPkgFN,SOhgFQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SPqhFN,SOnhFQ,WAAA,eAEF,SPqhFN,SOnhFQ,aAAA,eAEF,SPqhFN,SOnhFQ,cAAA,eAEF,SPqhFN,SOnhFQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SPwiFN,SOtiFQ,YAAA,YAEF,SPwiFN,SOtiFQ,cAAA,YAEF,SPwiFN,SOtiFQ,eAAA,YAEF,SPwiFN,SOtiFQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SP2jFN,SOzjFQ,YAAA,iBAEF,SP2jFN,SOzjFQ,cAAA,iBAEF,SP2jFN,SOzjFQ,eAAA,iBAEF,SP2jFN,SOzjFQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SP8kFN,SO5kFQ,YAAA,gBAEF,SP8kFN,SO5kFQ,cAAA,gBAEF,SP8kFN,SO5kFQ,eAAA,gBAEF,SP8kFN,SO5kFQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SPimFN,SO/lFQ,YAAA,eAEF,SPimFN,SO/lFQ,cAAA,eAEF,SPimFN,SO/lFQ,eAAA,eAEF,SPimFN,SO/lFQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SPonFN,SOlnFQ,YAAA,iBAEF,SPonFN,SOlnFQ,cAAA,iBAEF,SPonFN,SOlnFQ,eAAA,iBAEF,SPonFN,SOlnFQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SPuoFN,SOroFQ,YAAA,eAEF,SPuoFN,SOroFQ,cAAA,eAEF,SPuoFN,SOroFQ,eAAA,eAEF,SPuoFN,SOroFQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UPmoFN,UOjoFQ,WAAA,kBAEF,UPmoFN,UOjoFQ,aAAA,kBAEF,UPmoFN,UOjoFQ,cAAA,kBAEF,UPmoFN,UOjoFQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UPspFN,UOppFQ,WAAA,iBAEF,UPspFN,UOppFQ,aAAA,iBAEF,UPspFN,UOppFQ,cAAA,iBAEF,UPspFN,UOppFQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UPyqFN,UOvqFQ,WAAA,gBAEF,UPyqFN,UOvqFQ,aAAA,gBAEF,UPyqFN,UOvqFQ,cAAA,gBAEF,UPyqFN,UOvqFQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UP4rFN,UO1rFQ,WAAA,kBAEF,UP4rFN,UO1rFQ,aAAA,kBAEF,UP4rFN,UO1rFQ,cAAA,kBAEF,UP4rFN,UO1rFQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UP+sFN,UO7sFQ,WAAA,gBAEF,UP+sFN,UO7sFQ,aAAA,gBAEF,UP+sFN,UO7sFQ,cAAA,gBAEF,UP+sFN,UO7sFQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YP6sFF,YO3sFI,WAAA,eAEF,YP6sFF,YO3sFI,aAAA,eAEF,YP6sFF,YO3sFI,cAAA,eAEF,YP6sFF,YO3sFI,YAAA,gBJTF,yBIlDI,QAAgC,OAAA,YAChC,SP+wFN,SO7wFQ,WAAA,YAEF,SP+wFN,SO7wFQ,aAAA,YAEF,SP+wFN,SO7wFQ,cAAA,YAEF,SP+wFN,SO7wFQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SPkyFN,SOhyFQ,WAAA,iBAEF,SPkyFN,SOhyFQ,aAAA,iBAEF,SPkyFN,SOhyFQ,cAAA,iBAEF,SPkyFN,SOhyFQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPqzFN,SOnzFQ,WAAA,gBAEF,SPqzFN,SOnzFQ,aAAA,gBAEF,SPqzFN,SOnzFQ,cAAA,gBAEF,SPqzFN,SOnzFQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SPw0FN,SOt0FQ,WAAA,eAEF,SPw0FN,SOt0FQ,aAAA,eAEF,SPw0FN,SOt0FQ,cAAA,eAEF,SPw0FN,SOt0FQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SP21FN,SOz1FQ,WAAA,iBAEF,SP21FN,SOz1FQ,aAAA,iBAEF,SP21FN,SOz1FQ,cAAA,iBAEF,SP21FN,SOz1FQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SP82FN,SO52FQ,WAAA,eAEF,SP82FN,SO52FQ,aAAA,eAEF,SP82FN,SO52FQ,cAAA,eAEF,SP82FN,SO52FQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SPi4FN,SO/3FQ,YAAA,YAEF,SPi4FN,SO/3FQ,cAAA,YAEF,SPi4FN,SO/3FQ,eAAA,YAEF,SPi4FN,SO/3FQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SPo5FN,SOl5FQ,YAAA,iBAEF,SPo5FN,SOl5FQ,cAAA,iBAEF,SPo5FN,SOl5FQ,eAAA,iBAEF,SPo5FN,SOl5FQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SPu6FN,SOr6FQ,YAAA,gBAEF,SPu6FN,SOr6FQ,cAAA,gBAEF,SPu6FN,SOr6FQ,eAAA,gBAEF,SPu6FN,SOr6FQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SP07FN,SOx7FQ,YAAA,eAEF,SP07FN,SOx7FQ,cAAA,eAEF,SP07FN,SOx7FQ,eAAA,eAEF,SP07FN,SOx7FQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SP68FN,SO38FQ,YAAA,iBAEF,SP68FN,SO38FQ,cAAA,iBAEF,SP68FN,SO38FQ,eAAA,iBAEF,SP68FN,SO38FQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SPg+FN,SO99FQ,YAAA,eAEF,SPg+FN,SO99FQ,cAAA,eAEF,SPg+FN,SO99FQ,eAAA,eAEF,SPg+FN,SO99FQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UP49FN,UO19FQ,WAAA,kBAEF,UP49FN,UO19FQ,aAAA,kBAEF,UP49FN,UO19FQ,cAAA,kBAEF,UP49FN,UO19FQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UP++FN,UO7+FQ,WAAA,iBAEF,UP++FN,UO7+FQ,aAAA,iBAEF,UP++FN,UO7+FQ,cAAA,iBAEF,UP++FN,UO7+FQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UPkgGN,UOhgGQ,WAAA,gBAEF,UPkgGN,UOhgGQ,aAAA,gBAEF,UPkgGN,UOhgGQ,cAAA,gBAEF,UPkgGN,UOhgGQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPqhGN,UOnhGQ,WAAA,kBAEF,UPqhGN,UOnhGQ,aAAA,kBAEF,UPqhGN,UOnhGQ,cAAA,kBAEF,UPqhGN,UOnhGQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UPwiGN,UOtiGQ,WAAA,gBAEF,UPwiGN,UOtiGQ,aAAA,gBAEF,UPwiGN,UOtiGQ,cAAA,gBAEF,UPwiGN,UOtiGQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YPsiGF,YOpiGI,WAAA,eAEF,YPsiGF,YOpiGI,aAAA,eAEF,YPsiGF,YOpiGI,cAAA,eAEF,YPsiGF,YOpiGI,YAAA,gBJTF,yBIlDI,QAAgC,OAAA,YAChC,SPwmGN,SOtmGQ,WAAA,YAEF,SPwmGN,SOtmGQ,aAAA,YAEF,SPwmGN,SOtmGQ,cAAA,YAEF,SPwmGN,SOtmGQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SP2nGN,SOznGQ,WAAA,iBAEF,SP2nGN,SOznGQ,aAAA,iBAEF,SP2nGN,SOznGQ,cAAA,iBAEF,SP2nGN,SOznGQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SP8oGN,SO5oGQ,WAAA,gBAEF,SP8oGN,SO5oGQ,aAAA,gBAEF,SP8oGN,SO5oGQ,cAAA,gBAEF,SP8oGN,SO5oGQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SPiqGN,SO/pGQ,WAAA,eAEF,SPiqGN,SO/pGQ,aAAA,eAEF,SPiqGN,SO/pGQ,cAAA,eAEF,SPiqGN,SO/pGQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SPorGN,SOlrGQ,WAAA,iBAEF,SPorGN,SOlrGQ,aAAA,iBAEF,SPorGN,SOlrGQ,cAAA,iBAEF,SPorGN,SOlrGQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SPusGN,SOrsGQ,WAAA,eAEF,SPusGN,SOrsGQ,aAAA,eAEF,SPusGN,SOrsGQ,cAAA,eAEF,SPusGN,SOrsGQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SP0tGN,SOxtGQ,YAAA,YAEF,SP0tGN,SOxtGQ,cAAA,YAEF,SP0tGN,SOxtGQ,eAAA,YAEF,SP0tGN,SOxtGQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SP6uGN,SO3uGQ,YAAA,iBAEF,SP6uGN,SO3uGQ,cAAA,iBAEF,SP6uGN,SO3uGQ,eAAA,iBAEF,SP6uGN,SO3uGQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SPgwGN,SO9vGQ,YAAA,gBAEF,SPgwGN,SO9vGQ,cAAA,gBAEF,SPgwGN,SO9vGQ,eAAA,gBAEF,SPgwGN,SO9vGQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SPmxGN,SOjxGQ,YAAA,eAEF,SPmxGN,SOjxGQ,cAAA,eAEF,SPmxGN,SOjxGQ,eAAA,eAEF,SPmxGN,SOjxGQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SPsyGN,SOpyGQ,YAAA,iBAEF,SPsyGN,SOpyGQ,cAAA,iBAEF,SPsyGN,SOpyGQ,eAAA,iBAEF,SPsyGN,SOpyGQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SPyzGN,SOvzGQ,YAAA,eAEF,SPyzGN,SOvzGQ,cAAA,eAEF,SPyzGN,SOvzGQ,eAAA,eAEF,SPyzGN,SOvzGQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UPqzGN,UOnzGQ,WAAA,kBAEF,UPqzGN,UOnzGQ,aAAA,kBAEF,UPqzGN,UOnzGQ,cAAA,kBAEF,UPqzGN,UOnzGQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UPw0GN,UOt0GQ,WAAA,iBAEF,UPw0GN,UOt0GQ,aAAA,iBAEF,UPw0GN,UOt0GQ,cAAA,iBAEF,UPw0GN,UOt0GQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UP21GN,UOz1GQ,WAAA,gBAEF,UP21GN,UOz1GQ,aAAA,gBAEF,UP21GN,UOz1GQ,cAAA,gBAEF,UP21GN,UOz1GQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UP82GN,UO52GQ,WAAA,kBAEF,UP82GN,UO52GQ,aAAA,kBAEF,UP82GN,UO52GQ,cAAA,kBAEF,UP82GN,UO52GQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UPi4GN,UO/3GQ,WAAA,gBAEF,UPi4GN,UO/3GQ,aAAA,gBAEF,UPi4GN,UO/3GQ,cAAA,gBAEF,UPi4GN,UO/3GQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YP+3GF,YO73GI,WAAA,eAEF,YP+3GF,YO73GI,aAAA,eAEF,YP+3GF,YO73GI,cAAA,eAEF,YP+3GF,YO73GI,YAAA,gBJTF,0BIlDI,QAAgC,OAAA,YAChC,SPi8GN,SO/7GQ,WAAA,YAEF,SPi8GN,SO/7GQ,aAAA,YAEF,SPi8GN,SO/7GQ,cAAA,YAEF,SPi8GN,SO/7GQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SPo9GN,SOl9GQ,WAAA,iBAEF,SPo9GN,SOl9GQ,aAAA,iBAEF,SPo9GN,SOl9GQ,cAAA,iBAEF,SPo9GN,SOl9GQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SPu+GN,SOr+GQ,WAAA,gBAEF,SPu+GN,SOr+GQ,aAAA,gBAEF,SPu+GN,SOr+GQ,cAAA,gBAEF,SPu+GN,SOr+GQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SP0/GN,SOx/GQ,WAAA,eAEF,SP0/GN,SOx/GQ,aAAA,eAEF,SP0/GN,SOx/GQ,cAAA,eAEF,SP0/GN,SOx/GQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SP6gHN,SO3gHQ,WAAA,iBAEF,SP6gHN,SO3gHQ,aAAA,iBAEF,SP6gHN,SO3gHQ,cAAA,iBAEF,SP6gHN,SO3gHQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SPgiHN,SO9hHQ,WAAA,eAEF,SPgiHN,SO9hHQ,aAAA,eAEF,SPgiHN,SO9hHQ,cAAA,eAEF,SPgiHN,SO9hHQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SPmjHN,SOjjHQ,YAAA,YAEF,SPmjHN,SOjjHQ,cAAA,YAEF,SPmjHN,SOjjHQ,eAAA,YAEF,SPmjHN,SOjjHQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SPskHN,SOpkHQ,YAAA,iBAEF,SPskHN,SOpkHQ,cAAA,iBAEF,SPskHN,SOpkHQ,eAAA,iBAEF,SPskHN,SOpkHQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SPylHN,SOvlHQ,YAAA,gBAEF,SPylHN,SOvlHQ,cAAA,gBAEF,SPylHN,SOvlHQ,eAAA,gBAEF,SPylHN,SOvlHQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SP4mHN,SO1mHQ,YAAA,eAEF,SP4mHN,SO1mHQ,cAAA,eAEF,SP4mHN,SO1mHQ,eAAA,eAEF,SP4mHN,SO1mHQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SP+nHN,SO7nHQ,YAAA,iBAEF,SP+nHN,SO7nHQ,cAAA,iBAEF,SP+nHN,SO7nHQ,eAAA,iBAEF,SP+nHN,SO7nHQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SPkpHN,SOhpHQ,YAAA,eAEF,SPkpHN,SOhpHQ,cAAA,eAEF,SPkpHN,SOhpHQ,eAAA,eAEF,SPkpHN,SOhpHQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UP8oHN,UO5oHQ,WAAA,kBAEF,UP8oHN,UO5oHQ,aAAA,kBAEF,UP8oHN,UO5oHQ,cAAA,kBAEF,UP8oHN,UO5oHQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UPiqHN,UO/pHQ,WAAA,iBAEF,UPiqHN,UO/pHQ,aAAA,iBAEF,UPiqHN,UO/pHQ,cAAA,iBAEF,UPiqHN,UO/pHQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UPorHN,UOlrHQ,WAAA,gBAEF,UPorHN,UOlrHQ,aAAA,gBAEF,UPorHN,UOlrHQ,cAAA,gBAEF,UPorHN,UOlrHQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UPusHN,UOrsHQ,WAAA,kBAEF,UPusHN,UOrsHQ,aAAA,kBAEF,UPusHN,UOrsHQ,cAAA,kBAEF,UPusHN,UOrsHQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UP0tHN,UOxtHQ,WAAA,gBAEF,UP0tHN,UOxtHQ,aAAA,gBAEF,UP0tHN,UOxtHQ,cAAA,gBAEF,UP0tHN,UOxtHQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YPwtHF,YOttHI,WAAA,eAEF,YPwtHF,YOttHI,aAAA,eAEF,YPwtHF,YOttHI,cAAA,eAEF,YPwtHF,YOttHI,YAAA","sourcesContent":["/*!\n * Bootstrap Grid v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/breakpoints\";\n@import \"mixins/grid-framework\";\n@import \"mixins/grid\";\n\n@import \"grid\";\n@import \"utilities/display\";\n@import \"utilities/flex\";\n@import \"utilities/spacing\";\n","/*!\n * Bootstrap Grid v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n.container,\n.container-fluid,\n.container-sm,\n.container-md,\n.container-lg,\n.container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n -ms-flex-order: -1;\n order: -1;\n}\n\n.order-last {\n -ms-flex-order: 13;\n order: 13;\n}\n\n.order-0 {\n -ms-flex-order: 0;\n order: 0;\n}\n\n.order-1 {\n -ms-flex-order: 1;\n order: 1;\n}\n\n.order-2 {\n -ms-flex-order: 2;\n order: 2;\n}\n\n.order-3 {\n -ms-flex-order: 3;\n order: 3;\n}\n\n.order-4 {\n -ms-flex-order: 4;\n order: 4;\n}\n\n.order-5 {\n -ms-flex-order: 5;\n order: 5;\n}\n\n.order-6 {\n -ms-flex-order: 6;\n order: 6;\n}\n\n.order-7 {\n -ms-flex-order: 7;\n order: 7;\n}\n\n.order-8 {\n -ms-flex-order: 8;\n order: 8;\n}\n\n.order-9 {\n -ms-flex-order: 9;\n order: 9;\n}\n\n.order-10 {\n -ms-flex-order: 10;\n order: 10;\n}\n\n.order-11 {\n -ms-flex-order: 11;\n order: 11;\n}\n\n.order-12 {\n -ms-flex-order: 12;\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-sm-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-sm-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-sm-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-sm-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-sm-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-sm-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-sm-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-sm-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-sm-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-sm-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-sm-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-sm-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-sm-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-sm-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-md-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-md-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-md-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-md-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-md-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-md-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-md-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-md-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-md-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-md-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-md-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-md-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-md-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-md-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-lg-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-lg-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-lg-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-lg-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-lg-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-lg-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-lg-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-lg-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-lg-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-lg-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-lg-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-lg-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-lg-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-lg-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-xl-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-xl-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-xl-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-xl-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-xl-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-xl-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-xl-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-xl-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-xl-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-xl-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-xl-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-xl-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-xl-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-xl-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n}\n\n.d-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-md-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-print-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n.flex-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n}\n\n.flex-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n}\n\n.justify-content-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n}\n\n.align-items-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n}\n\n.align-items-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n}\n\n.align-items-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n}\n\n.align-items-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n}\n\n.align-content-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n}\n\n.align-content-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n}\n\n.align-content-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n}\n\n.align-content-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n}\n\n.align-content-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n}\n\n.align-self-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n}\n\n.align-self-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n}\n\n.align-self-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n}\n\n.align-self-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n}\n\n.align-self-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-sm-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-sm-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-sm-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-sm-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-sm-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-sm-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-sm-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-sm-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-md-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-md-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-md-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-md-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-md-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-md-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-md-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-md-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-md-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-md-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-md-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-md-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-md-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-md-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-md-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-md-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-lg-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-lg-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-lg-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-lg-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-lg-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-lg-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-lg-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-lg-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-xl-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-xl-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-xl-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-xl-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-xl-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-xl-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-xl-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-xl-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n @include deprecate(\"The `make-container-max-widths` mixin\", \"v4.5.2\", \"v5\");\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @if $columns > 0 {\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n }\n\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n @if $columns > 0 {\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n"]} \ No newline at end of file diff --git a/assets/css/bootstrap-reboot.css b/assets/css/bootstrap-reboot.css new file mode 100644 index 0000000000..4c642187d3 --- /dev/null +++ b/assets/css/bootstrap-reboot.css @@ -0,0 +1,326 @@ +/*! + * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} + +[tabindex="-1"]:focus:not(:focus-visible) { + outline: 0 !important; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: .5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -.25em; +} + +sup { + top: -.5em; +} + +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +a:not([href]):not([class]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + -ms-overflow-style: scrollbar; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +[role="button"] { + cursor: pointer; +} + +select { + word-wrap: normal; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +textarea { + overflow: auto; + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: none; +} + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +summary { + display: list-item; + cursor: pointer; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} +/*# sourceMappingURL=bootstrap-reboot.css.map */ \ No newline at end of file diff --git a/assets/css/bootstrap-reboot.css.map b/assets/css/bootstrap-reboot.css.map new file mode 100644 index 0000000000..e79cab0cf8 --- /dev/null +++ b/assets/css/bootstrap-reboot.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-reboot.scss","bootstrap-reboot.css","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/vendor/_rfs.scss","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ECME;ACYF;;;EAGE,sBAAsB;ADVxB;;ACaA;EACE,uBAAuB;EACvB,iBAAiB;EACjB,8BAA8B;EAC9B,6CCXa;AFCf;;ACgBA;EACE,cAAc;ADbhB;;ACuBA;EACE,SAAS;EACT,kMCqOiN;ECrJ7M,eAtCY;EFxChB,gBC8O+B;ED7O/B,gBCkP+B;EDjP/B,cCnCgB;EDoChB,gBAAgB;EAChB,sBC9Ca;AF0Bf;;AAEA;EC+BE,qBAAqB;AD7BvB;;ACsCA;EACE,uBAAuB;EACvB,SAAS;EACT,iBAAiB;ADnCnB;;ACgDA;EACE,aAAa;EACb,qBCgNuC;AF7PzC;;ACoDA;EACE,aAAa;EACb,mBCoF8B;AFrIhC;;AC4DA;;EAEE,0BAA0B;EAC1B,yCAAiC;EAAjC,iCAAiC;EACjC,YAAY;EACZ,gBAAgB;EAChB,sCAA8B;EAA9B,8BAA8B;ADzDhC;;AC4DA;EACE,mBAAmB;EACnB,kBAAkB;EAClB,oBAAoB;ADzDtB;;AC4DA;;;EAGE,aAAa;EACb,mBAAmB;ADzDrB;;AC4DA;;;;EAIE,gBAAgB;ADzDlB;;AC4DA;EACE,gBCiJ+B;AF1MjC;;AC4DA;EACE,oBAAoB;EACpB,cAAc;ADzDhB;;AC4DA;EACE,gBAAgB;ADzDlB;;AC4DA;;EAEE,mBCoIkC;AF7LpC;;AC4DA;EExFI,cAAW;AHgCf;;ACiEA;;EAEE,kBAAkB;EEnGhB,cAAW;EFqGb,cAAc;EACd,wBAAwB;AD9D1B;;ACiEA;EAAM,cAAc;AD7DpB;;AC8DA;EAAM,UAAU;AD1DhB;;ACiEA;EACE,cCvJe;EDwJf,qBCX4C;EDY5C,6BAA6B;AD9D/B;;AIlHE;EHmLE,cCd8D;EDe9D,0BCd+C;AF/CnD;;ACsEA;EACE,cAAc;EACd,qBAAqB;ADnEvB;;AI5HE;EHkME,cAAc;EACd,qBAAqB;ADlEzB;;AC2EA;;;;EAIE,iGCyDgH;EC7M9G,cAAW;AH6Ef;;AC2EA;EAEE,aAAa;EAEb,mBAAmB;EAEnB,cAAc;EAGd,6BAA6B;AD7E/B;;ACqFA;EAEE,gBAAgB;ADnFlB;;AC2FA;EACE,sBAAsB;EACtB,kBAAkB;ADxFpB;;AC2FA;EAGE,gBAAgB;EAChB,sBAAsB;AD1FxB;;ACkGA;EACE,yBAAyB;AD/F3B;;ACkGA;EACE,oBC6EkC;ED5ElC,uBC4EkC;ED3ElC,cCtQgB;EDuQhB,gBAAgB;EAChB,oBAAoB;AD/FtB;;ACsGA;EAEE,mBAAmB;EACnB,gCAAgC;ADpGlC;;AC4GA;EAEE,qBAAqB;EACrB,qBC2J2C;AFrQ7C;;ACgHA;EAEE,gBAAgB;AD9GlB;;ACqHA;EACE,mBAAmB;EACnB,0CAA0C;ADlH5C;;ACqHA;;;;;EAKE,SAAS;EACT,oBAAoB;EE5PlB,kBAAW;EF8Pb,oBAAoB;ADlHtB;;ACqHA;;EAEE,iBAAiB;ADlHnB;;ACqHA;;EAEE,oBAAoB;ADlHtB;;AAEA;ECuHE,eAAe;ADrHjB;;AC2HA;EACE,iBAAiB;ADxHnB;;AC+HA;;;;EAIE,0BAA0B;AD5H5B;;ACiIE;;;;EAKI,eAAe;AD/HrB;;ACqIA;;;;EAIE,UAAU;EACV,kBAAkB;ADlIpB;;ACqIA;;EAEE,sBAAsB;EACtB,UAAU;ADlIZ;;ACsIA;EACE,cAAc;EAEd,gBAAgB;ADpIlB;;ACuIA;EAME,YAAY;EAEZ,UAAU;EACV,SAAS;EACT,SAAS;AD1IX;;AC+IA;EACE,cAAc;EACd,WAAW;EACX,eAAe;EACf,UAAU;EACV,oBAAoB;EEnShB,iBAtCY;EF2UhB,oBAAoB;EACpB,cAAc;EACd,mBAAmB;AD5IrB;;AC+IA;EACE,wBAAwB;AD5I1B;;AAEA;;ECgJE,YAAY;AD7Id;;AAEA;ECmJE,oBAAoB;EACpB,wBAAwB;ADjJ1B;;AAEA;ECuJE,wBAAwB;ADrJ1B;;AC6JA;EACE,aAAa;EACb,0BAA0B;AD1J5B;;ACiKA;EACE,qBAAqB;AD9JvB;;ACiKA;EACE,kBAAkB;EAClB,eAAe;AD9JjB;;ACiKA;EACE,aAAa;AD9Jf;;AAEA;ECkKE,wBAAwB;ADhK1B","file":"bootstrap-reboot.css","sourcesContent":["/*!\n * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","/*!\n * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=\"button\"] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Set the cursor for non-`',close:'',arrowLeft:'',arrowRight:'',smallBtn:''},parentEl:"body",hideScrollbar:!0,autoFocus:!0,backFocus:!0,trapFocus:!0,fullScreen:{autoStart:!1},touch:{vertical:!0,momentum:!0},hash:null,media:{},slideShow:{autoStart:!1,speed:3e3},thumbs:{autoStart:!1,hideOnClose:!0,parentEl:".fancybox-container",axis:"y"},wheel:"auto",onInit:n.noop,beforeLoad:n.noop,afterLoad:n.noop,beforeShow:n.noop,afterShow:n.noop,beforeClose:n.noop,afterClose:n.noop,onActivate:n.noop,onDeactivate:n.noop,clickContent:function(t,e){return"image"===t.type&&"zoom"},clickSlide:"close",clickOutside:"close",dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1,mobile:{preventCaptionOverlap:!1,idleTime:!1,clickContent:function(t,e){return"image"===t.type&&"toggleControls"},clickSlide:function(t,e){return"image"===t.type?"toggleControls":"close"},dblclickContent:function(t,e){return"image"===t.type&&"zoom"},dblclickSlide:function(t,e){return"image"===t.type&&"zoom"}},lang:"en",i18n:{en:{CLOSE:"Close",NEXT:"Next",PREV:"Previous",ERROR:"The requested content cannot be loaded.
Please try again later.",PLAY_START:"Start slideshow",PLAY_STOP:"Pause slideshow",FULL_SCREEN:"Full screen",THUMBS:"Thumbnails",DOWNLOAD:"Download",SHARE:"Share",ZOOM:"Zoom"},de:{CLOSE:"Schließen",NEXT:"Weiter",PREV:"Zurück",ERROR:"Die angeforderten Daten konnten nicht geladen werden.
Bitte versuchen Sie es später nochmal.",PLAY_START:"Diaschau starten",PLAY_STOP:"Diaschau beenden",FULL_SCREEN:"Vollbild",THUMBS:"Vorschaubilder",DOWNLOAD:"Herunterladen",SHARE:"Teilen",ZOOM:"Vergrößern"}}},s=n(t),r=n(e),c=0,l=function(t){return t&&t.hasOwnProperty&&t instanceof n},d=function(){return t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||function(e){return t.setTimeout(e,1e3/60)}}(),u=function(){return t.cancelAnimationFrame||t.webkitCancelAnimationFrame||t.mozCancelAnimationFrame||t.oCancelAnimationFrame||function(e){t.clearTimeout(e)}}(),f=function(){var t,n=e.createElement("fakeelement"),o={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(t in o)if(void 0!==n.style[t])return o[t];return"transitionend"}(),p=function(t){return t&&t.length&&t[0].offsetHeight},h=function(t,e){var o=n.extend(!0,{},t,e);return n.each(e,function(t,e){n.isArray(e)&&(o[t]=e)}),o},g=function(t){var o,i;return!(!t||t.ownerDocument!==e)&&(n(".fancybox-container").css("pointer-events","none"),o={x:t.getBoundingClientRect().left+t.offsetWidth/2,y:t.getBoundingClientRect().top+t.offsetHeight/2},i=e.elementFromPoint(o.x,o.y)===t,n(".fancybox-container").css("pointer-events",""),i)},b=function(t,e,o){var i=this;i.opts=h({index:o},n.fancybox.defaults),n.isPlainObject(e)&&(i.opts=h(i.opts,e)),n.fancybox.isMobile&&(i.opts=h(i.opts,i.opts.mobile)),i.id=i.opts.id||++c,i.currIndex=parseInt(i.opts.index,10)||0,i.prevIndex=null,i.prevPos=null,i.currPos=0,i.firstRun=!0,i.group=[],i.slides={},i.addContent(t),i.group.length&&i.init()};n.extend(b.prototype,{init:function(){var o,i,a=this,s=a.group[a.currIndex],r=s.opts;r.closeExisting&&n.fancybox.close(!0),n("body").addClass("fancybox-active"),!n.fancybox.getInstance()&&!1!==r.hideScrollbar&&!n.fancybox.isMobile&&e.body.scrollHeight>t.innerHeight&&(n("head").append('"),n("body").addClass("compensate-for-scrollbar")),i="",n.each(r.buttons,function(t,e){i+=r.btnTpl[e]||""}),o=n(a.translate(a,r.baseTpl.replace("{{buttons}}",i).replace("{{arrows}}",r.btnTpl.arrowLeft+r.btnTpl.arrowRight))).attr("id","fancybox-container-"+a.id).addClass(r.baseClass).data("FancyBox",a).appendTo(r.parentEl),a.$refs={container:o},["bg","inner","infobar","toolbar","stage","caption","navigation"].forEach(function(t){a.$refs[t]=o.find(".fancybox-"+t)}),a.trigger("onInit"),a.activate(),a.jumpTo(a.currIndex)},translate:function(t,e){var n=t.opts.i18n[t.opts.lang]||t.opts.i18n.en;return e.replace(/\{\{(\w+)\}\}/g,function(t,e){return void 0===n[e]?t:n[e]})},addContent:function(t){var e,o=this,i=n.makeArray(t);n.each(i,function(t,e){var i,a,s,r,c,l={},d={};n.isPlainObject(e)?(l=e,d=e.opts||e):"object"===n.type(e)&&n(e).length?(i=n(e),d=i.data()||{},d=n.extend(!0,{},d,d.options),d.$orig=i,l.src=o.opts.src||d.src||i.attr("href"),l.type||l.src||(l.type="inline",l.src=e)):l={type:"html",src:e+""},l.opts=n.extend(!0,{},o.opts,d),n.isArray(d.buttons)&&(l.opts.buttons=d.buttons),n.fancybox.isMobile&&l.opts.mobile&&(l.opts=h(l.opts,l.opts.mobile)),a=l.type||l.opts.type,r=l.src||"",!a&&r&&((s=r.match(/\.(mp4|mov|ogv|webm)((\?|#).*)?$/i))?(a="video",l.opts.video.format||(l.opts.video.format="video/"+("ogv"===s[1]?"ogg":s[1]))):r.match(/(^data:image\/[a-z0-9+\/=]*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg|ico)((\?|#).*)?$)/i)?a="image":r.match(/\.(pdf)((\?|#).*)?$/i)?(a="iframe",l=n.extend(!0,l,{contentType:"pdf",opts:{iframe:{preload:!1}}})):"#"===r.charAt(0)&&(a="inline")),a?l.type=a:o.trigger("objectNeedsType",l),l.contentType||(l.contentType=n.inArray(l.type,["html","inline","ajax"])>-1?"html":l.type),l.index=o.group.length,"auto"==l.opts.smallBtn&&(l.opts.smallBtn=n.inArray(l.type,["html","inline","ajax"])>-1),"auto"===l.opts.toolbar&&(l.opts.toolbar=!l.opts.smallBtn),l.$thumb=l.opts.$thumb||null,l.opts.$trigger&&l.index===o.opts.index&&(l.$thumb=l.opts.$trigger.find("img:first"),l.$thumb.length&&(l.opts.$orig=l.opts.$trigger)),l.$thumb&&l.$thumb.length||!l.opts.$orig||(l.$thumb=l.opts.$orig.find("img:first")),l.$thumb&&!l.$thumb.length&&(l.$thumb=null),l.thumb=l.opts.thumb||(l.$thumb?l.$thumb[0].src:null),"function"===n.type(l.opts.caption)&&(l.opts.caption=l.opts.caption.apply(e,[o,l])),"function"===n.type(o.opts.caption)&&(l.opts.caption=o.opts.caption.apply(e,[o,l])),l.opts.caption instanceof n||(l.opts.caption=void 0===l.opts.caption?"":l.opts.caption+""),"ajax"===l.type&&(c=r.split(/\s+/,2),c.length>1&&(l.src=c.shift(),l.opts.filter=c.shift())),l.opts.modal&&(l.opts=n.extend(!0,l.opts,{trapFocus:!0,infobar:0,toolbar:0,smallBtn:0,keyboard:0,slideShow:0,fullScreen:0,thumbs:0,touch:0,clickContent:!1,clickSlide:!1,clickOutside:!1,dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1})),o.group.push(l)}),Object.keys(o.slides).length&&(o.updateControls(),(e=o.Thumbs)&&e.isActive&&(e.create(),e.focus()))},addEvents:function(){var e=this;e.removeEvents(),e.$refs.container.on("click.fb-close","[data-fancybox-close]",function(t){t.stopPropagation(),t.preventDefault(),e.close(t)}).on("touchstart.fb-prev click.fb-prev","[data-fancybox-prev]",function(t){t.stopPropagation(),t.preventDefault(),e.previous()}).on("touchstart.fb-next click.fb-next","[data-fancybox-next]",function(t){t.stopPropagation(),t.preventDefault(),e.next()}).on("click.fb","[data-fancybox-zoom]",function(t){e[e.isScaledDown()?"scaleToActual":"scaleToFit"]()}),s.on("orientationchange.fb resize.fb",function(t){t&&t.originalEvent&&"resize"===t.originalEvent.type?(e.requestId&&u(e.requestId),e.requestId=d(function(){e.update(t)})):(e.current&&"iframe"===e.current.type&&e.$refs.stage.hide(),setTimeout(function(){e.$refs.stage.show(),e.update(t)},n.fancybox.isMobile?600:250))}),r.on("keydown.fb",function(t){var o=n.fancybox?n.fancybox.getInstance():null,i=o.current,a=t.keyCode||t.which;if(9==a)return void(i.opts.trapFocus&&e.focus(t));if(!(!i.opts.keyboard||t.ctrlKey||t.altKey||t.shiftKey||n(t.target).is("input,textarea,video,audio,select")))return 8===a||27===a?(t.preventDefault(),void e.close(t)):37===a||38===a?(t.preventDefault(),void e.previous()):39===a||40===a?(t.preventDefault(),void e.next()):void e.trigger("afterKeydown",t,a)}),e.group[e.currIndex].opts.idleTime&&(e.idleSecondsCounter=0,r.on("mousemove.fb-idle mouseleave.fb-idle mousedown.fb-idle touchstart.fb-idle touchmove.fb-idle scroll.fb-idle keydown.fb-idle",function(t){e.idleSecondsCounter=0,e.isIdle&&e.showControls(),e.isIdle=!1}),e.idleInterval=t.setInterval(function(){++e.idleSecondsCounter>=e.group[e.currIndex].opts.idleTime&&!e.isDragging&&(e.isIdle=!0,e.idleSecondsCounter=0,e.hideControls())},1e3))},removeEvents:function(){var e=this;s.off("orientationchange.fb resize.fb"),r.off("keydown.fb .fb-idle"),this.$refs.container.off(".fb-close .fb-prev .fb-next"),e.idleInterval&&(t.clearInterval(e.idleInterval),e.idleInterval=null)},previous:function(t){return this.jumpTo(this.currPos-1,t)},next:function(t){return this.jumpTo(this.currPos+1,t)},jumpTo:function(t,e){var o,i,a,s,r,c,l,d,u,f=this,h=f.group.length;if(!(f.isDragging||f.isClosing||f.isAnimating&&f.firstRun)){if(t=parseInt(t,10),!(a=f.current?f.current.opts.loop:f.opts.loop)&&(t<0||t>=h))return!1;if(o=f.firstRun=!Object.keys(f.slides).length,r=f.current,f.prevIndex=f.currIndex,f.prevPos=f.currPos,s=f.createSlide(t),h>1&&((a||s.index0)&&f.createSlide(t-1)),f.current=s,f.currIndex=s.index,f.currPos=s.pos,f.trigger("beforeShow",o),f.updateControls(),s.forcedDuration=void 0,n.isNumeric(e)?s.forcedDuration=e:e=s.opts[o?"animationDuration":"transitionDuration"],e=parseInt(e,10),i=f.isMoved(s),s.$slide.addClass("fancybox-slide--current"),o)return s.opts.animationEffect&&e&&f.$refs.container.css("transition-duration",e+"ms"),f.$refs.container.addClass("fancybox-is-open").trigger("focus"),f.loadSlide(s),void f.preload("image");c=n.fancybox.getTranslate(r.$slide),l=n.fancybox.getTranslate(f.$refs.stage),n.each(f.slides,function(t,e){n.fancybox.stop(e.$slide,!0)}),r.pos!==s.pos&&(r.isComplete=!1),r.$slide.removeClass("fancybox-slide--complete fancybox-slide--current"),i?(u=c.left-(r.pos*c.width+r.pos*r.opts.gutter),n.each(f.slides,function(t,o){o.$slide.removeClass("fancybox-animated").removeClass(function(t,e){return(e.match(/(^|\s)fancybox-fx-\S+/g)||[]).join(" ")});var i=o.pos*c.width+o.pos*o.opts.gutter;n.fancybox.setTranslate(o.$slide,{top:0,left:i-l.left+u}),o.pos!==s.pos&&o.$slide.addClass("fancybox-slide--"+(o.pos>s.pos?"next":"previous")),p(o.$slide),n.fancybox.animate(o.$slide,{top:0,left:(o.pos-s.pos)*c.width+(o.pos-s.pos)*o.opts.gutter},e,function(){o.$slide.css({transform:"",opacity:""}).removeClass("fancybox-slide--next fancybox-slide--previous"),o.pos===f.currPos&&f.complete()})})):e&&s.opts.transitionEffect&&(d="fancybox-animated fancybox-fx-"+s.opts.transitionEffect,r.$slide.addClass("fancybox-slide--"+(r.pos>s.pos?"next":"previous")),n.fancybox.animate(r.$slide,d,e,function(){r.$slide.removeClass(d).removeClass("fancybox-slide--next fancybox-slide--previous")},!1)),s.isLoaded?f.revealContent(s):f.loadSlide(s),f.preload("image")}},createSlide:function(t){var e,o,i=this;return o=t%i.group.length,o=o<0?i.group.length+o:o,!i.slides[t]&&i.group[o]&&(e=n('

').appendTo(i.$refs.stage),i.slides[t]=n.extend(!0,{},i.group[o],{pos:t,$slide:e,isLoaded:!1}),i.updateSlide(i.slides[t])),i.slides[t]},scaleToActual:function(t,e,o){var i,a,s,r,c,l=this,d=l.current,u=d.$content,f=n.fancybox.getTranslate(d.$slide).width,p=n.fancybox.getTranslate(d.$slide).height,h=d.width,g=d.height;l.isAnimating||l.isMoved()||!u||"image"!=d.type||!d.isLoaded||d.hasError||(l.isAnimating=!0,n.fancybox.stop(u),t=void 0===t?.5*f:t,e=void 0===e?.5*p:e,i=n.fancybox.getTranslate(u),i.top-=n.fancybox.getTranslate(d.$slide).top,i.left-=n.fancybox.getTranslate(d.$slide).left,r=h/i.width,c=g/i.height,a=.5*f-.5*h,s=.5*p-.5*g,h>f&&(a=i.left*r-(t*r-t),a>0&&(a=0),ap&&(s=i.top*c-(e*c-e),s>0&&(s=0),se-.5&&(l=e),d>o-.5&&(d=o),"image"===t.type?(u.top=Math.floor(.5*(o-d))+parseFloat(c.css("paddingTop")),u.left=Math.floor(.5*(e-l))+parseFloat(c.css("paddingLeft"))):"video"===t.contentType&&(a=t.opts.width&&t.opts.height?l/d:t.opts.ratio||16/9,d>l/a?d=l/a:l>d*a&&(l=d*a)),u.width=l,u.height=d,u)},update:function(t){var e=this;n.each(e.slides,function(n,o){e.updateSlide(o,t)})},updateSlide:function(t,e){var o=this,i=t&&t.$content,a=t.width||t.opts.width,s=t.height||t.opts.height,r=t.$slide;o.adjustCaption(t),i&&(a||s||"video"===t.contentType)&&!t.hasError&&(n.fancybox.stop(i),n.fancybox.setTranslate(i,o.getFitPos(t)),t.pos===o.currPos&&(o.isAnimating=!1,o.updateCursor())),o.adjustLayout(t),r.length&&(r.trigger("refresh"),t.pos===o.currPos&&o.$refs.toolbar.add(o.$refs.navigation.find(".fancybox-button--arrow_right")).toggleClass("compensate-for-scrollbar",r.get(0).scrollHeight>r.get(0).clientHeight)),o.trigger("onUpdate",t,e)},centerSlide:function(t){var e=this,o=e.current,i=o.$slide;!e.isClosing&&o&&(i.siblings().css({transform:"",opacity:""}),i.parent().children().removeClass("fancybox-slide--previous fancybox-slide--next"),n.fancybox.animate(i,{top:0,left:0,opacity:1},void 0===t?0:t,function(){i.css({transform:"",opacity:""}),o.isComplete||e.complete()},!1))},isMoved:function(t){var e,o,i=t||this.current;return!!i&&(o=n.fancybox.getTranslate(this.$refs.stage),e=n.fancybox.getTranslate(i.$slide),!i.$slide.hasClass("fancybox-animated")&&(Math.abs(e.top-o.top)>.5||Math.abs(e.left-o.left)>.5))},updateCursor:function(t,e){var o,i,a=this,s=a.current,r=a.$refs.container;s&&!a.isClosing&&a.Guestures&&(r.removeClass("fancybox-is-zoomable fancybox-can-zoomIn fancybox-can-zoomOut fancybox-can-swipe fancybox-can-pan"),o=a.canPan(t,e),i=!!o||a.isZoomable(),r.toggleClass("fancybox-is-zoomable",i),n("[data-fancybox-zoom]").prop("disabled",!i),o?r.addClass("fancybox-can-pan"):i&&("zoom"===s.opts.clickContent||n.isFunction(s.opts.clickContent)&&"zoom"==s.opts.clickContent(s))?r.addClass("fancybox-can-zoomIn"):s.opts.touch&&(s.opts.touch.vertical||a.group.length>1)&&"video"!==s.contentType&&r.addClass("fancybox-can-swipe"))},isZoomable:function(){var t,e=this,n=e.current;if(n&&!e.isClosing&&"image"===n.type&&!n.hasError){if(!n.isLoaded)return!0;if((t=e.getFitPos(n))&&(n.width>t.width||n.height>t.height))return!0}return!1},isScaledDown:function(t,e){var o=this,i=!1,a=o.current,s=a.$content;return void 0!==t&&void 0!==e?i=t1.5||Math.abs(a.height-s.height)>1.5)),s},loadSlide:function(t){var e,o,i,a=this;if(!t.isLoading&&!t.isLoaded){if(t.isLoading=!0,!1===a.trigger("beforeLoad",t))return t.isLoading=!1,!1;switch(e=t.type,o=t.$slide,o.off("refresh").trigger("onReset").addClass(t.opts.slideClass),e){case"image":a.setImage(t);break;case"iframe":a.setIframe(t);break;case"html":a.setContent(t,t.src||t.content);break;case"video":a.setContent(t,t.opts.video.tpl.replace(/\{\{src\}\}/gi,t.src).replace("{{format}}",t.opts.videoFormat||t.opts.video.format||"").replace("{{poster}}",t.thumb||""));break;case"inline":n(t.src).length?a.setContent(t,n(t.src)):a.setError(t);break;case"ajax":a.showLoading(t),i=n.ajax(n.extend({},t.opts.ajax.settings,{url:t.src,success:function(e,n){"success"===n&&a.setContent(t,e)},error:function(e,n){e&&"abort"!==n&&a.setError(t)}})),o.one("onReset",function(){i.abort()});break;default:a.setError(t)}return!0}},setImage:function(t){var o,i=this;setTimeout(function(){var e=t.$image;i.isClosing||!t.isLoading||e&&e.length&&e[0].complete||t.hasError||i.showLoading(t)},50),i.checkSrcset(t),t.$content=n('
').addClass("fancybox-is-hidden").appendTo(t.$slide.addClass("fancybox-slide--image")),!1!==t.opts.preload&&t.opts.width&&t.opts.height&&t.thumb&&(t.width=t.opts.width,t.height=t.opts.height,o=e.createElement("img"),o.onerror=function(){n(this).remove(),t.$ghost=null},o.onload=function(){i.afterLoad(t)},t.$ghost=n(o).addClass("fancybox-image").appendTo(t.$content).attr("src",t.thumb)),i.setBigImage(t)},checkSrcset:function(e){var n,o,i,a,s=e.opts.srcset||e.opts.image.srcset;if(s){i=t.devicePixelRatio||1,a=t.innerWidth*i,o=s.split(",").map(function(t){var e={};return t.trim().split(/\s+/).forEach(function(t,n){var o=parseInt(t.substring(0,t.length-1),10);if(0===n)return e.url=t;o&&(e.value=o,e.postfix=t[t.length-1])}),e}),o.sort(function(t,e){return t.value-e.value});for(var r=0;r=a||"x"===c.postfix&&c.value>=i){n=c;break}}!n&&o.length&&(n=o[o.length-1]),n&&(e.src=n.url,e.width&&e.height&&"w"==n.postfix&&(e.height=e.width/e.height*n.value,e.width=n.value),e.opts.srcset=s)}},setBigImage:function(t){var o=this,i=e.createElement("img"),a=n(i);t.$image=a.one("error",function(){o.setError(t)}).one("load",function(){var e;t.$ghost||(o.resolveImageSlideSize(t,this.naturalWidth,this.naturalHeight),o.afterLoad(t)),o.isClosing||(t.opts.srcset&&(e=t.opts.sizes,e&&"auto"!==e||(e=(t.width/t.height>1&&s.width()/s.height()>1?"100":Math.round(t.width/t.height*100))+"vw"),a.attr("sizes",e).attr("srcset",t.opts.srcset)),t.$ghost&&setTimeout(function(){t.$ghost&&!o.isClosing&&t.$ghost.hide()},Math.min(300,Math.max(1e3,t.height/1600))),o.hideLoading(t))}).addClass("fancybox-image").attr("src",t.src).appendTo(t.$content),(i.complete||"complete"==i.readyState)&&a.naturalWidth&&a.naturalHeight?a.trigger("load"):i.error&&a.trigger("error")},resolveImageSlideSize:function(t,e,n){var o=parseInt(t.opts.width,10),i=parseInt(t.opts.height,10);t.width=e,t.height=n,o>0&&(t.width=o,t.height=Math.floor(o*n/e)),i>0&&(t.width=Math.floor(i*e/n),t.height=i)},setIframe:function(t){var e,o=this,i=t.opts.iframe,a=t.$slide;t.$content=n('
').css(i.css).appendTo(a),a.addClass("fancybox-slide--"+t.contentType),t.$iframe=e=n(i.tpl.replace(/\{rnd\}/g,(new Date).getTime())).attr(i.attr).appendTo(t.$content),i.preload?(o.showLoading(t),e.on("load.fb error.fb",function(e){this.isReady=1,t.$slide.trigger("refresh"),o.afterLoad(t)}),a.on("refresh.fb",function(){var n,o,s=t.$content,r=i.css.width,c=i.css.height;if(1===e[0].isReady){try{n=e.contents(),o=n.find("body")}catch(t){}o&&o.length&&o.children().length&&(a.css("overflow","visible"),s.css({width:"100%","max-width":"100%",height:"9999px"}),void 0===r&&(r=Math.ceil(Math.max(o[0].clientWidth,o.outerWidth(!0)))),s.css("width",r||"").css("max-width",""),void 0===c&&(c=Math.ceil(Math.max(o[0].clientHeight,o.outerHeight(!0)))),s.css("height",c||""),a.css("overflow","auto")),s.removeClass("fancybox-is-hidden")}})):o.afterLoad(t),e.attr("src",t.src),a.one("onReset",function(){try{n(this).find("iframe").hide().unbind().attr("src","//about:blank")}catch(t){}n(this).off("refresh.fb").empty(),t.isLoaded=!1,t.isRevealed=!1})},setContent:function(t,e){var o=this;o.isClosing||(o.hideLoading(t),t.$content&&n.fancybox.stop(t.$content),t.$slide.empty(),l(e)&&e.parent().length?((e.hasClass("fancybox-content")||e.parent().hasClass("fancybox-content"))&&e.parents(".fancybox-slide").trigger("onReset"),t.$placeholder=n("
").hide().insertAfter(e),e.css("display","inline-block")):t.hasError||("string"===n.type(e)&&(e=n("
").append(n.trim(e)).contents()),t.opts.filter&&(e=n("
").html(e).find(t.opts.filter))),t.$slide.one("onReset",function(){n(this).find("video,audio").trigger("pause"),t.$placeholder&&(t.$placeholder.after(e.removeClass("fancybox-content").hide()).remove(),t.$placeholder=null),t.$smallBtn&&(t.$smallBtn.remove(),t.$smallBtn=null),t.hasError||(n(this).empty(),t.isLoaded=!1,t.isRevealed=!1)}),n(e).appendTo(t.$slide),n(e).is("video,audio")&&(n(e).addClass("fancybox-video"),n(e).wrap("
"),t.contentType="video",t.opts.width=t.opts.width||n(e).attr("width"),t.opts.height=t.opts.height||n(e).attr("height")),t.$content=t.$slide.children().filter("div,form,main,video,audio,article,.fancybox-content").first(),t.$content.siblings().hide(),t.$content.length||(t.$content=t.$slide.wrapInner("
").children().first()),t.$content.addClass("fancybox-content"),t.$slide.addClass("fancybox-slide--"+t.contentType),o.afterLoad(t))},setError:function(t){t.hasError=!0,t.$slide.trigger("onReset").removeClass("fancybox-slide--"+t.contentType).addClass("fancybox-slide--error"),t.contentType="html",this.setContent(t,this.translate(t,t.opts.errorTpl)),t.pos===this.currPos&&(this.isAnimating=!1)},showLoading:function(t){var e=this;(t=t||e.current)&&!t.$spinner&&(t.$spinner=n(e.translate(e,e.opts.spinnerTpl)).appendTo(t.$slide).hide().fadeIn("fast"))},hideLoading:function(t){var e=this;(t=t||e.current)&&t.$spinner&&(t.$spinner.stop().remove(),delete t.$spinner)},afterLoad:function(t){var e=this;e.isClosing||(t.isLoading=!1,t.isLoaded=!0,e.trigger("afterLoad",t),e.hideLoading(t),!t.opts.smallBtn||t.$smallBtn&&t.$smallBtn.length||(t.$smallBtn=n(e.translate(t,t.opts.btnTpl.smallBtn)).appendTo(t.$content)),t.opts.protect&&t.$content&&!t.hasError&&(t.$content.on("contextmenu.fb",function(t){return 2==t.button&&t.preventDefault(),!0}),"image"===t.type&&n('
').appendTo(t.$content)),e.adjustCaption(t),e.adjustLayout(t),t.pos===e.currPos&&e.updateCursor(),e.revealContent(t))},adjustCaption:function(t){var e,n=this,o=t||n.current,i=o.opts.caption,a=o.opts.preventCaptionOverlap,s=n.$refs.caption,r=!1;s.toggleClass("fancybox-caption--separate",a),a&&i&&i.length&&(o.pos!==n.currPos?(e=s.clone().appendTo(s.parent()),e.children().eq(0).empty().html(i),r=e.outerHeight(!0),e.empty().remove()):n.$caption&&(r=n.$caption.outerHeight(!0)),o.$slide.css("padding-bottom",r||""))},adjustLayout:function(t){var e,n,o,i,a=this,s=t||a.current;s.isLoaded&&!0!==s.opts.disableLayoutFix&&(s.$content.css("margin-bottom",""),s.$content.outerHeight()>s.$slide.height()+.5&&(o=s.$slide[0].style["padding-bottom"],i=s.$slide.css("padding-bottom"),parseFloat(i)>0&&(e=s.$slide[0].scrollHeight,s.$slide.css("padding-bottom",0),Math.abs(e-s.$slide[0].scrollHeight)<1&&(n=i),s.$slide.css("padding-bottom",o))),s.$content.css("margin-bottom",n))},revealContent:function(t){var e,o,i,a,s=this,r=t.$slide,c=!1,l=!1,d=s.isMoved(t),u=t.isRevealed;return t.isRevealed=!0,e=t.opts[s.firstRun?"animationEffect":"transitionEffect"],i=t.opts[s.firstRun?"animationDuration":"transitionDuration"],i=parseInt(void 0===t.forcedDuration?i:t.forcedDuration,10),!d&&t.pos===s.currPos&&i||(e=!1),"zoom"===e&&(t.pos===s.currPos&&i&&"image"===t.type&&!t.hasError&&(l=s.getThumbPos(t))?c=s.getFitPos(t):e="fade"),"zoom"===e?(s.isAnimating=!0,c.scaleX=c.width/l.width,c.scaleY=c.height/l.height,a=t.opts.zoomOpacity,"auto"==a&&(a=Math.abs(t.width/t.height-l.width/l.height)>.1),a&&(l.opacity=.1,c.opacity=1),n.fancybox.setTranslate(t.$content.removeClass("fancybox-is-hidden"),l),p(t.$content),void n.fancybox.animate(t.$content,c,i,function(){s.isAnimating=!1,s.complete()})):(s.updateSlide(t),e?(n.fancybox.stop(r),o="fancybox-slide--"+(t.pos>=s.prevPos?"next":"previous")+" fancybox-animated fancybox-fx-"+e,r.addClass(o).removeClass("fancybox-slide--current"),t.$content.removeClass("fancybox-is-hidden"),p(r),"image"!==t.type&&t.$content.hide().show(0),void n.fancybox.animate(r,"fancybox-slide--current",i,function(){r.removeClass(o).css({transform:"",opacity:""}),t.pos===s.currPos&&s.complete()},!0)):(t.$content.removeClass("fancybox-is-hidden"),u||!d||"image"!==t.type||t.hasError||t.$content.hide().fadeIn("fast"),void(t.pos===s.currPos&&s.complete())))},getThumbPos:function(t){var e,o,i,a,s,r=!1,c=t.$thumb;return!(!c||!g(c[0]))&&(e=n.fancybox.getTranslate(c),o=parseFloat(c.css("border-top-width")||0),i=parseFloat(c.css("border-right-width")||0),a=parseFloat(c.css("border-bottom-width")||0),s=parseFloat(c.css("border-left-width")||0),r={top:e.top+o,left:e.left+s,width:e.width-i-s,height:e.height-o-a,scaleX:1,scaleY:1},e.width>0&&e.height>0&&r)},complete:function(){var t,e=this,o=e.current,i={};!e.isMoved()&&o.isLoaded&&(o.isComplete||(o.isComplete=!0,o.$slide.siblings().trigger("onReset"),e.preload("inline"),p(o.$slide),o.$slide.addClass("fancybox-slide--complete"),n.each(e.slides,function(t,o){o.pos>=e.currPos-1&&o.pos<=e.currPos+1?i[o.pos]=o:o&&(n.fancybox.stop(o.$slide),o.$slide.off().remove())}),e.slides=i),e.isAnimating=!1,e.updateCursor(),e.trigger("afterShow"),o.opts.video.autoStart&&o.$slide.find("video,audio").filter(":visible:first").trigger("play").one("ended",function(){Document.exitFullscreen?Document.exitFullscreen():this.webkitExitFullscreen&&this.webkitExitFullscreen(),e.next()}),o.opts.autoFocus&&"html"===o.contentType&&(t=o.$content.find("input[autofocus]:enabled:visible:first"),t.length?t.trigger("focus"):e.focus(null,!0)),o.$slide.scrollTop(0).scrollLeft(0))},preload:function(t){var e,n,o=this;o.group.length<2||(n=o.slides[o.currPos+1],e=o.slides[o.currPos-1],e&&e.type===t&&o.loadSlide(e),n&&n.type===t&&o.loadSlide(n))},focus:function(t,o){var i,a,s=this,r=["a[href]","area[href]",'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',"select:not([disabled]):not([aria-hidden])","textarea:not([disabled]):not([aria-hidden])","button:not([disabled]):not([aria-hidden])","iframe","object","embed","video","audio","[contenteditable]",'[tabindex]:not([tabindex^="-"])'].join(",");s.isClosing||(i=!t&&s.current&&s.current.isComplete?s.current.$slide.find("*:visible"+(o?":not(.fancybox-close-small)":"")):s.$refs.container.find("*:visible"),i=i.filter(r).filter(function(){return"hidden"!==n(this).css("visibility")&&!n(this).hasClass("disabled")}),i.length?(a=i.index(e.activeElement),t&&t.shiftKey?(a<0||0==a)&&(t.preventDefault(),i.eq(i.length-1).trigger("focus")):(a<0||a==i.length-1)&&(t&&t.preventDefault(),i.eq(0).trigger("focus"))):s.$refs.container.trigger("focus"))},activate:function(){var t=this;n(".fancybox-container").each(function(){var e=n(this).data("FancyBox");e&&e.id!==t.id&&!e.isClosing&&(e.trigger("onDeactivate"),e.removeEvents(),e.isVisible=!1)}),t.isVisible=!0,(t.current||t.isIdle)&&(t.update(),t.updateControls()),t.trigger("onActivate"),t.addEvents()},close:function(t,e){var o,i,a,s,r,c,l,u=this,f=u.current,h=function(){u.cleanUp(t)};return!u.isClosing&&(u.isClosing=!0,!1===u.trigger("beforeClose",t)?(u.isClosing=!1,d(function(){u.update()}),!1):(u.removeEvents(),a=f.$content,o=f.opts.animationEffect,i=n.isNumeric(e)?e:o?f.opts.animationDuration:0,f.$slide.removeClass("fancybox-slide--complete fancybox-slide--next fancybox-slide--previous fancybox-animated"),!0!==t?n.fancybox.stop(f.$slide):o=!1,f.$slide.siblings().trigger("onReset").remove(),i&&u.$refs.container.removeClass("fancybox-is-open").addClass("fancybox-is-closing").css("transition-duration",i+"ms"),u.hideLoading(f),u.hideControls(!0),u.updateCursor(),"zoom"!==o||a&&i&&"image"===f.type&&!u.isMoved()&&!f.hasError&&(l=u.getThumbPos(f))||(o="fade"),"zoom"===o?(n.fancybox.stop(a),s=n.fancybox.getTranslate(a),c={top:s.top,left:s.left,scaleX:s.width/l.width,scaleY:s.height/l.height,width:l.width,height:l.height},r=f.opts.zoomOpacity, +"auto"==r&&(r=Math.abs(f.width/f.height-l.width/l.height)>.1),r&&(l.opacity=0),n.fancybox.setTranslate(a,c),p(a),n.fancybox.animate(a,l,i,h),!0):(o&&i?n.fancybox.animate(f.$slide.addClass("fancybox-slide--previous").removeClass("fancybox-slide--current"),"fancybox-animated fancybox-fx-"+o,i,h):!0===t?setTimeout(h,i):h(),!0)))},cleanUp:function(e){var o,i,a,s=this,r=s.current.opts.$orig;s.current.$slide.trigger("onReset"),s.$refs.container.empty().remove(),s.trigger("afterClose",e),s.current.opts.backFocus&&(r&&r.length&&r.is(":visible")||(r=s.$trigger),r&&r.length&&(i=t.scrollX,a=t.scrollY,r.trigger("focus"),n("html, body").scrollTop(a).scrollLeft(i))),s.current=null,o=n.fancybox.getInstance(),o?o.activate():(n("body").removeClass("fancybox-active compensate-for-scrollbar"),n("#fancybox-style-noscroll").remove())},trigger:function(t,e){var o,i=Array.prototype.slice.call(arguments,1),a=this,s=e&&e.opts?e:a.current;if(s?i.unshift(s):s=a,i.unshift(a),n.isFunction(s.opts[t])&&(o=s.opts[t].apply(s,i)),!1===o)return o;"afterClose"!==t&&a.$refs?a.$refs.container.trigger(t+".fb",i):r.trigger(t+".fb",i)},updateControls:function(){var t=this,o=t.current,i=o.index,a=t.$refs.container,s=t.$refs.caption,r=o.opts.caption;o.$slide.trigger("refresh"),r&&r.length?(t.$caption=s,s.children().eq(0).html(r)):t.$caption=null,t.hasHiddenControls||t.isIdle||t.showControls(),a.find("[data-fancybox-count]").html(t.group.length),a.find("[data-fancybox-index]").html(i+1),a.find("[data-fancybox-prev]").prop("disabled",!o.opts.loop&&i<=0),a.find("[data-fancybox-next]").prop("disabled",!o.opts.loop&&i>=t.group.length-1),"image"===o.type?a.find("[data-fancybox-zoom]").show().end().find("[data-fancybox-download]").attr("href",o.opts.image.src||o.src).show():o.opts.toolbar&&a.find("[data-fancybox-download],[data-fancybox-zoom]").hide(),n(e.activeElement).is(":hidden,[disabled]")&&t.$refs.container.trigger("focus")},hideControls:function(t){var e=this,n=["infobar","toolbar","nav"];!t&&e.current.opts.preventCaptionOverlap||n.push("caption"),this.$refs.container.removeClass(n.map(function(t){return"fancybox-show-"+t}).join(" ")),this.hasHiddenControls=!0},showControls:function(){var t=this,e=t.current?t.current.opts:t.opts,n=t.$refs.container;t.hasHiddenControls=!1,t.idleSecondsCounter=0,n.toggleClass("fancybox-show-toolbar",!(!e.toolbar||!e.buttons)).toggleClass("fancybox-show-infobar",!!(e.infobar&&t.group.length>1)).toggleClass("fancybox-show-caption",!!t.$caption).toggleClass("fancybox-show-nav",!!(e.arrows&&t.group.length>1)).toggleClass("fancybox-is-modal",!!e.modal)},toggleControls:function(){this.hasHiddenControls?this.showControls():this.hideControls()}}),n.fancybox={version:"3.5.7",defaults:a,getInstance:function(t){var e=n('.fancybox-container:not(".fancybox-is-closing"):last').data("FancyBox"),o=Array.prototype.slice.call(arguments,1);return e instanceof b&&("string"===n.type(t)?e[t].apply(e,o):"function"===n.type(t)&&t.apply(e,o),e)},open:function(t,e,n){return new b(t,e,n)},close:function(t){var e=this.getInstance();e&&(e.close(),!0===t&&this.close(t))},destroy:function(){this.close(!0),r.add("body").off("click.fb-start","**")},isMobile:/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),use3d:function(){var n=e.createElement("div");return t.getComputedStyle&&t.getComputedStyle(n)&&t.getComputedStyle(n).getPropertyValue("transform")&&!(e.documentMode&&e.documentMode<11)}(),getTranslate:function(t){var e;return!(!t||!t.length)&&(e=t[0].getBoundingClientRect(),{top:e.top||0,left:e.left||0,width:e.width,height:e.height,opacity:parseFloat(t.css("opacity"))})},setTranslate:function(t,e){var n="",o={};if(t&&e)return void 0===e.left&&void 0===e.top||(n=(void 0===e.left?t.position().left:e.left)+"px, "+(void 0===e.top?t.position().top:e.top)+"px",n=this.use3d?"translate3d("+n+", 0px)":"translate("+n+")"),void 0!==e.scaleX&&void 0!==e.scaleY?n+=" scale("+e.scaleX+", "+e.scaleY+")":void 0!==e.scaleX&&(n+=" scaleX("+e.scaleX+")"),n.length&&(o.transform=n),void 0!==e.opacity&&(o.opacity=e.opacity),void 0!==e.width&&(o.width=e.width),void 0!==e.height&&(o.height=e.height),t.css(o)},animate:function(t,e,o,i,a){var s,r=this;n.isFunction(o)&&(i=o,o=null),r.stop(t),s=r.getTranslate(t),t.on(f,function(c){(!c||!c.originalEvent||t.is(c.originalEvent.target)&&"z-index"!=c.originalEvent.propertyName)&&(r.stop(t),n.isNumeric(o)&&t.css("transition-duration",""),n.isPlainObject(e)?void 0!==e.scaleX&&void 0!==e.scaleY&&r.setTranslate(t,{top:e.top,left:e.left,width:s.width*e.scaleX,height:s.height*e.scaleY,scaleX:1,scaleY:1}):!0!==a&&t.removeClass(e),n.isFunction(i)&&i(c))}),n.isNumeric(o)&&t.css("transition-duration",o+"ms"),n.isPlainObject(e)?(void 0!==e.scaleX&&void 0!==e.scaleY&&(delete e.width,delete e.height,t.parent().hasClass("fancybox-slide--image")&&t.parent().addClass("fancybox-is-scaling")),n.fancybox.setTranslate(t,e)):t.addClass(e),t.data("timer",setTimeout(function(){t.trigger(f)},o+33))},stop:function(t,e){t&&t.length&&(clearTimeout(t.data("timer")),e&&t.trigger(f),t.off(f).css("transition-duration",""),t.parent().removeClass("fancybox-is-scaling"))}},n.fn.fancybox=function(t){var e;return t=t||{},e=t.selector||!1,e?n("body").off("click.fb-start",e).on("click.fb-start",e,{options:t},i):this.off("click.fb-start").on("click.fb-start",{items:this,options:t},i),this},r.on("click.fb-start","[data-fancybox]",i),r.on("click.fb-start","[data-fancybox-trigger]",function(t){n('[data-fancybox="'+n(this).attr("data-fancybox-trigger")+'"]').eq(n(this).attr("data-fancybox-index")||0).trigger("click.fb-start",{$trigger:n(this)})}),function(){var t=null;r.on("mousedown mouseup focus blur",".fancybox-button",function(e){switch(e.type){case"mousedown":t=n(this);break;case"mouseup":t=null;break;case"focusin":n(".fancybox-button").removeClass("fancybox-focus"),n(this).is(t)||n(this).is("[disabled]")||n(this).addClass("fancybox-focus");break;case"focusout":n(".fancybox-button").removeClass("fancybox-focus")}})}()}}(window,document,jQuery),function(t){"use strict";var e={youtube:{matcher:/(youtube\.com|youtu\.be|youtube\-nocookie\.com)\/(watch\?(.*&)?v=|v\/|u\/|embed\/?)?(videoseries\?list=(.*)|[\w-]{11}|\?listType=(.*)&list=(.*))(.*)/i,params:{autoplay:1,autohide:1,fs:1,rel:0,hd:1,wmode:"transparent",enablejsapi:1,html5:1},paramPlace:8,type:"iframe",url:"https://www.youtube-nocookie.com/embed/$4",thumb:"https://img.youtube.com/vi/$4/hqdefault.jpg"},vimeo:{matcher:/^.+vimeo.com\/(.*\/)?([\d]+)(.*)?/,params:{autoplay:1,hd:1,show_title:1,show_byline:1,show_portrait:0,fullscreen:1},paramPlace:3,type:"iframe",url:"//player.vimeo.com/video/$2"},instagram:{matcher:/(instagr\.am|instagram\.com)\/p\/([a-zA-Z0-9_\-]+)\/?/i,type:"image",url:"//$1/p/$2/media/?size=l"},gmap_place:{matcher:/(maps\.)?google\.([a-z]{2,3}(\.[a-z]{2})?)\/(((maps\/(place\/(.*)\/)?\@(.*),(\d+.?\d+?)z))|(\?ll=))(.*)?/i,type:"iframe",url:function(t){return"//maps.google."+t[2]+"/?ll="+(t[9]?t[9]+"&z="+Math.floor(t[10])+(t[12]?t[12].replace(/^\//,"&"):""):t[12]+"").replace(/\?/,"&")+"&output="+(t[12]&&t[12].indexOf("layer=c")>0?"svembed":"embed")}},gmap_search:{matcher:/(maps\.)?google\.([a-z]{2,3}(\.[a-z]{2})?)\/(maps\/search\/)(.*)/i,type:"iframe",url:function(t){return"//maps.google."+t[2]+"/maps?q="+t[5].replace("query=","q=").replace("api=1","")+"&output=embed"}}},n=function(e,n,o){if(e)return o=o||"","object"===t.type(o)&&(o=t.param(o,!0)),t.each(n,function(t,n){e=e.replace("$"+t,n||"")}),o.length&&(e+=(e.indexOf("?")>0?"&":"?")+o),e};t(document).on("objectNeedsType.fb",function(o,i,a){var s,r,c,l,d,u,f,p=a.src||"",h=!1;s=t.extend(!0,{},e,a.opts.media),t.each(s,function(e,o){if(c=p.match(o.matcher)){if(h=o.type,f=e,u={},o.paramPlace&&c[o.paramPlace]){d=c[o.paramPlace],"?"==d[0]&&(d=d.substring(1)),d=d.split("&");for(var i=0;i1&&("youtube"===n.contentSource||"vimeo"===n.contentSource)&&o.load(n.contentSource)}})}(jQuery),function(t,e,n){"use strict";var o=function(){return t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||function(e){return t.setTimeout(e,1e3/60)}}(),i=function(){return t.cancelAnimationFrame||t.webkitCancelAnimationFrame||t.mozCancelAnimationFrame||t.oCancelAnimationFrame||function(e){t.clearTimeout(e)}}(),a=function(e){var n=[];e=e.originalEvent||e||t.e,e=e.touches&&e.touches.length?e.touches:e.changedTouches&&e.changedTouches.length?e.changedTouches:[e];for(var o in e)e[o].pageX?n.push({x:e[o].pageX,y:e[o].pageY}):e[o].clientX&&n.push({x:e[o].clientX,y:e[o].clientY});return n},s=function(t,e,n){return e&&t?"x"===n?t.x-e.x:"y"===n?t.y-e.y:Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2)):0},r=function(t){if(t.is('a,area,button,[role="button"],input,label,select,summary,textarea,video,audio,iframe')||n.isFunction(t.get(0).onclick)||t.data("selectable"))return!0;for(var e=0,o=t[0].attributes,i=o.length;ee.clientHeight,a=("scroll"===o||"auto"===o)&&e.scrollWidth>e.clientWidth;return i||a},l=function(t){for(var e=!1;;){if(e=c(t.get(0)))break;if(t=t.parent(),!t.length||t.hasClass("fancybox-stage")||t.is("body"))break}return e},d=function(t){var e=this;e.instance=t,e.$bg=t.$refs.bg,e.$stage=t.$refs.stage,e.$container=t.$refs.container,e.destroy(),e.$container.on("touchstart.fb.touch mousedown.fb.touch",n.proxy(e,"ontouchstart"))};d.prototype.destroy=function(){var t=this;t.$container.off(".fb.touch"),n(e).off(".fb.touch"),t.requestId&&(i(t.requestId),t.requestId=null),t.tapped&&(clearTimeout(t.tapped),t.tapped=null)},d.prototype.ontouchstart=function(o){var i=this,c=n(o.target),d=i.instance,u=d.current,f=u.$slide,p=u.$content,h="touchstart"==o.type;if(h&&i.$container.off("mousedown.fb.touch"),(!o.originalEvent||2!=o.originalEvent.button)&&f.length&&c.length&&!r(c)&&!r(c.parent())&&(c.is("img")||!(o.originalEvent.clientX>c[0].clientWidth+c.offset().left))){if(!u||d.isAnimating||u.$slide.hasClass("fancybox-animated"))return o.stopPropagation(),void o.preventDefault();i.realPoints=i.startPoints=a(o),i.startPoints.length&&(u.touch&&o.stopPropagation(),i.startEvent=o,i.canTap=!0,i.$target=c,i.$content=p,i.opts=u.opts.touch,i.isPanning=!1,i.isSwiping=!1,i.isZooming=!1,i.isScrolling=!1,i.canPan=d.canPan(),i.startTime=(new Date).getTime(),i.distanceX=i.distanceY=i.distance=0,i.canvasWidth=Math.round(f[0].clientWidth),i.canvasHeight=Math.round(f[0].clientHeight),i.contentLastPos=null,i.contentStartPos=n.fancybox.getTranslate(i.$content)||{top:0,left:0},i.sliderStartPos=n.fancybox.getTranslate(f),i.stagePos=n.fancybox.getTranslate(d.$refs.stage),i.sliderStartPos.top-=i.stagePos.top,i.sliderStartPos.left-=i.stagePos.left,i.contentStartPos.top-=i.stagePos.top,i.contentStartPos.left-=i.stagePos.left,n(e).off(".fb.touch").on(h?"touchend.fb.touch touchcancel.fb.touch":"mouseup.fb.touch mouseleave.fb.touch",n.proxy(i,"ontouchend")).on(h?"touchmove.fb.touch":"mousemove.fb.touch",n.proxy(i,"ontouchmove")),n.fancybox.isMobile&&e.addEventListener("scroll",i.onscroll,!0),((i.opts||i.canPan)&&(c.is(i.$stage)||i.$stage.find(c).length)||(c.is(".fancybox-image")&&o.preventDefault(),n.fancybox.isMobile&&c.parents(".fancybox-caption").length))&&(i.isScrollable=l(c)||l(c.parent()),n.fancybox.isMobile&&i.isScrollable||o.preventDefault(),(1===i.startPoints.length||u.hasError)&&(i.canPan?(n.fancybox.stop(i.$content),i.isPanning=!0):i.isSwiping=!0,i.$container.addClass("fancybox-is-grabbing")),2===i.startPoints.length&&"image"===u.type&&(u.isLoaded||u.$ghost)&&(i.canTap=!1,i.isSwiping=!1,i.isPanning=!1,i.isZooming=!0,n.fancybox.stop(i.$content),i.centerPointStartX=.5*(i.startPoints[0].x+i.startPoints[1].x)-n(t).scrollLeft(),i.centerPointStartY=.5*(i.startPoints[0].y+i.startPoints[1].y)-n(t).scrollTop(),i.percentageOfImageAtPinchPointX=(i.centerPointStartX-i.contentStartPos.left)/i.contentStartPos.width,i.percentageOfImageAtPinchPointY=(i.centerPointStartY-i.contentStartPos.top)/i.contentStartPos.height,i.startDistanceBetweenFingers=s(i.startPoints[0],i.startPoints[1]))))}},d.prototype.onscroll=function(t){var n=this;n.isScrolling=!0,e.removeEventListener("scroll",n.onscroll,!0)},d.prototype.ontouchmove=function(t){var e=this;return void 0!==t.originalEvent.buttons&&0===t.originalEvent.buttons?void e.ontouchend(t):e.isScrolling?void(e.canTap=!1):(e.newPoints=a(t),void((e.opts||e.canPan)&&e.newPoints.length&&e.newPoints.length&&(e.isSwiping&&!0===e.isSwiping||t.preventDefault(),e.distanceX=s(e.newPoints[0],e.startPoints[0],"x"),e.distanceY=s(e.newPoints[0],e.startPoints[0],"y"),e.distance=s(e.newPoints[0],e.startPoints[0]),e.distance>0&&(e.isSwiping?e.onSwipe(t):e.isPanning?e.onPan():e.isZooming&&e.onZoom()))))},d.prototype.onSwipe=function(e){var a,s=this,r=s.instance,c=s.isSwiping,l=s.sliderStartPos.left||0;if(!0!==c)"x"==c&&(s.distanceX>0&&(s.instance.group.length<2||0===s.instance.current.index&&!s.instance.current.opts.loop)?l+=Math.pow(s.distanceX,.8):s.distanceX<0&&(s.instance.group.length<2||s.instance.current.index===s.instance.group.length-1&&!s.instance.current.opts.loop)?l-=Math.pow(-s.distanceX,.8):l+=s.distanceX),s.sliderLastPos={top:"x"==c?0:s.sliderStartPos.top+s.distanceY,left:l},s.requestId&&(i(s.requestId),s.requestId=null),s.requestId=o(function(){s.sliderLastPos&&(n.each(s.instance.slides,function(t,e){var o=e.pos-s.instance.currPos;n.fancybox.setTranslate(e.$slide,{top:s.sliderLastPos.top,left:s.sliderLastPos.left+o*s.canvasWidth+o*e.opts.gutter})}),s.$container.addClass("fancybox-is-sliding"))});else if(Math.abs(s.distance)>10){if(s.canTap=!1,r.group.length<2&&s.opts.vertical?s.isSwiping="y":r.isDragging||!1===s.opts.vertical||"auto"===s.opts.vertical&&n(t).width()>800?s.isSwiping="x":(a=Math.abs(180*Math.atan2(s.distanceY,s.distanceX)/Math.PI),s.isSwiping=a>45&&a<135?"y":"x"),"y"===s.isSwiping&&n.fancybox.isMobile&&s.isScrollable)return void(s.isScrolling=!0);r.isDragging=s.isSwiping,s.startPoints=s.newPoints,n.each(r.slides,function(t,e){var o,i;n.fancybox.stop(e.$slide),o=n.fancybox.getTranslate(e.$slide),i=n.fancybox.getTranslate(r.$refs.stage),e.$slide.css({transform:"",opacity:"","transition-duration":""}).removeClass("fancybox-animated").removeClass(function(t,e){return(e.match(/(^|\s)fancybox-fx-\S+/g)||[]).join(" ")}),e.pos===r.current.pos&&(s.sliderStartPos.top=o.top-i.top,s.sliderStartPos.left=o.left-i.left),n.fancybox.setTranslate(e.$slide,{top:o.top-i.top,left:o.left-i.left})}),r.SlideShow&&r.SlideShow.isActive&&r.SlideShow.stop()}},d.prototype.onPan=function(){var t=this;if(s(t.newPoints[0],t.realPoints[0])<(n.fancybox.isMobile?10:5))return void(t.startPoints=t.newPoints);t.canTap=!1,t.contentLastPos=t.limitMovement(),t.requestId&&i(t.requestId),t.requestId=o(function(){n.fancybox.setTranslate(t.$content,t.contentLastPos)})},d.prototype.limitMovement=function(){var t,e,n,o,i,a,s=this,r=s.canvasWidth,c=s.canvasHeight,l=s.distanceX,d=s.distanceY,u=s.contentStartPos,f=u.left,p=u.top,h=u.width,g=u.height;return i=h>r?f+l:f,a=p+d,t=Math.max(0,.5*r-.5*h),e=Math.max(0,.5*c-.5*g),n=Math.min(r-h,.5*r-.5*h),o=Math.min(c-g,.5*c-.5*g),l>0&&i>t&&(i=t-1+Math.pow(-t+f+l,.8)||0),l<0&&i0&&a>e&&(a=e-1+Math.pow(-e+p+d,.8)||0),d<0&&aa?(t=t>0?0:t,t=ts?(e=e>0?0:e,e=e1&&(o.dMs>130&&s>10||s>50);o.sliderLastPos=null,"y"==t&&!e&&Math.abs(o.distanceY)>50?(n.fancybox.animate(o.instance.current.$slide,{top:o.sliderStartPos.top+o.distanceY+150*o.velocityY,opacity:0},200),i=o.instance.close(!0,250)):r&&o.distanceX>0?i=o.instance.previous(300):r&&o.distanceX<0&&(i=o.instance.next(300)),!1!==i||"x"!=t&&"y"!=t||o.instance.centerSlide(200),o.$container.removeClass("fancybox-is-sliding")},d.prototype.endPanning=function(){var t,e,o,i=this;i.contentLastPos&&(!1===i.opts.momentum||i.dMs>350?(t=i.contentLastPos.left,e=i.contentLastPos.top):(t=i.contentLastPos.left+500*i.velocityX,e=i.contentLastPos.top+500*i.velocityY),o=i.limitPosition(t,e,i.contentStartPos.width,i.contentStartPos.height),o.width=i.contentStartPos.width,o.height=i.contentStartPos.height,n.fancybox.animate(i.$content,o,366))},d.prototype.endZooming=function(){var t,e,o,i,a=this,s=a.instance.current,r=a.newWidth,c=a.newHeight;a.contentLastPos&&(t=a.contentLastPos.left,e=a.contentLastPos.top,i={top:e,left:t,width:r,height:c,scaleX:1,scaleY:1},n.fancybox.setTranslate(a.$content,i),rs.width||c>s.height?a.instance.scaleToActual(a.centerPointStartX,a.centerPointStartY,150):(o=a.limitPosition(t,e,r,c),n.fancybox.animate(a.$content,o,150)))},d.prototype.onTap=function(e){var o,i=this,s=n(e.target),r=i.instance,c=r.current,l=e&&a(e)||i.startPoints,d=l[0]?l[0].x-n(t).scrollLeft()-i.stagePos.left:0,u=l[0]?l[0].y-n(t).scrollTop()-i.stagePos.top:0,f=function(t){var o=c.opts[t];if(n.isFunction(o)&&(o=o.apply(r,[c,e])),o)switch(o){case"close":r.close(i.startEvent);break;case"toggleControls":r.toggleControls();break;case"next":r.next();break;case"nextOrClose":r.group.length>1?r.next():r.close(i.startEvent);break;case"zoom":"image"==c.type&&(c.isLoaded||c.$ghost)&&(r.canPan()?r.scaleToFit():r.isScaledDown()?r.scaleToActual(d,u):r.group.length<2&&r.close(i.startEvent))}};if((!e.originalEvent||2!=e.originalEvent.button)&&(s.is("img")||!(d>s[0].clientWidth+s.offset().left))){if(s.is(".fancybox-bg,.fancybox-inner,.fancybox-outer,.fancybox-container"))o="Outside";else if(s.is(".fancybox-slide"))o="Slide";else{if(!r.current.$content||!r.current.$content.find(s).addBack().filter(s).length)return;o="Content"}if(i.tapped){if(clearTimeout(i.tapped),i.tapped=null,Math.abs(d-i.tapX)>50||Math.abs(u-i.tapY)>50)return this;f("dblclick"+o)}else i.tapX=d,i.tapY=u,c.opts["dblclick"+o]&&c.opts["dblclick"+o]!==c.opts["click"+o]?i.tapped=setTimeout(function(){i.tapped=null,r.isAnimating||f("click"+o)},500):f("click"+o);return this}},n(e).on("onActivate.fb",function(t,e){e&&!e.Guestures&&(e.Guestures=new d(e))}).on("beforeClose.fb",function(t,e){e&&e.Guestures&&e.Guestures.destroy()})}(window,document,jQuery),function(t,e){"use strict";e.extend(!0,e.fancybox.defaults,{btnTpl:{slideShow:''},slideShow:{autoStart:!1,speed:3e3,progress:!0}});var n=function(t){this.instance=t,this.init()};e.extend(n.prototype,{timer:null,isActive:!1,$button:null,init:function(){var t=this,n=t.instance,o=n.group[n.currIndex].opts.slideShow;t.$button=n.$refs.toolbar.find("[data-fancybox-play]").on("click",function(){t.toggle()}),n.group.length<2||!o?t.$button.hide():o.progress&&(t.$progress=e('
').appendTo(n.$refs.inner))},set:function(t){var n=this,o=n.instance,i=o.current;i&&(!0===t||i.opts.loop||o.currIndex'},fullScreen:{autoStart:!1}}),e(t).on(n.fullscreenchange,function(){var t=o.isFullscreen(),n=e.fancybox.getInstance();n&&(n.current&&"image"===n.current.type&&n.isAnimating&&(n.isAnimating=!1,n.update(!0,!0,0),n.isComplete||n.complete()),n.trigger("onFullscreenChange",t),n.$refs.container.toggleClass("fancybox-is-fullscreen",t),n.$refs.toolbar.find("[data-fancybox-fullscreen]").toggleClass("fancybox-button--fsenter",!t).toggleClass("fancybox-button--fsexit",t))})}e(t).on({"onInit.fb":function(t,e){var i;if(!n)return void e.$refs.toolbar.find("[data-fancybox-fullscreen]").remove();e&&e.group[e.currIndex].opts.fullScreen?(i=e.$refs.container,i.on("click.fb-fullscreen","[data-fancybox-fullscreen]",function(t){t.stopPropagation(),t.preventDefault(),o.toggle()}),e.opts.fullScreen&&!0===e.opts.fullScreen.autoStart&&o.request(),e.FullScreen=o):e&&e.$refs.toolbar.find("[data-fancybox-fullscreen]").hide()},"afterKeydown.fb":function(t,e,n,o,i){e&&e.FullScreen&&70===i&&(o.preventDefault(),e.FullScreen.toggle())},"beforeClose.fb":function(t,e){e&&e.FullScreen&&e.$refs.container.hasClass("fancybox-is-fullscreen")&&o.exit()}})}(document,jQuery),function(t,e){"use strict";var n="fancybox-thumbs";e.fancybox.defaults=e.extend(!0,{btnTpl:{thumbs:''},thumbs:{autoStart:!1,hideOnClose:!0,parentEl:".fancybox-container",axis:"y"}},e.fancybox.defaults);var o=function(t){this.init(t)};e.extend(o.prototype,{$button:null,$grid:null,$list:null,isVisible:!1,isActive:!1,init:function(t){var e=this,n=t.group,o=0;e.instance=t,e.opts=n[t.currIndex].opts.thumbs,t.Thumbs=e,e.$button=t.$refs.toolbar.find("[data-fancybox-thumbs]");for(var i=0,a=n.length;i1));i++);o>1&&e.opts?(e.$button.removeAttr("style").on("click",function(){e.toggle()}),e.isActive=!0):e.$button.hide()},create:function(){var t,o=this,i=o.instance,a=o.opts.parentEl,s=[];o.$grid||(o.$grid=e('
').appendTo(i.$refs.container.find(a).addBack().filter(a)),o.$grid.on("click","a",function(){i.jumpTo(e(this).attr("data-index"))})),o.$list||(o.$list=e('
').appendTo(o.$grid)),e.each(i.group,function(e,n){t=n.thumb,t||"image"!==n.type||(t=n.src),s.push('")}),o.$list[0].innerHTML=s.join(""),"x"===o.opts.axis&&o.$list.width(parseInt(o.$grid.css("padding-right"),10)+i.group.length*o.$list.children().eq(0).outerWidth(!0))},focus:function(t){var e,n,o=this,i=o.$list,a=o.$grid;o.instance.current&&(e=i.children().removeClass("fancybox-thumbs-active").filter('[data-index="'+o.instance.current.index+'"]').addClass("fancybox-thumbs-active"),n=e.position(),"y"===o.opts.axis&&(n.top<0||n.top>i.height()-e.outerHeight())?i.stop().animate({scrollTop:i.scrollTop()+n.top},t):"x"===o.opts.axis&&(n.lefta.scrollLeft()+(a.width()-e.outerWidth()))&&i.parent().stop().animate({scrollLeft:n.left},t))},update:function(){var t=this;t.instance.$refs.container.toggleClass("fancybox-show-thumbs",this.isVisible),t.isVisible?(t.$grid||t.create(),t.instance.trigger("onThumbsShow"),t.focus(0)):t.$grid&&t.instance.trigger("onThumbsHide"),t.instance.update()},hide:function(){this.isVisible=!1,this.update()},show:function(){this.isVisible=!0,this.update()},toggle:function(){this.isVisible=!this.isVisible,this.update()}}),e(t).on({"onInit.fb":function(t,e){var n;e&&!e.Thumbs&&(n=new o(e),n.isActive&&!0===n.opts.autoStart&&n.show())},"beforeShow.fb":function(t,e,n,o){var i=e&&e.Thumbs;i&&i.isVisible&&i.focus(o?0:250)},"afterKeydown.fb":function(t,e,n,o,i){var a=e&&e.Thumbs;a&&a.isActive&&71===i&&(o.preventDefault(),a.toggle())},"beforeClose.fb":function(t,e){var n=e&&e.Thumbs;n&&n.isVisible&&!1!==n.opts.hideOnClose&&n.$grid.hide()}})}(document,jQuery),function(t,e){"use strict";function n(t){var e={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};return String(t).replace(/[&<>"'`=\/]/g,function(t){return e[t]})}e.extend(!0,e.fancybox.defaults,{btnTpl:{share:''},share:{url:function(t,e){return!t.currentHash&&"inline"!==e.type&&"html"!==e.type&&(e.origSrc||e.src)||window.location}, +tpl:''}}),e(t).on("click","[data-fancybox-share]",function(){var t,o,i=e.fancybox.getInstance(),a=i.current||null;a&&("function"===e.type(a.opts.share.url)&&(t=a.opts.share.url.apply(a,[i,a])),o=a.opts.share.tpl.replace(/\{\{media\}\}/g,"image"===a.type?encodeURIComponent(a.src):"").replace(/\{\{url\}\}/g,encodeURIComponent(t)).replace(/\{\{url_raw\}\}/g,n(t)).replace(/\{\{descr\}\}/g,i.$caption?encodeURIComponent(i.$caption.text()):""),e.fancybox.open({src:i.translate(i,o),type:"html",opts:{touch:!1,animationEffect:!1,afterLoad:function(t,e){i.$refs.container.one("beforeClose.fb",function(){t.close(null,0)}),e.$content.find(".fancybox-share__button").click(function(){return window.open(this.href,"Share","width=550, height=450"),!1})},mobile:{autoFocus:!1}}}))})}(document,jQuery),function(t,e,n){"use strict";function o(){var e=t.location.hash.substr(1),n=e.split("-"),o=n.length>1&&/^\+?\d+$/.test(n[n.length-1])?parseInt(n.pop(-1),10)||1:1,i=n.join("-");return{hash:e,index:o<1?1:o,gallery:i}}function i(t){""!==t.gallery&&n("[data-fancybox='"+n.escapeSelector(t.gallery)+"']").eq(t.index-1).focus().trigger("click.fb-start")}function a(t){var e,n;return!!t&&(e=t.current?t.current.opts:t.opts,""!==(n=e.hash||(e.$orig?e.$orig.data("fancybox")||e.$orig.data("fancybox-trigger"):""))&&n)}n.escapeSelector||(n.escapeSelector=function(t){return(t+"").replace(/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,function(t,e){return e?"\0"===t?"�":t.slice(0,-1)+"\\"+t.charCodeAt(t.length-1).toString(16)+" ":"\\"+t})}),n(function(){!1!==n.fancybox.defaults.hash&&(n(e).on({"onInit.fb":function(t,e){var n,i;!1!==e.group[e.currIndex].opts.hash&&(n=o(),(i=a(e))&&n.gallery&&i==n.gallery&&(e.currIndex=n.index-1))},"beforeShow.fb":function(n,o,i,s){var r;i&&!1!==i.opts.hash&&(r=a(o))&&(o.currentHash=r+(o.group.length>1?"-"+(i.index+1):""),t.location.hash!=="#"+o.currentHash&&(s&&!o.origHash&&(o.origHash=t.location.hash),o.hashTimer&&clearTimeout(o.hashTimer),o.hashTimer=setTimeout(function(){"replaceState"in t.history?(t.history[s?"pushState":"replaceState"]({},e.title,t.location.pathname+t.location.search+"#"+o.currentHash),s&&(o.hasCreatedHistory=!0)):t.location.hash=o.currentHash,o.hashTimer=null},300)))},"beforeClose.fb":function(n,o,i){i&&!1!==i.opts.hash&&(clearTimeout(o.hashTimer),o.currentHash&&o.hasCreatedHistory?t.history.back():o.currentHash&&("replaceState"in t.history?t.history.replaceState({},e.title,t.location.pathname+t.location.search+(o.origHash||"")):t.location.hash=o.origHash),o.currentHash=null)}}),n(t).on("hashchange.fb",function(){var t=o(),e=null;n.each(n(".fancybox-container").get().reverse(),function(t,o){var i=n(o).data("FancyBox");if(i&&i.currentHash)return e=i,!1}),e?e.currentHash===t.gallery+"-"+t.index||1===t.index&&e.currentHash==t.gallery||(e.currentHash=null,e.close()):""!==t.gallery&&i(t)}),setTimeout(function(){n.fancybox.getInstance()||i(o())},50))})}(window,document,jQuery),function(t,e){"use strict";var n=(new Date).getTime();e(t).on({"onInit.fb":function(t,e,o){e.$refs.stage.on("mousewheel DOMMouseScroll wheel MozMousePixelScroll",function(t){var o=e.current,i=(new Date).getTime();e.group.length<2||!1===o.opts.wheel||"auto"===o.opts.wheel&&"image"!==o.type||(t.preventDefault(),t.stopPropagation(),o.$slide.hasClass("fancybox-animated")||(t=t.originalEvent||t,i-n<250||(n=i,e[(-t.deltaY||-t.deltaX||t.wheelDelta||-t.detail)<0?"next":"previous"]())))})}})}(document,jQuery); \ No newline at end of file diff --git a/assets/js/main-v2.js b/assets/js/main-v2.js new file mode 100644 index 0000000000..868ea7f5e4 --- /dev/null +++ b/assets/js/main-v2.js @@ -0,0 +1,266 @@ +$(document).ready(function(){ + + // Highlight init + function initHighlight(wrapperHighlight) { + hljs.highlightAll(); + wrapperHighlight(addClipboardJS); + $('body').append('
'); + } + + // Wrap highlight + function wrapperHighlight(addClipboardJS) { + $('.hljs').parent().wrap('
'); + $('.hljs-wrapper').append('
'); + $('.hljs-wrapper .hljs-actions-panel').prepend(''); + $('.hljs-wrapper .hljs-actions-panel').prepend(''); + addClipboardJS(); + } + + // Add clipboard functionality + function addClipboardJS() { + var clipboard = new ClipboardJS(".btn-clipboard",{ + target: function(clipboard) { + return clipboard.parentNode.previousElementSibling + } + }); + clipboard.on("success", function(clipboard) { + $(clipboard.trigger).text('Copied!'); + setTimeout(function () { + $(clipboard.trigger).html(''); + }, 1000); + clipboard.clearSelection(); + }); + clipboard.on("error", function(clipboard) { + var label = /Mac/i.test(navigator.userAgent) ? "⌘" : "Ctrl-"; + var text = "Press " + label + "C to copy"; + $(clipboard.trigger).text(text); + }); + } + + // Add fullscreen mode functionality + function addFullscreenMode() { + var isFullScreenModeCodeOn = false; + var screenScroll = 0; + var fullScreenWindow = $('.js-fullscreen-code')[0]; + + $('body').on('click', '.btn-fullscreen-mode', function() { + if (isFullScreenModeCodeOn) { + $('body').css('overflow', ''); + $(fullScreenWindow).removeClass('is-open').empty(); + isFullScreenModeCodeOn = false; + } else { + var codeBlock = this.parentNode.parentNode.cloneNode(true); + $('body').css('overflow', 'hidden'); + $(fullScreenWindow).append(codeBlock); + $(fullScreenWindow).find('.btn-fullscreen-mode').attr('title', 'Leave fullscreen mode'); + $(fullScreenWindow).find('.btn-fullscreen-mode i').removeClass('fa-expand').addClass('fa-compress'); + $(fullScreenWindow).addClass('is-open'); + isFullScreenModeCodeOn = true; + } + }); + + $(document).keyup(function(e) { + if($(fullScreenWindow).hasClass('is-open') && e.key === "Escape") { + $('body').css('overflow', ''); + $(fullScreenWindow).removeClass('is-open').empty(); + isFullScreenModeCodeOn = false; + } + }); + } + + initHighlight(wrapperHighlight); + addFullscreenMode(); + + // Gif Player + $('.gif').each(function(el){ + new gifsee(this); + }); + + // Style all tables - markdown fix + $('table').addClass('table'); + $('table thead').addClass('thead-light'); + + // Show sidebar on click + $('#show-sidebar').on('click', function(){ + $('body').addClass('sidebar-open'); + $('.sidebar').addClass('sidebar-active'); + }); + + // Sidebar li dropdown + $('.main-nav .li_dropdown .li_parent').on('click', function(){ + $(this).parent().toggleClass('active'); + }); + + // Close sidebar on click + $(document).on('click', '.sidebar-open', function(e){ + if($(e.target).hasClass('sidebar-open')) { + $('body').removeClass('sidebar-open'); + $('.sidebar').removeClass('sidebar-active'); + } + }); + + // Back to top button + $('#back-to-top').on('click', function(){ + $("html, body").animate({scrollTop: 0}, 800); + }); + + // Add permalink to headings 2 - 6 + $('.main-content-body h2, .main-content-body h3, .main-content-body h4, .main-content-body h5, .main-content-body h6').each(function(){ + var hash = window.location.hash.replace('#', ''); + var permalink = $(this).attr('id'), + text = $(this).text(); + + $(this).empty(); + + if (permalink !== 'undefined') { + $(this).append('' + text + ''); + } + }); + + // Click permalink on headings 2 - 6 + $('.main-content-body h2 > a, .main-content-body h3 > a, .main-content-body h4 > a, .main-content-body h5 > a, .main-content-body h6 > a').on('click', function(){ + // Scroll to target + $('html, body').animate({ + scrollTop: ($(this).offset().top - 80) + }, 500); + }); + + // On load, scroll to permalink + $(window).on('load', function() { + var hash = window.location.hash.replace('#', ''); + if(hash) { + $('.main-content-body h2, .main-content-body h3, .main-content-body h4, .main-content-body h5, .main-content-body h6').each(function(){ + var permalink = $(this).attr('id'); + if (permalink !== 'undefined' && hash === permalink) { + // Scroll to target + $('html, body').animate({ + scrollTop: ($(this).offset().top - 80) + }, 100); + return false; + } + }); + } + }); + + // TOC + if($('.toc').length) { + // Add title + var tocTitle = $('

Contents

'); + $('.toc').prepend(tocTitle); + + // Add functionality + $('.toc a').on('click', function(){ + var target = $(this.hash); + + // If there is a tab, get tab of the target and show it + if($('.nav-tabs').length && $('.tab-pane ' + this.hash).length) { + var targetTab = $(this.hash)[0].closest('.tab-pane'), + targetTabId = $(targetTab).attr('id'); + $('a[href="#'+ targetTabId +'"]').tab('show'); + } + + // Scroll to target + $('html, body').animate({ + scrollTop: (target.offset().top - 80) + }, 500); + }); + } + + // Search function + if($("#search").length != 0) { + $.getJSON('/search/search_index.json').done(searchData); + + function searchData(data) { + var container = $("#searchList"); + var options = { + shouldSort: true, + tokenize: true, + threshold: 0, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + ignoreLocation: true, + keys: ["title", "text"] + }; + + var fuse = new Fuse(data.docs, options); + + $('#search').on('keyup', function() { + if(this.value) { + var result = fuse.search(this.value); + + // prevent displaying duplicates on search + var filteredResults = result.filter(function(res) { + return res.item.location.match(/(\/#)/g) === null; + }); + + if(filteredResults.length === 0) { + container.html(''); + } else { + container.html(''); + container.append("

    Search results

"); + } + filteredResults.forEach(function(value){ + $("#searchList ul").append("
  • " + value.item.title + "
  • "); + }); + } else { + $("#searchList").empty(); + } + }); + } + } + + // Lightbox integration + $(".main-content-body img").each(function () { + if(!$(this).hasClass('no-lightbox') && !$(this).hasClass('gif') && !$(this).hasClass('emojione')) { + if(!$(this).parent("a").length) { + $(this).wrap(function () { return ""; }); + } + } + }); + + // Navbar scroll + // navbar background color change on scroll + function navbarScroll() { + var scroll = $(window).scrollTop(); + if(scroll < 10){ + $('.navbar-dark').removeClass('dark-mode'); + } else{ + $('.navbar-dark').addClass('dark-mode'); + } + } + $(window).scroll(function(){ + navbarScroll(); + }); + navbarScroll(); + + // Footer copyright year + $('#currentYear').text(new Date().getFullYear()); + + // Error added on company logo - bug head + document.querySelector('.bug-head').addEventListener('click', function(){ + throw new Error('Headshot'); + }); + + // Persistent tabs + $('a.nav-link[data-toggle="tab"]').on('click', function (event) { + var tabParent = event.currentTarget.closest('ul.nav-tabs'), + tabData = event.currentTarget.dataset.tab, + tabs = $('a.nav-link[data-toggle="tab"][data-tab="'+ tabData +'"]').filter(function(i, tab) { + return $(tab).closest('ul.nav-tabs').not(tabParent)[0]; + }); + + if (tabs.length > 0) { + var currentOffset = $(event.currentTarget).offset().top - $(document).scrollTop(); + $(tabs).tab('show'); + $(document).scrollTop($(event.currentTarget).offset().top - currentOffset); + } + }); +}); + +window.intercomSettings = { + app_id: 'i2hhgdvj', + system: 'elmah.io', + background_color: '#0da58e' +}; \ No newline at end of file diff --git a/assets/js/main.min.js b/assets/js/main.min.js new file mode 100644 index 0000000000..30fb2fe0ef --- /dev/null +++ b/assets/js/main.min.js @@ -0,0 +1,8 @@ +/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,(function(e,t){"use strict";var n=[],i=Object.getPrototypeOf,o=n.slice,r=n.flat?function(e){return n.flat.call(e)}:function(e){return n.concat.apply([],e)},a=n.push,s=n.indexOf,l={},c=l.toString,u=l.hasOwnProperty,d=u.toString,f=d.call(Object),h={},p=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},g=function(e){return null!=e&&e===e.window},m=e.document,v={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var i,o,r=(n=n||m).createElement("script");if(r.text=e,t)for(i in v)(o=t[i]||t.getAttribute&&t.getAttribute(i))&&r.setAttribute(i,o);n.head.appendChild(r).parentNode.removeChild(r)}function y(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var w="3.5.1",x=function(e,t){return new x.fn.init(e,t)};function _(e){var t=!!e&&"length"in e&&e.length,n=y(e);return!p(e)&&!g(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+P+")"+P+"*"),U=new RegExp(P+"|>"),X=new RegExp(B),V=new RegExp("^"+R+"$"),Y={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+F),PSEUDO:new RegExp("^"+B),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:new RegExp("^(?:"+I+")$","i"),needsContext:new RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,G=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+P+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},ie=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,oe=function(e,t){return t?"\0"===e?"�":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){f()},ae=we((function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()}),{dir:"parentNode",next:"legend"});try{D.apply(O=L.call(x.childNodes),x.childNodes),O[x.childNodes.length].nodeType}catch(t){D={apply:O.length?function(e,t){$.apply(e,L.call(t))}:function(e,t){for(var n=e.length,i=0;e[n++]=t[i++];);e.length=n-1}}}function se(e,t,i,o){var r,s,c,u,d,p,v,b=t&&t.ownerDocument,x=t?t.nodeType:9;if(i=i||[],"string"!=typeof e||!e||1!==x&&9!==x&&11!==x)return i;if(!o&&(f(t),t=t||h,g)){if(11!==x&&(d=J.exec(e)))if(r=d[1]){if(9===x){if(!(c=t.getElementById(r)))return i;if(c.id===r)return i.push(c),i}else if(b&&(c=b.getElementById(r))&&y(t,c)&&c.id===r)return i.push(c),i}else{if(d[2])return D.apply(i,t.getElementsByTagName(e)),i;if((r=d[3])&&n.getElementsByClassName&&t.getElementsByClassName)return D.apply(i,t.getElementsByClassName(r)),i}if(n.qsa&&!k[e+" "]&&(!m||!m.test(e))&&(1!==x||"object"!==t.nodeName.toLowerCase())){if(v=e,b=t,1===x&&(U.test(e)||W.test(e))){for((b=ee.test(e)&&ve(t.parentNode)||t)===t&&n.scope||((u=t.getAttribute("id"))?u=u.replace(ie,oe):t.setAttribute("id",u=w)),s=(p=a(e)).length;s--;)p[s]=(u?"#"+u:":scope")+" "+ye(p[s]);v=p.join(",")}try{return D.apply(i,b.querySelectorAll(v)),i}catch(t){k(e,!0)}finally{u===w&&t.removeAttribute("id")}}}return l(e.replace(q,"$1"),t,i,o)}function le(){var e=[];return function t(n,o){return e.push(n+" ")>i.cacheLength&&delete t[e.shift()],t[n+" "]=o}}function ce(e){return e[w]=!0,e}function ue(e){var t=h.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function de(e,t){for(var n=e.split("|"),o=n.length;o--;)i.attrHandle[n[o]]=t}function fe(e,t){var n=t&&e,i=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(i)return i;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function he(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ge(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ae(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function me(e){return ce((function(t){return t=+t,ce((function(n,i){for(var o,r=e([],n.length,t),a=r.length;a--;)n[o=r[a]]&&(n[o]=!(i[o]=n[o]))}))}))}function ve(e){return e&&void 0!==e.getElementsByTagName&&e}for(t in n=se.support={},r=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!K.test(t||n&&n.nodeName||"HTML")},f=se.setDocument=function(e){var t,o,a=e?e.ownerDocument||e:x;return a!=h&&9===a.nodeType&&a.documentElement&&(p=(h=a).documentElement,g=!r(h),x!=h&&(o=h.defaultView)&&o.top!==o&&(o.addEventListener?o.addEventListener("unload",re,!1):o.attachEvent&&o.attachEvent("onunload",re)),n.scope=ue((function(e){return p.appendChild(e).appendChild(h.createElement("div")),void 0!==e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length})),n.attributes=ue((function(e){return e.className="i",!e.getAttribute("className")})),n.getElementsByTagName=ue((function(e){return e.appendChild(h.createComment("")),!e.getElementsByTagName("*").length})),n.getElementsByClassName=Z.test(h.getElementsByClassName),n.getById=ue((function(e){return p.appendChild(e).id=w,!h.getElementsByName||!h.getElementsByName(w).length})),n.getById?(i.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},i.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(i.filter.ID=function(e){var t=e.replace(te,ne);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},i.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n,i,o,r=t.getElementById(e);if(r){if((n=r.getAttributeNode("id"))&&n.value===e)return[r];for(o=t.getElementsByName(e),i=0;r=o[i++];)if((n=r.getAttributeNode("id"))&&n.value===e)return[r]}return[]}}),i.find.TAG=n.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,i=[],o=0,r=t.getElementsByTagName(e);if("*"===e){for(;n=r[o++];)1===n.nodeType&&i.push(n);return i}return r},i.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],m=[],(n.qsa=Z.test(h.querySelectorAll))&&(ue((function(e){var t;p.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+P+"*(?:value|"+I+")"),e.querySelectorAll("[id~="+w+"-]").length||m.push("~="),(t=h.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||m.push("\\["+P+"*name"+P+"*="+P+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+w+"+*").length||m.push(".#.+[+~]"),e.querySelectorAll("\\\f"),m.push("[\\r\\n\\f]")})),ue((function(e){e.innerHTML="";var t=h.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+P+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&m.push(":enabled",":disabled"),p.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")}))),(n.matchesSelector=Z.test(b=p.matches||p.webkitMatchesSelector||p.mozMatchesSelector||p.oMatchesSelector||p.msMatchesSelector))&&ue((function(e){n.disconnectedMatch=b.call(e,"*"),b.call(e,"[s!='']:x"),v.push("!=",B)})),m=m.length&&new RegExp(m.join("|")),v=v.length&&new RegExp(v.join("|")),t=Z.test(p.compareDocumentPosition),y=t||Z.test(p.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,i=t&&t.parentNode;return e===i||!(!i||1!==i.nodeType||!(n.contains?n.contains(i):e.compareDocumentPosition&&16&e.compareDocumentPosition(i)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},N=t?function(e,t){if(e===t)return d=!0,0;var i=!e.compareDocumentPosition-!t.compareDocumentPosition;return i||(1&(i=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===i?e==h||e.ownerDocument==x&&y(x,e)?-1:t==h||t.ownerDocument==x&&y(x,t)?1:u?j(u,e)-j(u,t):0:4&i?-1:1)}:function(e,t){if(e===t)return d=!0,0;var n,i=0,o=e.parentNode,r=t.parentNode,a=[e],s=[t];if(!o||!r)return e==h?-1:t==h?1:o?-1:r?1:u?j(u,e)-j(u,t):0;if(o===r)return fe(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[i]===s[i];)i++;return i?fe(a[i],s[i]):a[i]==x?-1:s[i]==x?1:0}),h},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(f(e),n.matchesSelector&&g&&!k[t+" "]&&(!v||!v.test(t))&&(!m||!m.test(t)))try{var i=b.call(e,t);if(i||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return i}catch(e){k(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=new RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&C(e,(function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")}))},ATTR:function(e,t,n){return function(i){var o=se.attr(i,e);return null==o?"!="===t:!t||(o+="","="===t?o===n:"!="===t?o!==n:"^="===t?n&&0===o.indexOf(n):"*="===t?n&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function A(e,t,n){return p(t)?x.grep(e,(function(e,i){return!!t.call(e,i,e)!==n})):t.nodeType?x.grep(e,(function(e){return e===t!==n})):"string"!=typeof t?x.grep(e,(function(e){return-1)[^>]*|#([\w-]+))$/;(x.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||O,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:M.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:m,!0)),N.test(i[1])&&x.isPlainObject(t))for(i in t)p(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=m.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):p(e)?void 0!==n.ready?n.ready(e):e(x):x.makeArray(e,this)}).prototype=x.fn,O=x(m);var $=/^(?:parents|prev(?:Until|All))/,D={children:!0,contents:!0,next:!0,prev:!0};function L(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}x.fn.extend({has:function(e){var t=x(e,this),n=t.length;return this.filter((function(){for(var e=0;e\x20\t\r\n\f]*)/i,pe=/^$|^module$|\/(?:java|ecma)script/i;ue=m.createDocumentFragment().appendChild(m.createElement("div")),(de=m.createElement("input")).setAttribute("type","radio"),de.setAttribute("checked","checked"),de.setAttribute("name","t"),ue.appendChild(de),h.checkClone=ue.cloneNode(!0).cloneNode(!0).lastChild.checked,ue.innerHTML="",h.noCloneChecked=!!ue.cloneNode(!0).lastChild.defaultValue,ue.innerHTML="",h.option=!!ue.lastChild;var ge={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function me(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&k(e,t)?x.merge([e],n):n}function ve(e,t){for(var n=0,i=e.length;n",""]);var be=/<|&#?\w+;/;function ye(e,t,n,i,o){for(var r,a,s,l,c,u,d=t.createDocumentFragment(),f=[],h=0,p=e.length;h\s*$/g;function Me(e,t){return k(e,"table")&&k(11!==t.nodeType?t:t.firstChild,"tr")&&x(e).children("tbody")[0]||e}function $e(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function De(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,i,o,r,a,s;if(1===t.nodeType){if(K.hasData(e)&&(s=K.get(e).events))for(o in K.remove(t,"handle events"),s)for(n=0,i=s[o].length;n").attr(e.scriptAttrs||{}).prop({charset:e.scriptCharset,src:e.url}).on("load error",n=function(e){t.remove(),n=null,e&&o("error"===e.type?404:200,e.type)}),m.head.appendChild(t[0])},abort:function(){n&&n()}}}));var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||x.expando+"_"+Et.guid++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",(function(t,n,i){var o,r,a,s=!1!==t.jsonp&&(Vt.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(t.data)&&"data");if(s||"jsonp"===t.dataTypes[0])return o=t.jsonpCallback=p(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(Vt,"$1"+o):!1!==t.jsonp&&(t.url+=(Ct.test(t.url)?"&":"?")+t.jsonp+"="+o),t.converters["script json"]=function(){return a||x.error(o+" was not called"),a[0]},t.dataTypes[0]="json",r=e[o],e[o]=function(){a=arguments},i.always((function(){void 0===r?x(e).removeProp(o):e[o]=r,t[o]&&(t.jsonpCallback=n.jsonpCallback,Xt.push(o)),a&&p(r)&&r(a[0]),a=r=void 0})),"script"})),h.createHTMLDocument=((Ut=m.implementation.createHTMLDocument("").body).innerHTML="
    ",2===Ut.childNodes.length),x.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(h.createHTMLDocument?((i=(t=m.implementation.createHTMLDocument("")).createElement("base")).href=m.location.href,t.head.appendChild(i)):t=m),r=!n&&[],(o=N.exec(e))?[t.createElement(o[1])]:(o=ye([e],t,r),r&&r.length&&x(r).remove(),x.merge([],o.childNodes)));var i,o,r},x.fn.load=function(e,t,n){var i,o,r,a=this,s=e.indexOf(" ");return-1").append(x.parseHTML(e)).find(i):e)})).always(n&&function(e,t){a.each((function(){n.apply(this,r||[e.responseText,t,e])}))}),this},x.expr.pseudos.animated=function(e){return x.grep(x.timers,(function(t){return e===t.elem})).length},x.offset={setOffset:function(e,t,n){var i,o,r,a,s,l,c=x.css(e,"position"),u=x(e),d={};"static"===c&&(e.style.position="relative"),s=u.offset(),r=x.css(e,"top"),l=x.css(e,"left"),("absolute"===c||"fixed"===c)&&-1<(r+l).indexOf("auto")?(a=(i=u.position()).top,o=i.left):(a=parseFloat(r)||0,o=parseFloat(l)||0),p(t)&&(t=t.call(e,n,x.extend({},s))),null!=t.top&&(d.top=t.top-s.top+a),null!=t.left&&(d.left=t.left-s.left+o),"using"in t?t.using.call(e,d):("number"==typeof d.top&&(d.top+="px"),"number"==typeof d.left&&(d.left+="px"),u.css(d))}},x.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each((function(t){x.offset.setOffset(this,e,t)}));var t,n,i=this[0];return i?i.getClientRects().length?(t=i.getBoundingClientRect(),n=i.ownerDocument.defaultView,{top:t.top+n.pageYOffset,left:t.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,i=this[0],o={top:0,left:0};if("fixed"===x.css(i,"position"))t=i.getBoundingClientRect();else{for(t=this.offset(),n=i.ownerDocument,e=i.offsetParent||n.documentElement;e&&(e===n.body||e===n.documentElement)&&"static"===x.css(e,"position");)e=e.parentNode;e&&e!==i&&1===e.nodeType&&((o=x(e).offset()).top+=x.css(e,"borderTopWidth",!0),o.left+=x.css(e,"borderLeftWidth",!0))}return{top:t.top-o.top-x.css(i,"marginTop",!0),left:t.left-o.left-x.css(i,"marginLeft",!0)}}},offsetParent:function(){return this.map((function(){for(var e=this.offsetParent;e&&"static"===x.css(e,"position");)e=e.offsetParent;return e||ie}))}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},(function(e,t){var n="pageYOffset"===t;x.fn[e]=function(i){return q(this,(function(e,i,o){var r;if(g(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===o)return r?r[t]:e[i];r?r.scrollTo(n?r.pageXOffset:o,n?o:r.pageYOffset):e[i]=o}),e,i,arguments.length)}})),x.each(["top","left"],(function(e,t){x.cssHooks[t]=qe(h.pixelPosition,(function(e,n){if(n)return n=He(e,t),Pe.test(n)?x(e).position()[t]+"px":n}))})),x.each({Height:"height",Width:"width"},(function(e,t){x.each({padding:"inner"+e,content:t,"":"outer"+e},(function(n,i){x.fn[i]=function(o,r){var a=arguments.length&&(n||"boolean"!=typeof o),s=n||(!0===o||!0===r?"margin":"border");return q(this,(function(t,n,o){var r;return g(t)?0===i.indexOf("outer")?t["inner"+e]:t.document.documentElement["client"+e]:9===t.nodeType?(r=t.documentElement,Math.max(t.body["scroll"+e],r["scroll"+e],t.body["offset"+e],r["offset"+e],r["client"+e])):void 0===o?x.css(t,n,s):x.style(t,n,o,s)}),t,a?o:void 0,a)}}))})),x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],(function(e,t){x.fn[t]=function(e){return this.on(t,e)}})),x.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,i){return this.on(t,e,n,i)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),x.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),(function(e,t){x.fn[t]=function(e,n){return 0{throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{throw Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((n=>{var i=e[n];"object"!=typeof i||Object.isFrozen(i)||t(i)})),e}e.exports=t,e.exports.default=t;var n=e.exports;class i{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function o(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function r(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t];return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const a=e=>!!e.kind;class s{constructor(e,t){this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){this.buffer+=o(e)}openNode(e){if(!a(e))return;let t=e.kind;t=e.sublanguage?"language-"+t:((e,{prefix:t})=>{if(e.includes(".")){const n=e.split(".");return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ")}return`${t}${e}`})(t,{prefix:this.classPrefix}),this.span(t)}closeNode(e){a(e)&&(this.buffer+="")}value(){return this.buffer}span(e){this.buffer+=``}}class l{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){const t={kind:e,children:[]};this.add(t),this.stack.push(t)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{l._collapse(e)})))}}class c extends l{constructor(e){super(),this.options=e}addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root;n.kind=t,n.sublanguage=!0,this.add(n)}toHTML(){return new s(this,this.options).value()}finalize(){return!0}}function u(e){return e?"string"==typeof e?e:e.source:null}function d(...e){return e.map((e=>u(e))).join("")}function f(...e){return"("+((e=>{const t=e[e.length-1];return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}})(e).capture?"":"?:")+e.map((e=>u(e))).join("|")+")"}function h(e){return RegExp(e.toString()+"|").exec("").length-1}const p=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;function g(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n;let i=u(e),o="";for(;i.length>0;){const e=p.exec(i);if(!e){o+=i;break}o+=i.substring(0,e.index),i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?o+="\\"+(Number(e[1])+t):(o+=e[0],"("===e[0]&&n++)}return o})).map((e=>`(${e})`)).join(t)}const m="[a-zA-Z]\\w*",v="[a-zA-Z_]\\w*",b="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",x={begin:"\\\\[\\s\\S]",relevance:0},_={scope:"string",begin:"'",end:"'",illegal:"\\n",contains:[x]},E={scope:"string",begin:'"',end:'"',illegal:"\\n",contains:[x]},C=(e,t,n={})=>{const i=r({scope:"comment",begin:e,end:t,contains:[]},n);i.contains.push({scope:"doctag",begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0});const o=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/);return i.contains.push({begin:d(/[ ]+/,"(",o,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i},T=C("//","$"),S=C("/\\*","\\*/"),k=C("#","$");var N=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:m,UNDERSCORE_IDENT_RE:v,NUMBER_RE:b,C_NUMBER_RE:y,BINARY_NUMBER_RE:w,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(e={})=>{const t=/^#![ ]*\//;return e.binary&&(e.begin=d(t,/.*\b/,e.binary,/\b.*/)),r({scope:"meta",begin:t,end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)},BACKSLASH_ESCAPE:x,APOS_STRING_MODE:_,QUOTE_STRING_MODE:E,PHRASAL_WORDS_MODE:{begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT:C,C_LINE_COMMENT_MODE:T,C_BLOCK_COMMENT_MODE:S,HASH_COMMENT_MODE:k,NUMBER_MODE:{scope:"number",begin:b,relevance:0},C_NUMBER_MODE:{scope:"number",begin:y,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:w,relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[x,{begin:/\[/,end:/\]/,relevance:0,contains:[x]}]}]},TITLE_MODE:{scope:"title",begin:m,relevance:0},UNDERSCORE_TITLE_MODE:{scope:"title",begin:v,relevance:0},METHOD_GUARD:{begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function A(e,t){"."===e.input[e.index-1]&&t.ignoreMatch()}function O(e,t){void 0!==e.className&&(e.scope=e.className,delete e.className)}function M(e,t){t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,void 0===e.relevance&&(e.relevance=0))}function $(e,t){Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function D(e,t){if(e.match){if(e.begin||e.end)throw Error("begin & end are not supported with match");e.begin=e.match,delete e.match}}function L(e,t){void 0===e.relevance&&(e.relevance=1)}const j=(e,t)=>{if(!e.beforeMatch)return;if(e.starts)throw Error("beforeMatch cannot be used with starts");const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t]})),e.keywords=n.keywords,e.begin=d(n.beforeMatch,d("(?=",n.begin,")")),e.starts={relevance:0,contains:[Object.assign(n,{endsParent:!0})]},e.relevance=0,delete n.beforeMatch},I=["of","and","for","in","not","or","if","then","parent","list","value"];function P(e,t,n="keyword"){const i=Object.create(null);return"string"==typeof e?o(n,e.split(" ")):Array.isArray(e)?o(n,e):Object.keys(e).forEach((n=>{Object.assign(i,P(e[n],t,n))})),i;function o(e,n){t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|");i[n[0]]=[e,R(n[0],n[1])]}))}}function R(e,t){return t?Number(t):(e=>I.includes(e.toLowerCase()))(e)?0:1}const F={},B=e=>{console.error(e)},H=(e,...t)=>{console.log("WARN: "+e,...t)},q=(e,t)=>{F[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),F[`${e}/${t}`]=!0)},z=Error();function W(e,t,{key:n}){let i=0;const o=e[n],r={},a={};for(let e=1;e<=t.length;e++)a[e+i]=o[e],r[e+i]=!0,i+=h(t[e-1]);e[n]=a,e[n]._emit=r,e[n]._multi=!0}function U(e){(e=>{e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope}),(e=>{if(Array.isArray(e.begin)){if(e.skip||e.excludeBegin||e.returnBegin)throw B("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),z;if("object"!=typeof e.beginScope||null===e.beginScope)throw B("beginScope must be object"),z;W(e,e.begin,{key:"beginScope"}),e.begin=g(e.begin,{joinWith:""})}})(e),(e=>{if(Array.isArray(e.end)){if(e.skip||e.excludeEnd||e.returnEnd)throw B("skip, excludeEnd, returnEnd not compatible with endScope: {}"),z;if("object"!=typeof e.endScope||null===e.endScope)throw B("endScope must be object"),z;W(e,e.end,{key:"endScope"}),e.end=g(e.end,{joinWith:""})}})(e)}function X(e){function t(t,n){return RegExp(u(t),"m"+(e.case_insensitive?"i":"")+(n?"g":""))}class n{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,t){t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),this.matchAt+=h(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const e=this.regexes.map((e=>e[1]));this.matcherRe=t(g(e,{joinWith:"|"}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;const t=this.matcherRe.exec(e);if(!t)return null;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n];return t.splice(0,n),Object.assign(t,i)}}class i{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex;let n=t.exec(e);if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}return n&&(this.regexIndex+=n.position+1,this.regexIndex===this.count&&this.considerAll()),n}}if(e.compilerExtensions||(e.compilerExtensions=[]),e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return e.classNameAliases=r(e.classNameAliases||{}),function n(o,a){const s=o;if(o.isCompiled)return s;[O,D,U,j].forEach((e=>e(o,a))),e.compilerExtensions.forEach((e=>e(o,a))),o.__beforeBegin=null,[M,$,L].forEach((e=>e(o,a))),o.isCompiled=!0;let l=null;return"object"==typeof o.keywords&&o.keywords.$pattern&&(o.keywords=Object.assign({},o.keywords),l=o.keywords.$pattern,delete o.keywords.$pattern),l=l||/\w+/,o.keywords&&(o.keywords=P(o.keywords,e.case_insensitive)),s.keywordPatternRe=t(l,!0),a&&(o.begin||(o.begin=/\B|\b/),s.beginRe=t(o.begin),o.end||o.endsWithParent||(o.end=/\B|\b/),o.end&&(s.endRe=t(o.end)),s.terminatorEnd=u(o.end)||"",o.endsWithParent&&a.terminatorEnd&&(s.terminatorEnd+=(o.end?"|":"")+a.terminatorEnd)),o.illegal&&(s.illegalRe=t(o.illegal)),o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>r(e,{variants:null},t)))),e.cachedVariants?e.cachedVariants:V(e)?r(e,{starts:e.starts?r(e.starts):null}):Object.isFrozen(e)?r(e):e))("self"===e?o:e)))),o.contains.forEach((e=>{n(e,s)})),o.starts&&n(o.starts,a),s.matcher=(e=>{const t=new i;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(s),s}(e)}function V(e){return!!e&&(e.endsWithParent||V(e.starts))}const Y=o,K=r,Q=Symbol("nomatch");var G=(e=>{const t=Object.create(null),o=Object.create(null),r=[];let a=!0;const s="Could not find the language '{}', did you forget to load/include a language module?",l={disableAutodetect:!0,name:"Plain text",contains:[]};let u={ignoreUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",cssSelector:"pre code",languages:null,__emitter:c};function d(e){return u.noHighlightRe.test(e)}function f(e,t,n,i){let o="",r="";"object"==typeof t?(o=e,n=t.ignoreIllegals,r=t.language,i=void 0):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."),q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),r=e,o=t),void 0===n&&(n=!0);const a={code:o,language:r};x("before:highlight",a);const s=a.result?a.result:h(a.language,a.code,n,i);return s.code=a.code,x("after:highlight",s),s}function h(e,n,o,r){const l=Object.create(null);function c(){if(!C.keywords)return void S.addText(k);let e=0;C.keywordPatternRe.lastIndex=0;let t=C.keywordPatternRe.exec(k),n="";for(;t;){n+=k.substring(e,t.index);const o=x.case_insensitive?t[0].toLowerCase():t[0],r=(i=o,C.keywords[i]);if(r){const[e,i]=r;if(S.addText(n),n="",l[o]=(l[o]||0)+1,l[o]<=7&&(N+=i),e.startsWith("_"))n+=t[0];else{const n=x.classNameAliases[e]||e;S.addKeyword(t[0],n)}}else n+=t[0];e=C.keywordPatternRe.lastIndex,t=C.keywordPatternRe.exec(k)}var i;n+=k.substr(e),S.addText(n)}function d(){null!=C.subLanguage?(()=>{if(""===k)return;let e=null;if("string"==typeof C.subLanguage){if(!t[C.subLanguage])return void S.addText(k);e=h(C.subLanguage,k,!0,T[C.subLanguage]),T[C.subLanguage]=e._top}else e=p(k,C.subLanguage.length?C.subLanguage:null);C.relevance>0&&(N+=e.relevance),S.addSublanguage(e._emitter,e.language)})():c(),k=""}function f(e,t){let n=1;for(;void 0!==t[n];){if(!e._emit[n]){n++;continue}const i=x.classNameAliases[e[n]]||e[n],o=t[n];i?S.addKeyword(o,i):(k=o,c(),k=""),n++}}function g(e,t){return e.scope&&"string"==typeof e.scope&&S.openNode(x.classNameAliases[e.scope]||e.scope),e.beginScope&&(e.beginScope._wrap?(S.addKeyword(k,x.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),k=""):e.beginScope._multi&&(f(e.beginScope,t),k="")),C=Object.create(e,{parent:{value:C}}),C}function m(e,t,n){let o=((e,t)=>{const n=e&&e.exec(t);return n&&0===n.index})(e.endRe,n);if(o){if(e["on:end"]){const n=new i(e);e["on:end"](t,n),n.isMatchIgnored&&(o=!1)}if(o){for(;e.endsParent&&e.parent;)e=e.parent;return e}}if(e.endsWithParent)return m(e.parent,t,n)}function v(e){return 0===C.matcher.regexIndex?(k+=e[0],1):(M=!0,0)}let y={};function w(t,r){const s=r&&r[0];if(k+=t,null==s)return d(),0;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===s){if(k+=n.slice(r.index,r.index+1),!a){const t=Error(`0 width match regex (${e})`);throw t.languageName=e,t.badRule=y.rule,t}return 1}if(y=r,"begin"===r.type)return(e=>{const t=e[0],n=e.rule,o=new i(n),r=[n.__beforeBegin,n["on:begin"]];for(const n of r)if(n&&(n(e,o),o.isMatchIgnored))return v(t);return n.skip?k+=t:(n.excludeBegin&&(k+=t),d(),n.returnBegin||n.excludeBegin||(k=t)),g(n,e),n.returnBegin?0:t.length})(r);if("illegal"===r.type&&!o){const e=Error('Illegal lexeme "'+s+'" for mode "'+(C.scope||"")+'"');throw e.mode=C,e}if("end"===r.type){const e=function(e){const t=e[0],i=n.substr(e.index),o=m(C,e,i);if(!o)return Q;const r=C;C.endScope&&C.endScope._wrap?(d(),S.addKeyword(t,C.endScope._wrap)):C.endScope&&C.endScope._multi?(d(),f(C.endScope,e)):r.skip?k+=t:(r.returnEnd||r.excludeEnd||(k+=t),d(),r.excludeEnd&&(k=t));do{C.scope&&!C.isMultiClass&&S.closeNode(),C.skip||C.subLanguage||(N+=C.relevance),C=C.parent}while(C!==o.parent);return o.starts&&g(o.starts,e),r.returnEnd?0:t.length}(r);if(e!==Q)return e}if("illegal"===r.type&&""===s)return 1;if(O>1e5&&O>3*r.index)throw Error("potential infinite loop, way more iterations than matches");return k+=s,s.length}const x=b(e);if(!x)throw B(s.replace("{}",e)),Error('Unknown language: "'+e+'"');const _=X(x);let E="",C=r||_;const T={},S=new u.__emitter(u);(()=>{const e=[];for(let t=C;t!==x;t=t.parent)t.scope&&e.unshift(t.scope);e.forEach((e=>S.openNode(e)))})();let k="",N=0,A=0,O=0,M=!1;try{for(C.matcher.considerAll();;){O++,M?M=!1:C.matcher.considerAll(),C.matcher.lastIndex=A;const e=C.matcher.exec(n);if(!e)break;const t=w(n.substring(A,e.index),e);A=e.index+t}return w(n.substr(A)),S.closeAllNodes(),S.finalize(),E=S.toHTML(),{language:e,value:E,relevance:N,illegal:!1,_emitter:S,_top:C}}catch(t){if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n),illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A,context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:E},_emitter:S};if(a)return{language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:S,_top:C};throw t}}function p(e,n){n=n||u.languages||Object.keys(t);const i=(e=>{const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new u.__emitter(u)};return t._emitter.addText(e),t})(e),o=n.filter(b).filter(w).map((t=>h(t,e,!1)));o.unshift(i);const r=o.sort(((e,t)=>{if(e.relevance!==t.relevance)return t.relevance-e.relevance;if(e.language&&t.language){if(b(e.language).supersetOf===t.language)return 1;if(b(t.language).supersetOf===e.language)return-1}return 0})),[a,s]=r,c=a;return c.secondBest=s,c}function g(e){let t=null;const n=(e=>{let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"";const n=u.languageDetectRe.exec(t);if(n){const t=b(n[1]);return t||(H(s.replace("{}",n[1])),H("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}return t.split(/\s+/).find((e=>d(e)||b(e)))})(e);if(d(n))return;x("before:highlightElement",{el:e,language:n}),!u.ignoreUnescapedHTML&&e.children.length>0&&(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),console.warn("https://github.com/highlightjs/highlight.js/issues/2886"),console.warn(e)),t=e;const i=t.textContent,r=n?f(i,{language:n,ignoreIllegals:!0}):p(i);e.innerHTML=r.value,((e,t,n)=>{const i=t&&o[t]||n;e.classList.add("hljs"),e.classList.add("language-"+i)})(e,n,r.language),e.result={language:r.language,re:r.relevance,relevance:r.relevance},r.secondBest&&(e.secondBest={language:r.secondBest.language,relevance:r.secondBest.relevance}),x("after:highlightElement",{el:e,result:r,text:i})}let m=!1;function v(){"loading"!==document.readyState?document.querySelectorAll(u.cssSelector).forEach(g):m=!0}function b(e){return e=(e||"").toLowerCase(),t[e]||t[o[e]]}function y(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{o[e.toLowerCase()]=t}))}function w(e){const t=b(e);return t&&!t.disableAutodetect}function x(e,t){const n=e;r.forEach((e=>{e[n]&&e[n](t)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{m&&v()}),!1),Object.assign(e,{highlight:f,highlightAuto:p,highlightAll:v,highlightElement:g,highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"),q("10.7.0","Please use highlightElement now."),g(e)),configure:e=>{u=K(u,e)},initHighlighting:()=>{v(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},initHighlightingOnLoad:()=>{v(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")},registerLanguage:(n,i)=>{let o=null;try{o=i(e)}catch(e){if(B("Language definition for '{}' could not be registered.".replace("{}",n)),!a)throw e;B(e),o=l}o.name||(o.name=n),t[n]=o,o.rawDefinition=i.bind(null,e),o.aliases&&y(o.aliases,{languageName:n})},unregisterLanguage:e=>{delete t[e];for(const t of Object.keys(o))o[t]===e&&delete o[t]},listLanguages:()=>Object.keys(t),getLanguage:b,registerAliases:y,autoDetection:w,inherit:K,addPlugin:e=>{(e=>{e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{e["before:highlightBlock"](Object.assign({block:t.el},t))}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),r.push(e)}}),e.debugMode=()=>{a=!1},e.safeMode=()=>{a=!0},e.versionString="11.0.1";for(const e in N)"object"==typeof N[e]&&n(N[e]);return Object.assign(e,N),e})({}),Z=Object.freeze({__proto__:null});const J=G;for(const e of Object.keys(Z)){const t=e.replace("grmr_","");J.registerLanguage(t,Z[e])}return J}();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs),hljs.registerLanguage("csharp",(()=>{"use strict";return e=>{const t={keyword:["abstract","as","base","break","case","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]),built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"],literal:["default","false","null","true"]},n=e.inherit(e.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),i={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},o={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},r=e.inherit(o,{illegal:/\n/}),a={className:"subst",begin:/\{/,end:/\}/,keywords:t},s=e.inherit(a,{illegal:/\n/}),l={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/},e.BACKSLASH_ESCAPE,s]},c={className:"string",begin:/\$@"/,end:'"',contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},a]},u=e.inherit(c,{illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]});a.contains=[c,l,o,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,i,e.C_BLOCK_COMMENT_MODE],s.contains=[u,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,i,e.inherit(e.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];const d={variants:[c,l,o,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},f={begin:"<",end:">",contains:[{beginKeywords:"in out"},n]},h=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",p={begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],keywords:t,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:""}]}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{keyword:"if else elif endif define undef warning error line region endregion pragma checksum"}},d,i,{beginKeywords:"class interface",relevance:0,end:/[{;=]/,illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"},n,f,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",relevance:0,end:/[{;=]/,illegal:/[^\s:]/,contains:[n,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/,contains:[n,f,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+h+"\\s+)+"+e.IDENT_RE+"\\s*(<.+>\\s*)?\\(",returnBegin:!0,end:/\s*[{;=]/,excludeEnd:!0,keywords:t,contains:[{beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial",relevance:0},{begin:e.IDENT_RE+"\\s*(<.+>\\s*)?\\(",returnBegin:!0,contains:[e.TITLE_MODE,f],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,relevance:0,contains:[d,i,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},p]}}})()),hljs.registerLanguage("bash",(()=>{"use strict";function e(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}return t=>{const n={},i={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[n]}]};Object.assign(n,{className:"variable",variants:[{begin:e(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},i]});const o={className:"subst",begin:/\$\(/,end:/\)/,contains:[t.BACKSLASH_ESCAPE]},r={begin:/<<-?\s*(?=\w+)/,starts:{contains:[t.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},a={className:"string",begin:/"/,end:/"/,contains:[t.BACKSLASH_ESCAPE,n,o]};o.contains.push(a);const s={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},t.NUMBER_MODE,n]},l=t.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[t.inherit(t.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:["if","then","else","elif","fi","for","while","in","do","done","case","esac","function"],literal:["true","false"],built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[l,t.SHEBANG(),c,s,t.HASH_COMMENT_MODE,r,a,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},n]}}})()),hljs.registerLanguage("dos",(()=>{"use strict";return e=>{const t=e.COMMENT(/^\s*@?rem\b/,/$/,{relevance:10});return{name:"Batch file (DOS)",aliases:["bat","cmd"],case_insensitive:!0,illegal:/\/\*/,keywords:{keyword:["if","else","goto","for","in","do","call","exit","not","exist","errorlevel","defined","equ","neq","lss","leq","gtr","geq"],built_in:["prn","nul","lpt3","lpt2","lpt1","con","com4","com3","com2","com1","aux","shift","cd","dir","echo","setlocal","endlocal","set","pause","copy","append","assoc","at","attrib","break","cacls","cd","chcp","chdir","chkdsk","chkntfs","cls","cmd","color","comp","compact","convert","date","dir","diskcomp","diskcopy","doskey","erase","fs","find","findstr","format","ftype","graftabl","help","keyb","label","md","mkdir","mode","more","move","path","pause","print","popd","pushd","promt","rd","recover","rem","rename","replace","restore","rmdir","shift","sort","start","subst","time","title","tree","type","ver","verify","vol","ping","net","ipconfig","taskkill","xcopy","ren","del"]},contains:[{className:"variable",begin:/%%[^ ]|%[^ ]+?%|![^ ]+?!/},{className:"function",begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)",end:"goto:eof",contains:[e.inherit(e.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),t]},{className:"number",begin:"\\b\\d+",relevance:0},t]}}})()),hljs.registerLanguage("fsharp",(()=>{"use strict";return e=>{const t={begin:"<",end:">",contains:[e.inherit(e.TITLE_MODE,{begin:/'[a-zA-Z0-9_]+/})]};return{name:"F#",aliases:["fs"],keywords:["abstract","and","as","assert","base","begin","class","default","delegate","do","done","downcast","downto","elif","else","end","exception","extern","false","finally","for","fun","function","global","if","in","inherit","inline","interface","internal","lazy","let","match","member","module","mutable","namespace","new","null","of","open","or","override","private","public","rec","return","sig","static","struct","then","to","true","try","type","upcast","use","val","void","when","while","with","yield"],illegal:/\/\*/,contains:[{className:"keyword",begin:/\b(yield|return|let|do)!/},{className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},{className:"string",begin:'"""',end:'"""'},e.COMMENT("\\(\\*(\\s)","\\*\\)",{contains:["self"]}),{className:"class",beginKeywords:"type",end:"\\(|=|$",excludeEnd:!0,contains:[e.UNDERSCORE_TITLE_MODE,t]},{className:"meta",begin:"\\[<",end:">\\]",relevance:10},{className:"symbol",begin:"\\B('[A-Za-z])\\b",contains:[e.BACKSLASH_ESCAPE]},e.C_LINE_COMMENT_MODE,e.inherit(e.QUOTE_STRING_MODE,{illegal:null}),e.C_NUMBER_MODE]}}})()),hljs.registerLanguage("xml",(()=>{"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function t(e){return n("(?=",e,")")}function n(...t){return t.map((t=>e(t))).join("")}function i(...t){return"("+((e=>{const t=e[e.length-1];return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}})(t).capture?"":"?:")+t.map((t=>e(t))).join("|")+")"}return e=>{const o=n(/[A-Z_]/,n("(?:",/[A-Z0-9_.-]*:/,")?"),/[A-Z0-9_.-]*/),r={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},a={begin:/\s/,contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},s=e.inherit(a,{begin:/\(/,end:/\)/}),l=e.inherit(e.APOS_STRING_MODE,{className:"string"}),c=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),u={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[a,c,l,s,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[a,s,c,l]}]}]},e.COMMENT(//,{relevance:10}),{begin://,relevance:10},r,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[u],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[u],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:n(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:o,relevance:0,starts:u}]},{className:"tag",begin:n(/<\//,t(n(o,/>/))),contains:[{className:"name",begin:o,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}})()),hljs.registerLanguage("shell",(()=>{"use strict";return e=>({name:"Shell Session",aliases:["console","shellsession"],contains:[{className:"meta",begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/,subLanguage:"bash"}}]})})()),hljs.registerLanguage("powershell",(()=>{"use strict";return e=>{const t={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},n={begin:"`[\\s\\S]",relevance:0},i={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},o={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[n,i,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},r={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},a=e.inherit(e.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),s={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[e.TITLE_MODE]},l={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[i]}]},c={begin:/using\s/,end:/$/,returnBegin:!0,contains:[o,r,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},u={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(t.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},e.inherit(e.TITLE_MODE,{endsParent:!0})]},d=[u,a,n,e.NUMBER_MODE,o,r,{className:"built_in",variants:[{begin:"(Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where)+(-)[\\w\\d]+"}]},i,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],f={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",d,{begin:"(string|char|byte|int|long|bool|decimal|single|double|DateTime|xml|array|hashtable|void)",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return u.contains.unshift(f),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:t,contains:d.concat(s,l,c,{variants:[{className:"operator",begin:"(-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor)\\b"},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},f)}}})()),hljs.registerLanguage("ruby",(()=>{"use strict";function e(e){return t("(?=",e,")")}function t(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}return n=>{const i="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",o={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor __FILE__",built_in:"proc lambda",literal:"true false nil"},r={className:"doctag",begin:"@[A-Za-z]+"},a={begin:"#<",end:">"},s=[n.COMMENT("#","$",{contains:[r]}),n.COMMENT("^=begin","^=end",{contains:[r],relevance:10}),n.COMMENT("^__END__","\\n$")],l={className:"subst",begin:/#\{/,end:/\}/,keywords:o},c={className:"string",contains:[n.BACKSLASH_ESCAPE,l],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//,end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{begin:t(/<<[-~]?'?/,e(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)),contains:[n.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,contains:[n.BACKSLASH_ESCAPE,l]})]}]},u="[0-9](_?[0-9])*",d={className:"number",relevance:0,variants:[{begin:`\\b([1-9](_?[0-9])*|0)(\\.(${u}))?([eE][+-]?(${u})|r)?i?\\b`},{begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{begin:"\\b0(_?[0-7])+r?i?\\b"}]},f={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:o},h=[c,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[n.inherit(n.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|!)?"}),{begin:"<\\s*",contains:[{begin:"("+n.IDENT_RE+"::)?"+n.IDENT_RE,relevance:0}]}].concat(s)},{className:"function",begin:t(/def\s+/,e(i+"\\s*(\\(|;|$)")),relevance:0,keywords:"def",end:"$|;",contains:[n.inherit(n.TITLE_MODE,{begin:i}),f].concat(s)},{begin:n.IDENT_RE+"::"},{className:"symbol",begin:n.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[c,{begin:i}],relevance:0},d,{className:"variable",begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{className:"params",begin:/\|/,end:/\|/,relevance:0,keywords:o},{begin:"("+n.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[{className:"regexp",contains:[n.BACKSLASH_ESCAPE,l],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(a,s),relevance:0}].concat(a,s);l.contains=h,f.contains=h;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:h}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])",starts:{end:"$",contains:h}}];return s.unshift(a),{name:"Ruby",aliases:["rb","gemspec","podspec","thor","irb"],keywords:o,illegal:/\/\*/,contains:[n.SHEBANG({binary:"ruby"})].concat(p).concat(s).concat(h)}}})()),hljs.registerLanguage("yaml",(()=>{"use strict";return e=>{const t="true false yes no null",n="[\\w#;/?:@&=+$,.~*'()[\\]]+",i={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},o=e.inherit(i,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={end:",",endsWithParent:!0,excludeEnd:!0,keywords:t,relevance:0},a={begin:/\{/,end:/\}/,contains:[r],illegal:"\\n",relevance:0},s={begin:"\\[",end:"\\]",contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+n},{className:"type",begin:"!<"+n+">"},{className:"type",begin:"!"+n},{className:"type",begin:"!!"+n},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:t,keywords:{literal:t}},{className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},a,s,i],c=[...l];return c.pop(),c.push(o),r.contains=c,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:l}}})()),hljs.registerLanguage("sql",(()=>{"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function t(...t){return t.map((t=>e(t))).join("")}function n(...t){return"("+((e=>{const t=e[e.length-1];return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}})(t).capture?"":"?:")+t.map((t=>e(t))).join("|")+")"}return e=>{const i=e.COMMENT("--","$"),o=["true","false","unknown"],r=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],a=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],l=a,c=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!a.includes(e))),u={begin:t(/\b/,n(...l),/\s*\(/),relevance:0,keywords:{built_in:l}};return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{$pattern:/\b[\w\.]+/,keyword:((e,{exceptions:t,when:n}={})=>{const i=n;return t=t||[],e.map((e=>e.match(/\|\d+$/)||t.includes(e)?e:i(e)?e+"|0":e))})(c,{when:e=>e.length<3}),literal:o,type:r,built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"]},contains:[{begin:n(...s),relevance:0,keywords:{$pattern:/[\w\.]+/,keyword:c.concat(s),literal:o,type:r}},{className:"type",begin:n("double precision","large object","with timezone","without timezone")},u,{className:"variable",begin:/@[a-z0-9]+/},{className:"string",variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/,contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,i,{className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/,relevance:0}]}}})()),hljs.registerLanguage("javascript",(()=>{"use strict";const e="[A-Za-z$_][0-9A-Za-z$_]*",t=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],i=["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],o=["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],a=["arguments","this","super","console","window","document","localStorage","module","global"],s=[].concat(r,i,o);function l(e){return c("(?=",e,")")}function c(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}return u=>{const d=e,f={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,t)=>{const n=e[0].length+e.index,i=e.input[n];"<"!==i?">"===i&&(((e,{after:t})=>{const n="",L={match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,l(D)],className:{1:"keyword",3:"title.function"},contains:[T]};return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:h,exports:{PARAMS_CONTAINS:C},illegal:/#(?![$_A-z])/,contains:[u.SHEBANG({label:"shebang",binary:"node",relevance:5}),{label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},u.APOS_STRING_MODE,u.QUOTE_STRING_MODE,b,y,w,x,m,k,{className:"attr",begin:d+l(":"),relevance:0},L,{begin:"("+u.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",relevance:0,contains:[x,u.REGEXP_MODE,{className:"function",begin:D,returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:u.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:h,contains:C}]}]},{begin:/,/,relevance:0},{match:/\s+/,relevance:0},{variants:[{begin:"<>",end:""},{begin:f.begin,"on:begin":f.isTrulyOpeningTag,end:f.end}],subLanguage:"xml",contains:[{begin:f.begin,end:f.end,skip:!0,contains:["self"]}]}]},N,{beginKeywords:"while if switch catch for"},{begin:"\\b(?!function)"+u.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,label:"func.def",contains:[T,u.inherit(u.TITLE_MODE,{begin:d,className:"title.function"})]},{match:/\.\.\./,relevance:0},M,{match:"\\$"+d,relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"},contains:[T]},A,{relevance:0,match:/\b[A-Z][A-Z_]+\b/,className:"variable.constant"},S,$,{match:/\$[(.]/}]}}})()),hljs.registerLanguage("typescript",(()=>{"use strict";const e="[A-Za-z$_][0-9A-Za-z$_]*",t=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],i=["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],o=["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],a=["arguments","this","super","console","window","document","localStorage","module","global"],s=[].concat(r,i,o);function l(e){return c("(?=",e,")")}function c(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}return u=>{const d={$pattern:e,keyword:t.concat(["type","namespace","typedef","interface","public","private","protected","implements","declare","abstract","readonly"]),literal:n,built_in:s.concat(["any","void","number","boolean","string","object","never","enum"]),"variable.language":a},f={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},h=(e,t,n)=>{const i=e.contains.findIndex((e=>e.label===t));if(-1===i)throw Error("can not find mode to replace");e.contains.splice(i,1,n)},p=function(u){const d=e,f={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,t)=>{const n=e[0].length+e.index,i=e.input[n];"<"!==i?">"===i&&(((e,{after:t})=>{const n="",L={match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,l(D)],className:{1:"keyword",3:"title.function"},contains:[T]};return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:h,exports:{PARAMS_CONTAINS:C},illegal:/#(?![$_A-z])/,contains:[u.SHEBANG({label:"shebang",binary:"node",relevance:5}),{label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},u.APOS_STRING_MODE,u.QUOTE_STRING_MODE,b,y,w,x,m,k,{className:"attr",begin:d+l(":"),relevance:0},L,{begin:"("+u.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",relevance:0,contains:[x,u.REGEXP_MODE,{className:"function",begin:D,returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:u.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:h,contains:C}]}]},{begin:/,/,relevance:0},{match:/\s+/,relevance:0},{variants:[{begin:"<>",end:""},{begin:f.begin,"on:begin":f.isTrulyOpeningTag,end:f.end}],subLanguage:"xml",contains:[{begin:f.begin,end:f.end,skip:!0,contains:["self"]}]}]},N,{beginKeywords:"while if switch catch for"},{begin:"\\b(?!function)"+u.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,label:"func.def",contains:[T,u.inherit(u.TITLE_MODE,{begin:d,className:"title.function"})]},{match:/\.\.\./,relevance:0},M,{match:"\\$"+d,relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"},contains:[T]},A,{relevance:0,match:/\b[A-Z][A-Z_]+\b/,className:"variable.constant"},S,$,{match:/\$[(.]/}]}}(u);return Object.assign(p.keywords,d),p.exports.PARAMS_CONTAINS.push(f),p.contains=p.contains.concat([f,{beginKeywords:"namespace",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"}]),h(p,"shebang",u.SHEBANG()),h(p,"use_strict",{className:"meta",relevance:10,begin:/^\s*['"]use strict['"]/}),p.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(p,{name:"TypeScript",aliases:["ts","tsx"]}),p}})()),hljs.registerLanguage("json",(()=>{"use strict";return e=>({name:"JSON",contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0},e.QUOTE_STRING_MODE,{beginKeywords:"true false null"},e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],illegal:"\\S"})})()),function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Popper=t()}(this,(function(){"use strict";function e(e){return e&&"[object Function]"==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var n=e.ownerDocument.defaultView.getComputedStyle(e,null);return t?n[t]:n}function n(e){return"HTML"===e.nodeName?e:e.parentNode||e.host}function i(e){if(!e)return document.body;switch(e.nodeName){case"HTML":case"BODY":return e.ownerDocument.body;case"#document":return e.body}var o=t(e),r=o.overflow,a=o.overflowX,s=o.overflowY;return/(auto|scroll|overlay)/.test(r+s+a)?e:i(n(e))}function o(e){return e&&e.referenceNode?e.referenceNode:e}function r(e){return 11===e?Q:10===e?G:Q||G}function a(e){if(!e)return document.documentElement;for(var n=r(10)?document.body:null,i=e.offsetParent||null;i===n&&e.nextElementSibling;)i=(e=e.nextElementSibling).offsetParent;var o=i&&i.nodeName;return o&&"BODY"!==o&&"HTML"!==o?-1!==["TH","TD","TABLE"].indexOf(i.nodeName)&&"static"===t(i,"position")?a(i):i:e?e.ownerDocument.documentElement:document.documentElement}function s(e){return null===e.parentNode?e:s(e.parentNode)}function l(e,t){if(!(e&&e.nodeType&&t&&t.nodeType))return document.documentElement;var n=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,i=n?e:t,o=n?t:e,r=document.createRange();r.setStart(i,0),r.setEnd(o,0);var c=r.commonAncestorContainer;if(e!==c&&t!==c||i.contains(o))return function(e){var t=e.nodeName;return"BODY"!==t&&("HTML"===t||a(e.firstElementChild)===e)}(c)?c:a(c);var u=s(e);return u.host?l(u.host,t):l(e,s(t).host)}function c(e){var t="top"===(1=n.clientWidth&&i>=n.clientHeight})),u=0c[e]&&!t.escapeWithReference&&(i=z(d[n],c[e]-("right"===e?d.width:d.height))),J({},n,i)}};return u.forEach((function(e){var t=-1===["left","top"].indexOf(e)?"secondary":"primary";d=ee({},d,f[t](e))})),e.offsets.popper=d,e},priority:["left","right","top","bottom"],padding:5,boundariesElement:"scrollParent"},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,n=t.popper,i=t.reference,o=e.placement.split("-")[0],r=W,a=-1!==["top","bottom"].indexOf(o),s=a?"right":"bottom",l=a?"left":"top",c=a?"width":"height";return n[s]r(i[s])&&(e.offsets.popper[l]=r(i[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,n){var i;if(!B(e.instance.modifiers,"arrow","keepTogether"))return e;var o=n.element;if("string"==typeof o){if(!(o=e.instance.popper.querySelector(o)))return e}else if(!e.instance.popper.contains(o))return console.warn("WARNING: `arrow.element` must be child of its popper element!"),e;var r=e.placement.split("-")[0],a=e.offsets,s=a.popper,l=a.reference,c=-1!==["left","right"].indexOf(r),u=c?"height":"width",d=c?"Top":"Left",f=d.toLowerCase(),h=c?"left":"top",g=c?"bottom":"right",m=C(o)[u];l[g]-ms[g]&&(e.offsets.popper[f]+=l[f]+m-s[g]),e.offsets.popper=p(e.offsets.popper);var v=l[f]+l[u]/2-m/2,b=t(e.instance.popper),y=parseFloat(b["margin"+d]),w=parseFloat(b["border"+d+"Width"]),x=v-e.offsets.popper[f]-y-w;return x=X(z(s[u]-m,x),0),e.arrowElement=o,e.offsets.arrow=(J(i={},f,U(x)),J(i,h,""),i),e},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(e,t){if(O(e.instance.modifiers,"inner"))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var n=w(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),i=e.placement.split("-")[0],o=T(i),r=e.placement.split("-")[1]||"",a=[];switch(t.behavior){case oe:a=[i,o];break;case re:a=H(i);break;case ae:a=H(i,!0);break;default:a=t.behavior}return a.forEach((function(s,l){if(i!==s||a.length===l+1)return e;i=e.placement.split("-")[0],o=T(i);var c=e.offsets.popper,u=e.offsets.reference,d=W,f="left"===i&&d(c.right)>d(u.left)||"right"===i&&d(c.left)d(u.top)||"bottom"===i&&d(c.top)d(n.right),g=d(c.top)d(n.bottom),v="left"===i&&h||"right"===i&&p||"top"===i&&g||"bottom"===i&&m,b=-1!==["top","bottom"].indexOf(i),y=!!t.flipVariations&&(b&&"start"===r&&h||b&&"end"===r&&p||!b&&"start"===r&&g||!b&&"end"===r&&m),w=!!t.flipVariationsByContent&&(b&&"start"===r&&p||b&&"end"===r&&h||!b&&"start"===r&&m||!b&&"end"===r&&g),x=y||w;(f||v||x)&&(e.flipped=!0,(f||v)&&(i=a[l+1]),x&&(r=function(e){return"end"===e?"start":"start"===e?"end":e}(r)),e.placement=i+(r?"-"+r:""),e.offsets.popper=ee({},e.offsets.popper,S(e.instance.popper,e.offsets.reference,e.placement)),e=N(e.instance.modifiers,e,"flip"))})),e},behavior:"flip",padding:5,boundariesElement:"viewport",flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,n=t.split("-")[0],i=e.offsets,o=i.popper,r=i.reference,a=-1!==["left","right"].indexOf(n),s=-1===["top","left"].indexOf(n);return o[a?"left":"top"]=r[n]-(s?o[a?"width":"height"]:0),e.placement=T(t),e.offsets.popper=p(o),e}},hide:{order:800,enabled:!0,fn:function(e){if(!B(e.instance.modifiers,"hide","preventOverflow"))return e;var t=e.offsets.reference,n=k(e.instance.modifiers,(function(e){return"preventOverflow"===e.name})).boundaries;if(t.bottomn.right||t.top>n.bottom||t.rightwindow.devicePixelRatio||!te),p="bottom"===n?"top":"bottom",m="right"===i?"left":"right",v=M("transform");if(l="bottom"==p?"HTML"===u.nodeName?-u.clientHeight+h.bottom:-d.height+h.bottom:h.top,s="right"==m?"HTML"===u.nodeName?-u.clientWidth+h.right:-d.width+h.right:h.left,c&&v)f[v]="translate3d("+s+"px, "+l+"px, 0)",f[p]=0,f[m]=0,f.willChange="transform";else{var b="bottom"==p?-1:1,y="right"==m?-1:1;f[p]=l*b,f[m]=s*y,f.willChange=p+", "+m}var w={"x-placement":e.placement};return e.attributes=ee({},w,e.attributes),e.styles=ee({},f,e.styles),e.arrowStyles=ee({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:"bottom",y:"right"},applyStyle:{order:900,enabled:!0,fn:function(e){return F(e.instance.popper,e.styles),function(e,t){Object.keys(t).forEach((function(n){!1===t[n]?e.removeAttribute(n):e.setAttribute(n,t[n])}))}(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&F(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,n,i,o){var r=E(o,t,e,n.positionFixed),a=_(n.placement,r,t,e,n.modifiers.flip.boundariesElement,n.modifiers.flip.padding);return t.setAttribute("x-placement",a),F(t,{position:n.positionFixed?"fixed":"absolute"}),n},gpuAcceleration:void 0}}},se})),function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).bootstrap={},e.jQuery,e.Popper)}(this,(function(e,t,n){"use strict";function i(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var o=i(t),r=i(n);function a(e,t){for(var n=0;n=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};c.jQueryDetection(),o.default.fn.emulateTransitionEnd=function(e){var t=this,n=!1;return o.default(this).one(c.TRANSITION_END,(function(){n=!0})),setTimeout((function(){n||c.triggerTransitionEnd(t)}),e),this},o.default.event.special[c.TRANSITION_END]={bindType:"transitionend",delegateType:"transitionend",handle:function(e){if(o.default(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}};var u="alert",d=o.default.fn[u],f=function(){function e(e){this._element=e}var t=e.prototype;return t.close=function(e){var t=this._element;e&&(t=this._getRootElement(e)),this._triggerCloseEvent(t).isDefaultPrevented()||this._removeElement(t)},t.dispose=function(){o.default.removeData(this._element,"bs.alert"),this._element=null},t._getRootElement=function(e){var t=c.getSelectorFromElement(e),n=!1;return t&&(n=document.querySelector(t)),n||(n=o.default(e).closest(".alert")[0]),n},t._triggerCloseEvent=function(e){var t=o.default.Event("close.bs.alert");return o.default(e).trigger(t),t},t._removeElement=function(e){var t=this;if(o.default(e).removeClass("show"),o.default(e).hasClass("fade")){var n=c.getTransitionDurationFromElement(e);o.default(e).one(c.TRANSITION_END,(function(n){return t._destroyElement(e,n)})).emulateTransitionEnd(n)}else this._destroyElement(e)},t._destroyElement=function(e){o.default(e).detach().trigger("closed.bs.alert").remove()},e._jQueryInterface=function(t){return this.each((function(){var n=o.default(this),i=n.data("bs.alert");i||(i=new e(this),n.data("bs.alert",i)),"close"===t&&i[t](this)}))},e._handleDismiss=function(e){return function(t){t&&t.preventDefault(),e.close(this)}},s(e,null,[{key:"VERSION",get:function(){return"4.5.3"}}]),e}();o.default(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',f._handleDismiss(new f)),o.default.fn[u]=f._jQueryInterface,o.default.fn[u].Constructor=f,o.default.fn[u].noConflict=function(){return o.default.fn[u]=d,f._jQueryInterface};var h=o.default.fn.button,p=function(){function e(e){this._element=e,this.shouldAvoidTriggerChange=!1}var t=e.prototype;return t.toggle=function(){var e=!0,t=!0,n=o.default(this._element).closest('[data-toggle="buttons"]')[0];if(n){var i=this._element.querySelector('input:not([type="hidden"])');if(i){if("radio"===i.type)if(i.checked&&this._element.classList.contains("active"))e=!1;else{var r=n.querySelector(".active");r&&o.default(r).removeClass("active")}e&&("checkbox"!==i.type&&"radio"!==i.type||(i.checked=!this._element.classList.contains("active")),this.shouldAvoidTriggerChange||o.default(i).trigger("change")),i.focus(),t=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(t&&this._element.setAttribute("aria-pressed",!this._element.classList.contains("active")),e&&o.default(this._element).toggleClass("active"))},t.dispose=function(){o.default.removeData(this._element,"bs.button"),this._element=null},e._jQueryInterface=function(t,n){return this.each((function(){var i=o.default(this),r=i.data("bs.button");r||(r=new e(this),i.data("bs.button",r)),r.shouldAvoidTriggerChange=n,"toggle"===t&&r[t]()}))},s(e,null,[{key:"VERSION",get:function(){return"4.5.3"}}]),e}();o.default(document).on("click.bs.button.data-api",'[data-toggle^="button"]',(function(e){var t=e.target,n=t;if(o.default(t).hasClass("btn")||(t=o.default(t).closest(".btn")[0]),!t||t.hasAttribute("disabled")||t.classList.contains("disabled"))e.preventDefault();else{var i=t.querySelector('input:not([type="hidden"])');if(i&&(i.hasAttribute("disabled")||i.classList.contains("disabled")))return void e.preventDefault();"INPUT"!==n.tagName&&"LABEL"===t.tagName||p._jQueryInterface.call(o.default(t),"toggle","INPUT"===n.tagName)}})).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',(function(e){var t=o.default(e.target).closest(".btn")[0];o.default(t).toggleClass("focus",/^focus(in)?$/.test(e.type))})),o.default(window).on("load.bs.button.data-api",(function(){for(var e=[].slice.call(document.querySelectorAll('[data-toggle="buttons"] .btn')),t=0,n=e.length;t0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var t=e.prototype;return t.next=function(){this._isSliding||this._slide("next")},t.nextWhenVisible=function(){var e=o.default(this._element);!document.hidden&&e.is(":visible")&&"hidden"!==e.css("visibility")&&this.next()},t.prev=function(){this._isSliding||this._slide("prev")},t.pause=function(e){e||(this._isPaused=!0),this._element.querySelector(".carousel-item-next, .carousel-item-prev")&&(c.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},t.cycle=function(e){e||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},t.to=function(e){var t=this;this._activeElement=this._element.querySelector(".active.carousel-item");var n=this._getItemIndex(this._activeElement);if(!(e>this._items.length-1||e<0))if(this._isSliding)o.default(this._element).one("slid.bs.carousel",(function(){return t.to(e)}));else{if(n===e)return this.pause(),void this.cycle();var i=e>n?"next":"prev";this._slide(i,this._items[e])}},t.dispose=function(){o.default(this._element).off(".bs.carousel"),o.default.removeData(this._element,"bs.carousel"),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},t._getConfig=function(e){return e=l({},v,e),c.typeCheckConfig(g,e,b),e},t._handleSwipe=function(){var e=Math.abs(this.touchDeltaX);if(!(e<=40)){var t=e/this.touchDeltaX;this.touchDeltaX=0,t>0&&this.prev(),t<0&&this.next()}},t._addEventListeners=function(){var e=this;this._config.keyboard&&o.default(this._element).on("keydown.bs.carousel",(function(t){return e._keydown(t)})),"hover"===this._config.pause&&o.default(this._element).on("mouseenter.bs.carousel",(function(t){return e.pause(t)})).on("mouseleave.bs.carousel",(function(t){return e.cycle(t)})),this._config.touch&&this._addTouchEventListeners()},t._addTouchEventListeners=function(){var e=this;if(this._touchSupported){var t=function(t){e._pointerEvent&&y[t.originalEvent.pointerType.toUpperCase()]?e.touchStartX=t.originalEvent.clientX:e._pointerEvent||(e.touchStartX=t.originalEvent.touches[0].clientX)},n=function(t){e._pointerEvent&&y[t.originalEvent.pointerType.toUpperCase()]&&(e.touchDeltaX=t.originalEvent.clientX-e.touchStartX),e._handleSwipe(),"hover"===e._config.pause&&(e.pause(),e.touchTimeout&&clearTimeout(e.touchTimeout),e.touchTimeout=setTimeout((function(t){return e.cycle(t)}),500+e._config.interval))};o.default(this._element.querySelectorAll(".carousel-item img")).on("dragstart.bs.carousel",(function(e){return e.preventDefault()})),this._pointerEvent?(o.default(this._element).on("pointerdown.bs.carousel",(function(e){return t(e)})),o.default(this._element).on("pointerup.bs.carousel",(function(e){return n(e)})),this._element.classList.add("pointer-event")):(o.default(this._element).on("touchstart.bs.carousel",(function(e){return t(e)})),o.default(this._element).on("touchmove.bs.carousel",(function(t){return function(t){t.originalEvent.touches&&t.originalEvent.touches.length>1?e.touchDeltaX=0:e.touchDeltaX=t.originalEvent.touches[0].clientX-e.touchStartX}(t)})),o.default(this._element).on("touchend.bs.carousel",(function(e){return n(e)})))}},t._keydown=function(e){if(!/input|textarea/i.test(e.target.tagName))switch(e.which){case 37:e.preventDefault(),this.prev();break;case 39:e.preventDefault(),this.next()}},t._getItemIndex=function(e){return this._items=e&&e.parentNode?[].slice.call(e.parentNode.querySelectorAll(".carousel-item")):[],this._items.indexOf(e)},t._getItemByDirection=function(e,t){var n="next"===e,i="prev"===e,o=this._getItemIndex(t),r=this._items.length-1;if((i&&0===o||n&&o===r)&&!this._config.wrap)return t;var a=(o+("prev"===e?-1:1))%this._items.length;return-1===a?this._items[this._items.length-1]:this._items[a]},t._triggerSlideEvent=function(e,t){var n=this._getItemIndex(e),i=this._getItemIndex(this._element.querySelector(".active.carousel-item")),r=o.default.Event("slide.bs.carousel",{relatedTarget:e,direction:t,from:i,to:n});return o.default(this._element).trigger(r),r},t._setActiveIndicatorElement=function(e){if(this._indicatorsElement){var t=[].slice.call(this._indicatorsElement.querySelectorAll(".active"));o.default(t).removeClass("active");var n=this._indicatorsElement.children[this._getItemIndex(e)];n&&o.default(n).addClass("active")}},t._slide=function(e,t){var n,i,r,a=this,s=this._element.querySelector(".active.carousel-item"),l=this._getItemIndex(s),u=t||s&&this._getItemByDirection(e,s),d=this._getItemIndex(u),f=Boolean(this._interval);if("next"===e?(n="carousel-item-left",i="carousel-item-next",r="left"):(n="carousel-item-right",i="carousel-item-prev",r="right"),u&&o.default(u).hasClass("active"))this._isSliding=!1;else if(!this._triggerSlideEvent(u,r).isDefaultPrevented()&&s&&u){this._isSliding=!0,f&&this.pause(),this._setActiveIndicatorElement(u);var h=o.default.Event("slid.bs.carousel",{relatedTarget:u,direction:r,from:l,to:d});if(o.default(this._element).hasClass("slide")){o.default(u).addClass(i),c.reflow(u),o.default(s).addClass(n),o.default(u).addClass(n);var p=parseInt(u.getAttribute("data-interval"),10);p?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=p):this._config.interval=this._config.defaultInterval||this._config.interval;var g=c.getTransitionDurationFromElement(s);o.default(s).one(c.TRANSITION_END,(function(){o.default(u).removeClass(n+" "+i).addClass("active"),o.default(s).removeClass("active "+i+" "+n),a._isSliding=!1,setTimeout((function(){return o.default(a._element).trigger(h)}),0)})).emulateTransitionEnd(g)}else o.default(s).removeClass("active"),o.default(u).addClass("active"),this._isSliding=!1,o.default(this._element).trigger(h);f&&this.cycle()}},e._jQueryInterface=function(t){return this.each((function(){var n=o.default(this).data("bs.carousel"),i=l({},v,o.default(this).data());"object"==typeof t&&(i=l({},i,t));var r="string"==typeof t?t:i.slide;if(n||(n=new e(this,i),o.default(this).data("bs.carousel",n)),"number"==typeof t)n.to(t);else if("string"==typeof r){if(void 0===n[r])throw new TypeError('No method named "'+r+'"');n[r]()}else i.interval&&i.ride&&(n.pause(),n.cycle())}))},e._dataApiClickHandler=function(t){var n=c.getSelectorFromElement(this);if(n){var i=o.default(n)[0];if(i&&o.default(i).hasClass("carousel")){var r=l({},o.default(i).data(),o.default(this).data()),a=this.getAttribute("data-slide-to");a&&(r.interval=!1),e._jQueryInterface.call(o.default(i),r),a&&o.default(i).data("bs.carousel").to(a),t.preventDefault()}}},s(e,null,[{key:"VERSION",get:function(){return"4.5.3"}},{key:"Default",get:function(){return v}}]),e}();o.default(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",w._dataApiClickHandler),o.default(window).on("load.bs.carousel.data-api",(function(){for(var e=[].slice.call(document.querySelectorAll('[data-ride="carousel"]')),t=0,n=e.length;t0&&(this._selector=a,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var t=e.prototype;return t.toggle=function(){o.default(this._element).hasClass("show")?this.hide():this.show()},t.show=function(){var t,n,i=this;if(!(this._isTransitioning||o.default(this._element).hasClass("show")||(this._parent&&0===(t=[].slice.call(this._parent.querySelectorAll(".show, .collapsing")).filter((function(e){return"string"==typeof i._config.parent?e.getAttribute("data-parent")===i._config.parent:e.classList.contains("collapse")}))).length&&(t=null),t&&(n=o.default(t).not(this._selector).data("bs.collapse"))&&n._isTransitioning))){var r=o.default.Event("show.bs.collapse");if(o.default(this._element).trigger(r),!r.isDefaultPrevented()){t&&(e._jQueryInterface.call(o.default(t).not(this._selector),"hide"),n||o.default(t).data("bs.collapse",null));var a=this._getDimension();o.default(this._element).removeClass("collapse").addClass("collapsing"),this._element.style[a]=0,this._triggerArray.length&&o.default(this._triggerArray).removeClass("collapsed").attr("aria-expanded",!0),this.setTransitioning(!0);var s="scroll"+(a[0].toUpperCase()+a.slice(1)),l=c.getTransitionDurationFromElement(this._element);o.default(this._element).one(c.TRANSITION_END,(function(){o.default(i._element).removeClass("collapsing").addClass("collapse show"),i._element.style[a]="",i.setTransitioning(!1),o.default(i._element).trigger("shown.bs.collapse")})).emulateTransitionEnd(l),this._element.style[a]=this._element[s]+"px"}}},t.hide=function(){var e=this;if(!this._isTransitioning&&o.default(this._element).hasClass("show")){var t=o.default.Event("hide.bs.collapse");if(o.default(this._element).trigger(t),!t.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",c.reflow(this._element),o.default(this._element).addClass("collapsing").removeClass("collapse show");var i=this._triggerArray.length;if(i>0)for(var r=0;r0},t._getOffset=function(){var e=this,t={};return"function"==typeof this._config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e._config.offset(t.offsets,e._element)||{}),t}:t.offset=this._config.offset,t},t._getPopperConfig=function(){var e={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(e.modifiers.applyStyle={enabled:!1}),l({},e,this._config.popperConfig)},e._jQueryInterface=function(t){return this.each((function(){var n=o.default(this).data("bs.dropdown");if(n||(n=new e(this,"object"==typeof t?t:null),o.default(this).data("bs.dropdown",n)),"string"==typeof t){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},e._clearMenus=function(t){if(!t||3!==t.which&&("keyup"!==t.type||9===t.which))for(var n=[].slice.call(document.querySelectorAll('[data-toggle="dropdown"]')),i=0,r=n.length;i0&&a--,40===t.which&&adocument.documentElement.clientHeight;n||(this._element.style.overflowY="hidden"),this._element.classList.add("modal-static");var i=c.getTransitionDurationFromElement(this._dialog);o.default(this._element).off(c.TRANSITION_END),o.default(this._element).one(c.TRANSITION_END,(function(){e._element.classList.remove("modal-static"),n||o.default(e._element).one(c.TRANSITION_END,(function(){e._element.style.overflowY=""})).emulateTransitionEnd(e._element,i)})).emulateTransitionEnd(i),this._element.focus()}else this.hide()},t._showElement=function(e){var t=this,n=o.default(this._element).hasClass("fade"),i=this._dialog?this._dialog.querySelector(".modal-body"):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),o.default(this._dialog).hasClass("modal-dialog-scrollable")&&i?i.scrollTop=0:this._element.scrollTop=0,n&&c.reflow(this._element),o.default(this._element).addClass("show"),this._config.focus&&this._enforceFocus();var r=o.default.Event("shown.bs.modal",{relatedTarget:e}),a=function(){t._config.focus&&t._element.focus(),t._isTransitioning=!1,o.default(t._element).trigger(r)};if(n){var s=c.getTransitionDurationFromElement(this._dialog);o.default(this._dialog).one(c.TRANSITION_END,a).emulateTransitionEnd(s)}else a()},t._enforceFocus=function(){var e=this;o.default(document).off("focusin.bs.modal").on("focusin.bs.modal",(function(t){document!==t.target&&e._element!==t.target&&0===o.default(e._element).has(t.target).length&&e._element.focus()}))},t._setEscapeEvent=function(){var e=this;this._isShown?o.default(this._element).on("keydown.dismiss.bs.modal",(function(t){e._config.keyboard&&27===t.which?(t.preventDefault(),e.hide()):e._config.keyboard||27!==t.which||e._triggerBackdropTransition()})):this._isShown||o.default(this._element).off("keydown.dismiss.bs.modal")},t._setResizeEvent=function(){var e=this;this._isShown?o.default(window).on("resize.bs.modal",(function(t){return e.handleUpdate(t)})):o.default(window).off("resize.bs.modal")},t._hideModal=function(){var e=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop((function(){o.default(document.body).removeClass("modal-open"),e._resetAdjustments(),e._resetScrollbar(),o.default(e._element).trigger("hidden.bs.modal")}))},t._removeBackdrop=function(){this._backdrop&&(o.default(this._backdrop).remove(),this._backdrop=null)},t._showBackdrop=function(e){var t=this,n=o.default(this._element).hasClass("fade")?"fade":"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",n&&this._backdrop.classList.add(n),o.default(this._backdrop).appendTo(document.body),o.default(this._element).on("click.dismiss.bs.modal",(function(e){t._ignoreBackdropClick?t._ignoreBackdropClick=!1:e.target===e.currentTarget&&t._triggerBackdropTransition()})),n&&c.reflow(this._backdrop),o.default(this._backdrop).addClass("show"),!e)return;if(!n)return void e();var i=c.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(c.TRANSITION_END,e).emulateTransitionEnd(i)}else if(!this._isShown&&this._backdrop){o.default(this._backdrop).removeClass("show");var r=function(){t._removeBackdrop(),e&&e()};if(o.default(this._element).hasClass("fade")){var a=c.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(c.TRANSITION_END,r).emulateTransitionEnd(a)}else r()}else e&&e()},t._adjustDialog=function(){var e=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&e&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!e&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var e=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(e.left+e.right)
    ',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},V={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},Y=function(){function e(e,t){if(void 0===r.default)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=e,this.config=this._getConfig(t),this.tip=null,this._setListeners()}var t=e.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(e){if(this._isEnabled)if(e){var t=this.constructor.DATA_KEY,n=o.default(e.currentTarget).data(t);n||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),o.default(e.currentTarget).data(t,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(o.default(this.getTipElement()).hasClass("show"))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),o.default.removeData(this.element,this.constructor.DATA_KEY),o.default(this.element).off(this.constructor.EVENT_KEY),o.default(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&o.default(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===o.default(this.element).css("display"))throw new Error("Please use show on visible elements");var t=o.default.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){o.default(this.element).trigger(t);var n=c.findShadowRoot(this.element),i=o.default.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var a=this.getTipElement(),s=c.getUID(this.constructor.NAME);a.setAttribute("id",s),this.element.setAttribute("aria-describedby",s),this.setContent(),this.config.animation&&o.default(a).addClass("fade");var l="function"==typeof this.config.placement?this.config.placement.call(this,a,this.element):this.config.placement,u=this._getAttachment(l);this.addAttachmentClass(u);var d=this._getContainer();o.default(a).data(this.constructor.DATA_KEY,this),o.default.contains(this.element.ownerDocument.documentElement,this.tip)||o.default(a).appendTo(d),o.default(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new r.default(this.element,a,this._getPopperConfig(u)),o.default(a).addClass("show"),"ontouchstart"in document.documentElement&&o.default(document.body).children().on("mouseover",null,o.default.noop);var f=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,o.default(e.element).trigger(e.constructor.Event.SHOWN),"out"===t&&e._leave(null,e)};if(o.default(this.tip).hasClass("fade")){var h=c.getTransitionDurationFromElement(this.tip);o.default(this.tip).one(c.TRANSITION_END,f).emulateTransitionEnd(h)}else f()}},t.hide=function(e){var t=this,n=this.getTipElement(),i=o.default.Event(this.constructor.Event.HIDE),r=function(){"show"!==t._hoverState&&n.parentNode&&n.parentNode.removeChild(n),t._cleanTipClass(),t.element.removeAttribute("aria-describedby"),o.default(t.element).trigger(t.constructor.Event.HIDDEN),null!==t._popper&&t._popper.destroy(),e&&e()};if(o.default(this.element).trigger(i),!i.isDefaultPrevented()){if(o.default(n).removeClass("show"),"ontouchstart"in document.documentElement&&o.default(document.body).children().off("mouseover",null,o.default.noop),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,o.default(this.tip).hasClass("fade")){var a=c.getTransitionDurationFromElement(n);o.default(n).one(c.TRANSITION_END,r).emulateTransitionEnd(a)}else r();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(e){o.default(this.getTipElement()).addClass("bs-tooltip-"+e)},t.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},t.setContent=function(){var e=this.getTipElement();this.setElementContent(o.default(e.querySelectorAll(".tooltip-inner")),this.getTitle()),o.default(e).removeClass("fade show")},t.setElementContent=function(e,t){"object"!=typeof t||!t.nodeType&&!t.jquery?this.config.html?(this.config.sanitize&&(t=F(t,this.config.whiteList,this.config.sanitizeFn)),e.html(t)):e.text(t):this.config.html?o.default(t).parent().is(e)||e.empty().append(t):e.text(o.default(t).text())},t.getTitle=function(){var e=this.element.getAttribute("data-original-title");return e||(e="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),e},t._getPopperConfig=function(e){var t=this;return l({},{placement:e,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:".arrow"},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(e){e.originalPlacement!==e.placement&&t._handlePopperPlacementChange(e)},onUpdate:function(e){return t._handlePopperPlacementChange(e)}},this.config.popperConfig)},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:c.isElement(this.config.container)?o.default(this.config.container):o.default(document).find(this.config.container)},t._getAttachment=function(e){return U[e.toUpperCase()]},t._setListeners=function(){var e=this;this.config.trigger.split(" ").forEach((function(t){if("click"===t)o.default(e.element).on(e.constructor.Event.CLICK,e.config.selector,(function(t){return e.toggle(t)}));else if("manual"!==t){var n="hover"===t?e.constructor.Event.MOUSEENTER:e.constructor.Event.FOCUSIN,i="hover"===t?e.constructor.Event.MOUSELEAVE:e.constructor.Event.FOCUSOUT;o.default(e.element).on(n,e.config.selector,(function(t){return e._enter(t)})).on(i,e.config.selector,(function(t){return e._leave(t)}))}})),this._hideModalHandler=function(){e.element&&e.hide()},o.default(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var e=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==e)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(e,t){var n=this.constructor.DATA_KEY;(t=t||o.default(e.currentTarget).data(n))||(t=new this.constructor(e.currentTarget,this._getDelegateConfig()),o.default(e.currentTarget).data(n,t)),e&&(t._activeTrigger["focusin"===e.type?"focus":"hover"]=!0),o.default(t.getTipElement()).hasClass("show")||"show"===t._hoverState?t._hoverState="show":(clearTimeout(t._timeout),t._hoverState="show",t.config.delay&&t.config.delay.show?t._timeout=setTimeout((function(){"show"===t._hoverState&&t.show()}),t.config.delay.show):t.show())},t._leave=function(e,t){var n=this.constructor.DATA_KEY;(t=t||o.default(e.currentTarget).data(n))||(t=new this.constructor(e.currentTarget,this._getDelegateConfig()),o.default(e.currentTarget).data(n,t)),e&&(t._activeTrigger["focusout"===e.type?"focus":"hover"]=!1),t._isWithActiveTrigger()||(clearTimeout(t._timeout),t._hoverState="out",t.config.delay&&t.config.delay.hide?t._timeout=setTimeout((function(){"out"===t._hoverState&&t.hide()}),t.config.delay.hide):t.hide())},t._isWithActiveTrigger=function(){for(var e in this._activeTrigger)if(this._activeTrigger[e])return!0;return!1},t._getConfig=function(e){var t=o.default(this.element).data();return Object.keys(t).forEach((function(e){-1!==z.indexOf(e)&&delete t[e]})),"number"==typeof(e=l({},this.constructor.Default,t,"object"==typeof e&&e?e:{})).delay&&(e.delay={show:e.delay,hide:e.delay}),"number"==typeof e.title&&(e.title=e.title.toString()),"number"==typeof e.content&&(e.content=e.content.toString()),c.typeCheckConfig(B,e,this.constructor.DefaultType),e.sanitize&&(e.template=F(e.template,e.whiteList,e.sanitizeFn)),e},t._getDelegateConfig=function(){var e={};if(this.config)for(var t in this.config)this.constructor.Default[t]!==this.config[t]&&(e[t]=this.config[t]);return e},t._cleanTipClass=function(){var e=o.default(this.getTipElement()),t=e.attr("class").match(q);null!==t&&t.length&&e.removeClass(t.join(""))},t._handlePopperPlacementChange=function(e){this.tip=e.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(e.placement))},t._fixTransition=function(){var e=this.getTipElement(),t=this.config.animation;null===e.getAttribute("x-placement")&&(o.default(e).removeClass("fade"),this.config.animation=!1,this.hide(),this.show(),this.config.animation=t)},e._jQueryInterface=function(t){return this.each((function(){var n=o.default(this),i=n.data("bs.tooltip"),r="object"==typeof t&&t;if((i||!/dispose|hide/.test(t))&&(i||(i=new e(this,r),n.data("bs.tooltip",i)),"string"==typeof t)){if(void 0===i[t])throw new TypeError('No method named "'+t+'"');i[t]()}}))},s(e,null,[{key:"VERSION",get:function(){return"4.5.3"}},{key:"Default",get:function(){return X}},{key:"NAME",get:function(){return B}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return V}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return W}}]),e}();o.default.fn[B]=Y._jQueryInterface,o.default.fn[B].Constructor=Y,o.default.fn[B].noConflict=function(){return o.default.fn[B]=H,Y._jQueryInterface};var K="popover",Q=o.default.fn[K],G=new RegExp("(^|\\s)bs-popover\\S+","g"),Z=l({},Y.Default,{placement:"right",trigger:"click",content:"",template:''}),J=l({},Y.DefaultType,{content:"(string|element|function)"}),ee={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},te=function(e){var t,n;function i(){return e.apply(this,arguments)||this}n=e,(t=i).prototype=Object.create(n.prototype),t.prototype.constructor=t,t.__proto__=n;var r=i.prototype;return r.isWithContent=function(){return this.getTitle()||this._getContent()},r.addAttachmentClass=function(e){o.default(this.getTipElement()).addClass("bs-popover-"+e)},r.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},r.setContent=function(){var e=o.default(this.getTipElement());this.setElementContent(e.find(".popover-header"),this.getTitle());var t=this._getContent();"function"==typeof t&&(t=t.call(this.element)),this.setElementContent(e.find(".popover-body"),t),e.removeClass("fade show")},r._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},r._cleanTipClass=function(){var e=o.default(this.getTipElement()),t=e.attr("class").match(G);null!==t&&t.length>0&&e.removeClass(t.join(""))},i._jQueryInterface=function(e){return this.each((function(){var t=o.default(this).data("bs.popover"),n="object"==typeof e?e:null;if((t||!/dispose|hide/.test(e))&&(t||(t=new i(this,n),o.default(this).data("bs.popover",t)),"string"==typeof e)){if(void 0===t[e])throw new TypeError('No method named "'+e+'"');t[e]()}}))},s(i,null,[{key:"VERSION",get:function(){return"4.5.3"}},{key:"Default",get:function(){return Z}},{key:"NAME",get:function(){return K}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return ee}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return J}}]),i}(Y);o.default.fn[K]=te._jQueryInterface,o.default.fn[K].Constructor=te,o.default.fn[K].noConflict=function(){return o.default.fn[K]=Q,te._jQueryInterface};var ne="scrollspy",ie=o.default.fn[ne],oe={offset:10,method:"auto",target:""},re={offset:"number",method:"string",target:"(string|element)"},ae=function(){function e(e,t){var n=this;this._element=e,this._scrollElement="BODY"===e.tagName?window:e,this._config=this._getConfig(t),this._selector=this._config.target+" .nav-link,"+this._config.target+" .list-group-item,"+this._config.target+" .dropdown-item",this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,o.default(this._scrollElement).on("scroll.bs.scrollspy",(function(e){return n._process(e)})),this.refresh(),this._process()}var t=e.prototype;return t.refresh=function(){var e=this,t=this._scrollElement===this._scrollElement.window?"offset":"position",n="auto"===this._config.method?t:this._config.method,i="position"===n?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(e){var t,r=c.getSelectorFromElement(e);if(r&&(t=document.querySelector(r)),t){var a=t.getBoundingClientRect();if(a.width||a.height)return[o.default(t)[n]().top+i,r]}return null})).filter((function(e){return e})).sort((function(e,t){return e[0]-t[0]})).forEach((function(t){e._offsets.push(t[0]),e._targets.push(t[1])}))},t.dispose=function(){o.default.removeData(this._element,"bs.scrollspy"),o.default(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},t._getConfig=function(e){if("string"!=typeof(e=l({},oe,"object"==typeof e&&e?e:{})).target&&c.isElement(e.target)){var t=o.default(e.target).attr("id");t||(t=c.getUID(ne),o.default(e.target).attr("id",t)),e.target="#"+t}return c.typeCheckConfig(ne,e,re),e},t._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},t._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},t._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},t._process=function(){var e=this._getScrollTop()+this._config.offset,t=this._getScrollHeight(),n=this._config.offset+t-this._getOffsetHeight();if(this._scrollHeight!==t&&this.refresh(),e>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&e0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;)this._activeTarget!==this._targets[o]&&e>=this._offsets[o]&&(void 0===this._offsets[o+1]||e li > .active":".active";n=(n=o.default.makeArray(o.default(i).find(a)))[n.length-1]}var s=o.default.Event("hide.bs.tab",{relatedTarget:this._element}),l=o.default.Event("show.bs.tab",{relatedTarget:n});if(n&&o.default(n).trigger(s),o.default(this._element).trigger(l),!l.isDefaultPrevented()&&!s.isDefaultPrevented()){r&&(t=document.querySelector(r)),this._activate(this._element,i);var u=function(){var t=o.default.Event("hidden.bs.tab",{relatedTarget:e._element}),i=o.default.Event("shown.bs.tab",{relatedTarget:n});o.default(n).trigger(t),o.default(e._element).trigger(i)};t?this._activate(t,t.parentNode,u):u()}}},t.dispose=function(){o.default.removeData(this._element,"bs.tab"),this._element=null},t._activate=function(e,t,n){var i=this,r=(!t||"UL"!==t.nodeName&&"OL"!==t.nodeName?o.default(t).children(".active"):o.default(t).find("> li > .active"))[0],a=n&&r&&o.default(r).hasClass("fade"),s=function(){return i._transitionComplete(e,r,n)};if(r&&a){var l=c.getTransitionDurationFromElement(r);o.default(r).removeClass("show").one(c.TRANSITION_END,s).emulateTransitionEnd(l)}else s()},t._transitionComplete=function(e,t,n){if(t){o.default(t).removeClass("active");var i=o.default(t.parentNode).find("> .dropdown-menu .active")[0];i&&o.default(i).removeClass("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!1)}if(o.default(e).addClass("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!0),c.reflow(e),e.classList.contains("fade")&&e.classList.add("show"),e.parentNode&&o.default(e.parentNode).hasClass("dropdown-menu")){var r=o.default(e).closest(".dropdown")[0];if(r){var a=[].slice.call(r.querySelectorAll(".dropdown-toggle"));o.default(a).addClass("active")}e.setAttribute("aria-expanded",!0)}n&&n()},e._jQueryInterface=function(t){return this.each((function(){var n=o.default(this),i=n.data("bs.tab");if(i||(i=new e(this),n.data("bs.tab",i)),"string"==typeof t){if(void 0===i[t])throw new TypeError('No method named "'+t+'"');i[t]()}}))},s(e,null,[{key:"VERSION",get:function(){return"4.5.3"}}]),e}();o.default(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',(function(e){e.preventDefault(),le._jQueryInterface.call(o.default(this),"show")})),o.default.fn.tab=le._jQueryInterface,o.default.fn.tab.Constructor=le,o.default.fn.tab.noConflict=function(){return o.default.fn.tab=se,le._jQueryInterface};var ce=o.default.fn.toast,ue={animation:"boolean",autohide:"boolean",delay:"number"},de={animation:!0,autohide:!0,delay:500},fe=function(){function e(e,t){this._element=e,this._config=this._getConfig(t),this._timeout=null,this._setListeners()}var t=e.prototype;return t.show=function(){var e=this,t=o.default.Event("show.bs.toast");if(o.default(this._element).trigger(t),!t.isDefaultPrevented()){this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");var n=function(){e._element.classList.remove("showing"),e._element.classList.add("show"),o.default(e._element).trigger("shown.bs.toast"),e._config.autohide&&(e._timeout=setTimeout((function(){e.hide()}),e._config.delay))};if(this._element.classList.remove("hide"),c.reflow(this._element),this._element.classList.add("showing"),this._config.animation){var i=c.getTransitionDurationFromElement(this._element);o.default(this._element).one(c.TRANSITION_END,n).emulateTransitionEnd(i)}else n()}},t.hide=function(){if(this._element.classList.contains("show")){var e=o.default.Event("hide.bs.toast");o.default(this._element).trigger(e),e.isDefaultPrevented()||this._close()}},t.dispose=function(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),o.default(this._element).off("click.dismiss.bs.toast"),o.default.removeData(this._element,"bs.toast"),this._element=null,this._config=null},t._getConfig=function(e){return e=l({},de,o.default(this._element).data(),"object"==typeof e&&e?e:{}),c.typeCheckConfig("toast",e,this.constructor.DefaultType),e},t._setListeners=function(){var e=this;o.default(this._element).on("click.dismiss.bs.toast",'[data-dismiss="toast"]',(function(){return e.hide()}))},t._close=function(){var e=this,t=function(){e._element.classList.add("hide"),o.default(e._element).trigger("hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){var n=c.getTransitionDurationFromElement(this._element);o.default(this._element).one(c.TRANSITION_END,t).emulateTransitionEnd(n)}else t()},t._clearTimeout=function(){clearTimeout(this._timeout),this._timeout=null},e._jQueryInterface=function(t){return this.each((function(){var n=o.default(this),i=n.data("bs.toast");if(i||(i=new e(this,"object"==typeof t&&t),n.data("bs.toast",i)),"string"==typeof t){if(void 0===i[t])throw new TypeError('No method named "'+t+'"');i[t](this)}}))},s(e,null,[{key:"VERSION",get:function(){return"4.5.3"}},{key:"DefaultType",get:function(){return ue}},{key:"Default",get:function(){return de}}]),e}();o.default.fn.toast=fe._jQueryInterface,o.default.fn.toast.Constructor=fe,o.default.fn.toast.noConflict=function(){return o.default.fn.toast=ce,fe._jQueryInterface},e.Alert=f,e.Button=p,e.Carousel=w,e.Collapse=T,e.Dropdown=M,e.Modal=j,e.Popover=te,e.Scrollspy=ae,e.Tab=le,e.Toast=fe,e.Tooltip=Y,e.Util=c,Object.defineProperty(e,"__esModule",{value:!0})})),e=this,t=function(){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,i=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:{}).getFn,i=void 0===n?T.getFn:n;t(this,e),this.norm=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:3,t=new Map;return{get:function(n){var i=n.match(S).length;if(t.has(i))return t.get(i);var o=parseFloat((1/Math.sqrt(i)).toFixed(e));return t.set(i,o),o},clear:function(){t.clear()}}}(3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return i(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,p(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();p(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t2&&void 0!==arguments[2]?arguments[2]:{}).getFn,i=void 0===n?T.getFn:n,o=new k({getFn:i});return o.setKeys(e.map(_)),o.setSources(t),o.create(),o}function A(e,t){var n=e.matches;t.matches=[],v(n)&&n.forEach((function(e){if(v(e.indices)&&e.indices.length){var n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.src),e.idx>-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function O(e,t){t.score=e.score}function M(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,i=void 0===n?0:n,o=t.currentLocation,r=void 0===o?0:o,a=t.expectedLocation,s=void 0===a?0:a,l=t.distance,c=void 0===l?T.distance:l,u=t.ignoreLocation,d=void 0===u?T.ignoreLocation:u,f=i/e.length;if(d)return f;var h=Math.abs(s-r);return c?f+h/c:h?1:f}function $(e){for(var t={},n=0,i=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},r=o.location,a=void 0===r?T.location:r,s=o.threshold,l=void 0===s?T.threshold:s,c=o.distance,u=void 0===c?T.distance:c,d=o.includeMatches,f=void 0===d?T.includeMatches:d,h=o.findAllMatches,p=void 0===h?T.findAllMatches:h,g=o.minMatchCharLength,m=void 0===g?T.minMatchCharLength:g,v=o.isCaseSensitive,b=void 0===v?T.isCaseSensitive:v,y=o.ignoreLocation,w=void 0===y?T.ignoreLocation:y;if(t(this,e),this.options={location:a,threshold:l,distance:u,includeMatches:f,findAllMatches:p,minMatchCharLength:m,isCaseSensitive:b,ignoreLocation:w},this.pattern=b?n:n.toLowerCase(),this.chunks=[],this.pattern.length){var x=function(e,t){i.chunks.push({pattern:e,alphabet:$(e),startIndex:t})},_=this.pattern.length;if(_>32){for(var E=0,C=_%32,S=_-C;E3&&void 0!==arguments[3]?arguments[3]:{},o=i.location,r=void 0===o?T.location:o,a=i.distance,s=void 0===a?T.distance:a,l=i.threshold,c=void 0===l?T.threshold:l,u=i.findAllMatches,d=void 0===u?T.findAllMatches:u,f=i.minMatchCharLength,h=void 0===f?T.minMatchCharLength:f,p=i.includeMatches,g=void 0===p?T.includeMatches:p,m=i.ignoreLocation,v=void 0===m?T.ignoreLocation:m;if(t.length>32)throw new Error(function(e){return"Pattern length exceeds max of ".concat(e,".")}(32));for(var b,y=t.length,w=e.length,x=Math.max(0,Math.min(r,w)),_=c,E=x,C=h>1||g,S=C?Array(w):[];(b=e.indexOf(t,E))>-1;){var k=M(t,{currentLocation:b,expectedLocation:x,distance:s,ignoreLocation:v});if(_=Math.min(k,_),E=b+y,C)for(var N=0;N=P;B-=1){var H=B-1,q=n[e.charAt(H)];if(C&&(S[H]=+!!q),F[B]=(F[B+1]<<1|1)&q,L&&(F[B]|=(A[B+1]|A[B])<<1|1|A[B+1]),F[B]&D&&(O=M(t,{errors:L,currentLocation:H,expectedLocation:x,distance:s,ignoreLocation:v}))<=_){if(_=O,(E=H)<=x)break;P=Math.max(1,2*x-E)}}if(M(t,{errors:L+1,currentLocation:x,expectedLocation:x,distance:s,ignoreLocation:v})>_)break;A=F}var z={isMatch:E>=0,score:Math.max(.001,O)};if(C){var W=function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:T.minMatchCharLength,n=[],i=-1,o=-1,r=0,a=e.length;r=t&&n.push([i,o]),i=-1)}return e[r-1]&&r-i>=t&&n.push([i,r-1]),n}(S,h);W.length?g&&(z.indices=W):z.isMatch=!1}return z}(e,n,o,{location:a+r,distance:s,threshold:l,findAllMatches:c,minMatchCharLength:u,includeMatches:i,ignoreLocation:f}),v=m.isMatch,b=m.score,y=m.indices;v&&(g=!0),p+=b,v&&y&&(h=[].concat(d(h),d(y)))}));var m={isMatch:g,score:g?p/this.chunks.length:1};return g&&i&&(m.indices=h),m}}]),e}(),L=function(){function e(n){t(this,e),this.pattern=n}return i(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return j(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return j(e,this.singleRegex)}}]),e}();function j(e,t){var n=e.match(t);return n?n[1]:null}var I=function(e){s(o,e);var n=u(o);function o(e){return t(this,o),n.call(this,e)}return i(o,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),o}(L),P=function(e){s(o,e);var n=u(o);function o(e){return t(this,o),n.call(this,e)}return i(o,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),o}(L),R=function(e){s(o,e);var n=u(o);function o(e){return t(this,o),n.call(this,e)}return i(o,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),o}(L),F=function(e){s(o,e);var n=u(o);function o(e){return t(this,o),n.call(this,e)}return i(o,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),o}(L),B=function(e){s(o,e);var n=u(o);function o(e){return t(this,o),n.call(this,e)}return i(o,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),o}(L),H=function(e){s(o,e);var n=u(o);function o(e){return t(this,o),n.call(this,e)}return i(o,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),o}(L),q=function(e){s(o,e);var n=u(o);function o(e){var i,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},a=r.location,s=void 0===a?T.location:a,l=r.threshold,c=void 0===l?T.threshold:l,u=r.distance,d=void 0===u?T.distance:u,f=r.includeMatches,h=void 0===f?T.includeMatches:f,p=r.findAllMatches,g=void 0===p?T.findAllMatches:p,m=r.minMatchCharLength,v=void 0===m?T.minMatchCharLength:m,b=r.isCaseSensitive,y=void 0===b?T.isCaseSensitive:b;return t(this,o),(i=n.call(this,e))._bitapSearch=new D(e,{location:s,threshold:c,distance:d,includeMatches:h,findAllMatches:g,minMatchCharLength:v,isCaseSensitive:y}),i}return i(o,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),o}(L),z=function(e){s(o,e);var n=u(o);function o(e){return t(this,o),n.call(this,e)}return i(o,[{key:"search",value:function(e){for(var t,n=0,i=[],o=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+o,i.push([t,n-1]);var r=!!i.length;return{isMatch:r,score:r?1:0,indices:i}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),o}(L),W=[I,z,R,F,H,B,P,q],U=W.length,X=/ +(?=([^\"]*\"[^\"]*\")*[^\"]*$)/;var V=new Set([q.type,z.type]),Y=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=i.isCaseSensitive,r=void 0===o?T.isCaseSensitive:o,a=i.includeMatches,s=void 0===a?T.includeMatches:a,l=i.minMatchCharLength,c=void 0===l?T.minMatchCharLength:l,u=i.findAllMatches,d=void 0===u?T.findAllMatches:u,f=i.location,h=void 0===f?T.location:f,p=i.threshold,g=void 0===p?T.threshold:p,m=i.distance,v=void 0===m?T.distance:m;t(this,e),this.query=null,this.options={isCaseSensitive:r,includeMatches:s,minMatchCharLength:c,findAllMatches:d,location:h,threshold:g,distance:v},this.pattern=r?n:n.toLowerCase(),this.query=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(X).filter((function(e){return e&&!!e.trim()})),i=[],o=0,r=n.length;o1&&void 0!==arguments[1]?arguments[1]:{},o=arguments.length>2?arguments[2]:void 0;t(this,e),this.options=a({},T,{},i),this.options.useExtendedSearch,this._keyStore=new x(this.options.keys),this.setCollection(n,o)}return i(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof k))throw new Error("Incorrect 'index' type");this._myIndex=t||N(this.options.keys,this._docs,{getFn:this.options.getFn})}},{key:"add",value:function(e){v(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,i=this._docs.length;n1&&void 0!==arguments[1]?arguments[1]:{}).limit,n=void 0===t?-1:t,i=this.options,o=i.includeMatches,r=i.includeScore,a=i.shouldSort,s=i.sortFn,l=i.ignoreFieldNorm,c=p(e)?p(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return function(e,t){var n=t.ignoreFieldNorm,i=void 0===n?T.ignoreFieldNorm:n;e.forEach((function(e){var t=1;e.matches.forEach((function(e){var n=e.key,o=e.norm,r=e.score,a=n?n.weight:null;t*=Math.pow(0===r&&a?Number.EPSILON:r,(a||1)*(i?1:o))})),e.score=t}))}(c,{ignoreFieldNorm:l}),a&&c.sort(s),g(n)&&n>-1&&(c=c.slice(0,n)),function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=n.includeMatches,o=void 0===i?T.includeMatches:i,r=n.includeScore,a=void 0===r?T.includeScore:r,s=[];return o&&s.push(A),a&&s.push(O),e.map((function(e){var n=e.idx,i={item:t[n],refIndex:n};return s.length&&s.forEach((function(t){t(e,i)})),i}))}(c,this._docs,{includeMatches:o,includeScore:r})}},{key:"_searchStringList",value:function(e){var t=Q(e,this.options),n=this._myIndex.records,i=[];return n.forEach((function(e){var n=e.v,o=e.i,r=e.n;if(v(n)){var a=t.searchIn(n),s=a.isMatch,l=a.score,c=a.indices;s&&i.push({item:n,idx:o,matches:[{score:l,value:n,norm:r,indices:c}]})}})),i}},{key:"_searchLogical",value:function(e){var t=this,n=function(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).auto,i=void 0===n||n;return J(e)||(e=ee(e)),function e(n){var o=Object.keys(n),r=function(e){return!!e[Z]}(n);if(!r&&o.length>1&&!J(n))return e(ee(n));if(function(e){return!h(e)&&m(e)&&!J(e)}(n)){var a=r?n[Z]:o[0],s=r?n.$val:n[a];if(!p(s))throw new Error(function(e){return"Invalid value for key ".concat(e)}(a));var l={keyId:C(a),pattern:s};return i&&(l.searcher=Q(s,t)),l}var c={children:[],operator:o[0]};return o.forEach((function(t){var i=n[t];h(i)&&i.forEach((function(t){c.children.push(e(t))}))})),c}(e)}(e,this.options),i=this._myIndex.records,o={},r=[];return i.forEach((function(e){var i=e.$,a=e.i;if(v(i)){var s=function e(n,i,o){if(!n.children){var r=n.keyId,a=n.searcher,s=t._findMatches({key:t._keyStore.get(r),value:t._myIndex.getValueForItemAtKeyId(i,r),searcher:a});return s&&s.length?[{idx:o,item:i,matches:s}]:[]}switch(n.operator){case G:for(var l=[],c=0,u=n.children.length;c1&&void 0!==arguments[1]?arguments[1]:{}).getFn,n=void 0===t?T.getFn:t,i=e.keys,o=e.records,r=new k({getFn:n});return r.setKeys(i),r.setIndexRecords(o),r},te.config=T,function(){K.push.apply(K,arguments)}(Y),te},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Fuse=t(),function(e,t,n,i){"use strict";function o(e,t){var i,o,r,a=[],s=0;e&&e.isDefaultPrevented()||(e.preventDefault(),t=t||{},e&&e.data&&(t=h(e.data.options,t)),i=t.$target||n(e.currentTarget).trigger("blur"),(r=n.fancybox.getInstance())&&r.$trigger&&r.$trigger.is(i)||(t.selector?a=n(t.selector):(o=i.attr("data-fancybox")||"")?a=(a=e.data?e.data.items:[]).length?a.filter('[data-fancybox="'+o+'"]'):n('[data-fancybox="'+o+'"]'):a=[i],(s=n(a).index(i))<0&&(s=0),(r=n.fancybox.open(a,t,s)).$trigger=i))}if(e.console=e.console||{info:function(e){}},n){if(n.fn.fancybox)return void console.info("fancyBox already initialized");var r={closeExisting:!1,loop:!1,gutter:50,keyboard:!0,preventCaptionOverlap:!0,arrows:!0,infobar:!0,smallBtn:"auto",toolbar:"auto",buttons:["zoom","slideShow","thumbs","close"],idleTime:3,protect:!1,modal:!1,image:{preload:!1},ajax:{settings:{data:{fancybox:!0}}},iframe:{tpl:'',preload:!0,css:{},attr:{scrolling:"auto"}},video:{tpl:'',format:"",autoStart:!0},defaultType:"image",animationEffect:"zoom",animationDuration:366,zoomOpacity:"auto",transitionEffect:"fade",transitionDuration:366,slideClass:"",baseClass:"",baseTpl:'',spinnerTpl:'
    ',errorTpl:'

    {{ERROR}}

    ',btnTpl:{download:'',zoom:'',close:'',arrowLeft:'',arrowRight:'',smallBtn:''},parentEl:"body",hideScrollbar:!0,autoFocus:!0,backFocus:!0,trapFocus:!0,fullScreen:{autoStart:!1},touch:{vertical:!0,momentum:!0},hash:null,media:{},slideShow:{autoStart:!1,speed:3e3},thumbs:{autoStart:!1,hideOnClose:!0,parentEl:".fancybox-container",axis:"y"},wheel:"auto",onInit:n.noop,beforeLoad:n.noop,afterLoad:n.noop,beforeShow:n.noop,afterShow:n.noop,beforeClose:n.noop,afterClose:n.noop,onActivate:n.noop,onDeactivate:n.noop,clickContent:function(e,t){return"image"===e.type&&"zoom"},clickSlide:"close",clickOutside:"close",dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1,mobile:{preventCaptionOverlap:!1,idleTime:!1,clickContent:function(e,t){return"image"===e.type&&"toggleControls"},clickSlide:function(e,t){return"image"===e.type?"toggleControls":"close"},dblclickContent:function(e,t){return"image"===e.type&&"zoom"},dblclickSlide:function(e,t){return"image"===e.type&&"zoom"}},lang:"en",i18n:{en:{CLOSE:"Close",NEXT:"Next",PREV:"Previous",ERROR:"The requested content cannot be loaded.
    Please try again later.",PLAY_START:"Start slideshow",PLAY_STOP:"Pause slideshow",FULL_SCREEN:"Full screen",THUMBS:"Thumbnails",DOWNLOAD:"Download",SHARE:"Share",ZOOM:"Zoom"},de:{CLOSE:"Schließen",NEXT:"Weiter",PREV:"Zurück",ERROR:"Die angeforderten Daten konnten nicht geladen werden.
    Bitte versuchen Sie es später nochmal.",PLAY_START:"Diaschau starten",PLAY_STOP:"Diaschau beenden",FULL_SCREEN:"Vollbild",THUMBS:"Vorschaubilder",DOWNLOAD:"Herunterladen",SHARE:"Teilen",ZOOM:"Vergrößern"}}},a=n(e),s=n(t),l=0,c=e.requestAnimationFrame||e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||function(t){return e.setTimeout(t,1e3/60)},u=e.cancelAnimationFrame||e.webkitCancelAnimationFrame||e.mozCancelAnimationFrame||e.oCancelAnimationFrame||function(t){e.clearTimeout(t)},d=function(){var e,n=t.createElement("fakeelement"),i={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(e in i)if(void 0!==n.style[e])return i[e];return"transitionend"}(),f=function(e){return e&&e.length&&e[0].offsetHeight},h=function(e,t){var i=n.extend(!0,{},e,t);return n.each(t,(function(e,t){n.isArray(t)&&(i[e]=t)})),i},p=function(e){var i,o;return!(!e||e.ownerDocument!==t)&&(n(".fancybox-container").css("pointer-events","none"),i={x:e.getBoundingClientRect().left+e.offsetWidth/2,y:e.getBoundingClientRect().top+e.offsetHeight/2},o=t.elementFromPoint(i.x,i.y)===e,n(".fancybox-container").css("pointer-events",""),o)},g=function(e,t,i){var o=this;o.opts=h({index:i},n.fancybox.defaults),n.isPlainObject(t)&&(o.opts=h(o.opts,t)),n.fancybox.isMobile&&(o.opts=h(o.opts,o.opts.mobile)),o.id=o.opts.id||++l,o.currIndex=parseInt(o.opts.index,10)||0,o.prevIndex=null,o.prevPos=null,o.currPos=0,o.firstRun=!0,o.group=[],o.slides={},o.addContent(e),o.group.length&&o.init()};n.extend(g.prototype,{init:function(){var i,o,r=this,a=r.group[r.currIndex].opts;a.closeExisting&&n.fancybox.close(!0),n("body").addClass("fancybox-active"),!n.fancybox.getInstance()&&!1!==a.hideScrollbar&&!n.fancybox.isMobile&&t.body.scrollHeight>e.innerHeight&&(n("head").append('"),n("body").addClass("compensate-for-scrollbar")),o="",n.each(a.buttons,(function(e,t){o+=a.btnTpl[t]||""})),i=n(r.translate(r,a.baseTpl.replace("{{buttons}}",o).replace("{{arrows}}",a.btnTpl.arrowLeft+a.btnTpl.arrowRight))).attr("id","fancybox-container-"+r.id).addClass(a.baseClass).data("FancyBox",r).appendTo(a.parentEl),r.$refs={container:i},["bg","inner","infobar","toolbar","stage","caption","navigation"].forEach((function(e){r.$refs[e]=i.find(".fancybox-"+e)})),r.trigger("onInit"),r.activate(),r.jumpTo(r.currIndex)},translate:function(e,t){var n=e.opts.i18n[e.opts.lang]||e.opts.i18n.en;return t.replace(/\{\{(\w+)\}\}/g,(function(e,t){return void 0===n[t]?e:n[t]}))},addContent:function(e){var t,i=this,o=n.makeArray(e);n.each(o,(function(e,t){var o,r,a,s,l,c={},u={};n.isPlainObject(t)?(c=t,u=t.opts||t):"object"===n.type(t)&&n(t).length?(u=(o=n(t)).data()||{},(u=n.extend(!0,{},u,u.options)).$orig=o,c.src=i.opts.src||u.src||o.attr("href"),c.type||c.src||(c.type="inline",c.src=t)):c={type:"html",src:t+""},c.opts=n.extend(!0,{},i.opts,u),n.isArray(u.buttons)&&(c.opts.buttons=u.buttons),n.fancybox.isMobile&&c.opts.mobile&&(c.opts=h(c.opts,c.opts.mobile)),r=c.type||c.opts.type,s=c.src||"",!r&&s&&((a=s.match(/\.(mp4|mov|ogv|webm)((\?|#).*)?$/i))?(r="video",c.opts.video.format||(c.opts.video.format="video/"+("ogv"===a[1]?"ogg":a[1]))):s.match(/(^data:image\/[a-z0-9+\/=]*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg|ico)((\?|#).*)?$)/i)?r="image":s.match(/\.(pdf)((\?|#).*)?$/i)?(r="iframe",c=n.extend(!0,c,{contentType:"pdf",opts:{iframe:{preload:!1}}})):"#"===s.charAt(0)&&(r="inline")),r?c.type=r:i.trigger("objectNeedsType",c),c.contentType||(c.contentType=n.inArray(c.type,["html","inline","ajax"])>-1?"html":c.type),c.index=i.group.length,"auto"==c.opts.smallBtn&&(c.opts.smallBtn=n.inArray(c.type,["html","inline","ajax"])>-1),"auto"===c.opts.toolbar&&(c.opts.toolbar=!c.opts.smallBtn),c.$thumb=c.opts.$thumb||null,c.opts.$trigger&&c.index===i.opts.index&&(c.$thumb=c.opts.$trigger.find("img:first"),c.$thumb.length&&(c.opts.$orig=c.opts.$trigger)),c.$thumb&&c.$thumb.length||!c.opts.$orig||(c.$thumb=c.opts.$orig.find("img:first")),c.$thumb&&!c.$thumb.length&&(c.$thumb=null),c.thumb=c.opts.thumb||(c.$thumb?c.$thumb[0].src:null),"function"===n.type(c.opts.caption)&&(c.opts.caption=c.opts.caption.apply(t,[i,c])),"function"===n.type(i.opts.caption)&&(c.opts.caption=i.opts.caption.apply(t,[i,c])),c.opts.caption instanceof n||(c.opts.caption=void 0===c.opts.caption?"":c.opts.caption+""),"ajax"===c.type&&((l=s.split(/\s+/,2)).length>1&&(c.src=l.shift(),c.opts.filter=l.shift())),c.opts.modal&&(c.opts=n.extend(!0,c.opts,{trapFocus:!0,infobar:0,toolbar:0,smallBtn:0,keyboard:0,slideShow:0,fullScreen:0,thumbs:0,touch:0,clickContent:!1,clickSlide:!1,clickOutside:!1,dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1})),i.group.push(c)})),Object.keys(i.slides).length&&(i.updateControls(),(t=i.Thumbs)&&t.isActive&&(t.create(),t.focus()))},addEvents:function(){var t=this;t.removeEvents(),t.$refs.container.on("click.fb-close","[data-fancybox-close]",(function(e){e.stopPropagation(),e.preventDefault(),t.close(e)})).on("touchstart.fb-prev click.fb-prev","[data-fancybox-prev]",(function(e){e.stopPropagation(),e.preventDefault(),t.previous()})).on("touchstart.fb-next click.fb-next","[data-fancybox-next]",(function(e){e.stopPropagation(),e.preventDefault(),t.next()})).on("click.fb","[data-fancybox-zoom]",(function(e){t[t.isScaledDown()?"scaleToActual":"scaleToFit"]()})),a.on("orientationchange.fb resize.fb",(function(e){e&&e.originalEvent&&"resize"===e.originalEvent.type?(t.requestId&&u(t.requestId),t.requestId=c((function(){t.update(e)}))):(t.current&&"iframe"===t.current.type&&t.$refs.stage.hide(),setTimeout((function(){t.$refs.stage.show(),t.update(e)}),n.fancybox.isMobile?600:250))})),s.on("keydown.fb",(function(e){var i=(n.fancybox?n.fancybox.getInstance():null).current,o=e.keyCode||e.which;if(9!=o)return!i.opts.keyboard||e.ctrlKey||e.altKey||e.shiftKey||n(e.target).is("input,textarea,video,audio,select")?void 0:8===o||27===o?(e.preventDefault(),void t.close(e)):37===o||38===o?(e.preventDefault(),void t.previous()):39===o||40===o?(e.preventDefault(),void t.next()):void t.trigger("afterKeydown",e,o);i.opts.trapFocus&&t.focus(e)})),t.group[t.currIndex].opts.idleTime&&(t.idleSecondsCounter=0,s.on("mousemove.fb-idle mouseleave.fb-idle mousedown.fb-idle touchstart.fb-idle touchmove.fb-idle scroll.fb-idle keydown.fb-idle",(function(e){t.idleSecondsCounter=0,t.isIdle&&t.showControls(),t.isIdle=!1})),t.idleInterval=e.setInterval((function(){++t.idleSecondsCounter>=t.group[t.currIndex].opts.idleTime&&!t.isDragging&&(t.isIdle=!0,t.idleSecondsCounter=0,t.hideControls())}),1e3))},removeEvents:function(){var t=this;a.off("orientationchange.fb resize.fb"),s.off("keydown.fb .fb-idle"),this.$refs.container.off(".fb-close .fb-prev .fb-next"),t.idleInterval&&(e.clearInterval(t.idleInterval),t.idleInterval=null)},previous:function(e){return this.jumpTo(this.currPos-1,e)},next:function(e){return this.jumpTo(this.currPos+1,e)},jumpTo:function(e,t){var i,o,r,a,s,l,c,u,d,h=this,p=h.group.length;if(!(h.isDragging||h.isClosing||h.isAnimating&&h.firstRun)){if(e=parseInt(e,10),!(r=h.current?h.current.opts.loop:h.opts.loop)&&(e<0||e>=p))return!1;if(i=h.firstRun=!Object.keys(h.slides).length,s=h.current,h.prevIndex=h.currIndex,h.prevPos=h.currPos,a=h.createSlide(e),p>1&&((r||a.index0)&&h.createSlide(e-1)),h.current=a,h.currIndex=a.index,h.currPos=a.pos,h.trigger("beforeShow",i),h.updateControls(),a.forcedDuration=void 0,n.isNumeric(t)?a.forcedDuration=t:t=a.opts[i?"animationDuration":"transitionDuration"],t=parseInt(t,10),o=h.isMoved(a),a.$slide.addClass("fancybox-slide--current"),i)return a.opts.animationEffect&&t&&h.$refs.container.css("transition-duration",t+"ms"),h.$refs.container.addClass("fancybox-is-open").trigger("focus"),h.loadSlide(a),void h.preload("image");l=n.fancybox.getTranslate(s.$slide),c=n.fancybox.getTranslate(h.$refs.stage),n.each(h.slides,(function(e,t){n.fancybox.stop(t.$slide,!0)})),s.pos!==a.pos&&(s.isComplete=!1),s.$slide.removeClass("fancybox-slide--complete fancybox-slide--current"),o?(d=l.left-(s.pos*l.width+s.pos*s.opts.gutter),n.each(h.slides,(function(e,i){i.$slide.removeClass("fancybox-animated").removeClass((function(e,t){return(t.match(/(^|\s)fancybox-fx-\S+/g)||[]).join(" ")}));var o=i.pos*l.width+i.pos*i.opts.gutter;n.fancybox.setTranslate(i.$slide,{top:0,left:o-c.left+d}),i.pos!==a.pos&&i.$slide.addClass("fancybox-slide--"+(i.pos>a.pos?"next":"previous")),f(i.$slide),n.fancybox.animate(i.$slide,{top:0,left:(i.pos-a.pos)*l.width+(i.pos-a.pos)*i.opts.gutter},t,(function(){i.$slide.css({transform:"",opacity:""}).removeClass("fancybox-slide--next fancybox-slide--previous"),i.pos===h.currPos&&h.complete()}))}))):t&&a.opts.transitionEffect&&(u="fancybox-animated fancybox-fx-"+a.opts.transitionEffect,s.$slide.addClass("fancybox-slide--"+(s.pos>a.pos?"next":"previous")),n.fancybox.animate(s.$slide,u,t,(function(){s.$slide.removeClass(u).removeClass("fancybox-slide--next fancybox-slide--previous")}),!1)),a.isLoaded?h.revealContent(a):h.loadSlide(a),h.preload("image")}},createSlide:function(e){var t,i,o=this;return i=(i=e%o.group.length)<0?o.group.length+i:i,!o.slides[e]&&o.group[i]&&(t=n('
    ').appendTo(o.$refs.stage),o.slides[e]=n.extend(!0,{},o.group[i],{pos:e,$slide:t,isLoaded:!1}),o.updateSlide(o.slides[e])),o.slides[e]},scaleToActual:function(e,t,i){var o,r,a,s,l,c=this,u=c.current,d=u.$content,f=n.fancybox.getTranslate(u.$slide).width,h=n.fancybox.getTranslate(u.$slide).height,p=u.width,g=u.height;c.isAnimating||c.isMoved()||!d||"image"!=u.type||!u.isLoaded||u.hasError||(c.isAnimating=!0,n.fancybox.stop(d),e=void 0===e?.5*f:e,t=void 0===t?.5*h:t,(o=n.fancybox.getTranslate(d)).top-=n.fancybox.getTranslate(u.$slide).top,o.left-=n.fancybox.getTranslate(u.$slide).left,s=p/o.width,l=g/o.height,r=.5*f-.5*p,a=.5*h-.5*g,p>f&&((r=o.left*s-(e*s-e))>0&&(r=0),rh&&((a=o.top*l-(t*l-t))>0&&(a=0),at-.5&&(l=t),(c*=o)>i-.5&&(c=i),"image"===e.type?(u.top=Math.floor(.5*(i-c))+parseFloat(s.css("paddingTop")),u.left=Math.floor(.5*(t-l))+parseFloat(s.css("paddingLeft"))):"video"===e.contentType&&(c>l/(r=e.opts.width&&e.opts.height?l/c:e.opts.ratio||16/9)?c=l/r:l>c*r&&(l=c*r)),u.width=l,u.height=c,u)},update:function(e){var t=this;n.each(t.slides,(function(n,i){t.updateSlide(i,e)}))},updateSlide:function(e,t){var i=this,o=e&&e.$content,r=e.width||e.opts.width,a=e.height||e.opts.height,s=e.$slide;i.adjustCaption(e),o&&(r||a||"video"===e.contentType)&&!e.hasError&&(n.fancybox.stop(o),n.fancybox.setTranslate(o,i.getFitPos(e)),e.pos===i.currPos&&(i.isAnimating=!1,i.updateCursor())),i.adjustLayout(e),s.length&&(s.trigger("refresh"),e.pos===i.currPos&&i.$refs.toolbar.add(i.$refs.navigation.find(".fancybox-button--arrow_right")).toggleClass("compensate-for-scrollbar",s.get(0).scrollHeight>s.get(0).clientHeight)),i.trigger("onUpdate",e,t)},centerSlide:function(e){var t=this,i=t.current,o=i.$slide;!t.isClosing&&i&&(o.siblings().css({transform:"",opacity:""}),o.parent().children().removeClass("fancybox-slide--previous fancybox-slide--next"),n.fancybox.animate(o,{top:0,left:0,opacity:1},void 0===e?0:e,(function(){o.css({transform:"",opacity:""}),i.isComplete||t.complete()}),!1))},isMoved:function(e){var t,i,o=e||this.current;return!!o&&(i=n.fancybox.getTranslate(this.$refs.stage),t=n.fancybox.getTranslate(o.$slide),!o.$slide.hasClass("fancybox-animated")&&(Math.abs(t.top-i.top)>.5||Math.abs(t.left-i.left)>.5))},updateCursor:function(e,t){var i,o,r=this,a=r.current,s=r.$refs.container;a&&!r.isClosing&&r.Guestures&&(s.removeClass("fancybox-is-zoomable fancybox-can-zoomIn fancybox-can-zoomOut fancybox-can-swipe fancybox-can-pan"),o=!!(i=r.canPan(e,t))||r.isZoomable(),s.toggleClass("fancybox-is-zoomable",o),n("[data-fancybox-zoom]").prop("disabled",!o),i?s.addClass("fancybox-can-pan"):o&&("zoom"===a.opts.clickContent||n.isFunction(a.opts.clickContent)&&"zoom"==a.opts.clickContent(a))?s.addClass("fancybox-can-zoomIn"):a.opts.touch&&(a.opts.touch.vertical||r.group.length>1)&&"video"!==a.contentType&&s.addClass("fancybox-can-swipe"))},isZoomable:function(){var e,t=this,n=t.current;if(n&&!t.isClosing&&"image"===n.type&&!n.hasError){if(!n.isLoaded)return!0;if((e=t.getFitPos(n))&&(n.width>e.width||n.height>e.height))return!0}return!1},isScaledDown:function(e,t){var i=!1,o=this.current,r=o.$content;return void 0!==e&&void 0!==t?i=e1.5||Math.abs(o.height-r.height)>1.5)),r},loadSlide:function(e){var t,i,o,r=this;if(!e.isLoading&&!e.isLoaded){if(e.isLoading=!0,!1===r.trigger("beforeLoad",e))return e.isLoading=!1,!1;switch(t=e.type,(i=e.$slide).off("refresh").trigger("onReset").addClass(e.opts.slideClass),t){case"image":r.setImage(e);break;case"iframe":r.setIframe(e);break;case"html":r.setContent(e,e.src||e.content);break;case"video":r.setContent(e,e.opts.video.tpl.replace(/\{\{src\}\}/gi,e.src).replace("{{format}}",e.opts.videoFormat||e.opts.video.format||"").replace("{{poster}}",e.thumb||""));break;case"inline":n(e.src).length?r.setContent(e,n(e.src)):r.setError(e);break;case"ajax":r.showLoading(e),o=n.ajax(n.extend({},e.opts.ajax.settings,{url:e.src,success:function(t,n){"success"===n&&r.setContent(e,t)},error:function(t,n){t&&"abort"!==n&&r.setError(e)}})),i.one("onReset",(function(){o.abort()}));break;default:r.setError(e)}return!0}},setImage:function(e){var i,o=this;setTimeout((function(){var t=e.$image;o.isClosing||!e.isLoading||t&&t.length&&t[0].complete||e.hasError||o.showLoading(e)}),50),o.checkSrcset(e),e.$content=n('
    ').addClass("fancybox-is-hidden").appendTo(e.$slide.addClass("fancybox-slide--image")),!1!==e.opts.preload&&e.opts.width&&e.opts.height&&e.thumb&&(e.width=e.opts.width,e.height=e.opts.height,(i=t.createElement("img")).onerror=function(){n(this).remove(),e.$ghost=null},i.onload=function(){o.afterLoad(e)},e.$ghost=n(i).addClass("fancybox-image").appendTo(e.$content).attr("src",e.thumb)),o.setBigImage(e)},checkSrcset:function(t){var n,i,o,r,a=t.opts.srcset||t.opts.image.srcset;if(a){o=e.devicePixelRatio||1,r=e.innerWidth*o,i=a.split(",").map((function(e){var t={};return e.trim().split(/\s+/).forEach((function(e,n){var i=parseInt(e.substring(0,e.length-1),10);if(0===n)return t.url=e;i&&(t.value=i,t.postfix=e[e.length-1])})),t})),i.sort((function(e,t){return e.value-t.value}));for(var s=0;s=r||"x"===l.postfix&&l.value>=o){n=l;break}}!n&&i.length&&(n=i[i.length-1]),n&&(t.src=n.url,t.width&&t.height&&"w"==n.postfix&&(t.height=t.width/t.height*n.value,t.width=n.value),t.opts.srcset=a)}},setBigImage:function(e){var i=this,o=t.createElement("img"),r=n(o);e.$image=r.one("error",(function(){i.setError(e)})).one("load",(function(){var t;e.$ghost||(i.resolveImageSlideSize(e,this.naturalWidth,this.naturalHeight),i.afterLoad(e)),i.isClosing||(e.opts.srcset&&((t=e.opts.sizes)&&"auto"!==t||(t=(e.width/e.height>1&&a.width()/a.height()>1?"100":Math.round(e.width/e.height*100))+"vw"),r.attr("sizes",t).attr("srcset",e.opts.srcset)),e.$ghost&&setTimeout((function(){e.$ghost&&!i.isClosing&&e.$ghost.hide()}),Math.min(300,Math.max(1e3,e.height/1600))),i.hideLoading(e))})).addClass("fancybox-image").attr("src",e.src).appendTo(e.$content),(o.complete||"complete"==o.readyState)&&r.naturalWidth&&r.naturalHeight?r.trigger("load"):o.error&&r.trigger("error")},resolveImageSlideSize:function(e,t,n){var i=parseInt(e.opts.width,10),o=parseInt(e.opts.height,10);e.width=t,e.height=n,i>0&&(e.width=i,e.height=Math.floor(i*n/t)),o>0&&(e.width=Math.floor(o*t/n),e.height=o)},setIframe:function(e){var t,i=this,o=e.opts.iframe,r=e.$slide;e.$content=n('
    ').css(o.css).appendTo(r),r.addClass("fancybox-slide--"+e.contentType),e.$iframe=t=n(o.tpl.replace(/\{rnd\}/g,(new Date).getTime())).attr(o.attr).appendTo(e.$content),o.preload?(i.showLoading(e),t.on("load.fb error.fb",(function(t){this.isReady=1,e.$slide.trigger("refresh"),i.afterLoad(e)})),r.on("refresh.fb",(function(){var n,i=e.$content,a=o.css.width,s=o.css.height;if(1===t[0].isReady){try{n=t.contents().find("body")}catch(e){}n&&n.length&&n.children().length&&(r.css("overflow","visible"),i.css({width:"100%","max-width":"100%",height:"9999px"}),void 0===a&&(a=Math.ceil(Math.max(n[0].clientWidth,n.outerWidth(!0)))),i.css("width",a||"").css("max-width",""),void 0===s&&(s=Math.ceil(Math.max(n[0].clientHeight,n.outerHeight(!0)))),i.css("height",s||""),r.css("overflow","auto")),i.removeClass("fancybox-is-hidden")}}))):i.afterLoad(e),t.attr("src",e.src),r.one("onReset",(function(){try{n(this).find("iframe").hide().unbind().attr("src","//about:blank")}catch(e){}n(this).off("refresh.fb").empty(),e.isLoaded=!1,e.isRevealed=!1}))},setContent:function(e,t){var i=this;i.isClosing||(i.hideLoading(e),e.$content&&n.fancybox.stop(e.$content),e.$slide.empty(),function(e){return e&&e.hasOwnProperty&&e instanceof n}(t)&&t.parent().length?((t.hasClass("fancybox-content")||t.parent().hasClass("fancybox-content"))&&t.parents(".fancybox-slide").trigger("onReset"),e.$placeholder=n("
    ").hide().insertAfter(t),t.css("display","inline-block")):e.hasError||("string"===n.type(t)&&(t=n("
    ").append(n.trim(t)).contents()),e.opts.filter&&(t=n("
    ").html(t).find(e.opts.filter))),e.$slide.one("onReset",(function(){n(this).find("video,audio").trigger("pause"),e.$placeholder&&(e.$placeholder.after(t.removeClass("fancybox-content").hide()).remove(),e.$placeholder=null),e.$smallBtn&&(e.$smallBtn.remove(),e.$smallBtn=null),e.hasError||(n(this).empty(),e.isLoaded=!1,e.isRevealed=!1)})),n(t).appendTo(e.$slide),n(t).is("video,audio")&&(n(t).addClass("fancybox-video"),n(t).wrap("
    "),e.contentType="video",e.opts.width=e.opts.width||n(t).attr("width"),e.opts.height=e.opts.height||n(t).attr("height")),e.$content=e.$slide.children().filter("div,form,main,video,audio,article,.fancybox-content").first(),e.$content.siblings().hide(),e.$content.length||(e.$content=e.$slide.wrapInner("
    ").children().first()),e.$content.addClass("fancybox-content"),e.$slide.addClass("fancybox-slide--"+e.contentType),i.afterLoad(e))},setError:function(e){e.hasError=!0,e.$slide.trigger("onReset").removeClass("fancybox-slide--"+e.contentType).addClass("fancybox-slide--error"),e.contentType="html",this.setContent(e,this.translate(e,e.opts.errorTpl)),e.pos===this.currPos&&(this.isAnimating=!1)},showLoading:function(e){var t=this;(e=e||t.current)&&!e.$spinner&&(e.$spinner=n(t.translate(t,t.opts.spinnerTpl)).appendTo(e.$slide).hide().fadeIn("fast"))},hideLoading:function(e){(e=e||this.current)&&e.$spinner&&(e.$spinner.stop().remove(),delete e.$spinner)},afterLoad:function(e){var t=this;t.isClosing||(e.isLoading=!1,e.isLoaded=!0,t.trigger("afterLoad",e),t.hideLoading(e),!e.opts.smallBtn||e.$smallBtn&&e.$smallBtn.length||(e.$smallBtn=n(t.translate(e,e.opts.btnTpl.smallBtn)).appendTo(e.$content)),e.opts.protect&&e.$content&&!e.hasError&&(e.$content.on("contextmenu.fb",(function(e){return 2==e.button&&e.preventDefault(),!0})),"image"===e.type&&n('
    ').appendTo(e.$content)),t.adjustCaption(e),t.adjustLayout(e),e.pos===t.currPos&&t.updateCursor(),t.revealContent(e))},adjustCaption:function(e){var t,n=this,i=e||n.current,o=i.opts.caption,r=i.opts.preventCaptionOverlap,a=n.$refs.caption,s=!1;a.toggleClass("fancybox-caption--separate",r),r&&o&&o.length&&(i.pos!==n.currPos?((t=a.clone().appendTo(a.parent())).children().eq(0).empty().html(o),s=t.outerHeight(!0),t.empty().remove()):n.$caption&&(s=n.$caption.outerHeight(!0)),i.$slide.css("padding-bottom",s||""))},adjustLayout:function(e){var t,n,i,o,r=e||this.current;r.isLoaded&&!0!==r.opts.disableLayoutFix&&(r.$content.css("margin-bottom",""),r.$content.outerHeight()>r.$slide.height()+.5&&(i=r.$slide[0].style["padding-bottom"],o=r.$slide.css("padding-bottom"),parseFloat(o)>0&&(t=r.$slide[0].scrollHeight,r.$slide.css("padding-bottom",0),Math.abs(t-r.$slide[0].scrollHeight)<1&&(n=o),r.$slide.css("padding-bottom",i))),r.$content.css("margin-bottom",n))},revealContent:function(e){var t,i,o,r,a=this,s=e.$slide,l=!1,c=!1,u=a.isMoved(e),d=e.isRevealed;return e.isRevealed=!0,t=e.opts[a.firstRun?"animationEffect":"transitionEffect"],o=e.opts[a.firstRun?"animationDuration":"transitionDuration"],o=parseInt(void 0===e.forcedDuration?o:e.forcedDuration,10),!u&&e.pos===a.currPos&&o||(t=!1),"zoom"===t&&(e.pos===a.currPos&&o&&"image"===e.type&&!e.hasError&&(c=a.getThumbPos(e))?l=a.getFitPos(e):t="fade"),"zoom"===t?(a.isAnimating=!0,l.scaleX=l.width/c.width,l.scaleY=l.height/c.height,"auto"==(r=e.opts.zoomOpacity)&&(r=Math.abs(e.width/e.height-c.width/c.height)>.1),r&&(c.opacity=.1,l.opacity=1),n.fancybox.setTranslate(e.$content.removeClass("fancybox-is-hidden"),c),f(e.$content),void n.fancybox.animate(e.$content,l,o,(function(){a.isAnimating=!1,a.complete()}))):(a.updateSlide(e),t?(n.fancybox.stop(s),i="fancybox-slide--"+(e.pos>=a.prevPos?"next":"previous")+" fancybox-animated fancybox-fx-"+t,s.addClass(i).removeClass("fancybox-slide--current"),e.$content.removeClass("fancybox-is-hidden"),f(s),"image"!==e.type&&e.$content.hide().show(0),void n.fancybox.animate(s,"fancybox-slide--current",o,(function(){s.removeClass(i).css({transform:"",opacity:""}),e.pos===a.currPos&&a.complete()}),!0)):(e.$content.removeClass("fancybox-is-hidden"),d||!u||"image"!==e.type||e.hasError||e.$content.hide().fadeIn("fast"),void(e.pos===a.currPos&&a.complete())))},getThumbPos:function(e){var t,i,o,r,a,s=!1,l=e.$thumb;return!(!l||!p(l[0]))&&(t=n.fancybox.getTranslate(l),i=parseFloat(l.css("border-top-width")||0),o=parseFloat(l.css("border-right-width")||0),r=parseFloat(l.css("border-bottom-width")||0),a=parseFloat(l.css("border-left-width")||0),s={top:t.top+i,left:t.left+a,width:t.width-o-a,height:t.height-i-r,scaleX:1,scaleY:1},t.width>0&&t.height>0&&s)},complete:function(){var e,t=this,i=t.current,o={};!t.isMoved()&&i.isLoaded&&(i.isComplete||(i.isComplete=!0,i.$slide.siblings().trigger("onReset"),t.preload("inline"),f(i.$slide),i.$slide.addClass("fancybox-slide--complete"),n.each(t.slides,(function(e,i){i.pos>=t.currPos-1&&i.pos<=t.currPos+1?o[i.pos]=i:i&&(n.fancybox.stop(i.$slide),i.$slide.off().remove())})),t.slides=o),t.isAnimating=!1,t.updateCursor(),t.trigger("afterShow"),i.opts.video.autoStart&&i.$slide.find("video,audio").filter(":visible:first").trigger("play").one("ended",(function(){Document.exitFullscreen?Document.exitFullscreen():this.webkitExitFullscreen&&this.webkitExitFullscreen(),t.next()})),i.opts.autoFocus&&"html"===i.contentType&&((e=i.$content.find("input[autofocus]:enabled:visible:first")).length?e.trigger("focus"):t.focus(null,!0)),i.$slide.scrollTop(0).scrollLeft(0))},preload:function(e){var t,n,i=this;i.group.length<2||(n=i.slides[i.currPos+1],(t=i.slides[i.currPos-1])&&t.type===e&&i.loadSlide(t),n&&n.type===e&&i.loadSlide(n))},focus:function(e,i){var o,r,a=this,s=["a[href]","area[href]",'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',"select:not([disabled]):not([aria-hidden])","textarea:not([disabled]):not([aria-hidden])","button:not([disabled]):not([aria-hidden])","iframe","object","embed","video","audio","[contenteditable]",'[tabindex]:not([tabindex^="-"])'].join(",");a.isClosing||((o=(o=!e&&a.current&&a.current.isComplete?a.current.$slide.find("*:visible"+(i?":not(.fancybox-close-small)":"")):a.$refs.container.find("*:visible")).filter(s).filter((function(){return"hidden"!==n(this).css("visibility")&&!n(this).hasClass("disabled")}))).length?(r=o.index(t.activeElement),e&&e.shiftKey?(r<0||0==r)&&(e.preventDefault(),o.eq(o.length-1).trigger("focus")):(r<0||r==o.length-1)&&(e&&e.preventDefault(),o.eq(0).trigger("focus"))):a.$refs.container.trigger("focus"))},activate:function(){var e=this;n(".fancybox-container").each((function(){var t=n(this).data("FancyBox");t&&t.id!==e.id&&!t.isClosing&&(t.trigger("onDeactivate"),t.removeEvents(),t.isVisible=!1)})),e.isVisible=!0,(e.current||e.isIdle)&&(e.update(),e.updateControls()),e.trigger("onActivate"),e.addEvents()},close:function(e,t){var i,o,r,a,s,l,u,d=this,h=d.current,p=function(){d.cleanUp(e)};return!(d.isClosing||(d.isClosing=!0,!1===d.trigger("beforeClose",e)?(d.isClosing=!1,c((function(){d.update()})),1):(d.removeEvents(),r=h.$content,i=h.opts.animationEffect,o=n.isNumeric(t)?t:i?h.opts.animationDuration:0,h.$slide.removeClass("fancybox-slide--complete fancybox-slide--next fancybox-slide--previous fancybox-animated"),!0!==e?n.fancybox.stop(h.$slide):i=!1,h.$slide.siblings().trigger("onReset").remove(),o&&d.$refs.container.removeClass("fancybox-is-open").addClass("fancybox-is-closing").css("transition-duration",o+"ms"),d.hideLoading(h),d.hideControls(!0),d.updateCursor(),"zoom"!==i||r&&o&&"image"===h.type&&!d.isMoved()&&!h.hasError&&(u=d.getThumbPos(h))||(i="fade"),"zoom"===i?(n.fancybox.stop(r),a=n.fancybox.getTranslate(r),l={top:a.top,left:a.left,scaleX:a.width/u.width,scaleY:a.height/u.height,width:u.width,height:u.height},s=h.opts.zoomOpacity,"auto"==s&&(s=Math.abs(h.width/h.height-u.width/u.height)>.1),s&&(u.opacity=0),n.fancybox.setTranslate(r,l),f(r),n.fancybox.animate(r,u,o,p),0):(i&&o?n.fancybox.animate(h.$slide.addClass("fancybox-slide--previous").removeClass("fancybox-slide--current"),"fancybox-animated fancybox-fx-"+i,o,p):!0===e?setTimeout(p,o):p(),0))))},cleanUp:function(t){var i,o,r,a=this,s=a.current.opts.$orig;a.current.$slide.trigger("onReset"),a.$refs.container.empty().remove(),a.trigger("afterClose",t),a.current.opts.backFocus&&(s&&s.length&&s.is(":visible")||(s=a.$trigger),s&&s.length&&(o=e.scrollX,r=e.scrollY,s.trigger("focus"),n("html, body").scrollTop(r).scrollLeft(o))),a.current=null,(i=n.fancybox.getInstance())?i.activate():(n("body").removeClass("fancybox-active compensate-for-scrollbar"),n("#fancybox-style-noscroll").remove())},trigger:function(e,t){var i,o=Array.prototype.slice.call(arguments,1),r=this,a=t&&t.opts?t:r.current;if(a?o.unshift(a):a=r,o.unshift(r),n.isFunction(a.opts[e])&&(i=a.opts[e].apply(a,o)),!1===i)return i;"afterClose"!==e&&r.$refs?r.$refs.container.trigger(e+".fb",o):s.trigger(e+".fb",o)},updateControls:function(){var e=this,i=e.current,o=i.index,r=e.$refs.container,a=e.$refs.caption,s=i.opts.caption;i.$slide.trigger("refresh"),s&&s.length?(e.$caption=a,a.children().eq(0).html(s)):e.$caption=null,e.hasHiddenControls||e.isIdle||e.showControls(),r.find("[data-fancybox-count]").html(e.group.length),r.find("[data-fancybox-index]").html(o+1),r.find("[data-fancybox-prev]").prop("disabled",!i.opts.loop&&o<=0),r.find("[data-fancybox-next]").prop("disabled",!i.opts.loop&&o>=e.group.length-1),"image"===i.type?r.find("[data-fancybox-zoom]").show().end().find("[data-fancybox-download]").attr("href",i.opts.image.src||i.src).show():i.opts.toolbar&&r.find("[data-fancybox-download],[data-fancybox-zoom]").hide(),n(t.activeElement).is(":hidden,[disabled]")&&e.$refs.container.trigger("focus")},hideControls:function(e){var t=["infobar","toolbar","nav"];!e&&this.current.opts.preventCaptionOverlap||t.push("caption"),this.$refs.container.removeClass(t.map((function(e){return"fancybox-show-"+e})).join(" ")),this.hasHiddenControls=!0},showControls:function(){var e=this,t=e.current?e.current.opts:e.opts,n=e.$refs.container;e.hasHiddenControls=!1,e.idleSecondsCounter=0,n.toggleClass("fancybox-show-toolbar",!(!t.toolbar||!t.buttons)).toggleClass("fancybox-show-infobar",!!(t.infobar&&e.group.length>1)).toggleClass("fancybox-show-caption",!!e.$caption).toggleClass("fancybox-show-nav",!!(t.arrows&&e.group.length>1)).toggleClass("fancybox-is-modal",!!t.modal)},toggleControls:function(){this.hasHiddenControls?this.showControls():this.hideControls()}}),n.fancybox={version:"3.5.7",defaults:r,getInstance:function(e){var t=n('.fancybox-container:not(".fancybox-is-closing"):last').data("FancyBox"),i=Array.prototype.slice.call(arguments,1);return t instanceof g&&("string"===n.type(e)?t[e].apply(t,i):"function"===n.type(e)&&e.apply(t,i),t)},open:function(e,t,n){return new g(e,t,n)},close:function(e){var t=this.getInstance();t&&(t.close(),!0===e&&this.close(e))},destroy:function(){this.close(!0),s.add("body").off("click.fb-start","**")},isMobile:/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),use3d:function(){var n=t.createElement("div");return e.getComputedStyle&&e.getComputedStyle(n)&&e.getComputedStyle(n).getPropertyValue("transform")&&!(t.documentMode&&t.documentMode<11)}(),getTranslate:function(e){var t;return!(!e||!e.length)&&{top:(t=e[0].getBoundingClientRect()).top||0,left:t.left||0,width:t.width,height:t.height,opacity:parseFloat(e.css("opacity"))}},setTranslate:function(e,t){var n="",i={};if(e&&t)return void 0===t.left&&void 0===t.top||(n=(void 0===t.left?e.position().left:t.left)+"px, "+(void 0===t.top?e.position().top:t.top)+"px",n=this.use3d?"translate3d("+n+", 0px)":"translate("+n+")"),void 0!==t.scaleX&&void 0!==t.scaleY?n+=" scale("+t.scaleX+", "+t.scaleY+")":void 0!==t.scaleX&&(n+=" scaleX("+t.scaleX+")"),n.length&&(i.transform=n),void 0!==t.opacity&&(i.opacity=t.opacity),void 0!==t.width&&(i.width=t.width),void 0!==t.height&&(i.height=t.height),e.css(i)},animate:function(e,t,i,o,r){var a,s=this;n.isFunction(i)&&(o=i,i=null),s.stop(e),a=s.getTranslate(e),e.on(d,(function(l){(!l||!l.originalEvent||e.is(l.originalEvent.target)&&"z-index"!=l.originalEvent.propertyName)&&(s.stop(e),n.isNumeric(i)&&e.css("transition-duration",""),n.isPlainObject(t)?void 0!==t.scaleX&&void 0!==t.scaleY&&s.setTranslate(e,{top:t.top,left:t.left,width:a.width*t.scaleX,height:a.height*t.scaleY,scaleX:1,scaleY:1}):!0!==r&&e.removeClass(t),n.isFunction(o)&&o(l))})),n.isNumeric(i)&&e.css("transition-duration",i+"ms"),n.isPlainObject(t)?(void 0!==t.scaleX&&void 0!==t.scaleY&&(delete t.width,delete t.height,e.parent().hasClass("fancybox-slide--image")&&e.parent().addClass("fancybox-is-scaling")),n.fancybox.setTranslate(e,t)):e.addClass(t),e.data("timer",setTimeout((function(){e.trigger(d)}),i+33))},stop:function(e,t){e&&e.length&&(clearTimeout(e.data("timer")),t&&e.trigger(d),e.off(d).css("transition-duration",""),e.parent().removeClass("fancybox-is-scaling"))}},n.fn.fancybox=function(e){var t;return(t=(e=e||{}).selector||!1)?n("body").off("click.fb-start",t).on("click.fb-start",t,{options:e},o):this.off("click.fb-start").on("click.fb-start",{items:this,options:e},o),this},s.on("click.fb-start","[data-fancybox]",o),s.on("click.fb-start","[data-fancybox-trigger]",(function(e){n('[data-fancybox="'+n(this).attr("data-fancybox-trigger")+'"]').eq(n(this).attr("data-fancybox-index")||0).trigger("click.fb-start",{$trigger:n(this)})})),function(){var e=null;s.on("mousedown mouseup focus blur",".fancybox-button",(function(t){switch(t.type){case"mousedown":e=n(this);break;case"mouseup":e=null;break;case"focusin":n(".fancybox-button").removeClass("fancybox-focus"),n(this).is(e)||n(this).is("[disabled]")||n(this).addClass("fancybox-focus");break;case"focusout":n(".fancybox-button").removeClass("fancybox-focus")}}))}()}}(window,document,jQuery),function(e){"use strict";var t={youtube:{matcher:/(youtube\.com|youtu\.be|youtube\-nocookie\.com)\/(watch\?(.*&)?v=|v\/|u\/|embed\/?)?(videoseries\?list=(.*)|[\w-]{11}|\?listType=(.*)&list=(.*))(.*)/i,params:{autoplay:1,autohide:1,fs:1,rel:0,hd:1,wmode:"transparent",enablejsapi:1,html5:1},paramPlace:8,type:"iframe",url:"https://www.youtube-nocookie.com/embed/$4",thumb:"https://img.youtube.com/vi/$4/hqdefault.jpg"},vimeo:{matcher:/^.+vimeo.com\/(.*\/)?([\d]+)(.*)?/,params:{autoplay:1,hd:1,show_title:1,show_byline:1,show_portrait:0,fullscreen:1},paramPlace:3,type:"iframe",url:"//player.vimeo.com/video/$2"},instagram:{matcher:/(instagr\.am|instagram\.com)\/p\/([a-zA-Z0-9_\-]+)\/?/i,type:"image",url:"//$1/p/$2/media/?size=l"},gmap_place:{matcher:/(maps\.)?google\.([a-z]{2,3}(\.[a-z]{2})?)\/(((maps\/(place\/(.*)\/)?\@(.*),(\d+.?\d+?)z))|(\?ll=))(.*)?/i,type:"iframe",url:function(e){return"//maps.google."+e[2]+"/?ll="+(e[9]?e[9]+"&z="+Math.floor(e[10])+(e[12]?e[12].replace(/^\//,"&"):""):e[12]+"").replace(/\?/,"&")+"&output="+(e[12]&&e[12].indexOf("layer=c")>0?"svembed":"embed")}},gmap_search:{matcher:/(maps\.)?google\.([a-z]{2,3}(\.[a-z]{2})?)\/(maps\/search\/)(.*)/i,type:"iframe",url:function(e){return"//maps.google."+e[2]+"/maps?q="+e[5].replace("query=","q=").replace("api=1","")+"&output=embed"}}},n=function(t,n,i){if(t)return i=i||"","object"===e.type(i)&&(i=e.param(i,!0)),e.each(n,(function(e,n){t=t.replace("$"+e,n||"")})),i.length&&(t+=(t.indexOf("?")>0?"&":"?")+i),t};e(document).on("objectNeedsType.fb",(function(i,o,r){var a,s,l,c,u,d,f,h=r.src||"",p=!1;a=e.extend(!0,{},t,r.opts.media),e.each(a,(function(t,i){if(l=h.match(i.matcher)){if(p=i.type,f=t,d={},i.paramPlace&&l[i.paramPlace]){"?"==(u=l[i.paramPlace])[0]&&(u=u.substring(1)),u=u.split("&");for(var o=0;o1&&("youtube"===n.contentSource||"vimeo"===n.contentSource)&&i.load(n.contentSource)}})}(jQuery),function(e,t,n){"use strict";var i=e.requestAnimationFrame||e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||function(t){return e.setTimeout(t,1e3/60)},o=e.cancelAnimationFrame||e.webkitCancelAnimationFrame||e.mozCancelAnimationFrame||e.oCancelAnimationFrame||function(t){e.clearTimeout(t)},r=function(t){var n=[];for(var i in t=(t=t.originalEvent||t||e.e).touches&&t.touches.length?t.touches:t.changedTouches&&t.changedTouches.length?t.changedTouches:[t])t[i].pageX?n.push({x:t[i].pageX,y:t[i].pageY}):t[i].clientX&&n.push({x:t[i].clientX,y:t[i].clientY});return n},a=function(e,t,n){return t&&e?"x"===n?e.x-t.x:"y"===n?e.y-t.y:Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2)):0},s=function(e){if(e.is('a,area,button,[role="button"],input,label,select,summary,textarea,video,audio,iframe')||n.isFunction(e.get(0).onclick)||e.data("selectable"))return!0;for(var t=0,i=e[0].attributes,o=i.length;tt.clientHeight,r=("scroll"===i||"auto"===i)&&t.scrollWidth>t.clientWidth;return o||r},c=function(e){for(var t=!1;!(t=l(e.get(0)))&&((e=e.parent()).length&&!e.hasClass("fancybox-stage")&&!e.is("body")););return t},u=function(e){var t=this;t.instance=e,t.$bg=e.$refs.bg,t.$stage=e.$refs.stage,t.$container=e.$refs.container,t.destroy(),t.$container.on("touchstart.fb.touch mousedown.fb.touch",n.proxy(t,"ontouchstart"))};u.prototype.destroy=function(){var e=this;e.$container.off(".fb.touch"),n(t).off(".fb.touch"),e.requestId&&(o(e.requestId),e.requestId=null),e.tapped&&(clearTimeout(e.tapped),e.tapped=null)},u.prototype.ontouchstart=function(i){var o=this,l=n(i.target),u=o.instance,d=u.current,f=d.$slide,h=d.$content,p="touchstart"==i.type;if(p&&o.$container.off("mousedown.fb.touch"),(!i.originalEvent||2!=i.originalEvent.button)&&f.length&&l.length&&!s(l)&&!s(l.parent())&&(l.is("img")||!(i.originalEvent.clientX>l[0].clientWidth+l.offset().left))){if(!d||u.isAnimating||d.$slide.hasClass("fancybox-animated"))return i.stopPropagation(),void i.preventDefault();o.realPoints=o.startPoints=r(i),o.startPoints.length&&(d.touch&&i.stopPropagation(),o.startEvent=i,o.canTap=!0,o.$target=l,o.$content=h,o.opts=d.opts.touch,o.isPanning=!1,o.isSwiping=!1,o.isZooming=!1,o.isScrolling=!1,o.canPan=u.canPan(),o.startTime=(new Date).getTime(),o.distanceX=o.distanceY=o.distance=0,o.canvasWidth=Math.round(f[0].clientWidth),o.canvasHeight=Math.round(f[0].clientHeight),o.contentLastPos=null,o.contentStartPos=n.fancybox.getTranslate(o.$content)||{top:0,left:0},o.sliderStartPos=n.fancybox.getTranslate(f),o.stagePos=n.fancybox.getTranslate(u.$refs.stage),o.sliderStartPos.top-=o.stagePos.top,o.sliderStartPos.left-=o.stagePos.left,o.contentStartPos.top-=o.stagePos.top,o.contentStartPos.left-=o.stagePos.left,n(t).off(".fb.touch").on(p?"touchend.fb.touch touchcancel.fb.touch":"mouseup.fb.touch mouseleave.fb.touch",n.proxy(o,"ontouchend")).on(p?"touchmove.fb.touch":"mousemove.fb.touch",n.proxy(o,"ontouchmove")),n.fancybox.isMobile&&t.addEventListener("scroll",o.onscroll,!0),((o.opts||o.canPan)&&(l.is(o.$stage)||o.$stage.find(l).length)||(l.is(".fancybox-image")&&i.preventDefault(),n.fancybox.isMobile&&l.parents(".fancybox-caption").length))&&(o.isScrollable=c(l)||c(l.parent()),n.fancybox.isMobile&&o.isScrollable||i.preventDefault(),(1===o.startPoints.length||d.hasError)&&(o.canPan?(n.fancybox.stop(o.$content),o.isPanning=!0):o.isSwiping=!0,o.$container.addClass("fancybox-is-grabbing")),2===o.startPoints.length&&"image"===d.type&&(d.isLoaded||d.$ghost)&&(o.canTap=!1,o.isSwiping=!1,o.isPanning=!1,o.isZooming=!0,n.fancybox.stop(o.$content),o.centerPointStartX=.5*(o.startPoints[0].x+o.startPoints[1].x)-n(e).scrollLeft(),o.centerPointStartY=.5*(o.startPoints[0].y+o.startPoints[1].y)-n(e).scrollTop(),o.percentageOfImageAtPinchPointX=(o.centerPointStartX-o.contentStartPos.left)/o.contentStartPos.width,o.percentageOfImageAtPinchPointY=(o.centerPointStartY-o.contentStartPos.top)/o.contentStartPos.height,o.startDistanceBetweenFingers=a(o.startPoints[0],o.startPoints[1]))))}},u.prototype.onscroll=function(e){this.isScrolling=!0,t.removeEventListener("scroll",this.onscroll,!0)},u.prototype.ontouchmove=function(e){var t=this;return void 0!==e.originalEvent.buttons&&0===e.originalEvent.buttons?void t.ontouchend(e):t.isScrolling?void(t.canTap=!1):(t.newPoints=r(e),void((t.opts||t.canPan)&&t.newPoints.length&&t.newPoints.length&&(t.isSwiping&&!0===t.isSwiping||e.preventDefault(),t.distanceX=a(t.newPoints[0],t.startPoints[0],"x"),t.distanceY=a(t.newPoints[0],t.startPoints[0],"y"),t.distance=a(t.newPoints[0],t.startPoints[0]),t.distance>0&&(t.isSwiping?t.onSwipe(e):t.isPanning?t.onPan():t.isZooming&&t.onZoom()))))},u.prototype.onSwipe=function(t){var r,a=this,s=a.instance,l=a.isSwiping,c=a.sliderStartPos.left||0;if(!0!==l)"x"==l&&(a.distanceX>0&&(a.instance.group.length<2||0===a.instance.current.index&&!a.instance.current.opts.loop)?c+=Math.pow(a.distanceX,.8):a.distanceX<0&&(a.instance.group.length<2||a.instance.current.index===a.instance.group.length-1&&!a.instance.current.opts.loop)?c-=Math.pow(-a.distanceX,.8):c+=a.distanceX),a.sliderLastPos={top:"x"==l?0:a.sliderStartPos.top+a.distanceY,left:c},a.requestId&&(o(a.requestId),a.requestId=null),a.requestId=i((function(){a.sliderLastPos&&(n.each(a.instance.slides,(function(e,t){var i=t.pos-a.instance.currPos;n.fancybox.setTranslate(t.$slide,{top:a.sliderLastPos.top,left:a.sliderLastPos.left+i*a.canvasWidth+i*t.opts.gutter})})),a.$container.addClass("fancybox-is-sliding"))}));else if(Math.abs(a.distance)>10){if(a.canTap=!1,s.group.length<2&&a.opts.vertical?a.isSwiping="y":s.isDragging||!1===a.opts.vertical||"auto"===a.opts.vertical&&n(e).width()>800?a.isSwiping="x":(r=Math.abs(180*Math.atan2(a.distanceY,a.distanceX)/Math.PI),a.isSwiping=r>45&&r<135?"y":"x"),"y"===a.isSwiping&&n.fancybox.isMobile&&a.isScrollable)return void(a.isScrolling=!0);s.isDragging=a.isSwiping,a.startPoints=a.newPoints,n.each(s.slides,(function(e,t){var i,o;n.fancybox.stop(t.$slide),i=n.fancybox.getTranslate(t.$slide),o=n.fancybox.getTranslate(s.$refs.stage),t.$slide.css({transform:"",opacity:"","transition-duration":""}).removeClass("fancybox-animated").removeClass((function(e,t){return(t.match(/(^|\s)fancybox-fx-\S+/g)||[]).join(" ")})),t.pos===s.current.pos&&(a.sliderStartPos.top=i.top-o.top,a.sliderStartPos.left=i.left-o.left),n.fancybox.setTranslate(t.$slide,{top:i.top-o.top,left:i.left-o.left})})),s.SlideShow&&s.SlideShow.isActive&&s.SlideShow.stop()}},u.prototype.onPan=function(){var e=this;a(e.newPoints[0],e.realPoints[0])<(n.fancybox.isMobile?10:5)?e.startPoints=e.newPoints:(e.canTap=!1,e.contentLastPos=e.limitMovement(),e.requestId&&o(e.requestId),e.requestId=i((function(){n.fancybox.setTranslate(e.$content,e.contentLastPos)})))},u.prototype.limitMovement=function(){var e,t,n,i,o,r,a=this,s=a.canvasWidth,l=a.canvasHeight,c=a.distanceX,u=a.distanceY,d=a.contentStartPos,f=d.left,h=d.top,p=d.width,g=d.height;return o=p>s?f+c:f,r=h+u,e=Math.max(0,.5*s-.5*p),t=Math.max(0,.5*l-.5*g),n=Math.min(s-p,.5*s-.5*p),i=Math.min(l-g,.5*l-.5*g),c>0&&o>e&&(o=e-1+Math.pow(-e+f+c,.8)||0),c<0&&o0&&r>t&&(r=t-1+Math.pow(-t+h+u,.8)||0),u<0&&ro?e=(e=e>0?0:e)r?t=(t=t>0?0:t)1&&(i.dMs>130&&a>10||a>50);i.sliderLastPos=null,"y"==e&&!t&&Math.abs(i.distanceY)>50?(n.fancybox.animate(i.instance.current.$slide,{top:i.sliderStartPos.top+i.distanceY+150*i.velocityY,opacity:0},200),o=i.instance.close(!0,250)):s&&i.distanceX>0?o=i.instance.previous(300):s&&i.distanceX<0&&(o=i.instance.next(300)),!1!==o||"x"!=e&&"y"!=e||i.instance.centerSlide(200),i.$container.removeClass("fancybox-is-sliding")},u.prototype.endPanning=function(){var e,t,i,o=this;o.contentLastPos&&(!1===o.opts.momentum||o.dMs>350?(e=o.contentLastPos.left,t=o.contentLastPos.top):(e=o.contentLastPos.left+500*o.velocityX,t=o.contentLastPos.top+500*o.velocityY),(i=o.limitPosition(e,t,o.contentStartPos.width,o.contentStartPos.height)).width=o.contentStartPos.width,i.height=o.contentStartPos.height,n.fancybox.animate(o.$content,i,366))},u.prototype.endZooming=function(){var e,t,i,o,r=this,a=r.instance.current,s=r.newWidth,l=r.newHeight;r.contentLastPos&&(e=r.contentLastPos.left,o={top:t=r.contentLastPos.top,left:e,width:s,height:l,scaleX:1,scaleY:1},n.fancybox.setTranslate(r.$content,o),sa.width||l>a.height?r.instance.scaleToActual(r.centerPointStartX,r.centerPointStartY,150):(i=r.limitPosition(e,t,s,l),n.fancybox.animate(r.$content,i,150)))},u.prototype.onTap=function(t){var i,o=this,a=n(t.target),s=o.instance,l=s.current,c=t&&r(t)||o.startPoints,u=c[0]?c[0].x-n(e).scrollLeft()-o.stagePos.left:0,d=c[0]?c[0].y-n(e).scrollTop()-o.stagePos.top:0,f=function(e){var i=l.opts[e];if(n.isFunction(i)&&(i=i.apply(s,[l,t])),i)switch(i){case"close":s.close(o.startEvent);break;case"toggleControls":s.toggleControls();break;case"next":s.next();break;case"nextOrClose":s.group.length>1?s.next():s.close(o.startEvent);break;case"zoom":"image"==l.type&&(l.isLoaded||l.$ghost)&&(s.canPan()?s.scaleToFit():s.isScaledDown()?s.scaleToActual(u,d):s.group.length<2&&s.close(o.startEvent))}};if((!t.originalEvent||2!=t.originalEvent.button)&&(a.is("img")||!(u>a[0].clientWidth+a.offset().left))){if(a.is(".fancybox-bg,.fancybox-inner,.fancybox-outer,.fancybox-container"))i="Outside";else if(a.is(".fancybox-slide"))i="Slide";else{if(!s.current.$content||!s.current.$content.find(a).addBack().filter(a).length)return;i="Content"}if(o.tapped){if(clearTimeout(o.tapped),o.tapped=null,Math.abs(u-o.tapX)>50||Math.abs(d-o.tapY)>50)return this;f("dblclick"+i)}else o.tapX=u,o.tapY=d,l.opts["dblclick"+i]&&l.opts["dblclick"+i]!==l.opts["click"+i]?o.tapped=setTimeout((function(){o.tapped=null,s.isAnimating||f("click"+i)}),500):f("click"+i);return this}},n(t).on("onActivate.fb",(function(e,t){t&&!t.Guestures&&(t.Guestures=new u(t))})).on("beforeClose.fb",(function(e,t){t&&t.Guestures&&t.Guestures.destroy()}))}(window,document,jQuery),function(e,t){"use strict";t.extend(!0,t.fancybox.defaults,{btnTpl:{slideShow:''},slideShow:{autoStart:!1,speed:3e3,progress:!0}});var n=function(e){this.instance=e,this.init()};t.extend(n.prototype,{timer:null,isActive:!1,$button:null,init:function(){var e=this,n=e.instance,i=n.group[n.currIndex].opts.slideShow;e.$button=n.$refs.toolbar.find("[data-fancybox-play]").on("click",(function(){e.toggle()})),n.group.length<2||!i?e.$button.hide():i.progress&&(e.$progress=t('
    ').appendTo(n.$refs.inner))},set:function(e){var n=this,i=n.instance,o=i.current;o&&(!0===e||o.opts.loop||i.currIndex'},fullScreen:{autoStart:!1}}),t(e).on(n.fullscreenchange,(function(){var e=i.isFullscreen(),n=t.fancybox.getInstance();n&&(n.current&&"image"===n.current.type&&n.isAnimating&&(n.isAnimating=!1,n.update(!0,!0,0),n.isComplete||n.complete()),n.trigger("onFullscreenChange",e),n.$refs.container.toggleClass("fancybox-is-fullscreen",e),n.$refs.toolbar.find("[data-fancybox-fullscreen]").toggleClass("fancybox-button--fsenter",!e).toggleClass("fancybox-button--fsexit",e))}))}t(e).on({"onInit.fb":function(e,t){n?t&&t.group[t.currIndex].opts.fullScreen?(t.$refs.container.on("click.fb-fullscreen","[data-fancybox-fullscreen]",(function(e){e.stopPropagation(),e.preventDefault(),i.toggle()})),t.opts.fullScreen&&!0===t.opts.fullScreen.autoStart&&i.request(),t.FullScreen=i):t&&t.$refs.toolbar.find("[data-fancybox-fullscreen]").hide():t.$refs.toolbar.find("[data-fancybox-fullscreen]").remove()},"afterKeydown.fb":function(e,t,n,i,o){t&&t.FullScreen&&70===o&&(i.preventDefault(),t.FullScreen.toggle())},"beforeClose.fb":function(e,t){t&&t.FullScreen&&t.$refs.container.hasClass("fancybox-is-fullscreen")&&i.exit()}})}(document,jQuery),function(e,t){"use strict";var n="fancybox-thumbs";t.fancybox.defaults=t.extend(!0,{btnTpl:{thumbs:''},thumbs:{autoStart:!1,hideOnClose:!0,parentEl:".fancybox-container",axis:"y"}},t.fancybox.defaults);var i=function(e){this.init(e)};t.extend(i.prototype,{$button:null,$grid:null,$list:null,isVisible:!1,isActive:!1,init:function(e){var t=this,n=e.group,i=0;t.instance=e,t.opts=n[e.currIndex].opts.thumbs,e.Thumbs=t,t.$button=e.$refs.toolbar.find("[data-fancybox-thumbs]");for(var o=0,r=n.length;o1));o++);i>1&&t.opts?(t.$button.removeAttr("style").on("click",(function(){t.toggle()})),t.isActive=!0):t.$button.hide()},create:function(){var e,i=this,o=i.instance,r=i.opts.parentEl,a=[];i.$grid||(i.$grid=t('
    ').appendTo(o.$refs.container.find(r).addBack().filter(r)),i.$grid.on("click","a",(function(){o.jumpTo(t(this).attr("data-index"))}))),i.$list||(i.$list=t('
    ').appendTo(i.$grid)),t.each(o.group,(function(t,n){(e=n.thumb)||"image"!==n.type||(e=n.src),a.push('")})),i.$list[0].innerHTML=a.join(""),"x"===i.opts.axis&&i.$list.width(parseInt(i.$grid.css("padding-right"),10)+o.group.length*i.$list.children().eq(0).outerWidth(!0))},focus:function(e){var t,n,i=this,o=i.$list,r=i.$grid;i.instance.current&&(n=(t=o.children().removeClass("fancybox-thumbs-active").filter('[data-index="'+i.instance.current.index+'"]').addClass("fancybox-thumbs-active")).position(),"y"===i.opts.axis&&(n.top<0||n.top>o.height()-t.outerHeight())?o.stop().animate({scrollTop:o.scrollTop()+n.top},e):"x"===i.opts.axis&&(n.leftr.scrollLeft()+(r.width()-t.outerWidth()))&&o.parent().stop().animate({scrollLeft:n.left},e))},update:function(){var e=this;e.instance.$refs.container.toggleClass("fancybox-show-thumbs",this.isVisible),e.isVisible?(e.$grid||e.create(),e.instance.trigger("onThumbsShow"),e.focus(0)):e.$grid&&e.instance.trigger("onThumbsHide"),e.instance.update()},hide:function(){this.isVisible=!1,this.update()},show:function(){this.isVisible=!0,this.update()},toggle:function(){this.isVisible=!this.isVisible,this.update()}}),t(e).on({"onInit.fb":function(e,t){var n;t&&!t.Thumbs&&((n=new i(t)).isActive&&!0===n.opts.autoStart&&n.show())},"beforeShow.fb":function(e,t,n,i){var o=t&&t.Thumbs;o&&o.isVisible&&o.focus(i?0:250)},"afterKeydown.fb":function(e,t,n,i,o){var r=t&&t.Thumbs;r&&r.isActive&&71===o&&(i.preventDefault(),r.toggle())},"beforeClose.fb":function(e,t){var n=t&&t.Thumbs;n&&n.isVisible&&!1!==n.opts.hideOnClose&&n.$grid.hide()}})}(document,jQuery),function(e,t){"use strict";t.extend(!0,t.fancybox.defaults,{btnTpl:{share:''},share:{url:function(e,t){return!e.currentHash&&"inline"!==t.type&&"html"!==t.type&&(t.origSrc||t.src)||window.location},tpl:''}}),t(e).on("click","[data-fancybox-share]",(function(){var e,n,i=t.fancybox.getInstance(),o=i.current||null;o&&("function"===t.type(o.opts.share.url)&&(e=o.opts.share.url.apply(o,[i,o])),n=o.opts.share.tpl.replace(/\{\{media\}\}/g,"image"===o.type?encodeURIComponent(o.src):"").replace(/\{\{url\}\}/g,encodeURIComponent(e)).replace(/\{\{url_raw\}\}/g,function(e){var t={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};return String(e).replace(/[&<>"'`=\/]/g,(function(e){return t[e]}))}(e)).replace(/\{\{descr\}\}/g,i.$caption?encodeURIComponent(i.$caption.text()):""),t.fancybox.open({src:i.translate(i,n),type:"html",opts:{touch:!1,animationEffect:!1,afterLoad:function(e,t){i.$refs.container.one("beforeClose.fb",(function(){e.close(null,0)})),t.$content.find(".fancybox-share__button").click((function(){return window.open(this.href,"Share","width=550, height=450"),!1}))},mobile:{autoFocus:!1}}}))}))}(document,jQuery),function(e,t,n){"use strict";function i(){var t=e.location.hash.substr(1),n=t.split("-"),i=n.length>1&&/^\+?\d+$/.test(n[n.length-1])&&parseInt(n.pop(-1),10)||1;return{hash:t,index:i<1?1:i,gallery:n.join("-")}}function o(e){""!==e.gallery&&n("[data-fancybox='"+n.escapeSelector(e.gallery)+"']").eq(e.index-1).focus().trigger("click.fb-start")}function r(e){var t,n;return!!e&&(""!==(n=(t=e.current?e.current.opts:e.opts).hash||(t.$orig?t.$orig.data("fancybox")||t.$orig.data("fancybox-trigger"):""))&&n)}n.escapeSelector||(n.escapeSelector=function(e){return(e+"").replace(/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,(function(e,t){return t?"\0"===e?"�":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e}))}),n((function(){!1!==n.fancybox.defaults.hash&&(n(t).on({"onInit.fb":function(e,t){var n,o;!1!==t.group[t.currIndex].opts.hash&&(n=i(),(o=r(t))&&n.gallery&&o==n.gallery&&(t.currIndex=n.index-1))},"beforeShow.fb":function(n,i,o,a){var s;o&&!1!==o.opts.hash&&(s=r(i))&&(i.currentHash=s+(i.group.length>1?"-"+(o.index+1):""),e.location.hash!=="#"+i.currentHash&&(a&&!i.origHash&&(i.origHash=e.location.hash),i.hashTimer&&clearTimeout(i.hashTimer),i.hashTimer=setTimeout((function(){"replaceState"in e.history?(e.history[a?"pushState":"replaceState"]({},t.title,e.location.pathname+e.location.search+"#"+i.currentHash),a&&(i.hasCreatedHistory=!0)):e.location.hash=i.currentHash,i.hashTimer=null}),300)))},"beforeClose.fb":function(n,i,o){o&&!1!==o.opts.hash&&(clearTimeout(i.hashTimer),i.currentHash&&i.hasCreatedHistory?e.history.back():i.currentHash&&("replaceState"in e.history?e.history.replaceState({},t.title,e.location.pathname+e.location.search+(i.origHash||"")):e.location.hash=i.origHash),i.currentHash=null)}}),n(e).on("hashchange.fb",(function(){var e=i(),t=null;n.each(n(".fancybox-container").get().reverse(),(function(e,i){var o=n(i).data("FancyBox");if(o&&o.currentHash)return t=o,!1})),t?t.currentHash===e.gallery+"-"+e.index||1===e.index&&t.currentHash==e.gallery||(t.currentHash=null,t.close()):""!==e.gallery&&o(e)})),setTimeout((function(){n.fancybox.getInstance()||o(i())}),50))}))}(window,document,jQuery),function(e,t){"use strict";var n=(new Date).getTime();t(e).on({"onInit.fb":function(e,t,i){t.$refs.stage.on("mousewheel DOMMouseScroll wheel MozMousePixelScroll",(function(e){var i=t.current,o=(new Date).getTime();t.group.length<2||!1===i.opts.wheel||"auto"===i.opts.wheel&&"image"!==i.type||(e.preventDefault(),e.stopPropagation(),i.$slide.hasClass("fancybox-animated")||(e=e.originalEvent||e,o-n<250||(n=o,t[(-e.deltaY||-e.deltaX||e.wheelDelta||-e.detail)<0?"next":"previous"]())))}))}})}(document,jQuery),function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ClipboardJS=t():e.ClipboardJS=t()}(this,(function(){return n={},e.m=t=[function(e,t){e.exports=function(e){var t;if("SELECT"===e.nodeName)e.focus(),t=e.value;else if("INPUT"===e.nodeName||"TEXTAREA"===e.nodeName){var n=e.hasAttribute("readonly");n||e.setAttribute("readonly",""),e.select(),e.setSelectionRange(0,e.value.length),n||e.removeAttribute("readonly"),t=e.value}else{e.hasAttribute("contenteditable")&&e.focus();var i=window.getSelection(),o=document.createRange();o.selectNodeContents(e),i.removeAllRanges(),i.addRange(o),t=i.toString()}return t}},function(e,t){function n(){}n.prototype={on:function(e,t,n){var i=this.e||(this.e={});return(i[e]||(i[e]=[])).push({fn:t,ctx:n}),this},once:function(e,t,n){var i=this;function o(){i.off(e,o),t.apply(n,arguments)}return o._=t,this.on(e,o,n)},emit:function(e){for(var t=[].slice.call(arguments,1),n=((this.e||(this.e={}))[e]||[]).slice(),i=0,o=n.length;i{e.target===this.circle||e.target===this.circleText?this.showGif():this.isPlaying&&(e.target===this.circle||e.target===this.circleText||e.target===this.container||e.target===this.image)&&this.hideGif()}))):this.element.onload=()=>{this.gif=this.element.getAttribute("data-gif"),this.alt=this.element.getAttribute("alt"),this.src=this.element.src,this.classN=this.element.className,this.imageId=this.element.id,this.circle=this.generateCircle(),this.circleText=this.circle.children[0],this.image=this.generateImage(),this.container=this.generateContainer(),this.parentElement=this.element.parentElement,this.container.appendChild(this.image),this.container.appendChild(this.circle),this.loaded=!1,this.blobURL,this.isPlaying=!1,this.parentElement.replaceChild(this.container,this.element),this.parentElement.addEventListener("click",(e=>{e.target===this.circle||e.target===this.circleText?this.showGif():this.isPlaying&&(e.target===this.circle||e.target===this.circleText||e.target===this.container||e.target===this.image)&&this.hideGif()}))}};gifsee.prototype.generateCircle=function(){var e=document.createElement("div");e.className="gifsee-circle";var t=document.createElement("span");return t.className="text",e.appendChild(t),e},gifsee.prototype.generateContainer=function(){var e=document.createElement("div");return e.className="gifsee-wrapper",e},gifsee.prototype.generateImage=function(){var e=document.createElement("img");return e.className=this.classN,this.imageId&&(e.id=this.imageId),e.src=this.src,e.setAttribute("data-gif-location",this.gif),e.setAttribute("alt",this.alt),e},gifsee.prototype.showGif=function(){this.loaded?(this.image.src=this.blobURL,this.circle.style.display="none",this.isPlaying=!0):this.fetchGif().then((e=>{this.blobURL=URL.createObjectURL(e),this.loaded=!0,this.circle.classList.remove("loading"),this.image.src=this.blobURL,this.circle.style.display="none",this.isPlaying=!0}))},gifsee.prototype.fetchGif=function(){return this.circle.classList.add("loading"),new Promise(((e,t)=>{fetch(this.gif).then((function(n){n.blob().then((function(t){e(t)})).catch((function(e){t(e)}))})).catch((function(e){t(e)}))}))},gifsee.prototype.hideGif=function(){this.image.src=this.src,this.circle.style.display="block",this.isPlaying=!1},$(document).ready((function(){function e(){var e=new ClipboardJS(".btn-clipboard",{target:function(e){return e.parentNode.previousElementSibling}});e.on("success",(function(e){$(e.trigger).text("Copied!"),setTimeout((function(){$(e.trigger).html('')}),1e3),e.clearSelection()})),e.on("error",(function(e){var t="Press "+(/Mac/i.test(navigator.userAgent)?"⌘":"Ctrl-")+"C to copy";$(e.trigger).text(t)}))}var t,n;if(function(t){hljs.highlightAll(),t(e),$("body").append('
    ')}((function(e){$(".hljs").parent().wrap('
    '),$(".hljs-wrapper").append('
    '),$(".hljs-wrapper .hljs-actions-panel").prepend(''),$(".hljs-wrapper .hljs-actions-panel").prepend(''),e()})),t=!1,n=$(".js-fullscreen-code")[0],$("body").on("click",".btn-fullscreen-mode",(function(){if(t)$("body").css("overflow",""),$(n).removeClass("is-open").empty(),t=!1;else{var e=this.parentNode.parentNode.cloneNode(!0);$("body").css("overflow","hidden"),$(n).append(e),$(n).find(".btn-fullscreen-mode").attr("title","Leave fullscreen mode"),$(n).find(".btn-fullscreen-mode i").removeClass("fa-expand").addClass("fa-compress"),$(n).addClass("is-open"),t=!0}})),$(document).keyup((function(e){$(n).hasClass("is-open")&&"Escape"===e.key&&($("body").css("overflow",""),$(n).removeClass("is-open").empty(),t=!1)})),$(".gif").each((function(e){new gifsee(this)})),$("table").addClass("table"),$("table thead").addClass("thead-light"),$("#show-sidebar").on("click",(function(){$("body").addClass("sidebar-open"),$(".sidebar").addClass("sidebar-active")})),$(".main-nav .li_dropdown .li_parent").on("click",(function(){$(this).parent().toggleClass("active")})),$(document).on("click",".sidebar-open",(function(e){$(e.target).hasClass("sidebar-open")&&($("body").removeClass("sidebar-open"),$(".sidebar").removeClass("sidebar-active"))})),$("#back-to-top").on("click",(function(){$("html, body").animate({scrollTop:0},800)})),$(".main-content-body h2, .main-content-body h3, .main-content-body h4, .main-content-body h5, .main-content-body h6").each((function(){window.location.hash.replace("#","");var e=$(this).attr("id"),t=$(this).text();$(this).empty(),"undefined"!==e&&$(this).append(''+t+'')})),$(".main-content-body h2 > a, .main-content-body h3 > a, .main-content-body h4 > a, .main-content-body h5 > a, .main-content-body h6 > a").on("click",(function(){$("html, body").animate({scrollTop:$(this).offset().top-80},500)})),$(window).on("load",(function(){var e=window.location.hash.replace("#","");e&&$(".main-content-body h2, .main-content-body h3, .main-content-body h4, .main-content-body h5, .main-content-body h6").each((function(){var t=$(this).attr("id");if("undefined"!==t&&e===t)return $("html, body").animate({scrollTop:$(this).offset().top-80},100),!1}))})),$(".toc").length){var i=$('

    Contents

    ');$(".toc").prepend(i),$(".toc a").on("click",(function(){var e=$(this.hash);if($(".nav-tabs").length&&$(".tab-pane "+this.hash).length){var t=$(this.hash)[0].closest(".tab-pane"),n=$(t).attr("id");$('a[href="#'+n+'"]').tab("show")}$("html, body").animate({scrollTop:e.offset().top-80},500)}))}if(0!=$("#search").length){$.getJSON("/search/search_index.json").done((function(e){var t=$("#searchList"),n=new Fuse(e.docs,{shouldSort:!0,tokenize:!0,threshold:0,location:0,distance:100,maxPatternLength:32,minMatchCharLength:1,ignoreLocation:!0,keys:["title","text"]});$("#search").on("keyup",(function(){if(this.value){var e=n.search(this.value).filter((function(e){return null===e.item.location.match(/(\/#)/g)}));0===e.length?t.html(""):(t.html(""),t.append("

      Search results

    ")),e.forEach((function(e){$("#searchList ul").append("
  • "+e.item.title+"
  • ")}))}else $("#searchList").empty()}))}))}function o(){$(window).scrollTop()<10?$(".navbar-dark").removeClass("dark-mode"):$(".navbar-dark").addClass("dark-mode")}$(".main-content-body img").each((function(){$(this).hasClass("no-lightbox")||$(this).hasClass("gif")||$(this).hasClass("emojione")||$(this).parent("a").length||$(this).wrap((function(){return""}))})),$(window).scroll((function(){o()})),o(),$("#currentYear").text((new Date).getFullYear()),document.querySelector(".bug-head").addEventListener("click",(function(){throw new Error("Headshot")})),$('a.nav-link[data-toggle="tab"]').on("click",(function(e){var t=e.currentTarget.closest("ul.nav-tabs"),n=e.currentTarget.dataset.tab,i=$('a.nav-link[data-toggle="tab"][data-tab="'+n+'"]').filter((function(e,n){return $(n).closest("ul.nav-tabs").not(t)[0]}));if(i.length>0){var o=$(e.currentTarget).offset().top-$(document).scrollTop();$(i).tab("show"),$(document).scrollTop($(e.currentTarget).offset().top-o)}}))})),window.intercomSettings={app_id:"i2hhgdvj",system:"elmah.io",background_color:"#0da58e"},function(e){var t="undefined"!==e.app_id?e.app_id:"",n=void 0!==e.background_color?e.background_color:"#333333";if(t){var i=function(e,t=null,n=null){var i=document.createElement("div");return Object.keys(e).forEach((function(t){i.style[t]=e[t]})),t&&i.setAttribute("id",t),i.innerHTML=n,i},o=function(e){if(!window.Intercom){var n=window,i=n.Intercom;if("function"==typeof i)i("reattach_activator"),i("update",n.intercomSettings);else{var o=document,s=function(){s.c(arguments)};s.q=[],s.c=function(e){s.q.push(e)},n.Intercom=s;var l=function(){var e=o.createElement("script");e.type="text/javascript",e.async=!0,e.src="https://widget.intercom.io/widget/"+t+"/";var n=o.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)}}l()}e&&(r.style.opacity="0",a.style.opacity="1",a.style.transform="rotate(0deg)",window.Intercom("show"));var c=0,u=setInterval((function(){c++,window.Intercom.booted?(null!==document.querySelector("#intercom-facade-btn")&&document.querySelector("#intercom-facade-btn").remove(),clearInterval(u)):c>10&&clearInterval(u)}),1e3);return!0},r=i({display:"flex",WebkitBoxAlign:"center",alignItems:"center",WebkitBoxPack:"center",justifyContent:"center",position:"absolute",top:"0px",bottom:"0px",width:"100%",transform:"rotate(0deg) scale(1)",transition:"transform 0.16s linear 0s, opacity 0.08s linear 0s"},null,'\n\n'),a=i({display:"flex",WebkitBoxAlign:"center",alignItems:"center",WebkitBoxPack:"center",justifyContent:"center",position:"absolute",top:"0px",bottom:"0px",width:"100%",transition:"transform 0.16s linear 0s, opacity 0.08s linear 0s",opacity:"0",transform:"rotate(-30deg)"},null,'\n\n \n \n\n'),s=i({position:"absolute",top:"0px",left:"0px",width:"48px",height:"48px",borderRadius:"50%",cursor:"pointer",transformOrigin:"center",overflowX:"hidden",overflowY:"hidden",WebkitBackfaceVisibility:"hidden",WebkitFontSmoothing:"antialiased"}),l=i({fontFamily:"intercom-font, 'Helvetica Neue', 'Apple Color Emoji', Helvetica, Arial, sans-serif",fontSize:"100%",fontStyle:"normal",letterSpacing:"normal",fontStretch:"normal",fontVariantLigatures:"normal",fontVariantCaps:"normal",fontVariantEastAsian:"normal",fontVariantPosition:"normal",fontWeight:"normal",textAlign:"left",textDecorationLine:"none",textDecorationStyle:"initial",textDecorationColor:"initial",textDecoration:"none",textIndent:"0px",textShadow:"none",textTransform:"none",boxSizing:"content-box",WebkitTextEmphasisStyle:"none",WebkitTextEmphasisColor:"initial",WebkitFontSmoothing:"antialiased",lineHeight:1}),c=i({zIndex:2147483004,position:"fixed",bottom:"20px",display:"block",right:"20px",width:"48px",height:"48px",borderRadius:"50%",boxShadow:"rgba(0, 0, 0, 0.0588235) 0px 1px 6px 0px, rgba(0, 0, 0, 0.156863) 0px 2px 32px 0px",backgroundColor:n},"intercom-facade-btn");s.append(r),s.append(a),l.append(s),l.addEventListener("click",(function(){o(!0)})),l.addEventListener("mouseenter",(function(){o(!1)})),c.append(l),document.querySelector("body").append(c),void 0!==e.custom_launcher_selector&&document.querySelectorAll(e.custom_launcher_selector).forEach((function(e){e.addEventListener("click",(function(e){e.preventDefault(),o(!0)}))}))}}(window.intercomSettings); \ No newline at end of file diff --git a/assets/js/popper.min.js b/assets/js/popper.min.js new file mode 100644 index 0000000000..6040f6c006 --- /dev/null +++ b/assets/js/popper.min.js @@ -0,0 +1,4 @@ +/* + Copyright (C) Federico Zivolo 2020 + Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). + */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function i(e){return e&&e.referenceNode?e.referenceNode:e}function r(e){return 11===e?re:10===e?pe:re||pe}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent||null;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f]),E=parseFloat(w['border'+f+'Width']),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,$(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ce.FLIP:p=[n,i];break;case ce.CLOCKWISE:p=G(n);break;case ce.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u),E=!!t.flipVariationsByContent&&(w&&'start'===r&&c||w&&'end'===r&&h||!w&&'start'===r&&u||!w&&'end'===r&&g),v=y||E;(m||b||v)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),v&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport',flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!fe),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=B('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=le({},E,e.attributes),e.styles=le({},m,e.styles),e.arrowStyles=le({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return V(e.instance.popper,e.styles),j(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&V(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),V(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ge}); diff --git a/authentication/index.html b/authentication/index.html new file mode 100644 index 0000000000..c296162352 --- /dev/null +++ b/authentication/index.html @@ -0,0 +1,658 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Authentication on elmah.io + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Authentication

    +

    All of our integrations communicates with the elmah.io API. In order to request endpoints on the API, each client will need to provide a valid API key. API keys are available on the Organization Settings view, as well as on the Install tab on the Log Settings screen. We wrote a guide to help you find your API key here: Where is my API key?. A default API key is created when you create your organization, but new keys can be added, keys revoked, and more.

    +

    Sending the API key to the elmah.io API, is typically handled by the Elmah.Io.Client NuGet package. All integrations have a dependency to this package, which means that it will be automatically installed through NuGet. How you provide your API key depends on the integration you are installing. Some integrations expect the API key in a config file, while others, accept the key in C#. For details about how to provide the API key for each integration, click the various installation guides in the left menu.

    +

    Besides a unique string representing an API key, each key can have a set of permissions. As default, API keys only have the Write Messages permission, which means that the key cannot be used to read data from your logs. In 99% of all scenarios, you will browse through errors using the elmah.io UI, which will require you to sign in using username/password or one of the supported social providers. In the case you want to enable one of the native UIs provided by different integrations (like the /elmah.axd endpoint part of the ELMAH package) or you are building a third-party integration to elmah.io, you will need to assign additional permissions to your API key. For details about API key permissions, check out How to configure API key permissions.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/bot-detection/index.html b/bot-detection/index.html new file mode 100644 index 0000000000..03878e056e --- /dev/null +++ b/bot-detection/index.html @@ -0,0 +1,672 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Bot detection - Machine learning will help you identify bot errors + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Bot detection

    +

    elmah.io can help you with classifying log messages generated by bots and crawlers. When storing a log message, we run a range of checks to try and identify if a log message is generated by an automated script or a real human visitor of your website/application. In this case, a flag named isBot is set to true on the message. In case we couldn't identify a log message as generated by a bot, the flag is set to false.

    +

    Besides an automated check, you can also mark a log message as generated by a bot manually. This is done from within the elmah.io UI:

    +

    Mark message as bot

    +

    The benefit of marking log messages with the isBot flag manually is that elmah.io will then automatically mark new instances of this log message with isBot=true (this feature is available for automatically bot-marked log messages as well). By doing so you get the possibilities listed later in this article.

    +

    Log messages marked as generated by bots include a small robot icon on the search result:

    +

    Bot messages result

    +

    Search by or not by bots

    +

    If you want to show all log messages generated by bots you can create a search query like this:

    +

    isBot:true
    +

    +

    Or include a search filter for the Is Bot field:

    +

    Is Bot filter

    +

    By reversing the filter, you see a list of log messages NOT generated by a bot, which can make it easier to get an overview of "real" errors.

    +

    Hide or ignore log messages generated by bots

    +

    By using the isBot field in Hide and Ignore filters, you can let elmah.io automatically hide or ignore future log messages generated by bots. Be aware that creating ignore filters based on the isBot field is only available for users on the Enterprise plan.

    +

    To create a new filter, navigate to the Rules tab on log settings, and click the Add a new rule button. Input a query including the isBot field and select either the Hide or Ignore action:

    +

    Is Bot rule

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-clear/index.html b/cli-clear/index.html new file mode 100644 index 0000000000..9704c7376b --- /dev/null +++ b/cli-clear/index.html @@ -0,0 +1,683 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Clearing log messages from the CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Clearing log messages from the CLI

    +

    The clear command is used to delete one or more messages from a log.

    +
    +

    Be aware that clearing a log does not reset the monthly counter towards log messages included in your current plan. The clear command is intended for cleanup in non-expired log messages you no longer need.

    +
    +

    Usage

    +

    > elmahio clear --help
    +
    +Description:
    +  Delete one or more messages from a log
    +
    +Usage:
    +  elmahio clear [options]
    +
    +Options:
    +  --apiKey <apiKey> (REQUIRED)  An API key with permission to execute the command
    +  --logId <logId> (REQUIRED)    The log ID of the log to clear messages
    +  --query <query> (REQUIRED)    Clear messages matching this query (use * for all messages)
    +  --from <from>                 Optional date and time to clear messages from
    +  --to <to>                     Optional date and time to clear messages to
    +  -?, -h, --help                Show help and usage information
    +

    +

    Examples

    +

    Simple:

    +

    elmahio clear --apiKey API_KEY --logId LOG_ID --query "statusCode:404"
    +

    +

    Full:

    +

    elmahio clear --apiKey API_KEY --logId LOG_ID --query "statusCode:404" --from 2022-05-17 --to 2022-05-18
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-dataloader/index.html b/cli-dataloader/index.html new file mode 100644 index 0000000000..711cb77828 --- /dev/null +++ b/cli-dataloader/index.html @@ -0,0 +1,673 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Dataloader loads data from the CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Dataloader loads data from the CLI

    +

    The dataloader command loads 50 log messages into a specified log.

    +

    Usage

    +

    > elmahio dataloader --help
    +
    +Description:
    +  Load 50 log messages into the specified log
    +
    +Usage:
    +  elmahio dataloader [options]
    +
    +Options:
    +  --apiKey <apiKey> (REQUIRED)  An API key with permission to execute the command
    +  --logId <logId> (REQUIRED)    The log ID of the log to import messages into
    +  -?, -h, --help                Show help and usage information
    +

    +

    Example

    +

    elmahio dataloader --apiKey API_KEY --logId LOG_ID
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-deployment/index.html b/cli-deployment/index.html new file mode 100644 index 0000000000..1b10f95279 --- /dev/null +++ b/cli-deployment/index.html @@ -0,0 +1,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create a deployment from the CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create a deployment from the CLI

    +

    The deployment command is used to create new deployments on elmah.io.

    +

    Usage

    +

    > elmahio deployment --help
    +
    +Description:
    +  Create a new deployment
    +
    +Usage:
    +  elmahio deployment [options]
    +
    +Options:
    +  --apiKey <apiKey> (REQUIRED)    An API key with permission to execute the command
    +  --version <version> (REQUIRED)  The version number of this deployment
    +  --created <created>             When was this deployment created in UTC
    +  --description <description>     Description of this deployment
    +  --userName <userName>           The name of the person responsible for creating this deployment
    +  --userEmail <userEmail>         The email of the person responsible for creating this deployment
    +  --logId <logId>                 The ID of a log if this deployment is specific to a single log
    +  -?, -h, --help                  Show help and usage information
    +

    +

    Examples

    +

    Simple:

    +

    elmahio deployment --apiKey API_KEY --version 1.0.0
    +

    +

    Full:

    +

    elmahio deployment --apiKey API_KEY --version 1.0.0 --created 2022-02-08 --description "My new cool release" --userName "Thomas Ardal" --userEmail "thomas@elmah.io" --logId LOG_ID
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-diagnose/index.html b/cli-diagnose/index.html new file mode 100644 index 0000000000..3f42d4d9aa --- /dev/null +++ b/cli-diagnose/index.html @@ -0,0 +1,677 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Diagnose potential problems with an elmah.io installation + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Diagnose potential problems with an elmah.io installation

    +

    The diagnose command can be run in the root folder of an elmah.io installation to find potential problems with the configuration.

    +

    Usage

    +

    > elmahio diagnose --help
    +
    +Description:
    +  Diagnose potential problems with an elmah.io installation
    +
    +Usage:
    +  elmahio diagnose [options]
    +
    +Options:
    +  --directory <directory>  The root directory to check [default: C:\test]
    +  --verbose                Output verbose diagnostics to help debug problems [default: False]
    +  -?, -h, --help           Show help and usage information
    +

    +

    Examples

    +

    Simple:

    +

    elmahio diagnose --directory c:\projects\my-project
    +

    +

    Full:

    +

    elmahio diagnose --directory c:\projects\my-project --verbose
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-export/index.html b/cli-export/index.html new file mode 100644 index 0000000000..92f197b1ad --- /dev/null +++ b/cli-export/index.html @@ -0,0 +1,684 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Exporting log messages from the CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Exporting log messages from the CLI

    +

    The export command is used to export one or more log messages from a log to JSON.

    +

    Usage

    +

    > elmahio export --help
    +
    +Description:
    +  Export log messages from a specified log
    +
    +Usage:
    +  elmahio export [options]
    +
    +Options:
    +  --apiKey <apiKey> (REQUIRED)      An API key with permission to execute the command
    +  --logId <logId> (REQUIRED)        The ID of the log to export messages from
    +  --dateFrom <dateFrom> (REQUIRED)  Defines the Date from which the logs start. Ex. " --dateFrom 2023-03-09"
    +  --dateTo <dateTo> (REQUIRED)      Defines the Date from which the logs end. Ex. " --dateTo 2023-03-16"
    +  --filename <filename>             Defines the path and filename of the file to export to. Ex. " --filename
    +                                    C:\myDirectory\myFile.json" [default:
    +                                    C:\Users\thoma\Export-638145521994987555.json]
    +  --query <query>                   Defines the query that is passed to the API [default: *]
    +  --includeHeaders                  Include headers, cookies, etc. in output (will take longer to export)
    +  -?, -h, --help                    Show help and usage information
    +

    +

    Examples

    +

    Simple:

    +

    elmahio export --apiKey API_KEY --logId LOG_ID --dateFrom 2020-08-21 --dateTo 2020-08-28
    +

    +

    Full:

    +

    elmahio export --apiKey API_KEY --logId LOG_ID --dateFrom 2020-08-21 --dateTo 2020-08-28 --filename c:\temp\elmahio.json --query "statusCode: 404" --includeHeaders
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-import/index.html b/cli-import/index.html new file mode 100644 index 0000000000..3ecf261e13 --- /dev/null +++ b/cli-import/index.html @@ -0,0 +1,683 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Importing log messages to elmah.io from the CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Importing log messages from the CLI

    +

    The import command is used to import log messages from IIS log files and W3C Extended log files to an elmah.io log.

    +

    Usage

    +

    > elmahio import --help
    +
    +Description:
    +  Import log messages to a specified log
    +
    +Usage:
    +  elmahio import [options]
    +
    +Options:
    +  --apiKey <apiKey> (REQUIRED)      An API key with permission to execute the command
    +  --logId <logId> (REQUIRED)        The ID of the log to import messages to
    +  --type <iis|w3c> (REQUIRED)       The type of log file to import. Use 'w3c' for W3C Extended Log File Format and
    +                                    'iis' for IIS Log File Format
    +  --filename <filename> (REQUIRED)  Defines the path and filename of the file to import from. Ex. " --filename
    +                                    C:\myDirectory\log.txt"
    +  --dateFrom <dateFrom>             Defines the Date from which the logs start. Ex. " --dateFrom 2023-03-06"
    +  --dateTo <dateTo>                 Defines the Date from which the logs end. Ex. " --dateTo 2023-03-13"
    +  -?, -h, --help                    Show help and usage information
    +

    +

    Examples

    +

    IIS:

    +

    elmahio import --apiKey API_KEY --logId LOG_ID --type iis --filename u_inetsv1.log --dateFrom 2023-03-13T10:14 --dateTo 2023-03-13T10:16
    +

    +

    w3c:

    +

    elmahio import --apiKey API_KEY --logId LOG_ID --type w3c --filename u_extend1.log --dateFrom 2023-03-13T10:14 --dateTo 2023-03-13T10:16
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-log/index.html b/cli-log/index.html new file mode 100644 index 0000000000..e6a4b432d9 --- /dev/null +++ b/cli-log/index.html @@ -0,0 +1,714 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Log a message from the CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Log a message from the CLI

    +

    The log command is used to store a log message in a specified log.

    +

    Usage

    +

    > elmahio log --help
    +
    +Description:
    +  Log a message to the specified log
    +
    +Usage:
    +  elmahio log [options]
    +
    +Options:
    +  --apiKey <apiKey> (REQUIRED)     An API key with permission to execute the command
    +  --logId <logId> (REQUIRED)       The ID of the log to send the log message to
    +  --application <application>      Used to identify which application logged this message. You can use this if you have
    +                                   multiple applications and services logging to the same log
    +  --detail <detail>                A longer description of the message. For errors this could be a stacktrace, but it's
    +                                   really up to you what to log in there.
    +  --hostname <hostname>            The hostname of the server logging the message.
    +  --title <title> (REQUIRED)       The textual title or headline of the message to log.
    +  --titleTemplate <titleTemplate>  The title template of the message to log. This property can be used from logging
    +                                   frameworks that supports structured logging like: "{user} says {quote}". In the
    +                                   example, titleTemplate will be this string and title will be "Gilfoyle says It's not
    +                                   magic. It's talent and sweat".
    +  --source <source>                The source of the code logging the message. This could be the assembly name.
    +  --statusCode <statusCode>        If the message logged relates to a HTTP status code, you can put the code in this
    +                                   property. This would probably only be relevant for errors, but could be used for
    +                                   logging successful status codes as well.
    +  --dateTime <dateTime>            The date and time in UTC of the message. If you don't provide us with a value in
    +                                   dateTime, we will set the current date and time in UTC.
    +  --type <type>                    The type of message. If logging an error, the type of the exception would go into
    +                                   type but you can put anything in there, that makes sense for your domain.
    +  --user <user>                    An identification of the user triggering this message. You can put the users email
    +                                   address or your user key into this property.
    +  --severity <severity>            An enum value representing the severity of this message. The following values are
    +                                   allowed: Verbose, Debug, Information, Warning, Error, Fatal.
    +  --url <url>                      If message relates to a HTTP request, you may send the URL of that request. If you
    +                                   don't provide us with an URL, we will try to find a key named URL in
    +                                   serverVariables.
    +  --method <method>                If message relates to a HTTP request, you may send the HTTP method of that request.
    +                                   If you don't provide us with a method, we will try to find a key named
    +                                   REQUEST_METHOD in serverVariables.
    +  --version <version>              Versions can be used to distinguish messages from different versions of your
    +                                   software. The value of version can be a SemVer compliant string or any other syntax
    +                                   that you are using as your version numbering scheme.
    +  --correlationId <correlationId>  CorrelationId can be used to group similar log messages together into a single
    +                                   discoverable batch. A correlation ID could be a session ID from ASP.NET Core, a
    +                                   unique string spanning multiple microsservices handling the same request, or
    +                                   similar.
    +  --category <category>            The category to set on the message. Category can be used to emulate a logger name
    +                                   when created from a logging framework.
    +  -?, -h, --help                   Show help and usage information
    +

    +

    Examples

    +

    Simple:

    +

    elmahio log --apiKey API_KEY --logId LOG_ID --title "An error happened"
    +

    +

    Full:

    +

    elmahio log --apiKey API_KEY --logId LOG_ID --title "An error happened" --application "My app" --details "Some details" --hostname "localhost" --titleTemplate "An {severity} happened" --source "A source" --statusCode 500 --dateTime 2022-01-13 --type "The type" --user "A user" --severity "Error" --url "https://elmah.io" --method "GET" --version "1.0.0"
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-overview/index.html b/cli-overview/index.html new file mode 100644 index 0000000000..aad8f20f72 --- /dev/null +++ b/cli-overview/index.html @@ -0,0 +1,795 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + CLI overview - Automate repeating tasks with the elmah.io CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    CLI overview

    +

    The elmah.io CLI lets you execute common tasks against elmah.io.

    +

    Installing the CLI

    +
    +

    The elmah.io CLI requires .NET 6 or newer installed.

    +
    +

    The elmah.io CLI can be installed in several ways. To set up everything automatically, execute the following script from the command line:

    +

    dotnet tool install --global Elmah.Io.Cli
    +

    +

    or make sure to run on the latest version if you already have the CLI installed:

    +

    dotnet tool update --global Elmah.Io.Cli
    +

    +

    If you prefer downloading the CLI as a zip you can download the latest version from GitHub. To clone and build the CLI manually, check out the instructions below.

    +

    Run the CLI

    + + +

    Run the CLI to get help:

    +

    elmahio --help
    +

    +

    Help similar to this is outputted to the console:

    +

    elmahio:
    +  CLI for executing various actions against elmah.io
    +
    +Usage:
    +  elmahio [options] [command]
    +
    +Options:
    +  --nologo        Doesn't display the startup banner or the copyright message
    +  --version       Show version information
    +  -?, -h, --help  Show help and usage information
    +
    +Commands:
    +  clear       Delete one or more messages from a log
    +  dataloader  Load 50 log messages into the specified log
    +  deployment  Create a new deployment
    +  diagnose    Diagnose potential problems with an elmah.io installation
    +  export      Export log messages from a specified log
    +  import      Import log messages to a specified log
    +  log         Log a message to the specified log
    +  sourcemap   Upload a source map and minified JavaScript
    +  tail        Tail log messages from a specified log
    +

    +

    Cloning the CLI

    +

    Create a new folder and git clone the repository:

    +

    git clone https://github.com/elmahio/Elmah.Io.Cli.git
    +

    +

    Building the CLI

    +

    Navigate to the root repository of the code and execute the following command:

    +

    dotnet build
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-sourcemap/index.html b/cli-sourcemap/index.html new file mode 100644 index 0000000000..3396032d3d --- /dev/null +++ b/cli-sourcemap/index.html @@ -0,0 +1,679 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Upload source maps from the CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Upload a source map from the CLI

    +

    The sourcemap command is used to upload source maps and minified JavaScript files to elmah.io.

    +

    Usage

    +

    > elmahio sourcemap --help
    +
    +Description:
    +  Upload a source map and minified JavaScript
    +
    +Usage:
    +  elmahio sourcemap [options]
    +
    +Options:
    +  --apiKey <apiKey> (REQUIRED)                          An API key with permission to execute the command
    +  --logId <logId> (REQUIRED)                            The ID of the log which should contain the minified JavaScript
    +                                                        and source map
    +  --path <path> (REQUIRED)                              An URL to the online minified JavaScript file
    +  --sourceMap <sourceMap> (REQUIRED)                    The source map file. Only files with an extension of .map and
    +                                                        content type of application/json will be accepted
    +  --minifiedJavaScript <minifiedJavaScript> (REQUIRED)  The minified JavaScript file. Only files with an extension of
    +                                                        .js and content type of text/javascript will be accepted
    +  -?, -h, --help                                        Show help and usage information
    +

    +

    Examples

    +

    sourcemap --apiKey API_KEY --logId LOG_ID --path "/bundles/sharedbundle.min.js" --sourceMap "c:\path\to\sharedbundle.map" --minifiedJavaScript "c:\path\to\sharedbundle.min.js"
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/cli-tail/index.html b/cli-tail/index.html new file mode 100644 index 0000000000..07100fdce1 --- /dev/null +++ b/cli-tail/index.html @@ -0,0 +1,673 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Tail log messages from the CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Tail log messages from the CLI

    +

    The tail command is used to tail log messages in a specified log.

    +

    Usage

    +

    > elmahio tail --help
    +
    +Description:
    +  Tail log messages from a specified log
    +
    +Usage:
    +  elmahio tail [options]
    +
    +Options:
    +  --apiKey <apiKey> (REQUIRED)  An API key with permission to execute the command
    +  --logId <logId> (REQUIRED)    The ID of the log to send the log message to
    +  -?, -h, --help                Show help and usage information
    +

    +

    Example

    +

    elmahio tail --apiKey API_KEY --logId LOG_ID
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/configure-elmah-io-from-code/index.html b/configure-elmah-io-from-code/index.html new file mode 100644 index 0000000000..c6a79eaa67 --- /dev/null +++ b/configure-elmah-io-from-code/index.html @@ -0,0 +1,694 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Configure elmah.io from code + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Configure elmah.io from code

    +

    You typically configure elmah.io in your web.config file. With a little help from some custom code, you will be able to configure everything in code as well:

    +

    using Elmah;
    +using System.Collections.Generic;
    +using System.ComponentModel.Design;
    +
    +[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(ElmahFromCodeExample.ElmahConfig), "Start")]
    +
    +namespace ElmahFromCodeExample
    +{
    +    public static class ElmahConfig
    +    {
    +        public static void Start()
    +        {
    +            ServiceCenter.Current = CreateServiceProviderQueryHandler(ServiceCenter.Current);
    +            HttpApplication.RegisterModule(typeof(ErrorLogModule));
    +        }
    +
    +        private static ServiceProviderQueryHandler CreateServiceProviderQueryHandler(ServiceProviderQueryHandler sp)
    +        {
    +            return context =>
    +            {
    +                var container = new ServiceContainer(sp(context));
    +
    +                var config = new Dictionary<string, string>();
    +                config["apiKey"] = "API_KEY";
    +                config["logId"] = "LOG_ID";
    +                var log = new Elmah.Io.ErrorLog(config);
    +
    +                container.AddService(typeof(Elmah.ErrorLog), log);
    +                return container;
    +            };
    +        }
    +    }
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with a log ID (Where is my log ID?).

    +

    Let's look at the code. Our class ElmahConfig is configured as a PreApplicationStartMethod which means, that ASP.NET (MVC) will execute the Start method when the web application starts up. Inside this method, we set the ServiceCenter.Current property to the return type of the CreateServiceProviderQueryHandler method. This method is where the magic happens. Besides creating the new ServiceContainer, we created the Elmah.Io.ErrorLog class normally configured through XML. The Dictionary should contain the API key and log ID as explained earlier.

    +

    In the second line of the Start-method, we call the RegisterModule-method with ErrorLogModule as parameter. This replaces the need for registering the module in web.config as part of the system.webServer element.

    +

    That's it! You no longer need the <elmah> element, config sections, or anything else related to ELMAH and elmah.io in your web.config file.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/configure-elmah-io-manually/index.html b/configure-elmah-io-manually/index.html new file mode 100644 index 0000000000..c87a9d4992 --- /dev/null +++ b/configure-elmah-io-manually/index.html @@ -0,0 +1,738 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Configure elmah.io manually + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Configure elmah.io manually

    +

    The Elmah.Io NuGet package normally adds all of the necessary configuration, to get up and running with elmah.io. This is one of our killer features and our customers tell us, that we have the simplest installer on the market. In some cases, you may experience problems with the automatic configuration, though. Different reasons can cause the configuration not to be added automatically. The most common reason is restrictions to executing PowerShell inside Visual Studio.

    +

    Start by installing the Elmah.Io package:

    +
    Install-Package Elmah.Io
    +
    dotnet add package Elmah.Io
    +
    <PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add Elmah.Io
    +
    +

    If a dialog is shown during the installation, input your API key (Where is my API key?) and log ID (Where is my log ID?). Don't worry if the configuration isn't added, since we will verify this later.

    +

    Add the following to the <configSections> element in your web.config:

    +

    <sectionGroup name="elmah">
    +  <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
    +  <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
    +  <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
    +  <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
    +</sectionGroup>
    +

    +

    Add the following to the <httpModules> element (inside <system.web>) in your web.config:

    +

    <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
    +<add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
    +<add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
    +

    +

    Add the following to the <modules> element (inside <system.webServer>) in your web.config:

    +

    <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
    +<add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
    +<add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    +

    +

    Add the following to the system.webServer element in your web.config:

    +

    <validation validateIntegratedModeConfiguration="false" />
    +

    +

    Add the following as a root element beneath the <configuration> element in your web.config:

    +

    <elmah>
    +    <security allowRemoteAccess="false" />
    +    <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="API_KEY" logId="LOG_ID" />
    +</elmah>
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with your log ID (Where is my log ID?).

    +

    That's it. You managed to install elmah.io manually and you should go to your LinkedIn profile and update with a new certification called "Certified elmah.io installer" :)

    +

    Here's a full example of ELMAH configuration in a web.config file:

    +

    <configuration>
    +  <configSections>
    +    <sectionGroup name="elmah">
    +      <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
    +      <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
    +      <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
    +      <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
    +    </sectionGroup>
    +  </configSections>
    +  <system.web>
    +    <httpModules>
    +      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
    +      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
    +      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
    +    </httpModules>
    +  </system.web>
    +  <system.webServer>
    +    <validation validateIntegratedModeConfiguration="false" />
    +    <modules>
    +      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
    +      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
    +      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    +    </modules>
    +  </system.webServer>
    +  <elmah>
    +    <security allowRemoteAccess="false" />
    +    <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="API_KEY" logId="LOG_ID" />
    +  </elmah>
    +</configuration>
    +

    +

    In case you need to access your error log on /elmah.axd, you need to add the following to the <configuration> element in your web.config:

    +

    <location path="elmah.axd" inheritInChildApplications="false">
    +    <system.web>
    +        <httpHandlers>
    +            <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
    +        </httpHandlers>
    +    </system.web>
    +    <system.webServer>
    +        <handlers>
    +            <add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" />
    +        </handlers>
    +    </system.webServer>
    +</location>
    +

    +

    We don't recommend browsing your error logs through the /elmah.axd endpoint. The elmah.io UI will let you control different levels of access and more.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-atlassian-bamboo/index.html b/create-deployments-from-atlassian-bamboo/index.html new file mode 100644 index 0000000000..c46ddfe4ba --- /dev/null +++ b/create-deployments-from-atlassian-bamboo/index.html @@ -0,0 +1,681 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from Atlassian Bamboo + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from Atlassian Bamboo

    +

    Setting up elmah.io Deployment Tracking on Bamboo is easy using a bit of PowerShell.

    +
      +
    1. +

      Add a new Script Task and select Windows PowerShell in Interpreter.

      +
    2. +
    3. +

      Select Inline in Script location and add the following PowerShell to Script body:

      +
    4. +
    +

    $ProgressPreference = "SilentlyContinue"
    +
    +Write-Host $bamboo_buildNumber
    +
    +$url = "https://api.elmah.io/v3/deployments?api_key=API_KEY"
    +$body = @{
    +  version = $Env:bamboo_buildNumber
    +  logId = "LOG_ID"
    +}
    +[Net.ServicePointManager]::SecurityProtocol = `
    +    [Net.SecurityProtocolType]::Tls12,
    +    [Net.SecurityProtocolType]::Tls11,
    +    [Net.SecurityProtocolType]::Tls
    +Invoke-RestMethod -Method Post -Uri $url -Body $body
    +

    +

    PowerShell task in Bamboo

    +

    Replace API_KEY and LOG_ID and everything is configured. The script uses the build number of the current build as version number ($Env:bamboo_buildNumber). If you prefer another scheme, Bamboo offers a range of variables.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-azure-devops-pipelines/index.html b/create-deployments-from-azure-devops-pipelines/index.html new file mode 100644 index 0000000000..e33cf8b474 --- /dev/null +++ b/create-deployments-from-azure-devops-pipelines/index.html @@ -0,0 +1,733 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from Azure DevOps Pipelines + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from Azure DevOps Pipelines

    + +

    Notifying elmah.io about new deployments is possible as a build step in Azure DevOps, by adding a bit of PowerShell.

    +

    Using YAML

    +
      +
    1. +

      Edit your build definition YAML file.

      +
    2. +
    3. +

      If not already shown, open the assistant by clicking the Show assistant button.

      +
    4. +
    5. +

      Search for 'powershell'.

      +
    6. +
    7. +

      Click the PowerShell task.

      +
    8. +
    9. +

      Select the Inline radio button and input the following script:

      +
    10. +
    +

    $ProgressPreference = "SilentlyContinue"
    +
    +$url = "https://api.elmah.io/v3/deployments?api_key=API_KEY"
    +$body = @{
    +  version = "$env:BUILD_BUILDNUMBER"
    +  description = "$env:BUILD_SOURCEVERSIONMESSAGE"
    +  userName = "$env:BUILD_REQUESTEDFOR"
    +  userEmail = "$env:BUILD_REQUESTEDFOREMAIL"
    +  logId = "LOG_ID"
    +}
    +[Net.ServicePointManager]::SecurityProtocol = `
    +    [Net.SecurityProtocolType]::Tls12,
    +    [Net.SecurityProtocolType]::Tls11,
    +    [Net.SecurityProtocolType]::Tls
    +Invoke-RestMethod -Method Post -Uri $url -Body $body
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the id of the log representing the application deployed by this build configuration.

    +

    Click the Add button and the new task will be added to your YAML definition. You typically want to move the deployment task to the last placement in tasks.

    +

    Using Classic editor

    +
      +
    1. +

      Edit the build definition currently building your project(s).

      +
    2. +
    3. +

      Click the Add task button and locate the PowerShell task. Click Add. +Add PowerShell task

      +
    4. +
    5. +

      Fill in the details as shown in the screenshot. +Fill in PowerShell content

      +
    6. +
    +

    ... and here's the code from the screenshot above:

    +

    $ProgressPreference = "SilentlyContinue"
    +
    +$url = "https://api.elmah.io/v3/deployments?api_key=API_KEY"
    +$body = @{
    +  version = "$env:BUILD_BUILDNUMBER"
    +  description = "$env:BUILD_SOURCEVERSIONMESSAGE"
    +  userName = "$env:BUILD_REQUESTEDFOR"
    +  userEmail = "$env:BUILD_REQUESTEDFOREMAIL"
    +  logId = "LOG_ID"
    +}
    +[Net.ServicePointManager]::SecurityProtocol = `
    +    [Net.SecurityProtocolType]::Tls12,
    +    [Net.SecurityProtocolType]::Tls11,
    +    [Net.SecurityProtocolType]::Tls
    +Invoke-RestMethod -Method Post -Uri $url -Body $body
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the id of the log representing the application deployed by this build configuration.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-azure-devops-releases/index.html b/create-deployments-from-azure-devops-releases/index.html new file mode 100644 index 0000000000..7df61d8b3b --- /dev/null +++ b/create-deployments-from-azure-devops-releases/index.html @@ -0,0 +1,670 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from Azure DevOps Releases + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from Azure DevOps Releases

    +

    If you are using Releases in Azure DevOps, you should use our extension to notify elmah.io about new deployments. To install and configure the extension, follow the simple steps below:

    +
      +
    1. Go to the elmah.io Deployment Tasks extension on the Azure DevOps Marketplace and click the Get it free button:
    2. +
    +

    Install the extension

    +
      +
    1. Select your organization and click the Install button:
    2. +
    +

    Select organization

    +
      +
    1. Go to your Azure DevOps project and add the elmah.io Deployment Notification task. Fill in all fields as shown here:
    2. +
    +

    Add the task

    +

    You will need to replace API_KEY with an API key (Where is my API key?) with permission (How to configure API key permissions) to create deployments. If the deployment is specific to a single log, insert a log ID (Where is my log ID?) with the ID of the log instead of LOG_ID. Deployments without a log ID will show on all logs in the organization.

    +

    That's it! Azure DevOps will now notify elmah.io every time the release pipeline is executed.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-bitbucket-pipelines/index.html b/create-deployments-from-bitbucket-pipelines/index.html new file mode 100644 index 0000000000..18d4808874 --- /dev/null +++ b/create-deployments-from-bitbucket-pipelines/index.html @@ -0,0 +1,667 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from Bitbucket Pipelines + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from Bitbucket Pipelines

    +

    Pipelines use scripts, embedded in YAML files, to configure the different steps required to build and deploy software. To notify elmah.io as part of a build/deployment, the first you will need to do is to add your API key as a secure environment variable. To do so, go to Settings | Workspace Settings | Workspace variables and add a new variable:

    +

    Add environment variable

    +

    Where is my API key?

    +

    Then add a new script to your build YAML-file after building and deploying your software:

    +

    pipelines:
    +  default:
    +    - step:
    +        script:
    +          # ...
    +          - curl -X POST -d "{\"version\":\"$BITBUCKET_BUILD_NUMBER\"}" -H "Content-Type:application/json" https://api.elmah.io/v3/deployments?api_key=$ELMAHIO_APIKEY
    +

    +

    The script uses curl to invoke the elmah.io Deployments endpoint with the API key ($ELMAHIO_APIKEY) and a version number ($BITBUCKET_BUILD_NUMBER). The posted JSON can be extended to support additional properties like a changelog and the name of the person triggering the deployment. Check out the API documentation for details.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-cli/index.html b/create-deployments-from-cli/index.html new file mode 100644 index 0000000000..4365fa002d --- /dev/null +++ b/create-deployments-from-cli/index.html @@ -0,0 +1,665 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from the elmah.io CLI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from the elmah.io CLI

    +

    Deployments can be easily created from either the command-line or a build server using the elmah.io CLI. There's a help page dedicated to the deployment command but here's a quick recap.

    +

    If not already installed, start by installing the elmah.io CLI:

    +

    dotnet tool install --global Elmah.Io.Cli
    +

    +

    Then, create a new deployment using the deployment command:

    +

    elmahio deployment --apiKey API_KEY --version 1.0.0
    +

    +

    In case you are calling the CLI from a build server, you may want to exclude the elmah.io logo and copyright message using the --nologo parameter to reduce log output and to avoid cluttering the build output:

    +

    elmahio deployment --nologo --apiKey API_KEY --version 1.0.0
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-github-actions/index.html b/create-deployments-from-github-actions/index.html new file mode 100644 index 0000000000..c29bf94ccf --- /dev/null +++ b/create-deployments-from-github-actions/index.html @@ -0,0 +1,735 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from GitHub Actions + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from GitHub Actions

    +

    GitHub Actions is a great platform for building and releasing software. To notify elmah.io when you deploy a new version of your project, you will need an additional step in your build definition. Before you do that, start by creating new secrets:

    +
      +
    1. +

      Go to your project on GitHub.

      +
    2. +
    3. +

      Click the Settings tab.

      +
    4. +
    5. +

      Click the Secrets navigation item.

      +
    6. +
    7. +

      Click New repository secret.

      +
    8. +
    9. +

      Name the secret ELMAH_IO_API_KEY.

      +
    10. +
    11. +

      Insert your elmah.io API key in Value (Where is my API key?). Make sure to use an API key that includes the Deployments | Write permission (How to configure API key permissions).

      +
    12. +
    13. +

      Click Add secret

      +
    14. +
    15. +

      Do the same for your elmah.io log ID but name it ELMAH_IO_LOG_ID (Where is my log ID?).

      +
    16. +
    17. +

      Insert the following step as the last one in your YAML build specification:

      +
    18. +
    +

    - name: Create Deployment on elmah.io
    +  uses: elmahio/github-create-deployment-action@v1
    +  with:
    +    apiKey: ${{ secrets.ELMAH_IO_API_KEY }}
    +    version: ${{ github.run_number }}
    +    logId: ${{ secrets.ELMAH_IO_LOG_ID }}
    +

    +

    The configuration will automatically notify elmah.io every time the build script is running. The build number (github.run_number) is used as the version for this sample, but you can modify this if you prefer another scheme.

    +

    Here's a full overview of properties:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameRequiredDescription
    apiKey✔️An API key with permission to create deployments.
    version✔️The version number of this deployment. The value of version can be a SemVer compliant string or any other syntax that you are using as your version numbering scheme. You can use ${{ github.run_number }} to use the build number as the version or you can pick another scheme or combine the two.
    descriptionOptional description of this deployment. Can be markdown or cleartext. The latest commit message can be used as the description by using ${{ github.event.head_commit.message }}.
    userNameThe name of the person responsible for creating this deployment. This can be set manually or dynamically using the ${{ github.actor }} variable.
    userEmailThe email of the person responsible for creating this deployment. There doesn't seem to be a way to pull the email responsible for triggering the build through variables, why this will need to be set manually.
    logIdAs default, deployments are attached to all logs of the organization. If you want a deployment to attach to a single log only, set this to the ID of that log.
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-kudu/index.html b/create-deployments-from-kudu/index.html new file mode 100644 index 0000000000..32960f46c5 --- /dev/null +++ b/create-deployments-from-kudu/index.html @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from Kudu + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from Kudu

    +

    Kudu is the engine behind Git deployments on Microsoft Azure. To create a new elmah.io deployment every time you deploy a new app service to Azure, add a new post-deployment script by navigating your browser to https://yoursite.scm.azurewebsites.net where yoursite is the name of your Azure website. Click the Debug console and navigate to site\deployments\tools\PostDeploymentActions (create it if it doesn't exist).

    +

    To create the new PowerShell file, write the following in the prompt:

    +

    touch CreateDeployment.ps1
    +

    +

    With a post-deployment script running inside Kudu, we can to extract some more information about the current deployment. A full deployment PowerShell script for Kudu would look like this:

    +

    $version = Get-Date -format u
    +
    +(Get-Content ..\wwwroot\web.config).replace('$version', $version) | Set-Content ..\wwwroot\web.config
    +
    +$ProgressPreference = "SilentlyContinue"
    +
    +$commit = [System.Environment]::GetEnvironmentVariable("SCM_COMMIT_MESSAGE");
    +$commitId = [System.Environment]::GetEnvironmentVariable("SCM_COMMIT_ID");
    +$httpHost = [System.Environment]::GetEnvironmentVariable("HTTP_HOST");
    +$deployUrl = "https://$httpHost/api/deployments/$commitId"
    +
    +$username = "MY_USERNAME"
    +$password = "MY_PASSWORD"
    +$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
    +
    +$deployInfo = Invoke-RestMethod -Method Get -Uri $deployUrl -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)}
    +
    +$url = 'https://api.elmah.io/v3/deployments?api_key=API_KEY'
    +$body = @{
    +  version = $version
    +  description = $commit
    +  userName = $deployInfo.author
    +  userEmail = $deployInfo.author_email
    +}
    +
    +[Net.ServicePointManager]::SecurityProtocol = `
    +    [Net.SecurityProtocolType]::Tls12,
    +    [Net.SecurityProtocolType]::Tls11,
    +    [Net.SecurityProtocolType]::Tls
    +Invoke-RestMethod -Method Post -Uri $url -Body $body
    +

    +

    (replace MY_USERNAME and MY_PASSWORD with your Azure deployment credentials and API_KEY with your elmah.io API key located on your organization settings page)

    +

    The script generates a new version string from the current date and time. How you want your version string looking, is really up to you. To fetch additional information about the deployment, the Kudu deployments endpoint is requested with the current commit id. Finally, the script creates the deployment using the elmah.io REST API.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-octopus-deploy/index.html b/create-deployments-from-octopus-deploy/index.html new file mode 100644 index 0000000000..6a35085d0c --- /dev/null +++ b/create-deployments-from-octopus-deploy/index.html @@ -0,0 +1,673 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from Octopus Deploy + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from Octopus Deploy

    +

    Notifying elmah.io of a new deployment from Octopus Deploy is supported through a custom step template. The step template can be installed in multiple ways as explained on Community step templates. In this document, the step template will be installed directly from the Process Editor:

    +
      +
    1. +

      Go to the Process Editor and click the ADD STEP button. In the Choose Step Template section search for 'elmah.io': +Search step template

      +
    2. +
    3. +

      Hover over the 'elmah.io - Register Deployment' community template and click the INSTALL AND ADD button.

      +
    4. +
    5. +

      In the Install and add modal click the SAVE button.

      +
    6. +
    7. +

      The step template is now added to the process. Fill in your API key (Where is my API key?) and log ID (Where is my log ID?) in the step template fields and click the SAVE button: +Fill in fields

      +
    8. +
    +

    And we're done. On every new deployment, Octopus Deploy will notify elmah.io. In case you want an alternative version naming scheme, the Version field in the step template can be used to change the format.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-powershell/index.html b/create-deployments-from-powershell/index.html new file mode 100644 index 0000000000..b96379fc06 --- /dev/null +++ b/create-deployments-from-powershell/index.html @@ -0,0 +1,681 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from PowerShell + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from PowerShell

    +

    If you release your software using a build or deployment server, creating the new release is easy using a bit of PowerShell. To request the deployments endpoint, write the following PowerShell script:

    +

    $version = "1.42.7"
    +$ProgressPreference = "SilentlyContinue"
    +$url = 'https://api.elmah.io/v3/deployments?api_key=API_KEY'
    +$body = @{
    +  version = $version
    +}
    +[Net.ServicePointManager]::SecurityProtocol = `
    +    [Net.SecurityProtocolType]::Tls12,
    +    [Net.SecurityProtocolType]::Tls11,
    +    [Net.SecurityProtocolType]::Tls
    +Invoke-RestMethod -Method Post -Uri $url -Body $body
    +

    +

    (replace API_KEY with your API key found on your organization settings page)

    +

    In the example, a simple version string is sent to the API and elmah.io will automatically put a timestamp on that. Overriding user information and description make the experience within the elmah.io UI better. Pulling release notes and the name and email of the deployer, is usually available through environment variables or similar, depending on the technology used for creating the deployment.

    +

    Here's an example of a full payload for the create deployment endpoint:

    +

    $body = @{
    +  version = "1.0.0"
    +  created = [datetime]::UtcNow.ToString("o")
    +  description = "my deployment"
    +  userName = "Thomas"
    +  userEmail = "thomas@elmah.io"
    +  logId = "39e60b0b-21b4-4d12-8f09-81f3642c64be"
    +}
    +

    +

    In this example, the deployment belongs to a single log why the logId property is set. The description property can be used to include a changelog or similar. Markdown is supported.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/create-deployments-from-umbraco-cloud/index.html b/create-deployments-from-umbraco-cloud/index.html new file mode 100644 index 0000000000..a2c871b4c1 --- /dev/null +++ b/create-deployments-from-umbraco-cloud/index.html @@ -0,0 +1,703 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Create deployments from Umbraco Cloud + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Create deployments from Umbraco Cloud

    +

    Umbraco Cloud uses Azure to host Umbraco websites, so supporting deployment tracking pretty much corresponds to the steps specified in Using Kudu. Navigate to https://your-umbraco-site.scm.s1.umbraco.io where your-umbraco-site is the name of your Umbraco site. Click the Debug console link and navigate to site\deployments\tools\PostDeploymentActions\deploymenthooks (create it if it doesn't exist). Notice the folder deploymenthooks, which is required for your scripts to run on Umbraco Cloud.

    +

    Unlike Kudu, Umbraco Cloud only executes cmd and bat files. Create a new cmd file:

    +

    touch create-deployment.cmd
    +

    +

    with the following content:

    +

    echo "Creating elmah.io deployment"
    +
    +cd %POST_DEPLOYMENT_ACTIONS_DIR%
    +
    +cd deploymenthooks
    +
    +powershell -command ". .\create-deployment.ps1"
    +

    +

    The script executes a PowerShell script, which we will create next:

    +

    touch create-deployment.ps1
    +

    +

    The content of the PowerShell script looks a lot like in Using Kudu, but with some minor tweaks to support Umbraco Cloud:

    +

    $version = Get-Date -format u
    +
    +$ProgressPreference = "SilentlyContinue"
    +
    +$commitId = [System.Environment]::GetEnvironmentVariable("SCM_COMMIT_ID");
    +$deployUrl = "https://your-umbraco-site.scm.s1.umbraco.io/api/deployments/$commitId"
    +
    +$username = "MY_USERNAME"
    +$password = "MY_PASSWORD"
    +$logId = "LOG_ID"
    +$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
    +
    +$deployInfo = Invoke-RestMethod -Method Get -Uri $deployUrl -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)}
    +
    +$url = 'https://api.elmah.io/v3/deployments?api_key=API_KEY'
    +$body = @{
    +  version = $version
    +  description = $deployInfo.message
    +  userName = $deployInfo.author
    +  userEmail = $deployInfo.author_email
    +  logId = $logId
    +}
    +
    +[Net.ServicePointManager]::SecurityProtocol = `
    +    [Net.SecurityProtocolType]::Tls12,
    +    [Net.SecurityProtocolType]::Tls11,
    +    [Net.SecurityProtocolType]::Tls
    +Invoke-RestMethod -Method Post -Uri $url -Body $body
    +

    +

    Replace your-umbraco-site with the name of your site, MY_USERNAME with your Umbraco Cloud username, MY_PASSWORD with your Umbraco Cloud password, LOG_ID with the id if the elmah.io log that should contain the deployments (Where is my log ID?), and finally API_KEY with your elmah.io API key, found and your organization settings page.

    +

    There you go. When deploying changes to your Umbraco Cloud site, a new deployment is automatically created on elmah.io.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/creating-rules-to-perform-actions-on-messages/index.html b/creating-rules-to-perform-actions-on-messages/index.html new file mode 100644 index 0000000000..5deb2fcb8d --- /dev/null +++ b/creating-rules-to-perform-actions-on-messages/index.html @@ -0,0 +1,750 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Creating Rules to Perform Actions on Messages + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Creating Rules to Perform Actions on Messages

    +

    elmah.io comes with a great rule engine for performing various actions when messages are logged in your log.

    +

    This guide is also available as a short video tutorial here:

    +

    + Ignore filters and rules + +

    +

    The rule engine is located beneath each log on the log settings page:

    +

    Rules Tab

    +

    A rule consists of three parts: a title, a query, and an action.

    +

    The title should be a short text explaining what this rule does. We don't use the title for anything, so please write something that helps you identify rules and to keep them apart.

    +

    The query should contain either a full-text search string or a Lucene query. When new messages are logged, the message is matched up against all queries registered on that log. If and only if a message matches a query, the action registered on the rule is performed.

    +

    As mentioned above, the action part of a rule is executed when a message matches the query specified in the same rule. An action can be one of four types: Ignore, Hide, Mail, and HTTP Request. To illustrate how to use each action type, here are four examples of useful rules.

    +

    Ignore errors with an HTTP status code of 400

    +
    +

    Be aware that Ignore rules are only meant as a temporary way of ignoring messages. In case you want to permanently ignore one or more log messages, use client-side filtering as explained in the documentation for each client integration. In addition, there's a client-side filtering help dialog available on the log message details toolbar. Ignoring a large number of messages with Ignore rules will slow down your application logging, use unnecessary network bandwidth, and risk hitting the elmah.io API request limit.

    +
    +

    To ignore all messages with an HTTP status code of 400, you would need to set up the following:

    + + + + + + + + + + + + + + + +
    TitleQueryThen
    Ignore 400sstatusCode:400Ignore
    +

    The rule would look like this in the UI:

    +

    Then Ignore

    +

    Hide warnings

    +

    To hide all messages with a severity of Warning, you would need to set up the following:

    + + + + + + + + + + + + + + + +
    TitleQueryThen
    Hide Warningsseverity:WarningHide
    +

    The rule would look like this in the UI:

    +

    Then Ignore

    +

    Send an email on all messages containing a word

    +

    To send an email on all messages containing the word billing somewhere, you would need to set up the following:

    + + + + + + + + + + + + + + + +
    TitleQueryThen
    Mail on billingbillingEmail
    +

    The rule would look like this in the UI:

    +

    Then Email

    +

    Make an HTTP request on all new and fatal messages

    +

    To make an HTTP request on every new message with a severity of Fatal, you would need to set up the following:

    + + + + + + + + + + + + + + + +
    TitleQueryThen
    Request on new fatalisNew:true AND severity:FatalHTTP
    +

    The rule would look like this in the UI:

    +

    Then HTTP

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-and-custom-errors/index.html b/elmah-and-custom-errors/index.html new file mode 100644 index 0000000000..176aec9522 --- /dev/null +++ b/elmah-and-custom-errors/index.html @@ -0,0 +1,677 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + ELMAH and custom errors + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    ELMAH and custom errors

    +

    ELMAH and ASP.NET (MVC) custom errors aren't exactly known to be best friends. Question after question has been posted on forums like Stack Overflow, from people having problems with ELMAH, when custom errors are configured. These problems make perfect sense since both ELMAH and custom errors are designed to catch errors and do something about them.

    +

    Before looking at some code, we recommend you to read Web.config customErrors element with ASP.NET explained and Demystifying ASP.NET MVC 5 Error Pages and Error Logging. Together, the posts are a great introduction to different ways of implementing custom error pages in ASP.NET MVC.

    +

    Back to ELMAH. In most implementations of custom error pages, ASP.NET swallows any uncaught exceptions, putting ELMAH out of play. To overcome this issue, you can utilize MVC's IExceptionFilter to log all exceptions, whether or not it is handled by a custom error page:

    +

    public class ElmahExceptionLogger : IExceptionFilter
    +{
    +    public void OnException (ExceptionContext context)
    +    {
    +        if (context.ExceptionHandled)
    +        {
    +            ErrorSignal.FromCurrentContext().Raise(context.Exception);
    +        }
    +    }
    + }
    +

    +

    The OnException method on ElmahExceptionLogger is executed every time an error is happening, by registering it in Application_Start:

    +

    protected void Application_Start()
    +{
    +    // ...
    +    GlobalConfiguration.Configuration.Filters.Add(new ElmahExceptionLogger());
    +    // ...
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-and-elmah-io-differences/index.html b/elmah-and-elmah-io-differences/index.html new file mode 100644 index 0000000000..6e8e7b36e0 --- /dev/null +++ b/elmah-and-elmah-io-differences/index.html @@ -0,0 +1,759 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + ELMAH and elmah.io differences + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    ELMAH and elmah.io differences

    +

    We receive a lot of questions like these:

    +
      +
    • What is the difference between ELMAH and elmah.io?
    • +
    • I thought ELMAH was free. Why do you suddenly charge?
    • +
    • My ELMAH SQL Server configuration doesn't work. Why not?
    • +
    +

    We understand the confusion. The purpose of this article is to give a bit of background of the differences between ELMAH and elmah.io and why they share similar names.

    +

    What is ELMAH?

    +

    ELMAH is an error logging framework originally developed by Atif Aziz able to log all unhandled exceptions from .NET web applications. Errors can be logged to a variety of destinations through ELMAH’s plugin model called error logs. Plugins for XML, SQL Server, MySQL, Elasticsearch, and many more exists. ELMAH automatically collects a lot of information from the HTTP context when logging the error, giving you the possibility to inspect request parameters, cookies, and much more for the failed request. Custom errors can be logged to ELMAH, by manually calling the error log.

    +

    What is elmah.io?

    +

    elmah.io is a cloud-based error management system originally developed on top of ELMAH (see history for details). Besides supporting ELMAH, elmah.io also integrates with popular logging frameworks like log4net, NLog, Serilog, and web frameworks like ASP.NET Core. elmah.io offers a superior notification model to ELMAH, with integrations to mail, Slack, Microsoft Teams, and many others. elmah.io also built a lot of features outside the scope of ELMAH, like a complete uptime monitoring system.

    +

    Comparison

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FeatureELMAHelmah.io
    Error Logging
    Self-hosted
    Cloud-hosted
    Search
    New error detection
    Error grouping
    Issue tracking
    log4net/NLog/Serilog
    Clientside error logging
    Slack/Teams/HipChat/etc.
    Deployment tracking
    Uptime monitoring
    Heartbeats
    Machine learning
    Discount on popular software
    +

    History

    +

    So, why name a service elmah.io, when only a minor part of a client integration uses ELMAH? When elmah.io was introduced back in 2013, the intention was to create a cloud-based error logger for ELMAH. We had some simple search and graphing possibilities, but the platform was meant as an alternative to host your own errors logs in SQL Server or similar.

    +

    In time, elmah.io grew from being a hobby project to an actual company. During those years, we realized that the potential of the platform exceeded the possibilities with ELMAH in many ways. New features not available in ELMAH have been added constantly. A process that would have been nearly impossible with ELMAH's many storage integrations.

    +

    Today, elmah.io is a full error management system for everything from console applications to web apps and serverless code hosted on Azure or AWS. We've built an entire uptime monitoring system, able to monitor not only if your website fails but also if it even responds to requests.

    +

    Why not change the name to something else, you may be thinking? That is our wish as well. But changing your SaaS (software-as-a-service) company name isn't exactly easy. We have tried a couple of times, the first time back in 2016. We tried to name the different major features of elmah.io to sea creatures (like Stingray). We failed with the rename and people got confused. In 2017, we started looking at renaming the product again. This time to Unbug. We had learned from our previous mistake and this time silently started changing the name. We quickly realized that the domain change would cause a major risk in regards to SEO (search engine optimization) and confusion.

    +

    For now, we are elmah.io. The name is not ideal, but it's a lesson learned for another time :)

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-azure-boards/index.html b/elmah-io-apps-azure-boards/index.html new file mode 100644 index 0000000000..fa72e20c47 --- /dev/null +++ b/elmah-io-apps-azure-boards/index.html @@ -0,0 +1,667 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with Azure DevOps Boards + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install Azure Boards App for elmah.io

    +

    Get your personal access token

    +

    To create bugs on Azure Boards, you will need to generate a personal access token. Go to Azure DevOps and click the User settings icon in the top right corner. Select the Personal access tokens menu item in the dropdown. Finally, click the New Token button and fill in the details as shown below:

    +

    Create personal access token

    +

    For this example, we have picked 90 days expiration period, but you can decide on a shorter or longer period if you'd like. Remember to enable the Read & write scope under Work Items. Next, click the Create button and copy the generated token.

    +
    +

    Bugs created by elmah.io will have the CreatedBy set to the user generating the personal access token. If you want to identify bugs created by elmah.io, you should create the token from a new user (like elmahio@yourdomain.com).

    +
    +

    Install the Azure Boards App on elmah.io

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Azure Boards app and click the Install button:

    +

    Install Azure Boards App

    +

    Paste the token copied in the previous step into the Token textbox. In the Organization textbox, input the name of your organization. For https://dev.azure.com/myorg/myproject, the organization name would be myorg. In the Project textbox, input the name of the project containing your board. For https://dev.azure.com/myorg/myproject, the project name would be myproject. If you want to embed all bugs created by the app beneath an overall work item, epic, or similar, fill in the optional ID in the Parent field.

    +

    Click Save and the app is added to your log. When new errors are logged, bugs are automatically created in the configured Azure Board.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-bitbucket/index.html b/elmah-io-apps-bitbucket/index.html new file mode 100644 index 0000000000..f032114998 --- /dev/null +++ b/elmah-io-apps-bitbucket/index.html @@ -0,0 +1,665 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with Atlassian Bitbucket + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install Bitbucket App for elmah.io

    +

    Get your App password

    +

    To allow elmah.io to create issues on Bitbucket, you will need an App password. App passwords can be generated by clicking your user in the top right corner and selecting Personal settings. In the left menu, click the App passwords page (https://bitbucket.org/account/settings/app-passwords/). To create a new password, click the Create app password button and input the following information:

    +

    Add app password

    +

    elmah.io only need the Issues - Write permission to create issues. To test the inputted values on elmah.io (later step) also check the Repositories - Read permission.

    +

    After clicking the Create button, copy the generated app password.

    +

    Install the Bitbucket App on elmah.io

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Bitbucket app and click the Install button:

    +

    Install Bitbucket App

    +

    Paste the App password copied in the previous step into the APP PASSWORD textbox. In the TEAM textbox, input the name of the team/workspace owning the repository you want to create issues in. In the REPOSITORY textbox input the name of the repository. In the USERNAME textbox, input the name of the user generating the App password. In older installations, this can also contain the team/workspace name.

    +

    Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured Bitbucket repository.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-botbuster/index.html b/elmah-io-apps-botbuster/index.html new file mode 100644 index 0000000000..a540cf271d --- /dev/null +++ b/elmah-io-apps-botbuster/index.html @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Identify and ignore log messages from bots + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install BotBuster App for elmah.io

    +
    +

    The BotBuster app is deprecated. Enable the Filter Crawlers toggle on the Filters tab to ignore errors generated by crawlers.

    +
    +

    The BotBuster app for elmah.io identifies and ignores messages generated by white hat bots like spiders, search engine bots, and similar. Under normal circumstances, you want to allow access for white hat bots, but you don't want to get a notification every time one of them tries to request a resource not found on the server.

    +

    Installing BotBuster couldn't be simpler. Log into elmah.io and go to the log settings. Click the Apps tab. Locate the BotBuster app and click the Install button.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-chatgpt/index.html b/elmah-io-apps-chatgpt/index.html new file mode 100644 index 0000000000..cabc2db49f --- /dev/null +++ b/elmah-io-apps-chatgpt/index.html @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with ChatGPT + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install ChatGPT for elmah.io

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the ChatGPT app and click the Install button:

    +

    Install ChatGPT App

    +

    Input your OpenAI API key (Where do I find my OpenAI API Key?). Next, select which language model to use. We currently support GPT-3.5-Turbo and GPT-4.

    +

    As a default, elmah.io will only share the stack trace of an error with ChatGPT when you click the Get suggestion button in the elmah.io UI. If you want to include the source code and/or any SQL attached to the error, you can enable one or both toggles. Sharing the source will require you to bundle your source code alongside errors as documented here: How to include source code in log messages.

    +

    Click Save and the app is added to your log. When you open errors valid for ChatGPT help, you will see a tab named AI next to Detail, Inspector, etc.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-clickup/index.html b/elmah-io-apps-clickup/index.html new file mode 100644 index 0000000000..40af21b783 --- /dev/null +++ b/elmah-io-apps-clickup/index.html @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with ClickUp + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install ClickUp for elmah.io

    +

    Log into elmah.io and go to the log settings page. Click the Apps tab. Locate the ClickUp app and click the Install button:

    +

    Install ClickUp App

    +

    You will need to input a ClickUp API token and the ID of the list to create tasks. The API token can be generated by navigating to ClickUp, clicking the profile photo in the bottom left corner, and clicking Apps. It is important to click the Apps link beneath your profile and not the ClickApps link beneath the team. On the Apps page, you can generate and copy a new token beneath the API Token section.

    +

    The list ID can be found by going to the list on the ClickUp app and clicking the list name:

    +

    Copy link

    +

    When copying the link you will get a link similar to this:

    +

    https://app.clickup.com/.../v/li/901200300647
    +

    +

    The list ID is the last part of the URL (901200300647 in the example above).

    +

    When both the API token and list ID are inputted on elmah.io, click the Test button to test the values. When the Test button turns green, click the Save button, and the app is added to your log. When new errors are logged, tasks are automatically created in the configured ClickUp list.

    +

    Troubleshooting

    +

    If errors aren't showing up in ClickUp, please check that the following are all true:

    +
      +
    • When clicking the Test button on the ClickUp app settings screen, the button turns green.
    • +
    • There's a message logged in the log where you set up the ClickUp integration.
    • +
    • The message is marked as new (yellow star next to the title on the search result).
    • +
    • The message is either of severity Error or Fatal.
    • +
    +

    To trigger an error manually, go to https://api.elmah.io/swagger/index.html and input an elmah.io API key with the Messages - Write permission enabled. Expand Messages and the POST node with the URL /v3/messages/{logId}. Input your log ID and the following JSON:

    +

    {
    +  "title": "This is a test",
    +  "severity": "Error"
    +}
    +

    +

    Finally, click the Try it out! button and verify that the API returns a status code of 201. The new error should show up in ClickUp. If testing this multiple times, you will need to make small adjustments to the title field inside the JSON, for additional errors to be marked as new.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-github/index.html b/elmah-io-apps-github/index.html new file mode 100644 index 0000000000..f2c64fc850 --- /dev/null +++ b/elmah-io-apps-github/index.html @@ -0,0 +1,665 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with GitHub + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install GitHub App for elmah.io

    +

    Generate Personal Access Token

    +

    To allow elmah.io to create issues on GitHub, you need a Personal Access Token. Sign in to GitHub, click your profile photo in the top right corner, and click Settings. On the Settings page click Developer settings followed by Personal access token. Here you can create a new token by clicking the Generate new token (classic) button:

    +

    OAuth Tokens Page

    +

    Input a token note and select an expiration date. If the repository you want issues created in is public, make sure to check the public_repo checkbox. If the repository is private, check the repo checkbox. Finally, click the Generate token button, and copy the generated token (colored with a green background)

    +

    GitHub also supports fine-grained personal access tokens. This token can also be used on elmah.io. Make sure to select Read and write in the Issues permission.

    +

    Install the GitHub App on elmah.io

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the GitHub app and click the Install button:

    +

    Install GitHub App

    +

    Paste the token copied in the previous step into the Token textbox. In the Owner textbox, input the name of the user or organization owning the repository you want to create issues in. In the Repository textbox input the name of the repository.

    +

    Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured GitHub repository.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-gitlab/index.html b/elmah-io-apps-gitlab/index.html new file mode 100644 index 0000000000..8866bddbf9 --- /dev/null +++ b/elmah-io-apps-gitlab/index.html @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with GitLab + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install GitLab App for elmah.io

    +

    Generate Personal Access Token

    +

    To allow elmah.io to create issues on GitLab, you will need to generate a Personal Access Token. To do so, log into GitLab, click your profile photo in the top right corner, and select Preferences. On the Preferences page click the Access Tokens menu item:

    +

    GitLab Tokens Page

    +

    Input a token name, expiration date, and check the api checkbox. Click the Create personal access token button and copy the generated token.

    +

    Install the GitLab App on elmah.io

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the GitLab app and click the Install button:

    +

    Install GitLab App

    +

    Paste the token copied in the previous step into the Token textbox. In the Project textbox, input the ID or name of the project you want issues created on. If you are self-hosting GitLab, input your custom URL in the URL textbox (for example https://gitlab.hooli.com).

    +

    Click the Test button and observe it turn green. When clicking Save, the app is added to your log. When new errors are logged, issues are automatically created in the configured GitLab project.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-hipchat/index.html b/elmah-io-apps-hipchat/index.html new file mode 100644 index 0000000000..0353913101 --- /dev/null +++ b/elmah-io-apps-hipchat/index.html @@ -0,0 +1,670 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Install HipChat App for elmah.io + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install HipChat App for elmah.io

    +

    Generate OAuth 2 Token

    +

    To allow elmah.io to log messages to HipChat, you will need to generate an OAuth 2 token. To do so, log into HipChat and go to the API Access page (replace elmahio with your subdomain).

    +

    OAuth Tokens Page

    +

    Input a label, click the Create button and copy the generated token.

    +
    +

    If you want to test your configuration using the Test button on the elmah.io UI, you will need to select both Send Notification and View Room in Scopes.

    +
    +

    Install the HipChat App on elmah.io

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the HipChat app and click the Install button:

    +

    Install HipChat App

    +

    Paste the token copied in the previous step into the Token textbox. In the Room textbox, input the name of the HipChat chat room you want messages from elmah.io to show up in.

    +

    Click Save and the app is added to your log. When new errors are logged, messages start appearing in the chat room that you configured.

    +
    +

    HipChat doesn't allow more than 500 requests per 5 minutes. If you generate more messages to elmah.io, not all of them will show up in HipChat because of this.

    +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-ipfilter/index.html b/elmah-io-apps-ipfilter/index.html new file mode 100644 index 0000000000..688765a8ba --- /dev/null +++ b/elmah-io-apps-ipfilter/index.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Ignore log messages from one or more IPs + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install IP Filter App for elmah.io

    +
    +

    The IP Filter app is deprecated. The IP Filter filter on the Filters tab offers more advanced IP filtering.

    +
    +

    The IP Filter app for elmah.io automatically ignores messages from one or more IP addresses. This is a great way to ignore errors generated by both crawlers and errors generated by you.

    +

    To install IP Filter, click the Install button on the Apps tab. This will show the IP Filter settings page:

    +

    IP Filter Settings

    +

    To ignore messages from a single IP address, input the IP in both the From and To fields. To ignore messages from a range of IP addresses, input the start and end IP address in the From and To fields. Both IP addresses are included in the ignored range.

    +

    The IP Filter app ignores every message matching the specified IP range. This means that if you are logging something like Information messages through Serilog or similar, these messages are also ignored. For a message to have an IP, you will need to specify a server variable named REMOTE_ADDR when creating the message. This variable is automatically added (if available) when using the integration for ELMAH.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-jira/index.html b/elmah-io-apps-jira/index.html new file mode 100644 index 0000000000..d6c09733ef --- /dev/null +++ b/elmah-io-apps-jira/index.html @@ -0,0 +1,677 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with Atlassian Jira + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install Jira App for elmah.io

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Jira app and click the Install button:

    +

    Install Jira App

    +

    Input your site name, which is the first part of the URL you use to log into Jira. For the URL https://elmahio.atlassian.net/, the site parameter would be elmahio. In the Project field, input the key of the project. Note that a project has both a display name and a key. The property we are looking for here is the uppercase identifier of the project.

    +

    To create issues on Jira, you will need to input the username and password of a user with permission to create issues in the project specified above. You can use your user credentials, but we recommend using a combination of your username and an API token.

    +

    To generate a new token specific for elmah.io, go to the API Tokens page on your Jira account. Then click the Create API token button and input a label of your choice. Finally, click the Create button and an API token is generated for you. Make sure to copy this token, since you won't be able to access it once the dialog is closed.

    +

    Go back to elmah.io and input your email in the Username field and the API token from the previous step in the Password field. If you don't like to use an existing user account for the integration, you can create a new Atlassian account for elmah.io and generate the API token from that account instead.

    +

    Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured Jira project.

    +

    Troubleshooting

    +

    If errors aren't showing up in Jira, please check that the following are all true:

    +
      +
    • When clicking the Test button on the Jira app settings screen, the button turns green.
    • +
    • There's a message logged in the log where you set up the Jira integration.
    • +
    • The message is marked as new (yellow star next to the title on the search result).
    • +
    • The message is either of severity Error or Fatal.
    • +
    +

    To trigger an error manually, go to https://api.elmah.io/swagger/index.html and input an elmah.io API key with the Messages - Write permission enabled. Expand Messages and the POST node with the URL /v3/messages/{logId}. Input your log ID and the following JSON:

    +

    {
    +  "title": "This is a test",
    +  "severity": "Error"
    +}
    +

    +

    Finally, click the Try it out! button and verify that the API returns a status code of 201. The new error should show up in Jira. If testing this multiple times, you will need to make small adjustments to the title field inside the JSON, for additional errors to be marked as new.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-mailman/index.html b/elmah-io-apps-mailman/index.html new file mode 100644 index 0000000000..a4770205a4 --- /dev/null +++ b/elmah-io-apps-mailman/index.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Set up mail notifications on new errors + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install Mailman App for elmah.io

    +
    +

    The Mailman app is deprecated. Use an email rule available on the Rules tab for a more advanced email integration.

    +
    +

    The Mailman app for elmah.io sends out an email to an address of your choice, every time a new error is logged.

    +

    To install Mailman, click the Install button on the Apps tab. This will show the Mailman settings page:

    +

    Mailman Settings

    +

    Input a valid email address in the Email input box and click Save.

    +

    The Mailman app will look at new errors only. Errors are defined by messages with a severity of Error or Fatal and with isNew == true. isNew is a field automatically added by elmah.io when indexing each message. isNew is calculated by looking for similarities between the new message and already logged messages.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-pagerduty/index.html b/elmah-io-apps-pagerduty/index.html new file mode 100644 index 0000000000..4acbce01c7 --- /dev/null +++ b/elmah-io-apps-pagerduty/index.html @@ -0,0 +1,712 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with PagerDuty + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install PagerDuty for elmah.io

    +

    Using the PagerDuty integration for elmah.io, you can set up advanced notification rules in PagerDuty when new errors are logged on elmah.io. Receive a phone call, text message, or one of the other options provided by PagerDuty, the second new errors are introduced on your websites or services.

    +

    To integrate elmah.io with PagerDuty, you need to set up a new integration on PagerDuty and install the PagerDuty app on elmah.io.

    +

    Setting up an integration on PagerDuty

    +
      +
    • +

      Sign in to PagerDuty.

      +
    • +
    • +

      Navigate to the Services page.

      +
    • +
    • +

      Select the service that you want to integrate to from elmah.io in the list of services.

      +
    • +
    • +

      On the Integrations tab click the Add an integration button.

      +
    • +
    • +

      On the Add Integrations page search for elmah.io and select it in the search result:

      +
    • +
    +

    Select elmah.io

    +
      +
    • +

      Click the Add button.

      +
    • +
    • +

      Expand the newly created integration:

      +
    • +
    +

    Update name

    +
      +
    • Copy the value in the Integration Key field.
    • +
    +

    Install the PagerDuty app on elmah.io

    +

    Next, the PagerDuty app needs to be installed on elmah.io.

    +
      +
    • +

      Sign in to elmah.io.

      +
    • +
    • +

      Navigate to the Log Settings page of the log you want to integrate with PagerDuty.

      +
    • +
    • +

      Go to the Apps tab.

      +
    • +
    • +

      Locate the PagerDuty app and click the Install button.

      +
    • +
    • +

      Input the Integration Key that you copied in a previous step in the INTEGRATION KEY field:

      +
    • +
    +

    Insert Integration Key on elmah.io

    +
      +
    • Click the Save button.
    • +
    +

    That's it. New errors stored in the selected log now trigger incidents in PagerDuty. To get help with this integration, make sure to reach out through the support widget on the elmah.io website.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-request-a-new-integration/index.html b/elmah-io-apps-request-a-new-integration/index.html new file mode 100644 index 0000000000..ec698f4cb5 --- /dev/null +++ b/elmah-io-apps-request-a-new-integration/index.html @@ -0,0 +1,670 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Request a new integration between elmah.io and another tool + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Request a new integration between elmah.io and another tool

    +

    We are always on the lookout for creating new and useful integrations. We don't want to integrate with everything (that's what Zapier is for), but commonly used tools by .NET web developers are on our radar.

    +

    To suggest a new integration feel free to reach out through the support widget in the lower right corner.

    +
    +

    Not all integration requests are implemented. Don't feel bad if we decide not implement your suggestion.

    +
    +

    When recommending a new integration we need the following information:

    +
      +
    • Name of the tool.
    • +
    • URL of the tool.
    • +
    • Why do you need this integration?
    • +
    • What do you expect from this integration?
    • +
    • Does the tool provide an API?
    • +
    • Anything else you think would help?
    • +
    • Do you by any chance know anyone working on the tool?
    • +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-slack/index.html b/elmah-io-apps-slack/index.html new file mode 100644 index 0000000000..e4c6c1f07f --- /dev/null +++ b/elmah-io-apps-slack/index.html @@ -0,0 +1,668 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with Slack + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install Slack App for elmah.io

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Slack app and click the Install button. You will be redirected to Slack where you need to log into your workspace if not already. Once logged in, select the channel to send messages to:

    +

    Select channel

    +

    Click the Allow button and you will be redirected back to elmah.io. The integration to Slack is now installed.

    +
    +

    Slack doesn't allow more than a single request per second. If you generate more than one message to elmah.io per second, not all of them will show up in Slack because of this.

    +
    +

    Troubleshooting

    +

    Errors don't show up in Slack. Here are a few things to try out.

    +
      +
    • Make sure that the Slack app is installed on the log as described above.
    • +
    • Only new errors are sent to Slack. A new error has a severity of Error or Fatal and is marked with a yellow star on the search tab. We only send new errors to help you stay out of Slack's API limits. If sending all errors, you could quickly end up in a scenario where the same error is sent multiple times and more important errors get ignored by Slack.
    • +
    • Make sure that your token is still valid. The only way to resolve an issue where the token is no longer valid is to re-install the Slack app.
    • +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-teams/index.html b/elmah-io-apps-teams/index.html new file mode 100644 index 0000000000..c644f6e8cb --- /dev/null +++ b/elmah-io-apps-teams/index.html @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with Microsoft Teams + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install Microsoft Teams App for elmah.io

    +

    To install the integration with Microsoft Teams, go to teams and click the Apps menu item. Search for "elmah.io" and click the app:

    +

    Search for elmah.io

    +

    Click the Add to a team button. In the dropdown, search for your team and/or channel:

    +

    Search for elmah.io

    +

    Click the Set up a connector button.

    +

    A new webhook URL is generated. Click the Copy Text button followed by the Save button:

    +

    Copy the webhook URL

    +

    The elmah.io integration is now configured on Microsoft Teams and you should see the following screen:

    +

    Configured

    +

    The final step is to input the webhook URL that you just copied, into elmah.io.

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Microsoft Teams app and click the Install button. In the overlay, paste the URL from the previous step:

    +

    Install Microsoft Teams app

    +

    Click Save and the app is added to your log. When new errors are logged, messages start appearing in the channel that you configured.

    +
    +

    The Office 365 API used behind the scenes for this app uses throttling rather than a maximum of allowed requests. This means that you may start experiencing messages not being sent, if you start logging a large amount of messages. We have experienced a lot of weird error codes when communicating with the API. An example of this is an exception while posting data to the API, but the data is successfully shown on Teams. The result of this error is, that elmah.io retries the failing request multiple times, which causes the same message to be shown multiple times on Teams.

    +
    +

    Troubleshooting

    +

    Errors don't show up in Teams. Here are a few things to try out.

    +
      +
    • Make sure that the Teams app is installed on the log as described above.
    • +
    • Only new errors are sent to Teams. A new error has a severity of Error or Fatal and is marked with a yellow star on the search tab. We only send new errors to help you stay out of Teams' API limits. If sending all errors, you could quickly end up in a scenario where the same error is sent multiple times and more important errors get ignored by Teams.
    • +
    • Re-install the app on elmah.io with the webhook URL provided by Teams.
    • +
    • Remove the elmah.io configuration from Teams and re-install it. After re-installing the app, you will need to copy the new webhook URL provided by Teams and input it in the elmah.io Teams app as descrived above.
    • +
    • Go to the Apps page on Teams and search for 'elmah.io'. Remove the app entirely, click F5 to refresh the page, and install the app again. You may be stuck on an older version of our app, which can be fixed by simply removing and installing the app again.
    • +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-trello/index.html b/elmah-io-apps-trello/index.html new file mode 100644 index 0000000000..ea6faa2237 --- /dev/null +++ b/elmah-io-apps-trello/index.html @@ -0,0 +1,677 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with Trello + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install Trello App for elmah.io

    +

    For elmah.io to communicate with the Trello API, we will need an API key and token. The API key is available here: https://trello.com/app-key. If you don't have a personal token available on that site, create a new Power-Up as described on the page. When the Power-Up is created, you can create a new API key on that page.

    +

    To get the token, visit the following URL in your browser: https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&name=Server%20Token&key=API_KEY. Remember to replace API_KEY with your Trello API key located in the previous step. When clicking the Allow button, Trello will generate a new token for you and show it in the browser window.

    +

    elmah.io will create cards on a board list of your choice. Unfortunately, Trello didn't provide a way to obtain list IDs. The easiest way is to open Developer Tools in your browser and click an existing card inside the list you want elmah.io to create new cards in. Locate the request for the card details in the Network tab and click the Preview tab. The list id is in the card details:

    +

    Trello list ID

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Trello app and click the Install button:

    +

    Install the Trello app

    +

    Input the API key, token, and list ID, all located in the previous steps. Click the Test button to test that everything works and finally, click Save. New errors now trigger elmah.io to create a card with the details of the error in Trello.

    +

    Troubleshooting

    +

    If errors aren't showing up in Trello, please check that the following are all true:

    +
      +
    • When clicking the Test button on the Trello app settings screen, the button turns green.
    • +
    • There's a message logged in the log where you set up the Trello integration.
    • +
    • The message is marked as new (yellow star next to the title on the search result).
    • +
    • The message is either of severity Error or Fatal.
    • +
    +

    To trigger an error manually, go to https://api.elmah.io/swagger/index.html and input an elmah.io API key with the Messages - Write permission enabled. Expand Messages and the POST node with the URL /v3/messages/{logId}. Input your log ID and the following JSON:

    +

    {
    +  "title": "This is a test",
    +  "severity": "Error"
    +}
    +

    +

    Finally, click the Try it out! button and verify that the API returns a status code of 201. The new error should show up in Trello. If testing this multiple times, you will need to make small adjustments to the title field inside the JSON, for additional errors to be marked as new.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-twilio/index.html b/elmah-io-apps-twilio/index.html new file mode 100644 index 0000000000..b546b005c4 --- /dev/null +++ b/elmah-io-apps-twilio/index.html @@ -0,0 +1,683 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with Twilio + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install Twilio for elmah.io

    +

    To send SMS/Text messages with Twilio, you will need to sign up for Twilio first. Twilio provides a range of good tutorials when signing up, why we don't want to duplicate them here. When signed up, you will have access to a Twilio phone number to send messages from, an Account SID and a token needed to authenticate Twilio. These pieces of information will be used below when installing the Twilio app on elmah.io.

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Twilio app and click the Install button:

    +

    Install the Twilio app

    +

    Input your Twilio phone number (available on https://www.twilio.com/console/phone-numbers/incoming) in the From field. Input the phone number you want receiving error reports from elmah.io in the To field. Remember to fully qualify the number with a plus and the language code (US example: +12025550170 - UK example: +441632960775). Copy your Account SID and Auth Token from the Twilio Dashboard and input them in the fields on elmah.io.

    +

    Click Save and the app is added to your log. When new errors are logged, an SMS/Text message is automatically sent to the configured phone number.

    +

    Troubleshooting

    +

    If errors aren't being sent to your phone, verify that the configured variables work. To do so, replace the four variables in the top of this PowerShell script and execute it:

    +

    $sid = "INSERT_SID"
    +$token = "INSERT_TOKEN"
    +$from = "INSERT_FROM"
    +$to = "INSERT_TO"
    +
    +$url = "https://api.twilio.com/2010-04-01/Accounts/$sid/Messages.json"  
    +
    +$pair = "$($sid):$($token)"
    +$encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
    +$basicAuthValue = "Basic $encodedCreds"
    +$Headers = @{
    +    Authorization = $basicAuthValue
    +    ContentType = "application/x-www-form-urlencoded"
    +}
    +
    +$from = $from.Replace("+", "%2B")
    +$to = $to.Replace("+", "%2B")
    +
    +$response = Invoke-WebRequest -Uri $url -Method POST -Headers $Headers -Body "Body=Affirmative&From=$from&To=$to"
    +

    +

    You should see a text message on your phone. The script will output any errors from Twilio if something isn't working.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/elmah-io-apps-webclient/index.html b/elmah-io-apps-webclient/index.html new file mode 100644 index 0000000000..dd0a482870 --- /dev/null +++ b/elmah-io-apps-webclient/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/elmah-io-apps-youtrack/index.html b/elmah-io-apps-youtrack/index.html new file mode 100644 index 0000000000..76b696ec67 --- /dev/null +++ b/elmah-io-apps-youtrack/index.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with YouTrack + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Install YouTrack App for elmah.io

    +

    Get your token

    +

    To allow elmah.io to create issues on YouTrack, you will need a permanent token. Go to your YouTrack profile, click the Account Security. Here you can generate a new token:

    +

    Generate permanent token

    +

    Copy the generated token.

    +

    Install the YouTrack App on elmah.io

    +

    Log into elmah.io and go to the log settings. Click the Apps tab. Locate the YouTrack app and click the Install button. Input your token and the base URL of your YouTrack Cloud installation. Next, click the Login button to fetch the list of projects from YouTrack:

    +

    Install YouTrack App

    +

    Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured YouTrack project.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/email-troubleshooting/index.html b/email-troubleshooting/index.html new file mode 100644 index 0000000000..2f57deae51 --- /dev/null +++ b/email-troubleshooting/index.html @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Email troubleshooting + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Email troubleshooting

    + +

    So, you aren't receiving emails from elmah.io? Here is a collection of things to know/try out.

    +

    Emails on new errors only

    +

    The most common reason for not receiving emails when errors are logged is that elmah.io only sends the New Error email when an error that we haven't seen before is logged. New errors are marked with a yellow star next to the log message in the UI and can be searched through either search filters or full-text search:

    +

    isNew:true
    +

    +

    The new detection algorithm is implemented by looking at a range of fields like the title, type, and severity. Only severities Error and Fatal marked as isNew trigger an email.

    +

    Email bounced

    +

    We use AWS to send out all transactional emails from elmah.io. We get a notification from AWS when an email bounces and we stop sending to that email address, even if any new emails wouldn't cause a bounce. Beneath your profile, you will be able to see if your email caused a bounce:

    +

    Bounced email

    +

    As the error message says, get in contact for us to try and reach the email address again.

    +

    Invalid email

    +

    Ok, this may seem obvious. But this happens more often than you would think. Typos are a common cause of invalid emails. Specifying a mailing list or group address doesn't always play nice with elmah.io either. For instance, Office 365 distribution groups block external emails as the default. The easiest way to check your inputted email address is to send a new message to that address from an external email provider.

    +

    Check your promotional and/or spam folder

    +

    We do a lot to keep our email reputation high. But some email clients may treat similar-looking emails as promotional or spam. Remember to check those folders and mark messages as important if spotting them in the wrong folder.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/export-data-from-elmah-io-to-json/index.html b/export-data-from-elmah-io-to-json/index.html new file mode 100644 index 0000000000..7432e23d49 --- /dev/null +++ b/export-data-from-elmah-io-to-json/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000..12b824181a Binary files /dev/null and b/favicon.ico differ diff --git a/handle-elmah-io-downtime/index.html b/handle-elmah-io-downtime/index.html new file mode 100644 index 0000000000..b2ac288f45 --- /dev/null +++ b/handle-elmah-io-downtime/index.html @@ -0,0 +1,706 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Handle elmah.io downtime + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Handle elmah.io downtime

    +

    Like every other SaaS product out there, we cannot promise you 100% uptime on elmah.io. We understand, that your logging data is extremely important for your business and we do everything in our power to secure that elmah.io is running smoothly. To monitor our APIs and websites, check out status.elmah.io.

    +

    It is our general recommendation to implement code that listens for communication errors with the elmah.io API and log errors elsewhere. How you do this depends on which elmah.io NuGet package you have installed. The documentation for each package will show how to subscribe to errors. For Elmah.Io.Client it would look similar to this:

    +

    var elmahIo = ElmahioAPI.Create("API_KEY");
    +elmahIo.Messages.OnMessageFail += (sender, args) =>
    +{
    +    var message = args.Message;
    +    var exception = args.Error;
    +
    +    // TODO: log it
    +};
    +

    +

    For a logging framework like Serilog, it would look similar to this:

    +

    Log.Logger = new LoggerConfiguration()
    +    .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID"))
    +    {
    +        OnError = (msg, ex) =>
    +        {
    +            // TODO: log it
    +        }
    +    })
    +    .CreateLogger();
    +

    +

    It is important not to log errors in OnMessageFail and OnError callbacks to elmah.io, since that could cause an infinite loop. Check out the documentation for the package you are using for additional details.

    +

    Response explanation

    +

    Here's an overview of the types of errors you can experience from the API:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ResponseMeaning
    TimeoutSomething is very wrong with our API or Azure. You can be sure that we are working 24/7 to fix it.
    500The API is reachable, but we have a problem communicating with Azure Service bus. Azure has great uptime and all of our resources are dedicated and replicated. Still, we experience short periods of downtime from time to time.
    429We allow a maximum (per API key) of 500 requests per minute and 3600 per hour. 429 means that you have crossed that line. This status code doesn't indicate that the API is down.
    4xxSomething is wrong with the request. Check out the API documentation for details. This status code doesn't indicate that the API is down.
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/heartbeats-troubleshooting/index.html b/heartbeats-troubleshooting/index.html new file mode 100644 index 0000000000..f07cd9929a --- /dev/null +++ b/heartbeats-troubleshooting/index.html @@ -0,0 +1,678 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Heartbeats Troubleshooting + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Heartbeats Troubleshooting

    +

    Common problems and how to fix them

    +

    Here you will a list of common problems and how to solve them.

    +

    Timeout when creating heartbeats through Elmah.Io.Client

    +

    If you experience a timeout when calling the Healthy, Degraded, or Unhealthy method, you may want to adjust the default HTTP timeout. Elmah.Io.Client has a default timeout of 5 seconds to make sure that logging to elmah.io from a web application won't slow down the web app too much in case of slow response time from the elmah.io API. While 99.9% of the requests to the elmah.io API finish within this timeout, problems with Azure, the network connection, and a lot of other issues can happen.

    +

    Since heartbeats typically run outside the scope of a web request, it's safe to increase the default HTTP timeout in this case:

    +

    var api = ElmahioAPI.Create("API_KEY", new ElmahIoOptions
    +{
    +    Timeout = new TimeSpan(0, 0, 30)
    +});
    +

    +

    The example set a timeout of 30 seconds.

    +

    SocketException when creating heartbeats through Elmah.Io.Client

    +

    A System.Net.Sockets.SocketException when communicating with the elmah.io API can mean multiple things. The API can be down or there's network problems between your machine and the API. Increasing the timeout as shown in the previous section should be step one. If you still experience socket exceptions, it might help to implement retries. This can be done by setting up a custom HttpClient:

    +

    builder.Services
    +    .AddHttpClient("elmahio")
    +    .AddPolicyHandler(HttpPolicyExtensions
    +        .HandleTransientHttpError()
    +        .WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(i)));
    +

    +

    The AddPolicyHandler is available when installing the Microsoft.Extensions.Http.Polly NuGet package. Next, create the elmah.io client with the custom HttpClient:

    +

    var httpClient = httpClientFactory.CreateClient("elmahio");
    +var elmahIoClient = ElmahioAPI.Create("API_KEY", options, httpClient);
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-does-the-new-detection-work/index.html b/how-does-the-new-detection-work/index.html new file mode 100644 index 0000000000..e9ce53ff79 --- /dev/null +++ b/how-does-the-new-detection-work/index.html @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How does the new detection work + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How does the new detection work

    +

    Being able to identify when a logged error is new or a duplicate of an already logged error, is one of the most important features on elmah.io. A lot of other features are based on this mechanism to help you reduce the number of emails, Slack/Teams messages, and much more. We often get questions about how this works and how to tweak it, why this article should bring some clarity to questions about this feature.

    +

    When logging messages to elmah.io using either one of the integrations or through the API, we automatically set a flag named isNew on each log message. Calculating the value of this field is based on a rather complex algorithm. The implementation is closed-source but not a huge secret. Each message is assigned a hash value based on a range of fields like the message template, the severity, the URL, and more. Some values are normalized or modified before being sent as input to the hash function. An example of this is removing numbers from the log message which will ensure that Error on product 1 and Error on product 2 will be considered the same log message. When receiving a new log message, we check if an existing message with the same hash is already stored in the log. If not, the new message will be marked as New by setting the isNew flag to true. If we already found one or more log messages with the same hash, the new message will have its isNew flag set to false.

    +

    Messages and apps

    +

    Most apps and features around sending messages from elmah.io are based on the isNew flag. This means that only new errors trigger the New Error Email, the Slack and Teams apps, etc. This is done to avoid flooding the recipient system with emails or messages. You typically don't want 1,000 emails if the same error occurs 1,000 times. Error occurrence is still important, why there are other features to help you deal with this like the Error Occurrence Email and spike-based machine learning features.

    +

    Modifying the hash function

    +

    We sometimes get requests to modify the hash function, but unfortunately, that's currently not possible. We change the implementation from time to time to improve the uniqueness detection over time. If you have an example of two log messages that should have been considered unique or not considered unique, feel free to reach out. This may or may not result in changes to the hash function.

    +

    There are still some possibilities to force two log messages to not be unique. The obvious is to include different variables inside the log message. Remember that numbers are removed, why this must consist of letters. Another approach is to put individual values in the source field. This can be done in all integrations by implementing the OnMessage action. Some integrations also support setting the source field by including it as structured properties like Error from {source}.

    +

    Setting re-occurring messages as New

    +

    A common request is to get a notification if an error that you believed was fixed re-occur. This scenario is built into elmah.io's issue tracker. When marking a log message as fixed through the UI or API, elmah.io automatically marks all instances of the log message as fixed. If a new log message with the same hash is logged at some point, the isNew flag on this message will be set to true. This will trigger the New Error Email and most of the integrations again.

    +

    Retention

    +

    Depending on your current plan, each subscription provides x days of retention for log messages. This means that log messages are automatically deleted after x days in the database. Once all instances of a log message are deleted, a new log message generating the same hash as the deleted messages will be marked as new. To increase the chance of log messages being marked as new, you can lower the retention on each log on the Log Settings page.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-prices-are-calculated/index.html b/how-prices-are-calculated/index.html new file mode 100644 index 0000000000..a46cc73130 --- /dev/null +++ b/how-prices-are-calculated/index.html @@ -0,0 +1,668 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How prices are calculated + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How prices are calculated

    +

    When using elmah.io, you may experience the need to switch plans, update your credit card, or do other tasks which will require changes to your subscription. This document explains the different upgrade/downgrade and payment paths.

    +

    Switch to a higher plan

    +

    You can switch to a larger plan at all times. If you purchased a Small Business ($29) on June 1 and want to upgrade to Business ($49) on June 15, we charge you ~ $35. You already paid $15 for half of June on the Small Business plan, so the remaining amount is deducted from the $49. Your next payment will be on July 15.

    +

    Switch to a lower plan

    +

    You can switch to a lower plan when your current subscription is renewed. If you purchased a Business plan ($49) on June 15. and downgrade to a Small Business plan ($29) on July 1, you would be charged $49 on June 15, and $29 on July 15. You commit to either a month or a year in advance.

    +

    Update your credit card

    +

    A new credit card can be inputted at any time during your subscription. The payment provider will automatically charge the new credit card on the next payment.

    +

    Purchase a top-up

    +

    If you need additional messages but don't want to upgrade to a larger plan permanently, you can purchase a top-up. The purchase is made on the credit card already configured on the subscription. If you want to buy the top-up on a different credit card, you will need to use the Update credit card feature first. The price of the top-up is always $19, and you can purchase as many top-ups as you want.

    +

    Switch payment interval

    +

    Switching from monthly to annual or annual to monthly is possible. If you switch from monthly to annual, you are charged the difference between the two plans as in Switch to a higher plan. If you switch from annual to monthly, one of two scenarios can happen. If you switch to the same plan or lower, your plan will automatically switch when your current subscription renews (like in Switch to a lower plan). If you switch to a higher plan, your plan will switch immediately as in Switch to a higher plan. If the price of the remaining time on your current plan covers the price of the new plan, the exceeding amount will be credited to your balance and used to pay for the new plan and any upcoming invoices. Paying for top-ups with the balance is currently not supported.

    +

    Purchase a subscription from Denmark

    +

    We are based in Denmark, why selling services to another Danish company requires us to include 25% VAT (moms). The price on the various dialogs will automatically show the price including VAT as well as the VAT amount. Your invoices will include the amount in VAT to inform SKAT.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-avoid-emails-getting-classified-as-spam/index.html b/how-to-avoid-emails-getting-classified-as-spam/index.html new file mode 100644 index 0000000000..2d8afa7294 --- /dev/null +++ b/how-to-avoid-emails-getting-classified-as-spam/index.html @@ -0,0 +1,662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to avoid emails getting classified as spam + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to avoid emails getting classified as spam

    +

    We do everything in our power to maintain a good email domain reputation. Sometimes emails sent from elmah.io may be classified as spam in your email client. The easiest way to avoid this is to inspect the sender in an email received from @elmah.io and add it to your contact list (we primarily send emails from info@elmah.io and noreply@elmah.io). How you do this depends on your email client but all major clients have this option. In the sections below, there are alternatives to the contact list approach for various email clients.

    +

    Gmail

    +

    If you don't want to add elmah.io addresses to your contact list you can use Gmail's Filters feature to always classify *@elmah.io as not spam. To do so, go to Settings | Filters and create a new filter:

    +

    Search all conversations

    +

    Click Create filter and check the Never send it to Spam option:

    +

    Never send it to Spam

    +

    Finally, click the Create filter button, and emails from elmah.io will no longer be classified as spam.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-configure-api-key-permissions/index.html b/how-to-configure-api-key-permissions/index.html new file mode 100644 index 0000000000..c8c6c7ae93 --- /dev/null +++ b/how-to-configure-api-key-permissions/index.html @@ -0,0 +1,662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to configure API key permissions + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to configure API key permissions

    +

    Security on the elmah.io API is handled through the use of API keys. All requests to the API must be accompanied by an API key. When creating your organization, a default API key was automatically created. API keys can be revoked and you can create multiple API keys for different purposes and projects. Much like a user can be awarded different levels of access on elmah.io, API keys also have a set of permissions. The default created API key for your organization, only have permission to write log messages to elmah.io.

    +

    To configure permissions for a new or existing API key, either click the Edit or Add API Key button on the API Keys tab of your organization settings. This will show the API key editor view:

    +

    Edit API key

    +

    As mentioned previously, new keys have the messages_write permission enabled only. This permission will cover logging from your application to elmah.io. If your application needs to browse messages from elmah.io, create new logs/applications, etc. you will need to enable the corresponding permission. Notice that read permissions don't need to be enabled, for you to browse logs and log messages on the elmah.io UI. API keys are used by the range of client integrations only.

    +
    +

    Your API key shouldn't be shared outside your organization. In some situations, you will need to share your API key (like when logging from JavaScript). In these cases, it's essential that your API key only has the messages_write permission enabled. With all permissions enabled, everyone will be able to browse your logs.

    +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-correlate-messages-across-services/index.html b/how-to-correlate-messages-across-services/index.html new file mode 100644 index 0000000000..83ea978e42 --- /dev/null +++ b/how-to-correlate-messages-across-services/index.html @@ -0,0 +1,869 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to correlate messages across services + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to correlate messages across services

    + +

    A common architecture is to spread out code across multiple services. Some developers like to split up their code into microservices. Others have requirements for running code asynchronous behind a queue. The common theme here is that code often runs in multiple processes spread across multiple servers. While logging can be easily set up in different projects and services, being able to correlate log messages across multiple services isn't normally available when logging to individual log outputs.

    +

    Imagine a console application making an HTTP request to your public API. The API calls an internal API, which again puts a message on a queue. A service consumes messages from the queue and ends up logging an error in the error log. Seeing the entire log trace from receiving the request on the API straight down the chain resulting in the error, highly increases the chance of figuring out what went wrong. With the Correlation feature on elmah.io, we want to help you achieve just that. In this article, you will learn how to set up Correlations.

    +
    +

    Correlation currently requires all of your applications to log to the same log on elmah.io. For you to separate log messages from each service, we recommend setting the Application property on all log messages. Filtering on log messages is available through the elmah.io UI. We may want to explore ways to use Correlation across multiple error logs in the future.

    +
    +

    CorrelationId explained

    +

    The way log messages are correlated on elmah.io is based on a property on all log messages named correlationId. The field is available both when creating log messages through our API as well as in the Elmah.Io.Client NuGet package. The property is a string which means that you can use whatever schema you want for the correlation ID.

    +

    To set the correlation ID using the client, use the following code:

    +

    var myCorrelationId = "42";
    +var client = ElmahioAPI.Create("API_KEY");
    +await client.Messages.CreateAndNotifyAsync(new Guid("LOG_ID"), new CreateMessage
    +{
    +    Title = "Hello World",
    +    // ...
    +    CorrelationId = myCorrelationId
    +});
    +

    +

    In a real-world scenario, myCorrelationId wouldn't be hardcoded but pulled from a shared header, message ID, or similar. As long as all services set CorrelationId to the same ID, log messages within a correlation can be searched on the elmah.io UI:

    +

    Correlation filter

    +

    Setting CorrelationId

    +

    How you set the correlation ID depends on which integration you are using. In some cases, a correlation ID is set automatically while others will require a few lines of code.

    +

    Elmah.Io.Client.Extensions.Correlation

    +

    We have developed a NuGet package dedicated to setting the correlation ID from the current activity in an easy way. The package can be used together with all of the various client integrations we offer (like Elmah.Io.AspNetCore and Elmah.Io.NLog). Start by installing the package:

    +
    Install-Package Elmah.Io.Client.Extensions.Correlation
    +
    dotnet add package Elmah.Io.Client.Extensions.Correlation
    +
    <PackageReference Include="Elmah.Io.Client.Extensions.Correlation" Version="5.*" />
    +
    paket add Elmah.Io.Client.Extensions.Correlation
    +
    +

    Next, call the WithCorrelationIdFromActivity method as part of the OnMessage action/event. How you do this depends on which of the client integrations you are using. For Elmah.Io.Client it can be done like this:

    +

    var elmahIo = ElmahioAPI.Create("API_KEY");
    +elmahIo.Messages.OnMessage += (sender, args) =>
    +{
    +    args.Message.WithCorrelationIdFromActivity();
    +};
    +

    +

    For Elmah.Io.AspNetCore it can be done like this:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnMessage = msg =>
    +    {
    +        msg.WithCorrelationIdFromActivity();
    +    };
    +});
    +

    +

    ASP.NET Core

    +

    When logging uncaught errors using the Elmah.Io.AspNetCore package, we automatically pick up any traceparent header and put the trace ID as part of the error logged to elmah.io. For an overview of wrapping calls to your ASP.NET Core API in an Activity check out the section about W3C Trace Context.

    +

    If you want to set the correlation ID manually, you can use the OnMessage action:

    +

    builder.Services.Configure<ElmahIoOptions>(o =>
    +{
    +    o.OnMessage = msg =>
    +    {
    +        msg.CorrelationId = "42";
    +    };
    +});
    +

    +

    When requested through the browser, a traceparent is not automatically added, unless you manually do so by using an extension as shown in the W3C section. In this case, you can either install the Elmah.Io.Client.Extensions.Correlation package as already explained, or set the correlationId manually by installing the System.Diagnostics.DiagnosticSource NuGet package and adding the following code to the OnMessage action:

    +

    o.OnMessage = msg =>
    +{
    +    msg.CorrelationId = System.Diagnostics.Activity.Current?.TraceId.ToString();
    +};
    +

    +

    Microsoft.Extensions.Logging

    +

    To store a correlation ID when logging through Microsoft.Extensions.Logging you can either set the CorrelationId property (manually or using the Elmah.Io.Client.Extensions.Correlation NuGet package) or rely on the automatic behavior built into Microsoft.Extensions.Logging.

    +

    To manually set the correlation you can include correlationId as part of the log message:

    +

    logger.LogInformation("A log message with {correlationId}", "42");
    +

    +

    Or you can put the correlation ID as part of a logging scope:

    +

    using (logger.BeginScope(new { CorrelationId = "42" }))
    +{
    +    logger.LogInformation("A log message");
    +}
    +

    +

    In some cases, a correlation ID will be set automatically. If there is a current active Activity (see later), Microsoft.Extensions.Logging automatically decorates all log messages with a custom property named TraceId. The elmah.io backend will pick up any value in the TraceId and use that as the correlation ID.

    +

    Serilog

    +

    When logging through Serilog a correlation ID can be added to one or more log messages in multiple ways. The most obvious being on the log message itself:

    +

    Log.Information("A log message with {correlationId}", "42");
    +

    +

    If you don't want correlation ID as part of the log message, you can push the property using LogContext:

    +

    using (LogContext.PushProperty("correlationId", "42"))
    +{
    +    Log.Information("A log message");
    +}
    +

    +

    You will need to install the LogContext enricher for this to work:

    +

    Log.Logger =
    +    new LoggerConfiguration()
    +        .Enrich.FromLogContext()
    +        // ...
    +        .CreateLogger();
    +

    +

    The elmah.io sink for Serilog is an async batching sync. This means that log messages are not logged in the same millisecond as one of the logging methods on the Log class is called and the current activity is no longer set. When logging from a web application or other project types where the activity is short-lived, you either need to include the correlation ID as part of the message (as shown in the previous examples) or you need to store the correlation ID as part of the request. For ASP.NET Core this can be done using middleware:

    +

    app.Use(async (ctx, next) =>
    +{
    +    IDisposable disposeMe = null;
    +    var activity = Activity.Current;
    +    if (activity != null)
    +    {
    +        disposeMe = LogContext.PushProperty("correlationid", activity.TraceId.ToString());
    +    }
    +
    +    try
    +    {
    +        await next();
    +    }
    +    finally
    +    {
    +        disposeMe?.Dispose();
    +    }
    +});
    +

    +

    All calls to Serilog within the web request will have the correlation ID set to the TraceId of the current activity. Other frameworks support similar features for enriching the current request or invocation with custom properties.

    +

    NLog

    +

    Correlation ID can be set on log messages logged through NLog in multiple ways. The first approach is to include the ID directly in the log message:

    +

    logger.Info("A log message with {correlationId}", "42");
    +

    +

    If you don't want the ID as part of the log message you can use either NLog's two context objects:

    +

    using (MappedDiagnosticsLogicalContext.SetScoped("correlationId", "42"))
    +{
    +    logger.Info("A log message");
    +}
    +GlobalDiagnosticsContext.Set("correlationId", "42");
    +

    +

    You can also add the property manually to the log message using the log event builder API available in NLog:

    +

    var infoMessage = new LogEventInfo(LogLevel.Info, "", "A log message");
    +infoMessage.Properties.Add("correlationid", "42");
    +logger.Info(infoMessage);
    +

    +

    log4net

    +

    Correlation ID can be set on log messages logged through log4net in multiple ways. You can include it directly on the LoggingEvent:

    +

    var properties = new PropertiesDictionary();
    +properties["correlationid"] = "42";
    +log.Logger.Log(new LoggingEvent(new LoggingEventData
    +{
    +    Level = Level.Info,
    +    TimeStampUtc = DateTime.UtcNow,
    +    Properties = properties,
    +    Message = "A log message",
    +}));
    +

    +

    You most likely use the Info, Warn, and similar helper methods to store log messages. In this case, you can set the correlation ID on the ThreadContext:

    +

    ThreadContext.Properties["correlationid"] = "42";
    +log.Info("A log message");
    +

    +

    Please notice that correlationid in both examples must be in all lowercase.

    +

    W3C Trace Context

    +

    The class Activity has been mentioned a couple of times already. Let's take a look at what that is and how it relates to W3C Trace Context. Trace Context is a specification by W3C for implementing distributed tracing across multiple processes which are already widely adopted. If you generate a trace identifier in a client initiating a chain of events, different Microsoft technologies like ASP.NET Core already pick up the extended set of headers and include those as part of log messages logged through Microsoft.Extensions.Logging.

    +

    Let's say we have a console application calling an API and we want to log messages in both the console app and in the API and correlate them in elmah.io. In both the console app and in the ASP.NET Core application, you would set up Elmah.Io.Extensions.Logging using the default configuration. Then in the console application, you will wrap the call to the API in an Activity:

    +

    var httpClient = new HttpClient();
    +var activity = new Activity("ApiCall").Start();
    +try
    +{
    +    using (logger.BeginScope(new
    +    {
    +        TraceId = activity.TraceId,
    +        ParentId = activity.ParentSpanId,
    +        SpanId = activity.SpanId }))
    +    {
    +        logger.LogInformation("Fetching data from the API");
    +        var result = await httpClient.GetStreamAsync("https://localhost:44396/ping");
    +        // ...
    +    }
    +}
    +finally
    +{
    +    activity.Stop();
    +}
    +

    +

    By creating and starting a new Activity before we call the API, log messages in the console app can be decorated with three additional properties: TraceId, ParentId, and SpanId. This will make sure that all log messages logged within this scope will get the correlation ID set on elmah.io. HttpClient automatically picks up the activity and decorates the request with additional headers.

    +

    On the API we simply log as normal:

    +

    [ApiController]
    +[Route("[controller]")]
    +public class PingController : ControllerBase
    +{
    +    private readonly ILogger<PingController> _logger;
    +
    +    public PingController(ILogger<PingController> logger)
    +    {
    +        _logger = logger;
    +    }
    +
    +    [HttpGet]
    +    public string Get()
    +    {
    +        _logger.LogInformation("Received a ping");
    +        return "pong";
    +    }
    +}
    +

    +

    Notice that we didn't have to decorate log messages with additional properties. ASP.NET Core automatically picks up the new headers and decorates all log messages with the correct trace ID.

    +

    If you want to test this through a browser, you'll need to modify the request headers before the request is made to your web application. There is a range of extensions available to help with this. For the following example, we'll use ModHeader:

    +

    ModHeader

    +

    The extension will enrich all requests with a header named traceparent in the format VERSION-TRACE_ID-SPAN_ID-TRACE_FLAGS. elmah.io will automatically pick up this header and set correlationId to the value of TRACE_ID.

    +

    If you don't get a correlation ID set on your log messages, we recommend installing the Elmah.Io.Client.Extensions.Correlation NuGet package and calling the following method in the OnMessage event/action:

    +

    msg.WithCorrelationIdFromActivity();
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-enable-two-factor-login/index.html b/how-to-enable-two-factor-login/index.html new file mode 100644 index 0000000000..7898f4add2 --- /dev/null +++ b/how-to-enable-two-factor-login/index.html @@ -0,0 +1,678 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to enable two-factor login + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to enable two-factor login

    + +

    elmah.io supports two-factor login through either one of the social providers or a two-factor app like Google Authenticator or Authy.

    +

    Two-factor with an elmah.io username and password

    +

    When signing into elmah.io with a username and password, two-factor authentication can be enabled on the Security tab on your profile:

    +

    Enable two-factor

    +

    Follow the instructions on the page to install either Google Authenticator or Authy. Once you have the app installed, scan the on-screen QR code and input the generated token in the field in step 3.

    +

    Once two-factor authentication has been successfully set up, the following screen is shown:

    +

    Two-factor enabled

    +

    Two-factor authentication can be disabled at any time by inputting a new code from the authenticator app in the text field and clicking the Deactivate two-factor login button.

    +

    We recommend that you sign out after enabling two-factor authentication to invalidate the current session.

    +
    +

    Popular authenticator apps like Google Authenticator and Microsoft Authenticator support cloud backup. Make sure to enable this in case you lose your phone. When cloud backup is enabled, you can sign in with your main account when you get a new phone and all of your stored accounts will be automatically restored.

    +
    +

    Two-factor with a social provider

    +

    When using one of the social providers to log in to elmah.io, two-factor authentication can be enabled through either Twitter, Facebook, Microsoft, or Google. Check out the documentation for each authentication mechanism for details on how to enable two-factor authentication.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-get-elmah-io-to-resolve-the-correct-client-ip/index.html b/how-to-get-elmah-io-to-resolve-the-correct-client-ip/index.html new file mode 100644 index 0000000000..5b6089d240 --- /dev/null +++ b/how-to-get-elmah-io-to-resolve-the-correct-client-ip/index.html @@ -0,0 +1,671 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to get elmah.io to resolve the correct client IP + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to get elmah.io to resolve the correct client IP

    +

    elmah.io try to resolve the IP of the client causing a log message, no matter what severity (Error, Information, etc.) and platform (browser, web-server, etc.) a log message is sent from. This is done by looking at multiple pieces of information provided by the sender. In some cases, elmah.io may not be able to resolve the IP or resolve a wrong IP address. In this document, you will find help getting the right client IP into elmah.io.

    +

    Missing IP when using a proxy

    +

    If you are using a proxy layer in between the client and your web server, you may experience log messages without a client IP. This is probably caused by the proxy hiding the original IP from your web server. Most proxies offer an alternative server variable like the X-Forwarded-For header. You can inspect the server variables on the Server Variables tab on elmah.io and check if your proxy includes the original IP in any of the variables. We support custom headers from a range of proxies (like Cloudflare). Most proxies support some kind of settings area where the X-Forwarded-For header can be enabled. If you are using a proxy that uses custom headers, please make sure to reach out and we may want to include the custom header to elmah.io.

    +

    Wrong IP when using a proxy

    +

    If the client IP is wrong when behind a proxy, it is typically because the proxy replaces the client IP when calling your server with the IP of its server. This is a poor practice and makes it very hard for elmah.io to figure out which IP belongs to the user and which one to the proxy. Luckily, this is configurable in a lot of proxies through their settings area.

    +

    Missing IP

    +

    This can be caused by several issues. In most instances, the client doesn't include any server variables that reveal the IP address. In this case, the client IP will not be available within the elmah.io UI. In some cases, you may know the user's IP from session variables or similar. To include an IP on messages logged to elmah.io, you can implement the OnMessage event or action, depending on which integration you are using. In this example, we use the OnMessage event on the Elmah.Io.Client package to include the user's IP manually:

    +

    var userIp = "1.1.1.1"; // <-- just for the demo
    +var client = ElmahioAPI.Create("API_KEY");
    +client.Messages.OnMessage += (sender, args) =>
    +{
    +    var message = args.Message;
    +    if (message.ServerVariables == null) message.ServerVariables = new List<Item>();
    +    message.ServerVariables.Add(new Item("X-FORWARDED-FOR", userIp));
    +};
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-get-the-sql-tab-to-show-up/index.html b/how-to-get-the-sql-tab-to-show-up/index.html new file mode 100644 index 0000000000..1149869978 --- /dev/null +++ b/how-to-get-the-sql-tab-to-show-up/index.html @@ -0,0 +1,742 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to get the SQL tab to show up + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to get the SQL tab to show up

    +

    The elmah.io UI can offer to show any SQL code part of the current context of logging a message. The code will show up in the log message details as a tab named SQL:

    +

    SQL tab

    +

    The tab shows a formatted view of the SQL code including any parameters and/or syntax errors. This can help debug exceptions thrown while executing SQL code against a relational database.

    +

    To make the SQL tab show up, custom information needs to be included in the Data dictionary of a log message. The following sections will go into detail on how to include the custom information in various ways.

    +

    Entity Framework Core

    +

    Entity Framework Core is easy since it already includes any SQL code as part of the messages logged through Microsoft.Extensions.Logging's ILogger. The SQL code and parameters are logged as two properties named commandText and parameters. elmah.io will automatically pick up these properties and show the SQL tab with the formatted results.

    +

    As a default, all values in parameters are not logged as part of the message. You will see this from values being set to ? in the UI. To have the real values show up, you will need to enable sensitive data logging when setting up EF Core:

    +

    services.AddDbContext<Context>(options =>
    +{
    +    // Other code like: options.UseSqlServer(connectionString);
    +    options.EnableSensitiveDataLogging(true); // ⬅️ Set this to true
    +});
    +

    +

    This should not be set if you include sensitive details like social security numbers, passwords, and similar as SQL query parameters.

    +

    Manually

    +

    If you want to attach SQL to a log message made manually, you can go one of two ways. The first way is to fill in the commandText and parameters Data entries shown above. When creating a message on Elmah.Io.Client it could look like this:

    +

    client.Messages.CreateAndNotify(logId, new CreateMessage
    +{
    +    Title = "Log message with SQL attached",
    +    Severity = Severity.Error.ToString(),
    +    Data = new List<Item>
    +    {
    +        new Item
    +        {
    +            Key = "commandText",
    +            Value = "SELECT * FROM USERS WHERE Name = @columnValue OR Name = @columnTwoValue"
    +        },
    +        new Item
    +        {
    +            Key = "parameters",
    +            Value = "columnValue='Eduard' (Nullable = false) (Size = 6), columnTwoValue='Thomas' (Nullable = false) (Size = 6)"
    +        },
    +    },
    +});
    +

    +

    The value of the parameters item needs to correspond to the format of that Entity Framework and the System.Data namespace uses.

    +

    The second approach is to provide elmah.io with a single Data item named X-ELMAHIO-SQL. The value of this item should be a JSON format as seen in the following example:

    +

    var sql = new
    +{
    +    Raw = "SELECT * FROM USERS WHERE Name = @columnValue OR Name = @columnTwoValue",
    +    Parameters = new[]
    +    {
    +        new
    +        {
    +            IsNullable = false,
    +            Size = 6,
    +            Name = "columnValue",
    +            Value = "Eduard"
    +        },
    +        new
    +        {
    +            IsNullable = false,
    +            Size = 6,
    +            Name = "columnTwoValue",
    +            Value = "Thomas"
    +        },
    +    },
    +};
    +client.Messages.CreateAndNotify(logId, new CreateMessage
    +{
    +    Title = "Log message with SQL attached",
    +    Severity = Severity.Error.ToString(),
    +    Data = new List<Item>
    +    {
    +        new Item { Key = "X-ELMAHIO-SQL", Value = JsonConvert.SerializeObject(sql) },
    +    },
    +});
    +

    +

    The JSON generated by serializing the anonymous object will look like this:

    +

    {
    +    "Raw": "SELECT * FROM USERS WHERE Name = @columnValue OR Name = @columnTwoValue",
    +    "Parameters": [
    +        {
    +            "IsNullable": false,
    +            "Size": 6,
    +            "Name": "columnValue",
    +            "Value": "Eduard"
    +        },
    +        {
    +            "IsNullable": false,
    +            "Size": 6,
    +            "Name": "columnTwoValue",
    +            "Value": "Thomas"
    +        }
    +    ]
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-include-source-code-in-log-messages/index.html b/how-to-include-source-code-in-log-messages/index.html new file mode 100644 index 0000000000..7c8e2362ca --- /dev/null +++ b/how-to-include-source-code-in-log-messages/index.html @@ -0,0 +1,740 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to include source code in log messages + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to include source code in log messages

    + +

    Sometimes, being able to see the exact code causing an error, is much more helpful than looking at other details around the current HTTP context and similar. If you often find yourself opening Visual Studio or Code to inspect the failing line, embedding source code in errors and log messages will speed up the process. In this article, you will learn how to configure elmah.io to include source code when logging messages using the Elmah.Io.Client.Extensions.SourceCode NuGet package.

    +
    +

    The Elmah.Io.Client.Extensions.SourceCode package requires Elmah.Io.Client version 4.0 or newer.

    +
    +

    No matter what integration you are using (with a few exceptions) you are using the Elmah.Io.Client NuGet package to communicate with the elmah.io API. We have built a range of extensions for this package, to avoid including too many features not related to communicating with the API into the client package. One of them is for including source code when logging messages. Start by installing the Elmah.Io.Client.Extensions.SourceCode NuGet package:

    +
    Install-Package Elmah.Io.Client.Extensions.SourceCode
    +
    dotnet add package Elmah.Io.Client.Extensions.SourceCode
    +
    <PackageReference Include="Elmah.Io.Client.Extensions.SourceCode" Version="5.*" />
    +
    paket add Elmah.Io.Client.Extensions.SourceCode
    +
    +

    There are currently three ways of including source code with log messages. The first two ways require the Elmah.Io.Client.Extensions.SourceCode package, while the third one can be done manually.

    +

    From the file system

    +

    This is the most simple approach meant for local development. When logging a stack trace from your local machine, the trace includes the absolute path to the file on your file system, as well as the line causing a log message (typically an error). To set this up, you will need to implement the OnMessage event through the Elmah.Io.Client package. Depending on which integration you are using, the name of that event or action can vary. What you are looking to do is to call the WithSourceCodeFromFileSystem method on log messages you want to include source code. This is an example when using the Elmah.Io.Client directly:

    +

    var elmahIoClient = ElmahioAPI.Create("API_KEY");
    +elmahIoClient.Messages.OnMessage += (sender, e) => e.Message.WithSourceCodeFromFileSystem();
    +

    +

    Using an integration like Elmah.Io.AspNetCore uses the same method:

    +

    services.AddElmahIo(options =>
    +{
    +    options.OnMessage = msg => msg.WithSourceCodeFromFileSystem();
    +});
    +

    +

    This will automatically instruct Elmah.Io.Client.Extensions.Source to try and parse any stack trace in the details property and embed the source code.

    +

    For an example of how to use the WithSourceCodeFromFileSystem method, check out the following sample: Elmah.Io.Client.Extensions.SourceCode.FileSystem.

    +

    From the PDB file

    +

    When deploying your code on another environment, you typically don't have the original code available. If you copy your source code to the same absolute path as when building, you can use the file-system approach shown above. If not, embedding the source code in the PDB file can be the option. Before doing so, make sure you include filename and line numbers in stack traces on all environments as shown here: Include filename and line number in stack traces. For the old project template the Debugging information field needs a value of Portable. For the new project template the Debug symbols field needs a value of PDB file, portable across platforms.

    +

    To embed source code in the PDB file built alongside your DLL files, include the following property in your csproj file:

    +

    <PropertyGroup>
    +  <EmbedAllSources>true</EmbedAllSources>
    +</PropertyGroup>
    +

    +

    Be aware that this will include your original source code in your deployment which may not be a good approach if other people have access to the environment or binary files. Next, call the WithSourceCodeFromPdb method:

    +

    var elmahIoClient = ElmahioAPI.Create("API_KEY");
    +elmahIoClient.Messages.OnMessage += (sender, e) => e.Message.WithSourceCodeFromPdb();
    +

    +

    For an example of how to do this from ASP.NET Core, you can use the same approach as specified in the previous section:

    +

    services.AddElmahIo(options =>
    +{
    +    options.OnMessage = msg => msg.WithSourceCodeFromPdb();
    +});
    +

    +

    All of our integrations support a message callback somehow.

    +

    For an example of how to use the WithSourceCodeFromPdb method, check out the following sample: Elmah.Io.Client.Extensions.SourceCode.PdbSample for .NET and Elmah.Io.Client.Extensions.SourceCode.NetFrameworkPdb for .NET Framework.

    +

    Manually

    +

    In case you want to include source code manually, you can use the OnMessage event and the Code property on the CreateMessage class:

    +

    var elmahIoClient = ElmahioAPI.Create("API_KEY");
    +elmahIoClient.Messages.OnMessage += (sender, e) =>
    +{
    +    e.Message.Code = FetchCode();
    +}
    +

    +

    You will need to implement the FetchCode method to return the source code to include. Only 21 lines of code are supported for now.

    +

    In case you want elmah.io to show the correct line numbers, you will need to tell us how the first line number in the provided code matches your original source file as well as the line number causing the error. This is done by adding two Items to the Data dictionary on CreateMessage:

    +

    var elmahIoClient = ElmahioAPI.Create("API_KEY");
    +elmahIoClient.Messages.OnMessage += (sender, e) =>
    +{
    +    e.Message.Code = FetchCode();
    +    if (e.Message.Data == null) e.Message.Data = new List<Item>();
    +    e.Message.Data.Add(new Item("X-ELMAHIO-CODESTARTLINE", "42"));
    +    e.Message.Data.Add(new Item("X-ELMAHIO-CODELINE", "51"));
    +}
    +

    +

    This will show line number 42 next to the first code line and highlight line number 51 in the elmah.io UI.

    +

    Troubleshooting

    +

    If no source code shows up on elmah.io log messages, you can start by running through the following checks:

    +
      +
    • Make sure that the log message contains a stack trace in the details field.
    • +
    • Make sure that the stack trace contains absolute path filenames and line numbers for the code causing the stack trace.
    • +
    • Make sure that you are calling the WithSourceCodeFromPdb or WithSourceCodeFromFileSystem method.
    • +
    • Make sure that the Elmah.Io.Client.Extensions.SourceCode.dll file is in your deployed application.
    • +
    • Make sure that your project has Portable set in Debugging information or PDB File, portable across platforms set in Debug symbols.
    • +
    • For PDB files, make sure that you have included the EmbedAllSources element in your csproj file.
    • +
    • Look inside the Data tab on the logged message. It may contain a key named X-ELMAHIO-CODEERROR with a value explaining what went wrong.
    • +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-manage-subscriptions-update-credit-cards-etc/index.html b/how-to-manage-subscriptions-update-credit-cards-etc/index.html new file mode 100644 index 0000000000..0c2df7cc6c --- /dev/null +++ b/how-to-manage-subscriptions-update-credit-cards-etc/index.html @@ -0,0 +1,667 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to manage subscriptions, update credit cards, etc. + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to manage subscriptions, update credit cards, etc.

    +

    Your subscription is managed from the organization settings page.

    +

    To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard:

    +

    Organization settings

    +

    When on the organization settings page, click the Subscription tab. Your subscription can be managed by scrolling to the bottom of the page and looking for the currently active subscription:

    +

    Manage subscription

    +

    In the following sections, we will go through each button.

    +

    Purchase top-up

    +

    If getting near your monthly log message limit you can purchase a top-up to avoid having to permanently upgrade to a larger plan. Top-ups are priced at $19 and will add 25,000 messages and 1,000 emails to your subscription for the rest of the calendar month.

    +

    Update Credit Card

    +

    If your recent payment failed or you received a new credit card from your bank, you can use this button to input the new credit card.

    +

    Cancel Subscription

    +

    To cancel your current subscription you can click the Cancel Subscription button. This will open the chat where we will guide you through the process. We don't use a manual offboarding process to try and convince your to stay or to make it hard for you to leave. When leaving a system like elmah.io, we need additional details from you like when you want to cancel (mostly geared towards annual subscriptions) and to make sure that you have backed up all data from elmah.io before it is cleaned up.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-rename-a-log/index.html b/how-to-rename-a-log/index.html new file mode 100644 index 0000000000..f3f2601409 --- /dev/null +++ b/how-to-rename-a-log/index.html @@ -0,0 +1,658 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to rename a log + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to rename a log

    +

    Logs can be renamed from the Dashboard by anyone with Administrator access to the log. To rename a log, click the small icon in the lower right corner:

    +

    Rename log

    +

    When clicking the icon the log box will flip and you will be able to input a new name. The edit log box also lets you assign the log to an environment, subscribe/unsubscribe from emails, as well as change the color of the log. Be aware that changing the name, environment, or color will be visible for all users with access to the log.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-run-elmah-io-in-dark-mode/index.html b/how-to-run-elmah-io-in-dark-mode/index.html new file mode 100644 index 0000000000..7ae96606dd --- /dev/null +++ b/how-to-run-elmah-io-in-dark-mode/index.html @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to run elmah.io in dark mode + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to run elmah.io in dark mode

    +

    Some developers prefer applications with a dark mode either full-time or when working those late hours. The elmah.io application (app.elmah.io) has a light theme but can be run in dark mode using a browser extension. We recommend the following extensions for running elmah.io in dark mode:

    +

    Dark Mode - Night Eye - Chrome - Edge - Firefox

    +

    elmah.io in Dark Mode - Night Eye

    +

    Dark Reader - Chrome - Edge - Firefox

    +

    elmah.io in Dark Reader

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/how-to-search-custom-data/index.html b/how-to-search-custom-data/index.html new file mode 100644 index 0000000000..8f203a10b3 --- /dev/null +++ b/how-to-search-custom-data/index.html @@ -0,0 +1,743 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + How to search custom data + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    How to search custom data

    +

    Custom data is not searchable by default. Sometimes it makes sense that errors can be searched from values logged as part of custom data. For now, this feature is supported through the use of variable naming, but we may extend this to a configuration option through the API or UI as well.

    +

    To make a custom variable and its value searchable through the UI (as well as through the API), name the variable with the prefix X-ELMAHIO-SEARCH-. The variable will become searchable through the name added after the prefix.

    +

    Examples:

    + + +
    +
    +

    Elmah.ErrorLog.GetDefault(null);
    +var logger = Elmah.Io.ErrorLog.Client;
    +logger.OnMessage += (sender, args) =>
    +{
    +    if (args.Message.Data == null) args.Message.Data = new List<Item>();
    +    args.Message.Data.Add(new Item { Key = "X-ELMAHIO-SEARCH-author", Value = "Walter Sobchak" });
    +};
    +

    +
    +
    +

    builder.Services.AddElmahIo(o =>
    +{
    +    o.OnMessage = message =>
    +    {
    +        if (message.Data == null) message.Data = new List<Item>();
    +        message.Data.Add(new Item { Key = "X-ELMAHIO-SEARCH-author", Value = "Walter Sobchak" });
    +    };
    +});
    +

    +
    +
    +

    using (LogContext.PushProperty("X-ELMAHIO-SEARCH-author", "Walter Sobchak"))
    +{
    +    logger.Error("You see what happens, Larry?");
    +}
    +

    +
    +
    +

    var errorMessage = new LogEventInfo(LogLevel.Error, "", "You see what happens, Larry?");
    +errorMessage.Properties.Add("X-ELMAHIO-SEARCH-author", "Walter Sobchak");
    +log.Error(errorMessage);
    +

    +
    +
    +

    var properties = new PropertiesDictionary();
    +properties["X-ELMAHIO-SEARCH-author"] = "Walter Sobchak";
    +log.Logger.Log(new LoggingEvent(new LoggingEventData
    +{
    +    Level = Level.Error,
    +    TimeStampUtc = DateTime.UtcNow,
    +    Properties = properties,
    +    Message = "You see what happens, Larry?",
    +}));
    +

    +
    +
    +

    var scope = new Dictionary<string, object> { { "X-ELMAHIO-SEARCH-author", "Walter Sobchak" } };
    +using (logger.BeginScope(scope}))
    +{
    +    logger.LogError("You see what happens, Larry?");
    +}
    +

    +
    +
    +

    The examples will make author searchable using this query:

    +

    data.author:"Walter Sobchak"
    +

    +

    Observe how X-ELMAHIO-SEARCH- is replaced with the data. prefix when indexed in elmah.io.

    +

    Adding searchable properties is available when logging exceptions too:

    +

    try
    +{
    +    // ...
    +}
    +catch (NullReferenceException e)
    +{
    +    e.Data.Add("X-ELMAHIO-SEARCH-author", "Walter Sobchak");
    +    // Log the exception or throw e to use this catch for decorating the exception
    +}
    +

    +

    To avoid someone filling up our cluster with custom data, only the first three variables containing X-ELMAHIO-SEARCH- are made searchable. Also, variables with a value containing more than 256 characters are not indexed.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/add-api-key-and-log-id-to-application-settings-v2.png b/images/add-api-key-and-log-id-to-application-settings-v2.png new file mode 100644 index 0000000000..87b0d52686 Binary files /dev/null and b/images/add-api-key-and-log-id-to-application-settings-v2.png differ diff --git a/images/add-api-key-and-log-id-to-application-settings.png b/images/add-api-key-and-log-id-to-application-settings.png new file mode 100644 index 0000000000..dc68fd7733 Binary files /dev/null and b/images/add-api-key-and-log-id-to-application-settings.png differ diff --git a/images/add-powershell-task-v2.png b/images/add-powershell-task-v2.png new file mode 100644 index 0000000000..cdbf9e5b6d Binary files /dev/null and b/images/add-powershell-task-v2.png differ diff --git a/images/add-site-extension-v2.png b/images/add-site-extension-v2.png new file mode 100644 index 0000000000..ec1b5f550f Binary files /dev/null and b/images/add-site-extension-v2.png differ diff --git a/images/add-site-extension.png b/images/add-site-extension.png new file mode 100644 index 0000000000..1fcde3707f Binary files /dev/null and b/images/add-site-extension.png differ diff --git a/images/add_elmah_io_deployment_step.png b/images/add_elmah_io_deployment_step.png new file mode 100644 index 0000000000..144d4ac326 Binary files /dev/null and b/images/add_elmah_io_deployment_step.png differ diff --git a/images/add_new_hipchat_rule.png b/images/add_new_hipchat_rule.png new file mode 100644 index 0000000000..817af7393d Binary files /dev/null and b/images/add_new_hipchat_rule.png differ diff --git a/images/add_new_slack_rule.png b/images/add_new_slack_rule.png new file mode 100644 index 0000000000..11e60b4127 Binary files /dev/null and b/images/add_new_slack_rule.png differ diff --git a/images/add_powershell_task.png b/images/add_powershell_task.png new file mode 100644 index 0000000000..cce6e613e6 Binary files /dev/null and b/images/add_powershell_task.png differ diff --git a/images/add_step_template_to_octopus.png b/images/add_step_template_to_octopus.png new file mode 100644 index 0000000000..16bee0bae1 Binary files /dev/null and b/images/add_step_template_to_octopus.png differ diff --git a/images/add_user_to_org.png b/images/add_user_to_org.png new file mode 100644 index 0000000000..ed30625765 Binary files /dev/null and b/images/add_user_to_org.png differ diff --git a/images/admin_users_on_log.png b/images/admin_users_on_log.png new file mode 100644 index 0000000000..8bf21d35e4 Binary files /dev/null and b/images/admin_users_on_log.png differ diff --git a/images/advanced_build_settings.png b/images/advanced_build_settings.png new file mode 100644 index 0000000000..75ab760521 Binary files /dev/null and b/images/advanced_build_settings.png differ diff --git a/images/api-key-on-organization-settings-v2.png b/images/api-key-on-organization-settings-v2.png new file mode 100644 index 0000000000..72a1fe1cf8 Binary files /dev/null and b/images/api-key-on-organization-settings-v2.png differ diff --git a/images/api-key-on-organization-settings.png b/images/api-key-on-organization-settings.png new file mode 100644 index 0000000000..9746906cd9 Binary files /dev/null and b/images/api-key-on-organization-settings.png differ diff --git a/images/api-keys.png b/images/api-keys.png new file mode 100644 index 0000000000..6ec5e131ff Binary files /dev/null and b/images/api-keys.png differ diff --git a/images/apps/azureboards/install-settings.png b/images/apps/azureboards/install-settings.png new file mode 100644 index 0000000000..8cea053dfa Binary files /dev/null and b/images/apps/azureboards/install-settings.png differ diff --git a/images/apps/azureboards/personal-access-token.png b/images/apps/azureboards/personal-access-token.png new file mode 100644 index 0000000000..26af7e8c7f Binary files /dev/null and b/images/apps/azureboards/personal-access-token.png differ diff --git a/images/apps/bitbucket/apikey.png b/images/apps/bitbucket/apikey.png new file mode 100644 index 0000000000..c85ac430e9 Binary files /dev/null and b/images/apps/bitbucket/apikey.png differ diff --git a/images/apps/bitbucket/app_password.png b/images/apps/bitbucket/app_password.png new file mode 100644 index 0000000000..e8f3b1348e Binary files /dev/null and b/images/apps/bitbucket/app_password.png differ diff --git a/images/apps/bitbucket/install_settings.png b/images/apps/bitbucket/install_settings.png new file mode 100644 index 0000000000..5c9cea180f Binary files /dev/null and b/images/apps/bitbucket/install_settings.png differ diff --git a/images/apps/bitbucket/install_settings_v2.png b/images/apps/bitbucket/install_settings_v2.png new file mode 100644 index 0000000000..a8ffc5a69d Binary files /dev/null and b/images/apps/bitbucket/install_settings_v2.png differ diff --git a/images/apps/bitbucket/install_settings_v3.png b/images/apps/bitbucket/install_settings_v3.png new file mode 100644 index 0000000000..24b6eb3003 Binary files /dev/null and b/images/apps/bitbucket/install_settings_v3.png differ diff --git a/images/apps/chatgpt/chatgpt-install.png b/images/apps/chatgpt/chatgpt-install.png new file mode 100644 index 0000000000..c3371838d8 Binary files /dev/null and b/images/apps/chatgpt/chatgpt-install.png differ diff --git a/images/apps/clickup/copy-link.png b/images/apps/clickup/copy-link.png new file mode 100644 index 0000000000..ae13532b9b Binary files /dev/null and b/images/apps/clickup/copy-link.png differ diff --git a/images/apps/clickup/install-settings.png b/images/apps/clickup/install-settings.png new file mode 100644 index 0000000000..88f0eb2c2f Binary files /dev/null and b/images/apps/clickup/install-settings.png differ diff --git a/images/apps/github/generate_token.png b/images/apps/github/generate_token.png new file mode 100644 index 0000000000..debbdf8b96 Binary files /dev/null and b/images/apps/github/generate_token.png differ diff --git a/images/apps/github/generate_token_v2.png b/images/apps/github/generate_token_v2.png new file mode 100644 index 0000000000..d2c96a0432 Binary files /dev/null and b/images/apps/github/generate_token_v2.png differ diff --git a/images/apps/github/install_github.png b/images/apps/github/install_github.png new file mode 100644 index 0000000000..e995896d53 Binary files /dev/null and b/images/apps/github/install_github.png differ diff --git a/images/apps/gitlab/gitlab-access-token.png b/images/apps/gitlab/gitlab-access-token.png new file mode 100644 index 0000000000..d8f82dbd54 Binary files /dev/null and b/images/apps/gitlab/gitlab-access-token.png differ diff --git a/images/apps/gitlab/gitlab-settings.png b/images/apps/gitlab/gitlab-settings.png new file mode 100644 index 0000000000..d2352fb7b0 Binary files /dev/null and b/images/apps/gitlab/gitlab-settings.png differ diff --git a/images/apps/hipchat/generate_token.png b/images/apps/hipchat/generate_token.png new file mode 100644 index 0000000000..20f13223ae Binary files /dev/null and b/images/apps/hipchat/generate_token.png differ diff --git a/images/apps/hipchat/install_hipchat.png b/images/apps/hipchat/install_hipchat.png new file mode 100644 index 0000000000..e2fe844c2c Binary files /dev/null and b/images/apps/hipchat/install_hipchat.png differ diff --git a/images/apps/jira/install_settings.png b/images/apps/jira/install_settings.png new file mode 100644 index 0000000000..01f6affc85 Binary files /dev/null and b/images/apps/jira/install_settings.png differ diff --git a/images/apps/pagerduty/add-integration-v2.png b/images/apps/pagerduty/add-integration-v2.png new file mode 100644 index 0000000000..0a5f7ad2dd Binary files /dev/null and b/images/apps/pagerduty/add-integration-v2.png differ diff --git a/images/apps/pagerduty/add-integration.png b/images/apps/pagerduty/add-integration.png new file mode 100644 index 0000000000..5b8d0e4cb5 Binary files /dev/null and b/images/apps/pagerduty/add-integration.png differ diff --git a/images/apps/pagerduty/install-pagerduty-on-elmahio.png b/images/apps/pagerduty/install-pagerduty-on-elmahio.png new file mode 100644 index 0000000000..9bde414455 Binary files /dev/null and b/images/apps/pagerduty/install-pagerduty-on-elmahio.png differ diff --git a/images/apps/pagerduty/integration-name-and-key-v2.png b/images/apps/pagerduty/integration-name-and-key-v2.png new file mode 100644 index 0000000000..6e0eb23356 Binary files /dev/null and b/images/apps/pagerduty/integration-name-and-key-v2.png differ diff --git a/images/apps/pagerduty/integration-name-and-key.png b/images/apps/pagerduty/integration-name-and-key.png new file mode 100644 index 0000000000..3b93b2b811 Binary files /dev/null and b/images/apps/pagerduty/integration-name-and-key.png differ diff --git a/images/apps/slack/generate_token.png b/images/apps/slack/generate_token.png new file mode 100644 index 0000000000..9b8802c88e Binary files /dev/null and b/images/apps/slack/generate_token.png differ diff --git a/images/apps/slack/install_slack.png b/images/apps/slack/install_slack.png new file mode 100644 index 0000000000..9b76356c53 Binary files /dev/null and b/images/apps/slack/install_slack.png differ diff --git a/images/apps/teams/step1.png b/images/apps/teams/step1.png new file mode 100644 index 0000000000..b518dc5d0b Binary files /dev/null and b/images/apps/teams/step1.png differ diff --git a/images/apps/teams/step2.png b/images/apps/teams/step2.png new file mode 100644 index 0000000000..1146d792bc Binary files /dev/null and b/images/apps/teams/step2.png differ diff --git a/images/apps/teams/step3.png b/images/apps/teams/step3.png new file mode 100644 index 0000000000..b408e5d0f7 Binary files /dev/null and b/images/apps/teams/step3.png differ diff --git a/images/apps/teams/step4.png b/images/apps/teams/step4.png new file mode 100644 index 0000000000..20e62cb354 Binary files /dev/null and b/images/apps/teams/step4.png differ diff --git a/images/apps/teams/step5.png b/images/apps/teams/step5.png new file mode 100644 index 0000000000..0cf30e7e27 Binary files /dev/null and b/images/apps/teams/step5.png differ diff --git a/images/apps/trello/install_settings.png b/images/apps/trello/install_settings.png new file mode 100644 index 0000000000..8564140505 Binary files /dev/null and b/images/apps/trello/install_settings.png differ diff --git a/images/apps/trello/trello-list-id.png b/images/apps/trello/trello-list-id.png new file mode 100644 index 0000000000..8a595026e4 Binary files /dev/null and b/images/apps/trello/trello-list-id.png differ diff --git a/images/apps/twilio/install-twilio-app.png b/images/apps/twilio/install-twilio-app.png new file mode 100644 index 0000000000..9e1dfe9e8d Binary files /dev/null and b/images/apps/twilio/install-twilio-app.png differ diff --git a/images/apps/webclient/install.png b/images/apps/webclient/install.png new file mode 100644 index 0000000000..b3b828ceb2 Binary files /dev/null and b/images/apps/webclient/install.png differ diff --git a/images/apps/youtrack/generate_permanent_token.png b/images/apps/youtrack/generate_permanent_token.png new file mode 100644 index 0000000000..431067b9b9 Binary files /dev/null and b/images/apps/youtrack/generate_permanent_token.png differ diff --git a/images/apps/youtrack/generate_permanent_token_v2.png b/images/apps/youtrack/generate_permanent_token_v2.png new file mode 100644 index 0000000000..fa59a20bdf Binary files /dev/null and b/images/apps/youtrack/generate_permanent_token_v2.png differ diff --git a/images/apps/youtrack/install_youtrack_app.png b/images/apps/youtrack/install_youtrack_app.png new file mode 100644 index 0000000000..12bded4ea7 Binary files /dev/null and b/images/apps/youtrack/install_youtrack_app.png differ diff --git a/images/apps/youtrack/install_youtrack_app_v2.png b/images/apps/youtrack/install_youtrack_app_v2.png new file mode 100644 index 0000000000..dcbeb07e0e Binary files /dev/null and b/images/apps/youtrack/install_youtrack_app_v2.png differ diff --git a/images/apps/youtrack/install_youtrack_app_v3.png b/images/apps/youtrack/install_youtrack_app_v3.png new file mode 100644 index 0000000000..9b6a387c48 Binary files /dev/null and b/images/apps/youtrack/install_youtrack_app_v3.png differ diff --git a/images/authorize_elmah_io_and_zapier.png b/images/authorize_elmah_io_and_zapier.png new file mode 100644 index 0000000000..5ef01a17a8 Binary files /dev/null and b/images/authorize_elmah_io_and_zapier.png differ diff --git a/images/aws-environment-properties-core.png b/images/aws-environment-properties-core.png new file mode 100644 index 0000000000..071b016168 Binary files /dev/null and b/images/aws-environment-properties-core.png differ diff --git a/images/aws-environment-properties-v2.png b/images/aws-environment-properties-v2.png new file mode 100644 index 0000000000..b9ab978618 Binary files /dev/null and b/images/aws-environment-properties-v2.png differ diff --git a/images/aws-environment-properties.png b/images/aws-environment-properties.png new file mode 100644 index 0000000000..1f7325e013 Binary files /dev/null and b/images/aws-environment-properties.png differ diff --git a/images/azure-application-settings-v2.png b/images/azure-application-settings-v2.png new file mode 100644 index 0000000000..4c602d1200 Binary files /dev/null and b/images/azure-application-settings-v2.png differ diff --git a/images/azure-application-settings.png b/images/azure-application-settings.png new file mode 100644 index 0000000000..22bde7977d Binary files /dev/null and b/images/azure-application-settings.png differ diff --git a/images/bamboo.png b/images/bamboo.png new file mode 100644 index 0000000000..f3d6f315f2 Binary files /dev/null and b/images/bamboo.png differ diff --git a/images/bounced-email.png b/images/bounced-email.png new file mode 100644 index 0000000000..4e2551ea1c Binary files /dev/null and b/images/bounced-email.png differ diff --git a/images/choose_a_trigger_and_action.png b/images/choose_a_trigger_and_action.png new file mode 100644 index 0000000000..f45ce74630 Binary files /dev/null and b/images/choose_a_trigger_and_action.png differ diff --git a/images/choose_a_trigger_and_action2.png b/images/choose_a_trigger_and_action2.png new file mode 100644 index 0000000000..f45ce74630 Binary files /dev/null and b/images/choose_a_trigger_and_action2.png differ diff --git a/images/choose_a_trigger_and_action_filled.png b/images/choose_a_trigger_and_action_filled.png new file mode 100644 index 0000000000..b6a4f76ed2 Binary files /dev/null and b/images/choose_a_trigger_and_action_filled.png differ diff --git a/images/choose_elmah_io_account.png b/images/choose_elmah_io_account.png new file mode 100644 index 0000000000..693ae7053a Binary files /dev/null and b/images/choose_elmah_io_account.png differ diff --git a/images/choose_elmah_io_account2.png b/images/choose_elmah_io_account2.png new file mode 100644 index 0000000000..43f8bf5a6f Binary files /dev/null and b/images/choose_elmah_io_account2.png differ diff --git a/images/connect_elmah_io_account.png b/images/connect_elmah_io_account.png new file mode 100644 index 0000000000..0339d06da7 Binary files /dev/null and b/images/connect_elmah_io_account.png differ diff --git a/images/context_properties_from_log4net.png b/images/context_properties_from_log4net.png new file mode 100644 index 0000000000..8b2112e7d6 Binary files /dev/null and b/images/context_properties_from_log4net.png differ diff --git a/images/copy_log_id_dialog.png b/images/copy_log_id_dialog.png new file mode 100644 index 0000000000..241e3be369 Binary files /dev/null and b/images/copy_log_id_dialog.png differ diff --git a/images/correlation-id-filter.png b/images/correlation-id-filter.png new file mode 100644 index 0000000000..83962dd6fb Binary files /dev/null and b/images/correlation-id-filter.png differ diff --git a/images/create-cloudflare-firewall-rule.png b/images/create-cloudflare-firewall-rule.png new file mode 100644 index 0000000000..0461364cbf Binary files /dev/null and b/images/create-cloudflare-firewall-rule.png differ diff --git a/images/create-heartbeat-v2.png b/images/create-heartbeat-v2.png new file mode 100644 index 0000000000..1e2301cabd Binary files /dev/null and b/images/create-heartbeat-v2.png differ diff --git a/images/create-heartbeat.png b/images/create-heartbeat.png new file mode 100644 index 0000000000..d74415ffa1 Binary files /dev/null and b/images/create-heartbeat.png differ diff --git a/images/create-heartbeats-api-key-v2.png b/images/create-heartbeats-api-key-v2.png new file mode 100644 index 0000000000..0f159c3767 Binary files /dev/null and b/images/create-heartbeats-api-key-v2.png differ diff --git a/images/create-heartbeats-api-key.png b/images/create-heartbeats-api-key.png new file mode 100644 index 0000000000..53652137df Binary files /dev/null and b/images/create-heartbeats-api-key.png differ diff --git a/images/create_aspnetmvc_website.png b/images/create_aspnetmvc_website.png new file mode 100644 index 0000000000..b4f1135b8b Binary files /dev/null and b/images/create_aspnetmvc_website.png differ diff --git a/images/create_ignore_rule.png b/images/create_ignore_rule.png new file mode 100644 index 0000000000..ea38b0cb43 Binary files /dev/null and b/images/create_ignore_rule.png differ diff --git a/images/create_new_log.png b/images/create_new_log.png new file mode 100644 index 0000000000..0ea5b5110c Binary files /dev/null and b/images/create_new_log.png differ diff --git a/images/create_requestbin.png b/images/create_requestbin.png new file mode 100644 index 0000000000..d36dc6252b Binary files /dev/null and b/images/create_requestbin.png differ diff --git a/images/deploy-notification/marketplace_get_it_free.png b/images/deploy-notification/marketplace_get_it_free.png new file mode 100644 index 0000000000..fdf409361e Binary files /dev/null and b/images/deploy-notification/marketplace_get_it_free.png differ diff --git a/images/deploy-notification/marketplace_select_organization.png b/images/deploy-notification/marketplace_select_organization.png new file mode 100644 index 0000000000..12d0aafb4b Binary files /dev/null and b/images/deploy-notification/marketplace_select_organization.png differ diff --git a/images/deploy-notification/octopus-step-template-v2.png b/images/deploy-notification/octopus-step-template-v2.png new file mode 100644 index 0000000000..1c6b5e5144 Binary files /dev/null and b/images/deploy-notification/octopus-step-template-v2.png differ diff --git a/images/deploy-notification/octopus_search_step_template-v2.png b/images/deploy-notification/octopus_search_step_template-v2.png new file mode 100644 index 0000000000..1bb4847cbb Binary files /dev/null and b/images/deploy-notification/octopus_search_step_template-v2.png differ diff --git a/images/deploy-notification/octopus_search_step_template.png b/images/deploy-notification/octopus_search_step_template.png new file mode 100644 index 0000000000..aa6daa9ae2 Binary files /dev/null and b/images/deploy-notification/octopus_search_step_template.png differ diff --git a/images/deploy-notification/octopus_step_template.png b/images/deploy-notification/octopus_step_template.png new file mode 100644 index 0000000000..a46528bbf0 Binary files /dev/null and b/images/deploy-notification/octopus_step_template.png differ diff --git a/images/deploy-notification/release-pipeline-task-v2.png b/images/deploy-notification/release-pipeline-task-v2.png new file mode 100644 index 0000000000..6fbd45a060 Binary files /dev/null and b/images/deploy-notification/release-pipeline-task-v2.png differ diff --git a/images/deploy-notification/release_pipeline_task.png b/images/deploy-notification/release_pipeline_task.png new file mode 100644 index 0000000000..6fe13f50e7 Binary files /dev/null and b/images/deploy-notification/release_pipeline_task.png differ diff --git a/images/deployment-tracking-api-key-v2.png b/images/deployment-tracking-api-key-v2.png new file mode 100644 index 0000000000..d4d05d2334 Binary files /dev/null and b/images/deployment-tracking-api-key-v2.png differ diff --git a/images/deployment-tracking-api-key.png b/images/deployment-tracking-api-key.png new file mode 100644 index 0000000000..fe90bce27f Binary files /dev/null and b/images/deployment-tracking-api-key.png differ diff --git a/images/deployments_post.png b/images/deployments_post.png new file mode 100644 index 0000000000..3c8f2e3f49 Binary files /dev/null and b/images/deployments_post.png differ diff --git a/images/detailed-usage-report-v2.png b/images/detailed-usage-report-v2.png new file mode 100644 index 0000000000..7a4b10d2df Binary files /dev/null and b/images/detailed-usage-report-v2.png differ diff --git a/images/detailed-usage-report.png b/images/detailed-usage-report.png new file mode 100644 index 0000000000..ae82a649aa Binary files /dev/null and b/images/detailed-usage-report.png differ diff --git a/images/edit-api-key.png b/images/edit-api-key.png new file mode 100644 index 0000000000..6842c29c0e Binary files /dev/null and b/images/edit-api-key.png differ diff --git a/images/edit_api_key.png b/images/edit_api_key.png new file mode 100644 index 0000000000..c625196056 Binary files /dev/null and b/images/edit_api_key.png differ diff --git a/images/elmah_io_account_selected_on_zapier.png b/images/elmah_io_account_selected_on_zapier.png new file mode 100644 index 0000000000..b48ae7b578 Binary files /dev/null and b/images/elmah_io_account_selected_on_zapier.png differ diff --git a/images/elmah_io_and_trigger_selected.png b/images/elmah_io_and_trigger_selected.png new file mode 100644 index 0000000000..cb60a674e2 Binary files /dev/null and b/images/elmah_io_and_trigger_selected.png differ diff --git a/images/elmah_io_error_on_hipchat.png b/images/elmah_io_error_on_hipchat.png new file mode 100644 index 0000000000..0e63fb4cc5 Binary files /dev/null and b/images/elmah_io_error_on_hipchat.png differ diff --git a/images/elmah_io_error_on_slack.png b/images/elmah_io_error_on_slack.png new file mode 100644 index 0000000000..709935b9d6 Binary files /dev/null and b/images/elmah_io_error_on_slack.png differ diff --git a/images/elmah_io_vs1.png b/images/elmah_io_vs1.png new file mode 100644 index 0000000000..7b56260abe Binary files /dev/null and b/images/elmah_io_vs1.png differ diff --git a/images/elmah_io_vs2.png b/images/elmah_io_vs2.png new file mode 100644 index 0000000000..25951e712f Binary files /dev/null and b/images/elmah_io_vs2.png differ diff --git a/images/elmah_io_vs3.png b/images/elmah_io_vs3.png new file mode 100644 index 0000000000..3109599ebf Binary files /dev/null and b/images/elmah_io_vs3.png differ diff --git a/images/elmah_io_vs4.png b/images/elmah_io_vs4.png new file mode 100644 index 0000000000..78eb19f456 Binary files /dev/null and b/images/elmah_io_vs4.png differ diff --git a/images/elmahio-darkreader.png b/images/elmahio-darkreader.png new file mode 100644 index 0000000000..f78c2f863e Binary files /dev/null and b/images/elmahio-darkreader.png differ diff --git a/images/elmahio-nighteye.png b/images/elmahio-nighteye.png new file mode 100644 index 0000000000..7b23c1a1ac Binary files /dev/null and b/images/elmahio-nighteye.png differ diff --git a/images/enable-two-factor.png b/images/enable-two-factor.png new file mode 100644 index 0000000000..5893ce0d8d Binary files /dev/null and b/images/enable-two-factor.png differ diff --git a/images/enabled_disable_log.png b/images/enabled_disable_log.png new file mode 100644 index 0000000000..0b26d76b49 Binary files /dev/null and b/images/enabled_disable_log.png differ diff --git a/images/environments-dropdown.png b/images/environments-dropdown.png new file mode 100644 index 0000000000..ec7d0b7378 Binary files /dev/null and b/images/environments-dropdown.png differ diff --git a/images/environments.png b/images/environments.png new file mode 100644 index 0000000000..79c34bc30b Binary files /dev/null and b/images/environments.png differ diff --git a/images/error-details-v2.png b/images/error-details-v2.png new file mode 100644 index 0000000000..68b418231c Binary files /dev/null and b/images/error-details-v2.png differ diff --git a/images/error_details.png b/images/error_details.png new file mode 100644 index 0000000000..51c6d871a2 Binary files /dev/null and b/images/error_details.png differ diff --git a/images/errorsinumbraco.png b/images/errorsinumbraco.png new file mode 100644 index 0000000000..ce94b3e4b6 Binary files /dev/null and b/images/errorsinumbraco.png differ diff --git a/images/exclude_generated_debug_symbols.png b/images/exclude_generated_debug_symbols.png new file mode 100644 index 0000000000..34ba53dfc9 Binary files /dev/null and b/images/exclude_generated_debug_symbols.png differ diff --git a/images/extended-user-details-v2.png b/images/extended-user-details-v2.png new file mode 100644 index 0000000000..fd7faa20f1 Binary files /dev/null and b/images/extended-user-details-v2.png differ diff --git a/images/extended_user_details.png b/images/extended_user_details.png new file mode 100644 index 0000000000..19b7fc30c9 Binary files /dev/null and b/images/extended_user_details.png differ diff --git a/images/fill-powershell-task-v2.png b/images/fill-powershell-task-v2.png new file mode 100644 index 0000000000..d3b9929127 Binary files /dev/null and b/images/fill-powershell-task-v2.png differ diff --git a/images/fill_powershell_task.png b/images/fill_powershell_task.png new file mode 100644 index 0000000000..fa6743e8da Binary files /dev/null and b/images/fill_powershell_task.png differ diff --git a/images/filter_elmah_io_triggers.png b/images/filter_elmah_io_triggers.png new file mode 100644 index 0000000000..c78a6002c3 Binary files /dev/null and b/images/filter_elmah_io_triggers.png differ diff --git a/images/filters.png b/images/filters.png new file mode 100644 index 0000000000..6a81acf667 Binary files /dev/null and b/images/filters.png differ diff --git a/images/full-text-search-v2.png b/images/full-text-search-v2.png new file mode 100644 index 0000000000..8f7def6b9f Binary files /dev/null and b/images/full-text-search-v2.png differ diff --git a/images/full-text-search.png b/images/full-text-search.png new file mode 100644 index 0000000000..d4911c02e2 Binary files /dev/null and b/images/full-text-search.png differ diff --git a/images/gitlab-access-token.png b/images/gitlab-access-token.png new file mode 100644 index 0000000000..51c1799190 Binary files /dev/null and b/images/gitlab-access-token.png differ diff --git a/images/gitlab-settings.png b/images/gitlab-settings.png new file mode 100644 index 0000000000..d2352fb7b0 Binary files /dev/null and b/images/gitlab-settings.png differ diff --git a/images/gmail-never-send-it-to-spam.png b/images/gmail-never-send-it-to-spam.png new file mode 100644 index 0000000000..663349161f Binary files /dev/null and b/images/gmail-never-send-it-to-spam.png differ diff --git a/images/gmail-search-all-conversations.png b/images/gmail-search-all-conversations.png new file mode 100644 index 0000000000..a26fb41e24 Binary files /dev/null and b/images/gmail-search-all-conversations.png differ diff --git a/images/hipchat_api_page.png b/images/hipchat_api_page.png new file mode 100644 index 0000000000..eea469be0f Binary files /dev/null and b/images/hipchat_api_page.png differ diff --git a/images/ignore-like-this-v2.png b/images/ignore-like-this-v2.png new file mode 100644 index 0000000000..d786c33114 Binary files /dev/null and b/images/ignore-like-this-v2.png differ diff --git a/images/ignore_like_this.png b/images/ignore_like_this.png new file mode 100644 index 0000000000..6d73451e05 Binary files /dev/null and b/images/ignore_like_this.png differ diff --git a/images/input_log_id.png b/images/input_log_id.png new file mode 100644 index 0000000000..cdf013236c Binary files /dev/null and b/images/input_log_id.png differ diff --git a/images/invoices-tab-v2.png b/images/invoices-tab-v2.png new file mode 100644 index 0000000000..dd60d623c5 Binary files /dev/null and b/images/invoices-tab-v2.png differ diff --git a/images/invoices-tab.png b/images/invoices-tab.png new file mode 100644 index 0000000000..ed23a3e707 Binary files /dev/null and b/images/invoices-tab.png differ diff --git a/images/ipfiltersettings.png b/images/ipfiltersettings.png new file mode 100644 index 0000000000..b6fff68ec1 Binary files /dev/null and b/images/ipfiltersettings.png differ diff --git a/images/isbot-log-messages.png b/images/isbot-log-messages.png new file mode 100644 index 0000000000..f858b7a18e Binary files /dev/null and b/images/isbot-log-messages.png differ diff --git a/images/log-settings-page-v2.png b/images/log-settings-page-v2.png new file mode 100644 index 0000000000..04795c50bc Binary files /dev/null and b/images/log-settings-page-v2.png differ diff --git a/images/log-settings-page.png b/images/log-settings-page.png new file mode 100644 index 0000000000..161474565c Binary files /dev/null and b/images/log-settings-page.png differ diff --git a/images/log-settings-v2.png b/images/log-settings-v2.png new file mode 100644 index 0000000000..6c1a6b647d Binary files /dev/null and b/images/log-settings-v2.png differ diff --git a/images/log-settings.png b/images/log-settings.png new file mode 100644 index 0000000000..053194ff19 Binary files /dev/null and b/images/log-settings.png differ diff --git a/images/looking_at_custom_variables.gif b/images/looking_at_custom_variables.gif new file mode 100644 index 0000000000..004f421a4a Binary files /dev/null and b/images/looking_at_custom_variables.gif differ diff --git a/images/mailmansettings.png b/images/mailmansettings.png new file mode 100644 index 0000000000..452e5985c3 Binary files /dev/null and b/images/mailmansettings.png differ diff --git a/images/manage-email-access.png b/images/manage-email-access.png new file mode 100644 index 0000000000..f0ef58ad54 Binary files /dev/null and b/images/manage-email-access.png differ diff --git a/images/manage-log-access-v2.png b/images/manage-log-access-v2.png new file mode 100644 index 0000000000..a34bf721f5 Binary files /dev/null and b/images/manage-log-access-v2.png differ diff --git a/images/manage-log-access.png b/images/manage-log-access.png new file mode 100644 index 0000000000..c650ada631 Binary files /dev/null and b/images/manage-log-access.png differ diff --git a/images/manage-subscription.png b/images/manage-subscription.png new file mode 100644 index 0000000000..0171226894 Binary files /dev/null and b/images/manage-subscription.png differ diff --git a/images/managing-environments.png b/images/managing-environments.png new file mode 100644 index 0000000000..ad92bf4323 Binary files /dev/null and b/images/managing-environments.png differ diff --git a/images/mark-message-with-isbot.png b/images/mark-message-with-isbot.png new file mode 100644 index 0000000000..16d9d791f7 Binary files /dev/null and b/images/mark-message-with-isbot.png differ diff --git a/images/marketplace_get_it_free.png b/images/marketplace_get_it_free.png new file mode 100644 index 0000000000..3a4ee96f57 Binary files /dev/null and b/images/marketplace_get_it_free.png differ diff --git a/images/marketplace_select_organization.png b/images/marketplace_select_organization.png new file mode 100644 index 0000000000..3f064e30c4 Binary files /dev/null and b/images/marketplace_select_organization.png differ diff --git a/images/match_up_elmah_io_error_to_github_issue.png b/images/match_up_elmah_io_error_to_github_issue.png new file mode 100644 index 0000000000..faba16db29 Binary files /dev/null and b/images/match_up_elmah_io_error_to_github_issue.png differ diff --git a/images/modheader.png b/images/modheader.png new file mode 100644 index 0000000000..39f6299f6a Binary files /dev/null and b/images/modheader.png differ diff --git a/images/name_and_turn_this_zap_on.png b/images/name_and_turn_this_zap_on.png new file mode 100644 index 0000000000..11fee3ef5a Binary files /dev/null and b/images/name_and_turn_this_zap_on.png differ diff --git a/images/new-event-filter.png b/images/new-event-filter.png new file mode 100644 index 0000000000..e2cad8fa01 Binary files /dev/null and b/images/new-event-filter.png differ diff --git a/images/new-isbot-rule.png b/images/new-isbot-rule.png new file mode 100644 index 0000000000..fb0d5961de Binary files /dev/null and b/images/new-isbot-rule.png differ diff --git a/images/no-heartbeats-v2.png b/images/no-heartbeats-v2.png new file mode 100644 index 0000000000..55aad85c74 Binary files /dev/null and b/images/no-heartbeats-v2.png differ diff --git a/images/no-heartbeats.png b/images/no-heartbeats.png new file mode 100644 index 0000000000..e148596cef Binary files /dev/null and b/images/no-heartbeats.png differ diff --git a/images/octopus_deploy_library.png b/images/octopus_deploy_library.png new file mode 100644 index 0000000000..eed1f3359c Binary files /dev/null and b/images/octopus_deploy_library.png differ diff --git a/images/octopus_upload_source_map-v2.png b/images/octopus_upload_source_map-v2.png new file mode 100644 index 0000000000..aeaa3e4bdf Binary files /dev/null and b/images/octopus_upload_source_map-v2.png differ diff --git a/images/octopus_upload_source_map.png b/images/octopus_upload_source_map.png new file mode 100644 index 0000000000..b9bbbaecea Binary files /dev/null and b/images/octopus_upload_source_map.png differ diff --git a/images/open_manage_nuget_packages.png b/images/open_manage_nuget_packages.png new file mode 100644 index 0000000000..498b897f39 Binary files /dev/null and b/images/open_manage_nuget_packages.png differ diff --git a/images/organisation_settings-v2.png b/images/organisation_settings-v2.png new file mode 100644 index 0000000000..b5de67c8e1 Binary files /dev/null and b/images/organisation_settings-v2.png differ diff --git a/images/organisation_settings.png b/images/organisation_settings.png new file mode 100644 index 0000000000..55b80694bc Binary files /dev/null and b/images/organisation_settings.png differ diff --git a/images/organization-settings-v2.png b/images/organization-settings-v2.png new file mode 100644 index 0000000000..9c77e30d59 Binary files /dev/null and b/images/organization-settings-v2.png differ diff --git a/images/organization-settings.png b/images/organization-settings.png new file mode 100644 index 0000000000..219bebf8d2 Binary files /dev/null and b/images/organization-settings.png differ diff --git a/images/permalink-search-result.png b/images/permalink-search-result.png new file mode 100644 index 0000000000..929a640fb9 Binary files /dev/null and b/images/permalink-search-result.png differ diff --git a/images/permalink.png b/images/permalink.png new file mode 100644 index 0000000000..caae4df346 Binary files /dev/null and b/images/permalink.png differ diff --git a/images/pick-environment.png b/images/pick-environment.png new file mode 100644 index 0000000000..c315c2e975 Binary files /dev/null and b/images/pick-environment.png differ diff --git a/images/pipedream-add-a-trigger.png b/images/pipedream-add-a-trigger.png new file mode 100644 index 0000000000..e985dad986 Binary files /dev/null and b/images/pipedream-add-a-trigger.png differ diff --git a/images/pipedream-create-a-new-connection.png b/images/pipedream-create-a-new-connection.png new file mode 100644 index 0000000000..2ac9c67db3 Binary files /dev/null and b/images/pipedream-create-a-new-connection.png differ diff --git a/images/pipedream-create-trigger-done.png b/images/pipedream-create-trigger-done.png new file mode 100644 index 0000000000..a030abcef1 Binary files /dev/null and b/images/pipedream-create-trigger-done.png differ diff --git a/images/pipedream-create-trigger.png b/images/pipedream-create-trigger.png new file mode 100644 index 0000000000..a6cbaebdb4 Binary files /dev/null and b/images/pipedream-create-trigger.png differ diff --git a/images/pipedream-select-event.png b/images/pipedream-select-event.png new file mode 100644 index 0000000000..6e63f5e430 Binary files /dev/null and b/images/pipedream-select-event.png differ diff --git a/images/pipelines-environment-variable-v2.png b/images/pipelines-environment-variable-v2.png new file mode 100644 index 0000000000..9c6d0b23b9 Binary files /dev/null and b/images/pipelines-environment-variable-v2.png differ diff --git a/images/pipelines_environment_variable.png b/images/pipelines_environment_variable.png new file mode 100644 index 0000000000..8c166d23d9 Binary files /dev/null and b/images/pipelines_environment_variable.png differ diff --git a/images/release_pipeline_task.png b/images/release_pipeline_task.png new file mode 100644 index 0000000000..161bc34337 Binary files /dev/null and b/images/release_pipeline_task.png differ diff --git a/images/rename-log-v2.png b/images/rename-log-v2.png new file mode 100644 index 0000000000..5a55bf473b Binary files /dev/null and b/images/rename-log-v2.png differ diff --git a/images/rename-log.png b/images/rename-log.png new file mode 100644 index 0000000000..8f55957ba9 Binary files /dev/null and b/images/rename-log.png differ diff --git a/images/requestbin_message.png b/images/requestbin_message.png new file mode 100644 index 0000000000..2553c5cc1a Binary files /dev/null and b/images/requestbin_message.png differ diff --git a/images/requestbin_settings.png b/images/requestbin_settings.png new file mode 100644 index 0000000000..fcf5d7162b Binary files /dev/null and b/images/requestbin_settings.png differ diff --git a/images/roslyn-analyzers.png b/images/roslyn-analyzers.png new file mode 100644 index 0000000000..2da1ce6932 Binary files /dev/null and b/images/roslyn-analyzers.png differ diff --git a/images/rulestab.png b/images/rulestab.png new file mode 100644 index 0000000000..c0c6635f00 Binary files /dev/null and b/images/rulestab.png differ diff --git a/images/save_notification_step.png b/images/save_notification_step.png new file mode 100644 index 0000000000..2658b00e46 Binary files /dev/null and b/images/save_notification_step.png differ diff --git a/images/search-by-isbot.png b/images/search-by-isbot.png new file mode 100644 index 0000000000..657cb059b7 Binary files /dev/null and b/images/search-by-isbot.png differ diff --git a/images/search-filters-error-details-v2.gif b/images/search-filters-error-details-v2.gif new file mode 100644 index 0000000000..a42685db7f Binary files /dev/null and b/images/search-filters-error-details-v2.gif differ diff --git a/images/search-filters-error-details-v2.png b/images/search-filters-error-details-v2.png new file mode 100644 index 0000000000..c4dfa9199d Binary files /dev/null and b/images/search-filters-error-details-v2.png differ diff --git a/images/search-filters-error-details.gif b/images/search-filters-error-details.gif new file mode 100644 index 0000000000..6ee2718f75 Binary files /dev/null and b/images/search-filters-error-details.gif differ diff --git a/images/search-filters-error-details.png b/images/search-filters-error-details.png new file mode 100644 index 0000000000..96455fb7b7 Binary files /dev/null and b/images/search-filters-error-details.png differ diff --git a/images/search-filters-v2.gif b/images/search-filters-v2.gif new file mode 100644 index 0000000000..fe64dabe2a Binary files /dev/null and b/images/search-filters-v2.gif differ diff --git a/images/search-filters-v2.png b/images/search-filters-v2.png new file mode 100644 index 0000000000..6d4c52d09f Binary files /dev/null and b/images/search-filters-v2.png differ diff --git a/images/search-filters.gif b/images/search-filters.gif new file mode 100644 index 0000000000..de3b78ce52 Binary files /dev/null and b/images/search-filters.gif differ diff --git a/images/search-filters.png b/images/search-filters.png new file mode 100644 index 0000000000..a109961529 Binary files /dev/null and b/images/search-filters.png differ diff --git a/images/search_for_elmah_io.png b/images/search_for_elmah_io.png new file mode 100644 index 0000000000..ba9e4dc0ab Binary files /dev/null and b/images/search_for_elmah_io.png differ diff --git a/images/select-elmah-io-site-extension-v2.png b/images/select-elmah-io-site-extension-v2.png new file mode 100644 index 0000000000..bc1a66a729 Binary files /dev/null and b/images/select-elmah-io-site-extension-v2.png differ diff --git a/images/select-elmah-io-site-extension.png b/images/select-elmah-io-site-extension.png new file mode 100644 index 0000000000..4dcf410ff6 Binary files /dev/null and b/images/select-elmah-io-site-extension.png differ diff --git a/images/select_log_on_zapier.png b/images/select_log_on_zapier.png new file mode 100644 index 0000000000..40f15e915b Binary files /dev/null and b/images/select_log_on_zapier.png differ diff --git a/images/select_project_template.png b/images/select_project_template.png new file mode 100644 index 0000000000..85c7b089a1 Binary files /dev/null and b/images/select_project_template.png differ diff --git a/images/send_http_request_to_hipchat.png b/images/send_http_request_to_hipchat.png new file mode 100644 index 0000000000..bae458da5b Binary files /dev/null and b/images/send_http_request_to_hipchat.png differ diff --git a/images/send_http_request_to_slack.png b/images/send_http_request_to_slack.png new file mode 100644 index 0000000000..90220a828b Binary files /dev/null and b/images/send_http_request_to_slack.png differ diff --git a/images/sign_into_elmah_io_zapier_popup.png b/images/sign_into_elmah_io_zapier_popup.png new file mode 100644 index 0000000000..27ad0339d0 Binary files /dev/null and b/images/sign_into_elmah_io_zapier_popup.png differ diff --git a/images/slack_allow_access.png b/images/slack_allow_access.png new file mode 100644 index 0000000000..56890a7dca Binary files /dev/null and b/images/slack_allow_access.png differ diff --git a/images/slack_authentication_page.png b/images/slack_authentication_page.png new file mode 100644 index 0000000000..29f1fc1b64 Binary files /dev/null and b/images/slack_authentication_page.png differ diff --git a/images/slack_select_channel-v2.png b/images/slack_select_channel-v2.png new file mode 100644 index 0000000000..fb0e50abde Binary files /dev/null and b/images/slack_select_channel-v2.png differ diff --git a/images/slack_select_channel.png b/images/slack_select_channel.png new file mode 100644 index 0000000000..6140834413 Binary files /dev/null and b/images/slack_select_channel.png differ diff --git a/images/sql-tab.png b/images/sql-tab.png new file mode 100644 index 0000000000..2f7e032724 Binary files /dev/null and b/images/sql-tab.png differ diff --git a/images/ssl-checks.gif b/images/ssl-checks.gif new file mode 100644 index 0000000000..82b8db0fdf Binary files /dev/null and b/images/ssl-checks.gif differ diff --git a/images/start-a-program-powershell.png b/images/start-a-program-powershell.png new file mode 100644 index 0000000000..9216ec16a3 Binary files /dev/null and b/images/start-a-program-powershell.png differ diff --git a/images/teams_installapp.png b/images/teams_installapp.png new file mode 100644 index 0000000000..f8fd1d1a18 Binary files /dev/null and b/images/teams_installapp.png differ diff --git a/images/teams_webhook.png b/images/teams_webhook.png new file mode 100644 index 0000000000..70851c23d5 Binary files /dev/null and b/images/teams_webhook.png differ diff --git a/images/test_this_zap.png b/images/test_this_zap.png new file mode 100644 index 0000000000..8ec90cdc01 Binary files /dev/null and b/images/test_this_zap.png differ diff --git a/images/test_zapier_trigger.png b/images/test_zapier_trigger.png new file mode 100644 index 0000000000..f79573cea6 Binary files /dev/null and b/images/test_zapier_trigger.png differ diff --git a/images/thenemail.png b/images/thenemail.png new file mode 100644 index 0000000000..9ee15f72ca Binary files /dev/null and b/images/thenemail.png differ diff --git a/images/thenhide.png b/images/thenhide.png new file mode 100644 index 0000000000..28c43b36d1 Binary files /dev/null and b/images/thenhide.png differ diff --git a/images/thenhttp.png b/images/thenhttp.png new file mode 100644 index 0000000000..868ae7eca9 Binary files /dev/null and b/images/thenhttp.png differ diff --git a/images/thenignore.png b/images/thenignore.png new file mode 100644 index 0000000000..d6827361e6 Binary files /dev/null and b/images/thenignore.png differ diff --git a/images/tour/azure-apps-services.jpg b/images/tour/azure-apps-services.jpg new file mode 100644 index 0000000000..3a4a12838d Binary files /dev/null and b/images/tour/azure-apps-services.jpg differ diff --git a/images/tour/deployment-tracking.jpg b/images/tour/deployment-tracking.jpg new file mode 100644 index 0000000000..0dec1b5b9d Binary files /dev/null and b/images/tour/deployment-tracking.jpg differ diff --git a/images/tour/ignore-filters-and-rules.jpg b/images/tour/ignore-filters-and-rules.jpg new file mode 100644 index 0000000000..6a37365657 Binary files /dev/null and b/images/tour/ignore-filters-and-rules.jpg differ diff --git a/images/tour/installation.jpg b/images/tour/installation.jpg new file mode 100644 index 0000000000..7d7021fc3f Binary files /dev/null and b/images/tour/installation.jpg differ diff --git a/images/tour/uptime-monitoring.jpg b/images/tour/uptime-monitoring.jpg new file mode 100644 index 0000000000..c2fba530d0 Binary files /dev/null and b/images/tour/uptime-monitoring.jpg differ diff --git a/images/tour/user-administration.jpg b/images/tour/user-administration.jpg new file mode 100644 index 0000000000..4cda22bcd1 Binary files /dev/null and b/images/tour/user-administration.jpg differ diff --git a/images/two-factor-enabled.png b/images/two-factor-enabled.png new file mode 100644 index 0000000000..816dea040d Binary files /dev/null and b/images/two-factor-enabled.png differ diff --git a/images/umbraco-uno-enable-custom-code.png b/images/umbraco-uno-enable-custom-code.png new file mode 100644 index 0000000000..2d0381924f Binary files /dev/null and b/images/umbraco-uno-enable-custom-code.png differ diff --git a/images/under-construction.gif b/images/under-construction.gif new file mode 100644 index 0000000000..ab87cfc56c Binary files /dev/null and b/images/under-construction.gif differ diff --git a/images/uptime-checks.png b/images/uptime-checks.png new file mode 100644 index 0000000000..6a33e8e7c4 Binary files /dev/null and b/images/uptime-checks.png differ diff --git a/images/uptime_check_with_error.png b/images/uptime_check_with_error.png new file mode 100644 index 0000000000..89e9d3026d Binary files /dev/null and b/images/uptime_check_with_error.png differ diff --git a/images/usage-graph-v2.png b/images/usage-graph-v2.png new file mode 100644 index 0000000000..7e12d93c6b Binary files /dev/null and b/images/usage-graph-v2.png differ diff --git a/images/usage_graph.png b/images/usage_graph.png new file mode 100644 index 0000000000..9d19c81425 Binary files /dev/null and b/images/usage_graph.png differ diff --git a/images/users-security-new.png b/images/users-security-new.png new file mode 100644 index 0000000000..505aeb93ee Binary files /dev/null and b/images/users-security-new.png differ diff --git a/images/users-security.png b/images/users-security.png new file mode 100644 index 0000000000..3b780b6480 Binary files /dev/null and b/images/users-security.png differ diff --git a/images/version-details-v2.png b/images/version-details-v2.png new file mode 100644 index 0000000000..1c81145872 Binary files /dev/null and b/images/version-details-v2.png differ diff --git a/images/version-search-v2.png b/images/version-search-v2.png new file mode 100644 index 0000000000..43ec164037 Binary files /dev/null and b/images/version-search-v2.png differ diff --git a/images/versiondetails.png b/images/versiondetails.png new file mode 100644 index 0000000000..a78c566578 Binary files /dev/null and b/images/versiondetails.png differ diff --git a/images/versiondetails2.png b/images/versiondetails2.png new file mode 100644 index 0000000000..84196af41e Binary files /dev/null and b/images/versiondetails2.png differ diff --git a/images/versionsearch.png b/images/versionsearch.png new file mode 100644 index 0000000000..7301bc19a6 Binary files /dev/null and b/images/versionsearch.png differ diff --git a/images/visualstudio-authorize.png b/images/visualstudio-authorize.png new file mode 100644 index 0000000000..f4e1ecb4f1 Binary files /dev/null and b/images/visualstudio-authorize.png differ diff --git a/images/visualstudio-browse.png b/images/visualstudio-browse.png new file mode 100644 index 0000000000..c870978f06 Binary files /dev/null and b/images/visualstudio-browse.png differ diff --git a/images/visualstudio-details.png b/images/visualstudio-details.png new file mode 100644 index 0000000000..a8074919c8 Binary files /dev/null and b/images/visualstudio-details.png differ diff --git a/images/visualstudio-selectorganization.png b/images/visualstudio-selectorganization.png new file mode 100644 index 0000000000..9482908919 Binary files /dev/null and b/images/visualstudio-selectorganization.png differ diff --git a/images/visualstudio-signin.png b/images/visualstudio-signin.png new file mode 100644 index 0000000000..a93758da4e Binary files /dev/null and b/images/visualstudio-signin.png differ diff --git a/images/vsts_add_task.png b/images/vsts_add_task.png new file mode 100644 index 0000000000..f5805eb957 Binary files /dev/null and b/images/vsts_add_task.png differ diff --git a/images/vsts_extension.png b/images/vsts_extension.png new file mode 100644 index 0000000000..f8e563a140 Binary files /dev/null and b/images/vsts_extension.png differ diff --git a/images/vsts_release_definition.png b/images/vsts_release_definition.png new file mode 100644 index 0000000000..57b07862a4 Binary files /dev/null and b/images/vsts_release_definition.png differ diff --git a/images/vsts_select_account.png b/images/vsts_select_account.png new file mode 100644 index 0000000000..92c4675b83 Binary files /dev/null and b/images/vsts_select_account.png differ diff --git a/images/vsts_task_added.png b/images/vsts_task_added.png new file mode 100644 index 0000000000..4dd6605aaa Binary files /dev/null and b/images/vsts_task_added.png differ diff --git a/include-filename-and-line-number-in-stacktraces/index.html b/include-filename-and-line-number-in-stacktraces/index.html new file mode 100644 index 0000000000..42055041da --- /dev/null +++ b/include-filename-and-line-number-in-stacktraces/index.html @@ -0,0 +1,668 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Include filename and line number in stack traces + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Include filename and line number in stack traces

    +
    +

    If you are running .NET Core/.NET 5 you no longer need to enable filenames and line numbers manually.

    +
    +

    When deploying your application to the test and production environment, you normally want to use the Release configuration. When doing so, your code is optimized, web.config transformation is running, and a few additional things. But, part of running on a Release build is, that you lose the ability to see filenames and line numbers in the stack traces produced by your system.

    +

    .NET offers the concept of PDB files, which are automatically generated when building your code. The PDB file contains information for the debugger to work, like which file to look up when a breakpoint is reached in your code. Unless you have changed the default settings inside Visual Studio, both the Debug and Release configuration generates a PDB file.

    +

    So, if both Debug and Release produce a PDB file, why do Debug builds include file name and line number in stack traces, while the Release build doesn't? The reason is most often caused by the fact that PDB files aren't published as part of the deployment. To do so, right-click your project in Visual Studio and select Properties. Click the Package/Publish Web tab and make sure that the Release configuration is selected in the dropdown. Next, remove the checkmark in Exclude generated debug symbols:

    +

    Exclude generated debug symbols

    +

    Also, make sure that the PDB file is generated as part of Release builds. Select the Build tab and click Advanced.... In Debug Info you want to make sure that either Pdb-only or Portable is selected (Pdb-only being the default):

    +

    Advanced build settings

    +

    On your next deployment, PDB files are published as part of the build.

    +
    +

    Depending on who you talk to, deploying PDB files as part of your build may be considered a hack. Since PDB files can contain sensitive information about your implementation, publishing these files should only be done, if you have full control of the environment you are deploying to. When releasing software to external users/customers, you don't want to include your PDB files. In this case, you should store the PDB files internally, in a symbol server or similar.

    +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000000..a8056949b5 --- /dev/null +++ b/index.html @@ -0,0 +1,824 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Documentation for integrations and elmah.io features + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    elmah.io Installation Quick Start

    +

    Welcome to the quick-start installation guide. Here you will find a quick introduction to installing elmah.io. For the full overview, read through the individual guides by clicking a technology below:

    + + +

    ASP.NET / MVC / Web API

    +

    Install the Elmah.Io NuGet package:

    +
    Install-Package Elmah.Io
    +
    dotnet add package Elmah.Io
    +
    <PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add Elmah.Io
    +
    +

    During the installation, you will be asked for your API key and log ID.

    +

    For more information, check out the installation guides for WebForms, MVC, and Web API. There is a short video tutorial available here:

    +

    + elmah.io Introduction - Installation + +

    +

    ASP.NET Core

    +

    Install the Elmah.Io.AspNetCore NuGet package:

    +
    Install-Package Elmah.Io.AspNetCore
    +
    dotnet add package Elmah.Io.AspNetCore
    +
    <PackageReference Include="Elmah.Io.AspNetCore" Version="5.*" />
    +
    paket add Elmah.Io.AspNetCore
    +
    +

    Once installed, call AddElmahIo and UseElmahIo in the Program.cs file:

    +

    var builder = WebApplication.CreateBuilder(args);
    +// ...
    +builder.Services.AddElmahIo(options => // 👈
    +{
    +    options.ApiKey = "API_KEY";
    +    options.LogId = new Guid("LOG_ID");
    +});
    +// ...
    +var app = builder.Build();
    +// ...
    +app.UseElmahIo(); // 👈
    +// ...
    +app.Run();
    +

    +

    Make sure to insert your API key and log ID.

    +

    For more information, check out the installation guides for ASP.NET Core and Microsoft.Extensions.Logging.

    +

    JavaScript

    +

    Install the elmah.io.javascript npm package:

    +

    npm install elmah.io.javascript
    +

    +

    Reference the installed script and include your API key and log ID as part of the URL:

    +

    <script src="~/node_modules/elmah.io.javascript/dist/elmahio.min.js?apiKey=YOUR-API-KEY&logId=YOUR-LOG-ID" type="text/javascript"></script>
    +

    +

    For more information, check out the installation guide for JavaScript.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrate-elmah-io-with-hipchat/index.html b/integrate-elmah-io-with-hipchat/index.html new file mode 100644 index 0000000000..484a6c6ac2 --- /dev/null +++ b/integrate-elmah-io-with-hipchat/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/integrate-elmah-io-with-pipedream/index.html b/integrate-elmah-io-with-pipedream/index.html new file mode 100644 index 0000000000..57ca101752 --- /dev/null +++ b/integrate-elmah-io-with-pipedream/index.html @@ -0,0 +1,668 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with Pipedream + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Integrate with Pipedream

    +

    Pipedream is a service similar to Zapier and IFTTT to help integrate systems without having to write code. In this article, we use an integration point provided by elmah.io and Pipedream called a trigger. A trigger is something that triggers an action in Pipedream. In the case of elmah.io, the trigger available is when new errors are logged to your log. Actions exist on the other side of the integration and tell Pipedream what to do every time a trigger is fired. This guide will show you how to set up the trigger. What action you want to execute when new errors are logged will depend on the tools and workflows used in your organization.

    +

    Create a new account on pipedream.com. Then click the New button on the Workflows page. The create new workflow page is shown:

    +

    Add a trigger

    +

    Search for elmah.io in the search field and select the app and the New Error trigger:

    +

    Create trigger

    +

    Click the Connect new account button and input an API key with permission to both get logs and messages in the api_key field:

    +

    Connect elmah.io account

    +

    Click the Save button and select the log to integrate with in the Log ID dropdown:

    +

    Select log

    +

    Click the Create source button and wait for:

    +

    Select event

    +

    If no events are shown, force an error from the application integrated with the chosen log or create a test error through the API. Remember that only errors marked with the new flag are shown as events in Pipedream.

    +

    Select an event and click the Continue button. The elmah.io trigger is now configured. Select an app and event of your choice to create actions on the newly created trigger.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrate-elmah-io-with-slack/index.html b/integrate-elmah-io-with-slack/index.html new file mode 100644 index 0000000000..e84f479f6e --- /dev/null +++ b/integrate-elmah-io-with-slack/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/integrate-elmah-io-with-zapier/index.html b/integrate-elmah-io-with-zapier/index.html new file mode 100644 index 0000000000..fffbb8397f --- /dev/null +++ b/integrate-elmah-io-with-zapier/index.html @@ -0,0 +1,674 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrate elmah.io with Zapier + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Integrate with Zapier

    +

    Zapier is the place to go if you need to integrate two or more online systems. In this article, we use an integration point provided by elmah.io and Zapier called a trigger. A trigger is (as the name suggest) something that triggers an action in Zapier. In the case of elmah.io, the trigger available is when new errors are logged to your log. Actions exist on the other side of the integration and tell Zapier what to do every time a trigger is fired. This guide will show you how to set up the trigger. What action you want to execute when new errors are logged will depend on the tools and workflows used in your organization.

    +

    Create a new account on zapier.com. Then click the Make a Zap button. The create new Zap page is shown:

    +

    Choose a trigger and action

    +

    Search for elmah.io in the search field and select the app and the New Error trigger:

    +

    App and trigger selected

    +

    Click Continue and you will be presented with the following screen:

    +

    Choose elmah.io account

    +

    Click the Sign in to elmah.io button or select an existing account if you have already set up other zaps using elmah.io. Adding a new account will show a popup asking you to sign in to elmah.io:

    +

    Sign in to elmah.io popup

    +

    Sign in with your elmah.io username/password or social provider. On the following screen you will be asked to authorize elmah.io to notify Zapier every time a new error is logged in a log selected on a later stage:

    +

    Authorize zapier and elmah.io

    +

    Click the Authorize button and your account will be added to the account list on Zapier:

    +

    elmah.io account selected on Zapier

    +

    Click Continue. In the following step you will select the elmah.io log that you want to integrate with Zapier:

    +

    Select elmah.io log

    +

    The dropdown contains all of the logs you have access to within your organization. Select a log and click Continue. In the following step you will test the trigger:

    +

    Test trigger

    +

    Click Test trigger and Zapier will pull recent errors from the chosen log. Select the error the represents how a typical error in the chosen log looks like. The values from the chosen error will be used when filling in the action, why selecting a good example in this step can make it easier to configure later on.

    +

    When you are done, click the Continue button. The elmah.io trigger is now configured. Select an app and event of your choice to create actions on the newly created trigger.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrations-high-level-overview/index.html b/integrations-high-level-overview/index.html new file mode 100644 index 0000000000..06e64ab754 --- /dev/null +++ b/integrations-high-level-overview/index.html @@ -0,0 +1,824 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Integrations high-level overview + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Integrations high-level overview

    +

    elmah.io offers integration with an extensive list of logging- and web-frameworks. Get the full overview here. The table below shows all of the frameworks with official support. The Async column contains a checkmark if the integration is logging asynchronously. The Bulk column contains a checkmark if the integration is logging to elmah.io in bulk. In addition, elmah.io can be installed in a range of different products and services not mentioned (using the integrations below). Look through the menu to the left to see all of the possible integrations.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FrameworkStableBuildSourceSamplesAsyncBulk
    ASP.NETNuGetBuild statusSourceSamples✔️
    ASP.NET MVCNuGetBuild statusSourceSamples️✔️
    ASP.NET Web APINuGetBuild statusSourceSamples✔️
    ASP.NET CoreNuGetBuild statusSourceSamples✔️
    BlazorNuGetBuild statusSourceSamples✔️
    Microsoft.Extensions.LoggingNuGetBuild statusSourceSamples✔️✔️
    SerilogNuGetBuild statusSourceSamples✔️✔️
    NLogNuGetBuild statusSourceSamples✔️✔️
    log4netNuGetBuild statusSourceSamples✔️
    UmbracoNuGetBuild statusSourceSamples✔️
    Azure Websites✔️
    Azure FunctionsNuGetBuild statusSourceSamples✔️
    Isolated Azure FunctionsNuGetBuild statusSourceSamples✔️
    JavaScriptnpmSourceSamples✔️
    XamarinNuGetBuild statusSourceSamples✔️
    UnoNuGetBuild statusSourceSamples✔️
    WPFNuGetBuild statusSourceSamples✔️
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/javascript-troubleshooting/index.html b/javascript-troubleshooting/index.html new file mode 100644 index 0000000000..480b30310c --- /dev/null +++ b/javascript-troubleshooting/index.html @@ -0,0 +1,706 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + JavaScript Troubleshooting + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    JavaScript Troubleshooting

    +

    Errors aren't logged

    +

    If errors aren't logged from JavaScript, here's a list of things to try out:

    +
      +
    • Make sure that the log with the specified ID exists.
    • +
    • Make sure that the log isn't disabled and/or contains any ignore filters that could ignore client-side errors.
    • +
    • Make sure that the API key is valid and contains the Messages | Write permission.
    • +
    • Enable debugging when initializing elmah.io.javascript to get additional debug and error messages from within the script printed to the browser console:
    • +
    +

    new Elmahio({
    +    apiKey: 'API_KEY',
    +    logId: 'LOG_ID',
    +    debug: true
    +});
    +

    +
      +
    • If your webserver includes the Content-Security-Policy header make sure to include api.elmah.io as an allowed domain.
    • +
    +

    Missing information on log messages

    +

    When logging uncaught errors with elmah.io.javascript you get a lot of additional information stored as part of the log messages. Like the client IP and browser details. If you don't see this information on the messages logged from your application, it's probably because you are using the log function:

    +

    logger.log({
    +  title: 'This is a custom log message',
    +  severity: 'Error'
    +});
    +

    +

    The log function only logs what you tell it to log. To include the additional information, switch to use the message builder:

    +

    var msg = logger.message(); // Get a prefilled message
    +msg.title = 'This is a custom log message';
    +msg.severity = 'Error';
    +logger.log(msg);
    +

    +

    Missing stack trace on errors

    +

    If errors logged through elmah.io.javascript have a stack trace, it is logged as part of the error on elmah.io. If errors don't include a stack trace, the following actions may fix it:

    +
      +
    • Not all errors include a stack trace. Make sure that the thrown error does include a stack trace by inspecting:
    • +
    +

    e.stack
    +

    +
      +
    • Move the elmahio.js script import to the top of the list of all referenced JavaScript files.
    • +
    • Remove any defer or async attributes from the elmahio.js script import. The elmahio.js script import can include those attributes, but errors during initialization may not include stack trace or even be omitted if elmah.io.javascript hasn't been loaded yet.
    • +
    +

    CORS problems when running on localhost

    +

    When running with elmah.io.javascript on localhost you may see errors in the console like this:

    +

    Access to XMLHttpRequest at 'https://api.elmah.io/v3/messages/...' from origin 'http://localhost' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
    +

    +

    Browsers like Chrome don't allow CORS when running locally. There are three ways to fix this:

    +
      +
    1. Run Chrome with the --disable-web-security switch.
    2. +
    3. Run your website on a hostname like https://mymachine.
    4. +
    5. Allow CORS on localhost with extensions like CORS Unblock for Chrome or Allow CORS: Access-Control-Allow-Origin for Firefox.
    6. +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-breadcrumbs-from-asp-net-core/index.html b/logging-breadcrumbs-from-asp-net-core/index.html new file mode 100644 index 0000000000..c3c7c6e3cb --- /dev/null +++ b/logging-breadcrumbs-from-asp-net-core/index.html @@ -0,0 +1,736 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging breadcrumbs from ASP.NET Core + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging breadcrumbs from ASP.NET Core

    + +

    You can log one or more breadcrumbs as part of both automatic and manually logged errors. Breadcrumbs indicate steps happening just before a message logged by Elmah.Io.AspNetCore. Breadcrumbs with elmah.io and ASP.NET Core are supported in two ways: manual and through Microsoft.Extensions.Logging.

    +

    Manually logging breadcrumbs

    +

    If you want to log a breadcrumb manually as part of an MVC controller action or similar, you can use the ElmahIoApi class:

    +

    ElmahIoApi.AddBreadcrumb(
    +    new Breadcrumb(DateTime.UtcNow, message: "Requesting the front page"),
    +    HttpContext);
    +

    +

    Notice that the Breadcrumb class is located in the Elmah.Io.Client package that will be automatically installed when installing Elmah.Io.AspNetCore. The Breadcrumb class is either in the Elmah.Io.Client or Elmah.Io.Client.Models namespace, depending on which version of Elmah.Io.Client you have installed.

    +

    The best example of a helpful breadcrumb is logging the input model to all endpoints as a breadcrumb. This will show you exactly which parameters the user sends to your website. The following example is created for ASP.NET Core MVC, but similar solutions can be built for other MVC features as well.

    +

    Create a new class named BreadcrumbFilterAttribute and place it somewhere inside your MVC project:

    +

    public class BreadcrumbFilterAttribute : ActionFilterAttribute
    +{
    +    public override void OnActionExecuting(ActionExecutingContext context)
    +    {
    +        var arguments = context.ActionArguments;
    +        if (arguments.Count == 0) return;
    +
    +        ElmahIoApi.AddBreadcrumb(
    +            new Breadcrumb(
    +                DateTime.UtcNow,
    +                "Information",
    +                "Request",
    +                string.Join(", ", arguments.Select(a => $"{a.Key} = {JsonSerializer.Serialize(a.Value)}"))),
    +            context.HttpContext);
    +    }
    +}
    +

    +

    The action filter converts the action arguments to a comma-separated string and logs it as a breadcrumb. You can either decorate each controller with the BreadcrumbFilterAttribute or add it globally:

    +

    builder.Services.AddControllersWithViews(options =>
    +{
    +    options.Filters.Add(new BreadcrumbFilterAttribute());
    +});
    +

    +

    Logging breadcrumbs from Microsoft.Extensions.Logging

    +

    We also provide an automatic generation of breadcrumbs using Microsoft.Extensions.Logging. This will pick up all log messages logged through the ILogger and include those as part of an error logged. This behavior is currently in opt-in mode, meaning that you will need to enable it in options:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...    
    +    options.TreatLoggingAsBreadcrumbs = true;
    +});
    +

    +

    The boolean can also be configured through appsettings.json:

    +

    {
    +  // ...
    +  "ElmahIo": {
    +    // ...
    +    "TreatLoggingAsBreadcrumbs": true
    +  }
    +}
    +

    +

    When enabling this automatic behavior, you may need to adjust the log level included as breadcrumbs. This is done in the appsettings.json file by including the following JSON:

    +

    {
    +  "Logging": {
    +    // ...
    +    "ElmahIoBreadcrumbs": {
    +      "LogLevel": {
    +        "Default": "Information"
    +      }
    +    }
    +  }
    +}
    +

    +

    Filtering breadcrumbs

    +

    Breadcrumbs can be filtered using one or more rules as well:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnFilterBreadcrumb =
    +        breadcrumb => breadcrumb.Message == "A message we don't want as a breadcrumb";
    +});
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-custom-data/index.html b/logging-custom-data/index.html new file mode 100644 index 0000000000..db8ab6a06c --- /dev/null +++ b/logging-custom-data/index.html @@ -0,0 +1,684 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging custom data to elmah.io + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging custom data

    +

    ELMAH stores a lot of contextual information when an error occurs. Things like cookies, stack trace, server variables, and much more are stored to ease debugging the error at a later point in time. Most error log implementations for ELMAH doesn't support custom variables. Luckily, this is not the case for the elmah.io client.

    +

    Let's look at some code. You have two options for decorating your errors with custom variables.

    +

    Use the Data dictionary on .NET's Exception type

    +

    All exceptions in .NET contain a property named Data and of type IDictionary. The Data dictionary is intended for user-defined information about the exception. The elmah.io client iterates through key/values in this dictionary and ships it off to elmah.io's API. To log custom data using Data, just add a new key/value pair to the Data dictionary:

    +

    try
    +{
    +    CallSomeBusinessLogic(inputValue);
    +}
    +catch (Exception e)
    +{
    +    e.Data.Add("InputValueWas", inputValue);
    +    ErrorSignal.FromCurrentContext().Raise(e);
    +}
    +

    +

    In the example, a custom variable named InputValueWas with the value of the inputValue variable is added. This way you will be able to see which input value caused the exception.

    +

    Use the OnMessage hook in the elmah.io client

    +

    You may not use ELMAH's ErrorSignal feature but rely on ELMAH to log uncaught exceptions only. In this scenario, you probably don't have access to the thrown exception. The elmah.io client offers a hook for you to be able to execute code every time something is logged:

    +

    Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client
    +var logger = Elmah.Io.ErrorLog.Client;
    +logger.OnMessage += (sender, args) =>
    +{
    +    if (args.Message.Data == null) args.Message.Data = new List<Item>();
    +    args.Message.Data.Add(new Item { Key = "SomeOtherVariable", Value = someVariable });
    +};
    +

    +

    You may not have seen the Logger type of elmah.io before, but what's important to know right now is, that Logger is responsible for logging messages to the elmah.io API. Another new term here is Message. A message is the type encapsulating all of the information about the thrown exception.

    +

    In the code example, a new event handler is subscribed to the OnMessage event. This tells the elmah.io client to execute your event handler, before actually logging an exception to elmah.io. The event is used to add a custom variable to the Data dictionary of the message logged to elmah.io.

    +

    Looking at your custom data

    +

    Custom data are shown on the Data tab on the extended messages details page. To open inspect custom data go to the log search page, extend a log message, click the three bars (hamburger) icon in the upper right corner. The custom data is beneath the Data tab. As the content in the other tabs of the message details, you will be able to filter results by the variable key.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-errors-programmatically/index.html b/logging-errors-programmatically/index.html new file mode 100644 index 0000000000..60d47f4c4f --- /dev/null +++ b/logging-errors-programmatically/index.html @@ -0,0 +1,683 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging errors programmatically + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging errors programmatically

    +

    So you've set up a shiny new ELMAH log and all of your unhandled errors are logged to ELMAH. Now you're wondering: "How do I log my handled errors to ELMAH programmatically?"

    +

    You are in luck! ELMAH provides a nice API to do just that through error signaling. Consider the following code:

    +

    try
    +{
    +    int i = 0;
    +    int result = 42 / i;
    +}
    +catch (DivideByZeroException e)
    +{
    +    // What to do?
    +}
    +

    +

    In this example, a System.DivideByZeroException is thrown when trying to divide by zero, but what if we want to catch (and log) that exception instead of throwing it back through the call stack? With ELMAH's ErrorSignal class we can log the error:

    +

    try
    +{
    +    int i = 0;
    +    int result = 42 / i;
    +}
    +catch (DivideByZeroException e)
    +{
    +    ErrorSignal.FromCurrentContext().Raise(e);
    +}
    +

    +

    We call the static method FromCurrentContext on the ErrorSignal class, which returns a new object for doing the actual logging. Logging happens through the Raise method, which logs the exception to the configured ELMAH storage endpoint.

    +

    In the example above, I use the FromCurrentContext helper to create a new instance of ErrorSignal. ELMAH also works outside the context of a webserver and in this case, you would simply use the default logger with null as the HTTP context:

    +

    ErrorLog.GetDefault(null).Log(new Error(e));
    +

    +

    If you simply want to log text messages and don't need all of the HTTP context information, consider using one of our integrations for popular logging frameworks like log4net, NLog, or Serilog. Also, the Elmah.Io.Client package contains a logging API documented here.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-from-a-custom-http-module/index.html b/logging-from-a-custom-http-module/index.html new file mode 100644 index 0000000000..c1423d77a6 --- /dev/null +++ b/logging-from-a-custom-http-module/index.html @@ -0,0 +1,681 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging from a custom HTTP module + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging from a custom HTTP module

    +

    Some developers like to gather all logging into a single module. An example of this would be to log to multiple log destinations and maybe even enrich log messages to multiple loggers with the same info. We always recommend using the modules and handlers that come with ELMAH. But in case you want to log from a module manually, here's the recipe:

    +

    public class CustomLoggingModule : IHttpModule
    +{
    +    public void Init(HttpApplication context)
    +    {
    +        context.Error += Application_Error;
    +    }
    +
    +    public void Application_Error(object sender, EventArgs messageData)
    +    {
    +        HttpApplication application = (HttpApplication)sender;
    +        var context = application.Context;
    +        var error = new Error(application.Server.GetLastError(), context);
    +        var log = ErrorLog.GetDefault(context);
    +        log.Log(error);
    +    }
    +
    +    public void Dispose()
    +    {
    +    }
    +}
    +

    +

    In the example, I've created a new module named CustomLoggingModule. The module needs to be configured in web.config as explained here. When starting up the application, ASP.NET calls the Init-method. In this method, an Error event handler is set. Every time a new error is happening in your web application, ASP.NET now calls the Application_Error-method. In this method, I wrap the last thrown error in ELMAH's Error object and log it through the ErrorLog class.

    +
    +

    Be aware that logging errors this way, disables ELMAH's built-in events like filtering.

    +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-asp-net-core/index.html b/logging-heartbeats-from-asp-net-core/index.html new file mode 100644 index 0000000000..8251d1e5a9 --- /dev/null +++ b/logging-heartbeats-from-asp-net-core/index.html @@ -0,0 +1,784 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from ASP.NET Core + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging heartbeats from ASP.NET Core

    + +

    ASP.NET Core offers a feature called Health Checks from version 2.2 and forward. For more information about health checks, check out our blog post: ASP.NET Core 2.2 Health Checks Explained. The Heartbeats feature on elmah.io supports health checks too, by publishing health check results as heartbeats.

    +

    To publish health checks as elmah.io heartbeats, install the Elmah.Io.AspNetCore.HealthChecks NuGet package:

    +
    Install-Package Elmah.Io.AspNetCore.HealthChecks
    +
    dotnet add package Elmah.Io.AspNetCore.HealthChecks
    +
    <PackageReference Include="Elmah.Io.AspNetCore.HealthChecks" Version="5.*" />
    +
    paket add Elmah.Io.AspNetCore.HealthChecks
    +
    +

    Then configure the elmah.io health check publisher:

    +

    builder
    +    .Services
    +    .AddHealthChecks()
    +    .AddElmahIoPublisher(options =>
    +    {
    +        options.ApiKey = "API_KEY";
    +        options.LogId = new Guid("LOG_ID");
    +        options.HeartbeatId = "HEARTBEAT_ID";
    +    });
    +

    +

    Replace the variables with the correct values as explained in Set up Heartbeats. Remember to use an API key that includes the Heartbeats - Write permission.

    +

    Additional options

    +

    Application name

    +

    Much like the error logging integration with ASP.NET Core, you can set an application name on log messages produced by Heartbeats. To do so, set the Application property when adding the publisher:

    +

    .AddElmahIoPublisher(options =>
    +{
    +    ...
    +    options.Application = "My app";
    +});
    +

    +

    If Application is not set, log messages will receive a default value of Heartbeats to make the messages distinguishable from other messages.

    +

    Callbacks

    +

    The elmah.io publisher offer callbacks already known from Elmah.Io.AspNetCore.

    +

    OnHeartbeat

    +

    The OnHeartbeat callback can be used to set a version number on all log messages produced by a heartbeat and/or trigger custom code every time a heartbeat is logged to elmah.io:

    +

    .AddElmahIoPublisher(options =>
    +{
    +    ...
    +    options.OnHeartbeat = hb =>
    +    {
    +        hb.Version = "1.0.0";
    +    };
    +});
    +

    +

    OnFilter

    +

    The OnFilter callback can used to ignore one or more heartbeats:

    +

    .AddElmahIoPublisher(options =>
    +{
    +    ...
    +    options.OnFilter = hb =>
    +    {
    +        return hb.Result == "Degraded";
    +    };
    +});
    +

    +

    The example ignores any Degraded heartbeats.

    +

    OnError

    +

    The OnError callback can be used to listen for errors communicating with the elmah.io API:

    +

    .AddElmahIoPublisher(options =>
    +{
    +    ...
    +    options.OnError = (hb, ex) =>
    +    {
    +        // Do something
    +    };
    +});
    +

    +

    The elmah.io publisher already logs any internal errors through Microsoft.Extensions.Logging, why you don't need to do that in the OnError handler. If you are using another logging framework and don't have that hooked up on Microsoft.Extensions.Logging, the OnError is a good place to add some additional logging.

    +

    Period

    +

    As default, ASP.NET Core runs health checks every 30 seconds when setting up a publisher. To change this interval, add the following code:

    +

    builder.Services.Configure<HealthCheckPublisherOptions>(options =>
    +{
    +    options.Period = TimeSpan.FromMinutes(5);
    +});
    +

    +

    There's a bug in ASP.NET Core 2.2 that requires you to use reflection when setting Period:

    +

    builder.Services.Configure<HealthCheckPublisherOptions>(options =>
    +{
    +    var prop = options.GetType().GetField("_period", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
    +    prop.SetValue(options, TimeSpan.FromMinutes(5));
    +});
    +

    +

    If setting Period to 5 minutes, you should set the heartbeat interval on elmah.io to 5 minutes and grace to 1 minute.

    +

    Ignoring heartbeats on localhost, staging, etc.

    +

    Monitoring heartbeats is important in your production environment. When running locally or even on staging, you probably don't want to monitor heartbeats. ASP.NET Core health checks doesn't seem to support a great deal of configuration through appsettings.json, Azure app settings, etc. The easiest way to tell ASP.NET Core to log heartbeats to elmah.io is to avoid setting up health checks unless a heartbeat id is configured:

    +

    if (!string.IsNullOrWhiteSpace(Configuration["ElmahIo:HeartbeatId"]))
    +{
    +    builder.Services.AddHealthChecks().AddElmahIoPublisher();
    +}
    +

    +

    In this example, we only configure health checks and the elmah.io publisher if the ElmahIo:HeartbeatId setting is defined in config.

    +

    Troubleshooting

    +

    Here's a list of things to check for if no heartbeats are registered:

    +
      +
    • Did you include both API_KEY, LOG_ID, and HEARTBEAT_ID?
    • +
    • The publisher needs to be called before the AddElmahIo call from Elmah.Io.AspNetCore:
    • +
    +

    builder
    +    .Services
    +    .AddHealthChecks()
    +    .AddElmahIoPublisher();
    +
    +builder.Services.AddElmahIo();
    +

    +
      +
    • If you are using Health Checks UI, it needs to be configured after the AddElmahIoPublisher-method:
    • +
    +

    builder
    +    .Services
    +    .AddHealthChecks()
    +    .AddElmahIoPublisher();
    +
    +builder
    +    .Services
    +    .AddHealthChecksUI();
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-aws-lambdas/index.html b/logging-heartbeats-from-aws-lambdas/index.html new file mode 100644 index 0000000000..cf867f5095 --- /dev/null +++ b/logging-heartbeats-from-aws-lambdas/index.html @@ -0,0 +1,694 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from AWS Lambdas + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging heartbeats from AWS Lambdas

    +

    AWS Lambdas running on a schedule are great candidates for logging heartbeats to elmah.io. To send a healthy heartbeat when the Lambda runs successfully and an unhealthy heartbeat when an error happens, start by installing the Elmah.Io.Client NuGet package:

    +
    Install-Package Elmah.Io.Client
    +
    dotnet add package Elmah.Io.Client
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +
    +

    Include elmah.io API key, log ID, and heartbeat ID in your code. In this example, they are added as static fields:

    +

    private const string ApiKey = "API_KEY";
    +private const string HeartbeatId = "HEARTBEAT_ID";
    +private static Guid LogId = new Guid("LOG_ID");
    +

    +

    Replace API_KEY with an API key with the Heartbeats | Write permission (Where is my API key?), HEARTBEAT_ID with the ID of the heartbeat available on the elmah.io UI, and LOG_ID with the ID of the log containing the heartbeat (Where is my log ID?).

    +

    Create the elmah.io client and store the IHeartbeat object somewhere. In the following example, it is initialized in the Main method and stored in a static field:

    +

    private static IHeartbeats heartbeats;
    +
    +private static async Task Main(string[] args)
    +{
    +    heartbeats = ElmahioAPI.Create(ApiKey).Heartbeats;
    +    // ...
    +}
    +

    +

    In the function handler, wrap your code in try/catch and call either the Healthy or Unhealthy method:

    +

    public static string FunctionHandler(string input, ILambdaContext context)
    +{
    +    try
    +    {
    +        // Lambda code goes here
    +
    +        heartbeats.Healthy(LogId, HeartbeatId);
    +        return input?.ToUpper();
    +    }
    +    catch (Exception e)
    +    {
    +        heartbeats.Unhealthy(LogId, HeartbeatId, e.ToString());
    +        throw;
    +    }
    +}
    +

    +

    When the lambda code runs (replace the Lambda code goes here comment with your code) without exceptions, a healthy heartbeat is logged to elmah.io. In case of an exception, an unhealthy heartbeat is logged to elmah.io. In case your Lambda doesn't run at all, elmah.io automatically logs a missing heartbeat.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-azure-functions/index.html b/logging-heartbeats-from-azure-functions/index.html new file mode 100644 index 0000000000..e9940107c2 --- /dev/null +++ b/logging-heartbeats-from-azure-functions/index.html @@ -0,0 +1,883 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from Azure Functions + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging heartbeats from Azure Functions

    + +

    Azure Functions are great candidates for adding heartbeats. For web APIs implemented with Azure Functions, you should create a /health endpoint and ping that using Uptime Monitoring. But for timer triggered, queue triggers, and similar function apps, heartbeats are a great way to verify that your function is successfully running. The rest of this document is split into different ways of adding heartbeats to one or more functions.

    +

    Using a filter in Elmah.Io.Functions

    +

    The easiest way of including a heartbeat is to include the ElmahIoHeartbeatFilter available in the Elmah.Io.Functions package. This will automatically publish a Healthy or Unhealthy heartbeat, depending on if your functions execute successfully. This option is great for timer-triggered functions like nightly batch jobs.

    +

    Start by installing the Elmah.Io.Functions package:

    +
    Install-Package Elmah.Io.Functions
    +
    dotnet add package Elmah.Io.Functions
    +
    <PackageReference Include="Elmah.Io.Functions" Version="5.*" />
    +
    paket add Elmah.Io.Functions
    +
    +

    Elmah.Io.Functions requires dependency injection part of the Microsoft.Azure.Functions.Extensions package, why you will need this package if not already added.

    +

    Extend the Startup.cs (or whatever you named your function startup class) file with the following code:

    +

    using Microsoft.Azure.Functions.Extensions.DependencyInjection;
    +using Microsoft.Azure.WebJobs.Host;
    +using Microsoft.Extensions.Configuration;
    +using Microsoft.Extensions.DependencyInjection;
    +using Elmah.Io.Functions;
    +
    +[assembly: FunctionsStartup(typeof(My.FunctionApp.Startup))]
    +
    +namespace My.FunctionApp
    +{
    +    public class Startup : FunctionsStartup
    +    {
    +        public override void Configure(IFunctionsHostBuilder builder)
    +        {
    +            var config = new ConfigurationBuilder()
    +                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
    +                .AddEnvironmentVariables()
    +                .Build();
    +
    +            builder.Services.Configure<ElmahIoFunctionOptions>(o =>
    +            {
    +                o.ApiKey = config["apiKey"];
    +                o.LogId = new Guid(config["logId"]);
    +                o.HeartbeatId = config["heartbeatId"];
    +            });
    +
    +            builder.Services.AddSingleton<IFunctionFilter, ElmahIoHeartbeatFilter>();
    +        }
    +    }
    +}
    +

    +

    The code installs the ElmahIoHeartbeatFilter class, which will handle all of the communication with the elmah.io API.

    +

    Finally, add the config variables (apiKey, logId, and heartbeatId) to the local.settings.json file, environment variables, Azure configuration settings, or in whatever way you specify settings for your function app.

    +

    Manually using Elmah.Io.Client

    +

    The example above installs the heartbeat filter for all functions. If you have multiple functions inside your function app, or you want greater control of when and how to send heartbeats, you can use Elmah.Io.Client to create heartbeats.

    +

    Start by installing the Elmah.Io.Client NuGet package:

    +
    Install-Package Elmah.Io.Client
    +
    dotnet add package Elmah.Io.Client
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +
    +

    Extend the Startup.cs file with the following code:

    +

    using Microsoft.Azure.Functions.Extensions.DependencyInjection;
    +using Microsoft.Azure.WebJobs.Host;
    +using Microsoft.Extensions.Configuration;
    +using Microsoft.Extensions.DependencyInjection;
    +using Elmah.Io.Client;
    +
    +[assembly: FunctionsStartup(typeof(My.FunctionApp.Startup))]
    +
    +namespace My.FunctionApp
    +{
    +    public class Startup : FunctionsStartup
    +    {
    +        public override void Configure(IFunctionsHostBuilder builder)
    +        {
    +            var config = new ConfigurationBuilder()
    +                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
    +                .AddEnvironmentVariables()
    +                .Build();
    +            builder.Services.AddSingleton(config);
    +
    +            var elmahIo = ElmahioAPI.Create(config["apiKey"]);
    +            builder.Services.AddSingleton(elmahIo.Heartbeats);
    +        }
    +    }
    +}
    +

    +

    Inside your function, wrap all of the code in try/catch and add code to create either a Healthy or Unhealthy heartbeat:

    +

    using System;
    +using System.Threading.Tasks;
    +using Elmah.Io.Client;
    +using Microsoft.Azure.WebJobs;
    +using Microsoft.Extensions.Configuration;
    +
    +namespace My.FunctionApp
    +{
    +    public class TimedFunction
    +    {
    +        private readonly IHeartbeats heartbeats;
    +        private readonly IConfiguration configuration;
    +
    +        public TimedFunction(IHeartbeats heartbeats, IConfiguration configuration)
    +        {
    +            this.heartbeats = heartbeats;
    +            this.configuration = configuration;
    +        }
    +
    +        [FunctionName("TimedFunction")]
    +        public async Task Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer)
    +        {
    +            var heartbeatId = configuration["heartbeatId"];
    +            var logId = configuration["logId"];
    +            try
    +            {
    +                // Your function code goes here
    +
    +                await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat
    +                {
    +                    Result = "Healthy"
    +                });
    +            }
    +            catch (Exception e)
    +            {
    +                await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat
    +                {
    +                    Result = "Unhealthy",
    +                    Reason = e.ToString(),
    +                });
    +            }
    +        }
    +    }
    +}
    +

    +

    If your function code executes successfully, a Healthy heartbeat is created. If an exception is thrown, an Unhealthy heartbeat with the thrown exception in Reason is created.

    +

    Be aware that configuring a function to run in an internal (like every hour for the example above) does not ensure that the function is executed exactly on the hour. We recommend to set the grace period for these types of heartbeats to 15-30 minutes to avoid heartbeat errors when the timed function is past due.

    +

    Using a separate heartbeat function

    +

    You may want a single heartbeat representing your entire function app consisting of multiple functions. This is a good option if you want to create heartbeats from queue-triggered functions or similar. In these cases, you don't want to create a heartbeat every time a message from the queue is handled, but you will want to notify elmah.io if dependencies like database connection suddenly aren't available. We recommend creating a new heartbeat function for this kind of Function. Like in the previous example, make sure to extend your Startup.cs file like this:

    +

    using Microsoft.Azure.Functions.Extensions.DependencyInjection;
    +using Microsoft.Azure.WebJobs.Host;
    +using Microsoft.Extensions.Configuration;
    +using Microsoft.Extensions.DependencyInjection;
    +using Elmah.Io.Client;
    +
    +[assembly: FunctionsStartup(typeof(My.FunctionApp.Startup))]
    +
    +namespace My.FunctionApp
    +{
    +    public class Startup : FunctionsStartup
    +    {
    +        public override void Configure(IFunctionsHostBuilder builder)
    +        {
    +            var config = new ConfigurationBuilder()
    +                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
    +                .AddEnvironmentVariables()
    +                .Build();
    +            builder.Services.AddSingleton(config);
    +
    +            var elmahIo = ElmahioAPI.Create(config["apiKey"]);
    +            builder.Services.AddSingleton(elmahIo.Heartbeats);
    +        }
    +    }
    +}
    +

    +

    Then create a new timed function with the following code:

    +

    using System;
    +using System.Threading.Tasks;
    +using Elmah.Io.Client;
    +using Microsoft.Azure.WebJobs;
    +using Microsoft.Extensions.Configuration;
    +
    +namespace My.FunctionApp
    +{
    +    public class Heartbeat
    +    {
    +        private readonly IConfiguration config;
    +        private readonly IHeartbeats heartbeats;
    +
    +        public Heartbeat(IHeartbeats heartbeats, IConfiguration config)
    +        {
    +            this.heartbeats = heartbeats;
    +            this.config = config;
    +        }
    +
    +        [FunctionName("Heartbeat")]
    +        public async Task Run([TimerTrigger("0 */5 * * * *")]TimerInfo myTimer)
    +        {
    +            var result = "Healthy";
    +            var reason = (string)null;
    +            try
    +            {
    +                // Check your dependencies here
    +            }
    +            catch (Exception e)
    +            {
    +                result = "Unhealthy";
    +                reason = e.ToString();
    +            }
    +
    +            await heartbeats.CreateAsync(config["heartbeatId"], config["logId"], new CreateHeartbeat
    +            {
    +                Result = result,
    +                Reason = reason,
    +            });
    +        }
    +    }
    +}
    +

    +

    In the example above, the new function named Heartbeat (the name is entirely up to you) executes every 5 minutes. Replace the comment with your checks like opening a connection to the database. If everything works as it should, a Healthy heartbeat is logged to elmah.io. If an exception is thrown while checking your dependencies, an Unhealthy heartbeat is created.

    +

    When running locally, you may want to disable heartbeats. You can use the Disable attribute for that by including the following code:

    +

    #if DEBUG
    +    [Disable]
    +#endif
    +    public class Heartbeat
    +    {
    +        // ...
    +    }
    +

    +

    or add the following to local.settings.json:

    +

    {
    +  // ...
    +  "Values": {
    +    "AzureWebJobs.Heartbeat.Disabled": true,
    +    // ...
    +  }
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-curl/index.html b/logging-heartbeats-from-curl/index.html new file mode 100644 index 0000000000..495e8c1061 --- /dev/null +++ b/logging-heartbeats-from-curl/index.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from cURL + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging heartbeats from cURL

    +

    Sometimes is just easier to use cURL when needing to call a REST API. Creating elmah.io heartbeats is easy using cURL and fits well into scripts, scheduled tasks, and similar.

    +

    To create a new heartbeat, include the following cURL command in your script:

    +

    curl -X POST "https://api.elmah.io/v3/heartbeats/LOG_ID/HEARTBEAT_ID?api_key=API_KEY" -H "accept: application/json" -H "Content-Type: application/json-patch+json" -d "{ \"result\": \"Healthy\"}"
    +

    +

    Remember to place LOG_ID, HEARTBEAT_ID, and API_KEY with the values found on the Heartbeats tab in elmah.io.

    +

    To create an Unhealthy heartbeat, change the result in the body and include a reason:

    +

    curl -X POST "https://api.elmah.io/v3/heartbeats/LOG_ID/HEARTBEAT_ID?api_key=API_KEY" -H "accept: application/json" -H "Content-Type: application/json-patch+json" -d "{ \"result\": \"Unhealthy\", \"reason\": \"Something isn't working\" }"
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-hangfire/index.html b/logging-heartbeats-from-hangfire/index.html new file mode 100644 index 0000000000..64a6b2e18a --- /dev/null +++ b/logging-heartbeats-from-hangfire/index.html @@ -0,0 +1,743 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from Hangfire + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging heartbeats from Hangfire

    +

    Scheduling recurring tasks with Hangfire is easy. Monitoring if tasks are successfully executed or even run can be a challenge. With elmah.io Heartbeats, we provide native monitoring of Hangfire recurring tasks.

    +

    To publish heartbeats from Hangifre, install the Elmah.Io.Heartbeats.Hangfire NuGet package:

    +
    Install-Package Elmah.Io.Heartbeats.Hangfire
    +
    dotnet add package Elmah.Io.Heartbeats.Hangfire
    +
    <PackageReference Include="Elmah.Io.Heartbeats.Hangfire" Version="5.*" />
    +
    paket add Elmah.Io.Heartbeats.Hangfire
    +
    +

    For this example, we'll schedule a method named Test to execute every minute:

    +

    RecurringJob.AddOrUpdate(() => Test(), Cron.Minutely);
    +

    +

    To automatically publish a heartbeat when the job is executed, add the following using:

    +

    using Elmah.Io.Heartbeats.Hangfire;
    +

    +

    And decorate the Test-method with the ElmahIoHeartbeat attribute:

    +

    [ElmahIoHeartbeat("API_KEY", "LOG_ID", "HEARTBEAT_ID")]
    +public void Test()
    +{
    +    // ...
    +}
    +

    +

    Replace API_KEY (Where is my API key?), LOG_ID (Where is my log ID?), and HEARTBEAT_ID with the correct variables from elmah.io.

    +

    When the job successfully runs, a Healthy heartbeat is logged to elmah.io. If an exception is thrown an Unhealthy heartbeat is logged. elmah.io will automatically create an error if a heartbeat is missing, as long as the heartbeat is correctly configured as explained in Set up Heartbeats.

    +

    Move configuration to config files

    +

    You normally don't include your API key, log ID, and heartbeat ID in C# code as shown in the example above. Unfortunately, Hangfire attributes doesn't support dependency injection or configuration from config files. There's a small "hack" that you can use to move the configuration to a configuration file by creating a custom attribute:

    +

    using Elmah.Io.Heartbeats.Hangfire;
    +using Hangfire.Common;
    +using Hangfire.Server;
    +using System.Configuration;
    +
    +public class AppSettingsElmahIoHeartbeatAttribute : JobFilterAttribute, IServerFilter
    +{
    +    private readonly ElmahIoHeartbeatAttribute _inner;
    +
    +    public AppSettingsElmahIoHeartbeatAttribute()
    +    {
    +        var apiKey = ConfigurationManager.AppSettings["apiKey"];
    +        var logId = ConfigurationManager.AppSettings["logId"];
    +        var heartbeatId = ConfigurationManager.AppSettings["heartbeatId"];
    +        _inner = new ElmahIoHeartbeatAttribute(apiKey, logId, heartbeatId);
    +    }
    +
    +    public void OnPerformed(PerformedContext filterContext)
    +    {
    +        _inner.OnPerformed(filterContext);
    +    }
    +
    +    public void OnPerforming(PerformingContext filterContext)
    +    {
    +        _inner.OnPerforming(filterContext);
    +    }
    +}
    +

    +

    In the example the AppSettingsElmahIoHeartbeatAttribute class wrap ElmahIoHeartbeatAttribute. By doing so, it is possible to fetch configuration from application settings as part of the constructor. The approach would be similar when using IConfiguration (like in ASP.NET Core), but you will need to share a reference to the configuration object somehow.

    +

    To use AppSettingsElmahIoHeartbeatAttribute simply add it to the method:

    +

    [AppSettingsElmahIoHeartbeat]
    +public void Test()
    +{
    +    // ...
    +}
    +

    +

    As an alternative, you can register the ElmahIoHeartbeatAttribute as a global attribute. In this example we use IConfiguration in ASP.NET Core to fetch configuration from the appsettings.json file:

    +

    public class Startup
    +{
    +    public Startup(IConfiguration configuration)
    +    {
    +        Configuration = configuration;
    +    }
    +
    +    public IConfiguration Configuration { get; }
    +
    +    // This method gets called by the runtime. Use this method to add services to the container.
    +    public void ConfigureServices(IServiceCollection services)
    +    {
    +        services.AddHangfire(config => config
    +            // ...
    +            .UseFilter(new ElmahIoHeartbeatAttribute(
    +                Configuration["ElmahIo:ApiKey"],
    +                Configuration["ElmahIo:LogId"],
    +                Configuration["ElmahIo:HeartbeatId"])));
    +    }
    +
    +    // ...
    +}
    +

    +

    This will execute the ElmahIoHeartbeat filter for every Hangfire job which isn't ideal if running multiple jobs within the same project.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-isolated-azure-functions/index.html b/logging-heartbeats-from-isolated-azure-functions/index.html new file mode 100644 index 0000000000..278d9e7e13 --- /dev/null +++ b/logging-heartbeats-from-isolated-azure-functions/index.html @@ -0,0 +1,802 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from Isolated Azure Functions + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging heartbeats from Isolated Azure Functions

    + +

    Isolated Azure Functions are great candidates for adding heartbeats. For web APIs implemented with Isolated Azure Functions, you should create a /health endpoint and ping that using Uptime Monitoring. But for timer triggered, queue triggers, and similar function apps, heartbeats are a great way to verify that your function is successfully running. The rest of this document is split into different ways of adding heartbeats to one or more functions.

    +

    Using middleware in Elmah.Io.Functions.Isolated

    +

    Scheduled functions or functions not running often can use the heartbeat middleware part of the Elmah.Io.Functions.Isolated package. This will log a Healthy or Unhealthy endpoint every time a function is running. All functions within the same function app uses the same middleware, why this is primarily inteded for function apps with one scheduled function.

    +

    Start by installing the Elmah.Io.Functions.Isolated package:

    +
    Install-Package Elmah.Io.Functions.Isolated
    +
    dotnet add package Elmah.Io.Functions.Isolated
    +
    <PackageReference Include="Elmah.Io.Functions.Isolated" Version="5.*" />
    +
    paket add Elmah.Io.Functions.Isolated
    +
    +

    Extend the Program.cs file with the following code:

    +

    .ConfigureFunctionsWorkerDefaults((context, app) =>
    +{
    +    app.AddHeartbeat(options =>
    +    {
    +        options.ApiKey = "API_KEY";
    +        options.LogId = new Guid("LOG_ID");
    +        options.HeartbeatId = "HEARTBEAT_ID";
    +    });
    +})
    +

    +

    The code installs the heartbeat middleware, which will handle all of the communication with the elmah.io API.

    +

    Manually using Elmah.Io.Client

    +

    The example above installs the heartbeat filter for all functions. If you have multiple functions inside your function app, or you want greater control of when and how to send heartbeats, you can use Elmah.Io.Client to create heartbeats.

    +

    Start by installing the Elmah.Io.Client NuGet package:

    +
    Install-Package Elmah.Io.Client
    +
    dotnet add package Elmah.Io.Client
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +
    +

    Extend the Program.cs file with the following code:

    +

    .ConfigureServices((ctx, services) =>
    +{
    +    var elmahIo = ElmahioAPI.Create("API_KEY");
    +    services.AddSingleton(elmahIo.Heartbeats);
    +});
    +

    +

    Inside your function, wrap all of the code in try/catch and add code to create either a Healthy or Unhealthy heartbeat:

    +

    public class TimedFunction
    +{
    +    private readonly IHeartbeats heartbeats;
    +    private readonly IConfiguration configuration;
    +
    +    public TimedFunction(IHeartbeats heartbeats, IConfiguration configuration)
    +    {
    +        this.heartbeats = heartbeats;
    +        this.configuration = configuration;
    +    }
    +
    +    [FunctionName("TimedFunction")]
    +    public async Task Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer)
    +    {
    +        var heartbeatId = configuration["heartbeatId"];
    +        var logId = configuration["logId"];
    +        try
    +        {
    +            // Your function code goes here
    +
    +            await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat
    +            {
    +                Result = "Healthy"
    +            });
    +        }
    +        catch (Exception e)
    +        {
    +            await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat
    +            {
    +                Result = "Unhealthy",
    +                Reason = e.ToString(),
    +            });
    +        }
    +    }
    +}
    +

    +

    If your function code executes successfully, a Healthy heartbeat is created. If an exception is thrown, an Unhealthy heartbeat with the thrown exception in Reason is created.

    +

    Be aware that configuring a function to run in an internal (like every hour for the example above) does not ensure that the function is executed exactly on the hour. We recommend to set the grace period for these types of heartbeats to 15-30 minutes to avoid heartbeat errors when the timed function is past due.

    +

    Using a separate heartbeat function

    +

    You may want a single heartbeat representing your entire function app consisting of multiple functions. This is a good option if you want to create heartbeats from queue-triggered functions or similar. In these cases, you don't want to create a heartbeat every time a message from the queue is handled, but you will want to notify elmah.io if dependencies like database connection suddenly aren't available. We recommend creating a new heartbeat function for this kind of Function. Like in the previous example, make sure to extend your Program.cs file like this:

    +

    .ConfigureServices((ctx, services) =>
    +{
    +    var elmahIo = ElmahioAPI.Create("API_KEY");
    +    services.AddSingleton(elmahIo.Heartbeats);
    +});
    +

    +

    Then create a new timed function with the following code:

    +

    using System;
    +using System.Threading.Tasks;
    +using Elmah.Io.Client;
    +using Microsoft.Azure.WebJobs;
    +using Microsoft.Extensions.Configuration;
    +
    +namespace My.FunctionApp
    +{
    +    public class Heartbeat
    +    {
    +        private readonly IConfiguration config;
    +        private readonly IHeartbeats heartbeats;
    +
    +        public Heartbeat(IHeartbeats heartbeats, IConfiguration config)
    +        {
    +            this.heartbeats = heartbeats;
    +            this.config = config;
    +        }
    +
    +        [FunctionName("Heartbeat")]
    +        public async Task Run([TimerTrigger("0 */5 * * * *")]TimerInfo myTimer)
    +        {
    +            var result = "Healthy";
    +            var reason = (string)null;
    +            try
    +            {
    +                // Check your dependencies here
    +            }
    +            catch (Exception e)
    +            {
    +                result = "Unhealthy";
    +                reason = e.ToString();
    +            }
    +
    +            await heartbeats.CreateAsync(config["heartbeatId"], config["logId"], new CreateHeartbeat
    +            {
    +                Result = result,
    +                Reason = reason,
    +            });
    +        }
    +    }
    +}
    +

    +

    In the example above, the new function named Heartbeat (the name is entirely up to you) executes every 5 minutes. Replace the comment with your checks like opening a connection to the database. If everything works as it should, a Healthy heartbeat is logged to elmah.io. If an exception is thrown while checking your dependencies, an Unhealthy heartbeat is created.

    +

    When running locally, you may want to disable heartbeats:

    +

    #if DEBUG
    +[FunctionName("Heartbeat")]
    +#endif
    +public async Task Run([TimerTrigger("0 */5 * * * *")]TimerInfo myTimer)
    +{
    +    // ...
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-net-core-worker-services/index.html b/logging-heartbeats-from-net-core-worker-services/index.html new file mode 100644 index 0000000000..32af13b8f5 --- /dev/null +++ b/logging-heartbeats-from-net-core-worker-services/index.html @@ -0,0 +1,717 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from .NET (Core) Worker Services + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging heartbeats from .NET (Core) Worker Services

    +

    .NET (Core) offers Worker Services as a way to schedule recurring tasks either hosted inside an ASP.NET Core website or as a Windows Service. Monitoring that Worker Services run successfully, can be easily set up with elmah.io Heartbeats.

    +

    To register heartbeats from a worker service, start by creating a new heartbeat on the elmah.io UI. For this example, we want to monitor that a Service Worker is running every 5 minutes, why we set Interval to 5 minutes and Grace to 1 minute. Next, install the Elmah.Io.Client NuGet package:

    +
    Install-Package Elmah.Io.Client
    +
    dotnet add package Elmah.Io.Client
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +
    +

    In the Program.cs or Startup.cs file (depending on where you register dependencies), register IHeartbeats from the elmah.io client:

    +

    .ConfigureServices((hostContext, services) =>
    +{
    +    var elmahIoApi = ElmahioAPI.Create(hostContext.Configuration["ElmahIo:ApiKey"]);
    +    services.AddSingleton(elmahIoApi.Heartbeats);
    +    // ...
    +    services.AddHostedService<Worker>();
    +});
    +

    +

    In the example, the configuration should be made available in the appsettings.json file as shown later in this article.

    +

    In the service class (Worker) you can inject the IHeartbeats object, as well as additional configuration needed to create heartbeats:

    +

    public class Worker : BackgroundService
    +{
    +    private readonly IHeartbeats heartbeats;
    +    private readonly Guid logId;
    +    private readonly string heartbeatId;
    +
    +    public Worker(IHeartbeats heartbeats, IConfiguration configuration)
    +    {
    +        this.heartbeats = heartbeats;
    +        this.logId = new Guid(configuration["ElmahIo:LogId"]);
    +        this.heartbeatId = configuration["ElmahIo:HeartbeatId"];
    +    }
    +}
    +

    +

    Inside the ExecuteAsync method, wrap the worker code in try-catch and call the HealthyAsync method when the worker successfully run and the UnhealthyAsync method when an exception occurs:

    +

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    +{
    +    while (!stoppingToken.IsCancellationRequested)
    +    {
    +        try
    +        {
    +            // Do work
    +
    +            await heartbeats.HealthyAsync(logId, heartbeatId);
    +        }
    +        catch (Exception e)
    +        {
    +            await heartbeats.UnhealthyAsync(logId, heartbeatId, e.ToString());
    +        }
    +
    +        await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
    +    }
    +}
    +

    +

    In the appsettings.json file, add the elmah.io configuration:

    +

    {
    +  "ElmahIo": {
    +    "ApiKey": "API_KEY",
    +    "LogId": "LOG_ID",
    +    "HeartbeatId": "HEARTBEAT_ID"
    +  }
    +}
    +

    +

    Replace the values with values found in the elmah.io UI. Remember to enable the Heartbeats | Write permission on the used API key.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-powershell/index.html b/logging-heartbeats-from-powershell/index.html new file mode 100644 index 0000000000..e0983c13bb --- /dev/null +++ b/logging-heartbeats-from-powershell/index.html @@ -0,0 +1,681 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from PowerShell + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging heartbeats from PowerShell

    +

    The Heartbeats feature is a great way to verify that scripts run successfully too. A lot of people have PowerShell scripts running on a schedule to clean up folders on the file system, make batch changes in a database, and more.

    +

    To include heartbeats in your PowerShell script, wrap the code in try/catch and add either Healthy or Unhealthy result:

    +

    $apiKey = "API_KEY"
    +$logId = "LOG_ID"
    +$heartbeatId = "HEARTBEAT_ID"
    +$url = "https://api.elmah.io/v3/heartbeats/$logId/$heartbeatId/?api_key=$apiKey"
    +
    +try
    +{
    +    # Your script goes here
    +
    +    $body = @{
    +        result = "Healthy"
    +    }
    +    Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType "application/json-patch+json"
    +}
    +catch
    +{
    +    $body = @{
    +        result = "Unhealthy"
    +        reason = $_.Exception.Message
    +    }
    +    Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType "application/json-patch+json"
    +}
    +

    +

    If everything goes well, a Healthy heartbeat is logged using the Invoke-RestMethod cmdlet. If an exception is thrown in your script, an Unhealthy heartbeat is logged.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-umbraco/index.html b/logging-heartbeats-from-umbraco/index.html new file mode 100644 index 0000000000..9006ca4dbc --- /dev/null +++ b/logging-heartbeats-from-umbraco/index.html @@ -0,0 +1,745 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from Umbraco + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging heartbeats from Umbraco

    + +

    Umbraco comes with a nice health check feature that can carry out a range of built-in health checks as well as custom checks you may want to add. Umbraco Health Checks fits perfectly with elmah.io Heartbeats.

    +

    To start publishing Umbraco Health Checks to elmah.io, create a new health check. Select 1 day in Interval and 5 minutes in Grace. The next step depends on the major version of Umbraco. For both examples, replace API_KEY, LOG_ID, and HEARTBEAT_ID with the values found on the elmah.io UI.

    +

    When launching the website Umbraco automatically executes the health checks once every 24 hours and sends the results to elmah.io.

    +

    Umbraco >= 9

    +
    +

    Umbraco 9 is targeting .NET 5.0 which is no longer supported by Microsoft. This is why we have chosen to support Umbraco 10 and up only.

    +
    +

    Install the Elmah.Io.Umbraco NuGet package:

    +
    Install-Package Elmah.Io.Umbraco
    +
    dotnet add package Elmah.Io.Umbraco
    +
    <PackageReference Include="Elmah.Io.Umbraco" Version="5.*" />
    +
    paket add Elmah.Io.Umbraco
    +
    +

    To publish health check results to your newly created heartbeat, extend the appsettings.json file:

    +

    {
    +  ...
    +  "Umbraco": {
    +    "CMS": {
    +      ...
    +      "HealthChecks": {
    +        "Notification": {
    +          "Enabled": true,
    +          "NotificationMethods": {
    +            "elmah.io": {
    +              "Enabled": true,
    +              "Verbosity": "Summary",
    +              "Settings": {
    +                "apiKey": "API_KEY",
    +                "logId": "LOG_ID",
    +                "heartbeatId": "HEARTBEAT_ID"
    +              }
    +            }
    +          }
    +        }
    +      }
    +    }
    +  }
    +}
    +

    +

    Umbraco 8

    +

    install the Elmah.Io.Umbraco v4 NuGet package:

    +
    Install-Package Elmah.Io.Umbraco -Version 4.2.21
    +
    dotnet add package Elmah.Io.Umbraco --version 4.2.21
    +
    <PackageReference Include="Elmah.Io.Umbraco" Version="4.2.21" />
    +
    paket add Elmah.Io.Umbraco --version 4.2.21
    +
    +

    For Umbraco to automatically execute health checks, you will need to set your back office URL in the umbracoSettings.config file:

    +

    <?xml version="1.0" encoding="utf-8" ?>
    +<settings>
    +  <!-- ... -->
    +  <web.routing umbracoApplicationUrl="https://localhost:44381/umbraco/">
    +  </web.routing>
    +</settings>
    +

    +

    (localhost is used as an example and should be replaced with a real URL)

    +

    Umbraco comes with an email publisher already configured. To publish health check results to your newly created heartbeat, extend the HealthChecks.config file:

    +

    <?xml version ="1.0" encoding="utf-8" ?>
    +<HealthChecks>
    +  <disabledChecks>
    +  </disabledChecks>
    +  <notificationSettings enabled="true" firstRunTime="" periodInHours="24">
    +    <notificationMethods>
    +      <notificationMethod alias="elmah.io" enabled="true" verbosity="Summary">
    +        <settings>
    +          <add key="apiKey" value="API_KEY" />
    +          <add key="logId" value="LOG_ID" />
    +          <add key="heartbeatId" value="HEARTBEAT_ID" />
    +        </settings>
    +      </notificationMethod>
    +      <notificationMethod alias="email" enabled="false" verbosity="Summary">
    +        <settings>
    +          <add key="recipientEmail" value="" />
    +        </settings>
    +      </notificationMethod>
    +    </notificationMethods>
    +    <disabledChecks>
    +    </disabledChecks>
    +  </notificationSettings>
    +</HealthChecks>
    +

    +

    For this example, I have disabled the email notification publisher but you can run with both if you'd like.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-heartbeats-from-windows-scheduled-tasks/index.html b/logging-heartbeats-from-windows-scheduled-tasks/index.html new file mode 100644 index 0000000000..84d68e21d9 --- /dev/null +++ b/logging-heartbeats-from-windows-scheduled-tasks/index.html @@ -0,0 +1,703 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging heartbeats from Windows Scheduled Tasks + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging heartbeats from Windows Scheduled Tasks

    +

    How you want to implement heartbeats from Windows Scheduled Tasks depends on how your task is implemented. For tasks written in C#, you typically want to persist heartbeats using the Elmah.Io.Client package as shown in Set up Heartbeats. For scheduled PowerShell or other scripts, you can trigger the elmah.io API as shown in Logging heartbeats from PowerShell and Logging heartbeats from cURL.

    +

    Invoking the API either through the elmah.io client or using a REST client is the optimal way since you have greater control over what to log. This manual approach is not available when you are not in control of the scheduled code. Examples of this could be custom tools you install as Scheduled Tasks using schtasks.exe or tasks automatically registered when installing third-party software on your server.

    +

    When configuring a heartbeat through the elmah.io UI you set an expected interval and grace period. If a heartbeat is not received in time, we will automatically log a missing heartbeat error. This will indicate that the scheduled task didn't run or fail and is something that should be looked at. In case you want to log a failing heartbeat as soon as the scheduled task is failing, you can do that using events logged to the Windows Event Log. To set it up, go through the following steps:

    +
      +
    • Open Task Scheduler.
    • +
    • Go to Task Scheduler Library and click the Create Task... button.
    • +
    • Give the new task a proper name.
    • +
    • In the Triggers tab click the New... button.
    • +
    • In the New Trigger window select On an event in the Begin the task dropdown.
    • +
    • In Settings select the Custom radio button.
    • +
    • Click the New Event Filter... button.
    • +
    • Select the XML tab and check the Edit query manually checkbox.
    • +
    • Now input the XML from the following screenshot (source code later in this article). The custom query will trigger on all Application Error messages from an app named ConsoleApp14.exe. You will need to change the app name to the filename of the app running in the scheduled task. +New Event Filter
    • +
    • Click the OK button.
    • +
    • In the New Trigger window click the OK button to save the trigger.
    • +
    • Select the Actions tab.
    • +
    • Click the New... button.
    • +
    • Select the Start a program option in the Action dropdown.
    • +
    • Input values like shown here: +Start a program
    • +
    • Click the OK button to save the action.
    • +
    • Click the OK button to save the task.
    • +
    • The final step is to add the c:\scripts\heartbeat.ps1 PowerShell script invoked by the task. For a better understanding of storing heartbeats through PowerShell check out Logging heartbeats from PowerShell. To log a failing heartbeat to elmah.io you can use the following PowerShell code:
    • +
    +

    $apiKey = "API_KEY"
    +$logId = "LOG_ID"
    +$heartbeatId = "HEARTBEAT_ID"
    +$url = "https://api.elmah.io/v3/heartbeats/$logId/$heartbeatId/?api_key=$apiKey"
    +
    +$body = @{
    +    result = "Unhealthy"
    +    reason = "Error in scheduled task"
    +}
    +Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType "application/json-patch+json"
    +

    +

    That's it. The new task just created will trigger every time there's an application error from your application. Notice that in case your application is manually logging errors to the event log, this will also trigger the task.

    +

    Source code for reference:

    +

    <QueryList>
    +  <Query Id="0" Path="Application">
    +    <Select Path="Application">
    +*[System[Provider[@Name='Application Error']]]
    +and      
    +*[EventData[(Data[@Name="AppName"]="ConsoleApp14.exe")]]
    +    </Select>
    +  </Query>
    +</QueryList>
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-through-a-http-proxy/index.html b/logging-through-a-http-proxy/index.html new file mode 100644 index 0000000000..56a4a892b3 --- /dev/null +++ b/logging-through-a-http-proxy/index.html @@ -0,0 +1,717 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging through a HTTP proxy + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging through a HTTP proxy

    +

    You may find yourself in a situation, where your production web servers aren't allowing HTTP requests towards the public Internet. This also impacts the elmah.io client, which requires access to the URL https://api.elmah.io. A popular choice of implementing this kind of restriction nowadays is through a HTTP proxy like Squid or Nginx.

    +

    Luckily the elmah.io client supports proxy configuration out of the box. Let's look at how to configure a HTTP proxy through web.config:

    +

    <?xml version="1.0" encoding="utf-8"?>
    +<configuration>
    +  <configSections>
    +    <sectionGroup name="elmah">
    +      <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
    +      <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
    +      <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
    +      <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
    +    </sectionGroup>
    +  </configSections>
    +  <elmah>
    +    <security allowRemoteAccess="false" />
    +    <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="..." logId="..." />
    +  </elmah>
    +  <system.net>
    +    <defaultProxy>
    +      <proxy usesystemdefault="True" proxyaddress="http://192.168.0.1:3128" bypassonlocal="False"/>
    +    </defaultProxy>
    +  </system.net>
    +</configuration>
    +

    +

    The above example is of course greatly simplified.

    +

    The elmah.io client automatically picks up the defaultProxy configuration through the system.net element. defaultProxy tunnels every request from your server, including requests to elmah.io, through the proxy located on 192.18.0.1 port 3128 (or whatever IP/hostname and port you are using).

    +

    Proxies with username/password

    +

    Some proxies require a username/password. Unfortunately, the defaultProxy element doesn't support authentication. You have two ways to set this up:

    +

    Use default credentials

    +

    Make sure to set the useDefaultCredentials attribute to true:

    +

    <system.net>
    +  <defaultProxy useDefaultCredentials="true">
    +    <!-- ... -->
    +  </defaultProxy>
    +</system.net>
    +

    +

    Run your web app (application pool) as a user with access to the proxy.

    +

    Implement your own proxy

    +

    Add the following class:

    +

    public class AuthenticatingProxy : IWebProxy
    +{
    +    public ICredentials Credentials
    +    {
    +        get { return new NetworkCredential("username", "password"); }
    +        set {}
    +    }
    +
    +    public Uri GetProxy(Uri destination)
    +    {
    +        return new Uri("http://localhost:8888");
    +    }
    +
    +    public bool IsBypassed(Uri host)
    +    {
    +        return false;
    +    }
    +}
    +

    +

    Configure the new proxy in web.config:

    +

    <defaultProxy useDefaultCredentials="false">
    +  <module type="YourNamespace.AuthenticatingProxy, YourAssembly" />
    +</defaultProxy>
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-a-running-website-on-azure/index.html b/logging-to-elmah-io-from-a-running-website-on-azure/index.html new file mode 100644 index 0000000000..3de637379b --- /dev/null +++ b/logging-to-elmah-io-from-a-running-website-on-azure/index.html @@ -0,0 +1,694 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from a running website on Azure + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from a running website on Azure

    +
    +

    Adding elmah.io on a running website isn't the recommended way to install. It should be used if you are unable to deploy a new version only.

    +
    +

    To enable error logging to elmah.io, you usually install one of our client integrations through PowerShell or Visual Studio and deploy a new version of your website to a web server. Sometimes you need to monitor an already running website or don't want logging logic as part of your repository. Using the elmah.io Site Extension for Azure App Services, error logging can be added to an already running website.

    +

    Check out this video tutorial or keep reading for the text version:

    +

    + azure-apps-services + +

    +

    To start logging errors from your Azure web application, go to the Azure Portal and select the website you want to monitor. Click the Extensions tool:

    +

    Add Site Extension

    +

    Click the Add button and select .NET elmah.io for Azure:

    +

    Select elmah.io

    +

    Accept the terms and click the Add button. The elmah.io Site Extension is now added. Once added, restart the website for the new extension to load.

    +

    Finally, you need to add your API key (Where is my API key?) and log ID (Where is my log ID?) to Application settings:

    +

    Add API key and log ID to application settings

    +

    Make sure to use the app setting names ELMAHIO_APIKEY and ELMAHIO_LOGID.

    +

    Your Azure web application now logs all uncaught exceptions to elmah.io. The elmah.io Site Extension comes with a couple of limitations:

    +
      +
    • It only works for ASP.NET, MVC, Web API, and similar. ASP.NET Core websites should be installed locally and re-deployed.
    • +
    • .NET Full Framework 4.6 and newer is required.
    • +
    • Custom code or configuration may swallow exceptions. Like custom errors or when using the HandleErrorAttribute attribute in ASP.NET MVC. In this case, the correct NuGet package needs to be installed in your code and deployed to Azure (like the Elmah.Io.Mvc package for ASP.NET MVC).
    • +
    +

    Troubleshooting

    +

    ConfigurationErrorsException: Could not load file or assembly 'Elmah' or one of its dependencies. The system cannot find the file specified.

    +

    After uninstalling the elmah.io site extension, you may see the configuration error above. This means that elmah.io's uninstall script for some reason wasn't allowed to run or resulted in an error.

    +

    To make sure that elmah.io is completely removed, follow these steps:

    +
      +
    1. Stop your website.
    2. +
    3. Browse your website files through Kudu.
    4. +
    5. Remove all files starting with Elmah.
    6. +
    7. Start your website.
    8. +
    +

    Error while uninstalling the site extension

    +

    While uninstalling the site extension you may see errors like this:

    +

    Failed to delete Site Extension: .NET elmah.io for Azure.{"Message":"An error has occurred.","ExceptionMessage":"The system cannot find the file specified.
    +C:\home\SiteExtensions\Elmah.Io.Azure.SiteExtension\uninstall.cmd
    +

    +

    In this case, the elmah.io for Azure site extension needs to be uninstalled manually. To do that, go to Kudu Services beneath the Advanced Tools section in the website on Azure. In the Debug console navigate to site/wwwroot/bin and delete all files prefixed with Elmah (up to four files).

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-a-running-website-on-iis/index.html b/logging-to-elmah-io-from-a-running-website-on-iis/index.html new file mode 100644 index 0000000000..b5e1494afe --- /dev/null +++ b/logging-to-elmah-io-from-a-running-website-on-iis/index.html @@ -0,0 +1,671 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from a running website on IIS + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from a running website on IIS

    +
    +

    Adding elmah.io on a running website isn't the recommended way to install. It should be used if you are unable to deploy a new version only.

    +
    +

    To enable error logging to elmah.io, you usually install one of our client integrations through PowerShell or Visual Studio and deploy a new version of your website to a web server. Sometimes you need to monitor an already running website or don't want logging logic as part of your repository. elmah.io can be added to a running website by following this guide.

    +

    Run the following command somewhere on your computer:

    +

    nuget install elmah.io
    +

    +

    It is recommended to run this locally to avoid having to install nuget.exe on the machine running IIS (typically a production environment). If you don't have NuGet installed, there are a range of download options available here.

    +

    From the folder where you ran the command, copy the following files to the bin folder of your running website:

    +

    elmah.corelibrary.x.y.z\lib\Elmah.dll
    +elmah.io.x.y.z\lib\net45\Elmah.Io.dll
    +Elmah.Io.Client.x.y.z\lib\<.net version your website is using>\Elmah.Io.Client.dll
    +Newtonsoft.Json.x.y.z\lib\<.net version your website is using>\Newtonsoft.Json.dll
    +

    +

    Configure elmah.io in Web.config as described here: Configure elmah.io manually (you don't need to call the Install-Package command). Notice that the AppDomain will restart when saving changes to the Web.config file.

    +

    If the website doesn't start logging errors to elmah.io, you may need to restart it.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-angular/index.html b/logging-to-elmah-io-from-angular/index.html new file mode 100644 index 0000000000..6a6dba2e35 --- /dev/null +++ b/logging-to-elmah-io-from-angular/index.html @@ -0,0 +1,721 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Angular + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    NuGet +npm +Samples

    +

    Logging to elmah.io from Angular

    +

    elmah.io.javascript works great with Angular applications too. To log all errors happening in your Angular app, install elmah.io.javascript through npm as described in Logging from JavaScript.

    +

    In the same folder as the app.module.ts file add a new file named elmah-io-error-handler.ts and include the following content:

    +

    import {ErrorHandler} from '@angular/core';
    +
    +import * as Elmahio from 'elmah.io.javascript';
    +
    +export class ElmahIoErrorHandler implements ErrorHandler {
    +  logger: Elmahio;
    +  constructor() {
    +    this.logger = new Elmahio({
    +      apiKey: 'API_KEY',
    +      logId: 'LOG_ID'
    +    });
    +  }
    +  handleError(error) {
    +    if (error && error.message) {
    +      this.logger.error(error.message, error);
    +    } else {
    +      this.logger.error('Error in application', error);
    +    }
    +  }
    +}
    +

    +

    Reference both ErrorHandler and ElmahIoErrorHandler in the app.module.ts file:

    +

    import { BrowserModule } from '@angular/platform-browser';
    +import {ErrorHandler, NgModule} from '@angular/core'; // ⬅️ Add ErrorHandler
    +
    +import { AppRoutingModule } from './app-routing.module';
    +import { AppComponent } from './app.component';
    +
    +import {ElmahIoErrorHandler} from './elmah-io-error-handler'; // ⬅️ Reference ElmahIoErrorHandler
    +
    +@NgModule({
    +  declarations: [
    +    AppComponent
    +  ],
    +  imports: [
    +    BrowserModule,
    +    AppRoutingModule
    +  ],
    +  // ⬇️ Reference both handlers
    +  providers: [{ provide: ErrorHandler, useClass: ElmahIoErrorHandler }],
    +  bootstrap: [AppComponent]
    +})
    +export class AppModule { }
    +

    +

    All errors are shipped to the handleError-function by Angular and logged to elmah.io. Check out the Elmah.Io.JavaScript.AngularAspNetCore and Elmah.Io.JavaScript.AngularWebpack samples for some real working code.

    +

    AngularJS/Angular 1

    +

    For AngularJS you need to implement the $exceptionHandler instead:

    +

    (function () {
    +  'use strict';
    +  angular.module('app').factory('$exceptionHandler', ['$log', function controller($log) {
    +    var logger = new Elmahio({
    +      apiKey: 'API_KEY',
    +      logId: 'LOG_ID'
    +    });
    +    return function elmahExceptionHandler(exception, cause) {
    +      $log.error(exception, cause);
    +      logger.error(exception.message, exception);
    +    };
    +  }]);
    +})();
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-aspnet-core/index.html b/logging-to-elmah-io-from-aspnet-core/index.html new file mode 100644 index 0000000000..401f8a65ca --- /dev/null +++ b/logging-to-elmah-io-from-aspnet-core/index.html @@ -0,0 +1,1002 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging from ASP.NET Core + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from ASP.NET Core

    + +

    If you are looking to log all uncaught errors from ASP.NET Core, you've come to the right place. For help setting up general .NET Core logging similar to log4net, check out Logging from Microsoft.Extensions.Logging.

    +

    To log all warnings and errors from ASP.NET Core, install the following NuGet package:

    +
    Install-Package Elmah.Io.AspNetCore
    +
    dotnet add package Elmah.Io.AspNetCore
    +
    <PackageReference Include="Elmah.Io.AspNetCore" Version="5.*" />
    +
    paket add Elmah.Io.AspNetCore
    +
    +

    In the Startup.cs file, add a new using statement:

    +

    using Elmah.Io.AspNetCore;
    +

    + + +
    +
    +

    Call AddElmahIo in the ConfigureServices-method:

    +

    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.AddElmahIo(options =>
    +    {
    +        options.ApiKey = "API_KEY";
    +        options.LogId = new Guid("LOG_ID");
    +    });
    +    // ...
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the log Id of the log you want to log to.

    +

    Call UseElmahIo in the Configure-method:

    +

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory fac)
    +{
    +    // ...
    +    app.UseElmahIo();
    +    // ...
    +}
    +

    +
    +
    +

    Call AddElmahIo in the Program.cs file:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    options.ApiKey = "API_KEY";
    +    options.LogId = new Guid("LOG_ID");
    +});
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the log Id of the log you want to log to.

    +

    Call UseElmahIo in the Program.cs file:

    +

    app.UseElmahIo();
    +

    +
    +
    +
    +

    Make sure to call the UseElmahIo-method after installation of other pieces of middleware handling exceptions and auth (like UseDeveloperExceptionPage, UseExceptionHandler, UseAuthentication, and UseAuthorization), but before any calls to UseEndpoints, UseMvc, MapRazorPages, and similar.

    +
    +

    That's it. Every uncaught exception will be logged to elmah.io. For an example of configuring elmah.io with ASP.NET Core minimal APIs, check out this sample.

    +

    Configuring API key and log ID in options

    +

    If you have different environments (everyone has a least localhost and production), you should consider adding the API key and log ID in your appsettings.json file:

    +

    {
    +  // ...
    +  "ElmahIo": {
    +    "ApiKey": "API_KEY",
    +    "LogId": "LOG_ID"
    +  }
    +}
    +

    +

    Configuring elmah.io is done by calling the Configure-method before AddElmahIo:

    + + +
    +
    +

    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.Configure<ElmahIoOptions>(Configuration.GetSection("ElmahIo"));
    +    services.AddElmahIo();
    +}
    +

    +

    Notice that you still need to call AddElmahIo to correctly register middleware dependencies.

    +

    Finally, call the UseElmahIo-method (as you would do with config in C# too):

    +

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    +{
    +    // ...
    +    app.UseElmahIo();
    +    // ...
    +}
    +

    +

    You can still configure additional options on the ElmahIoOptions object:

    +

    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.Configure<ElmahIoOptions>(Configuration.GetSection("ElmahIo"));
    +    services.Configure<ElmahIoOptions>(options =>
    +    {
    +        options.OnMessage = msg =>
    +        {
    +            msg.Version = "1.0.0";
    +        };
    +    });
    +    services.AddElmahIo();
    +}
    +

    +
    +
    +

    builder.Services.Configure<ElmahIoOptions>(builder.Configuration.GetSection("ElmahIo"));
    +builder.Services.AddElmahIo();
    +

    +

    Notice that you still need to call AddElmahIo to correctly register middleware dependencies.

    +

    Finally, call the UseElmahIo-method (as you would do with config in C# too):

    +

    app.UseElmahIo();
    +

    +

    You can still configure additional options on the ElmahIoOptions object:

    +

    builder.Services.Configure<ElmahIoOptions>(builder.Configuration.GetSection("ElmahIo"));
    +builder.Services.Configure<ElmahIoOptions>(o =>
    +{
    +    o.OnMessage = msg =>
    +    {
    +        msg.Version = "1.0.0";
    +    };
    +});
    +builder.Services.AddElmahIo();
    +

    +
    +
    +

    Logging exceptions manually

    +

    While automatically logging all uncaught exceptions is definitely a nice feature, sometimes you may want to catch exceptions and log them manually. If you just want to log the exception details, without all of the contextual information about the HTTP context (cookies, server variables, etc.), we recommend you to look at our integration for Microsoft.Extensions.Logging. If the context is important for the error, you can utilize the Ship-methods available in Elmah.Io.AspNetCore:

    +

    try
    +{
    +    var i = 0;
    +    var result = 42/i;
    +}
    +catch (DivideByZeroException e)
    +{
    +    e.Ship(HttpContext);
    +}
    +

    +

    When catching an exception (in this example an DivideByZeroException), you call the Ship extension method with the current HTTP context as parameter.

    +

    From Elmah.Io.AspNetCore version 3.12.* or newer, you can log manually using the ElmahIoApi class as well:

    +

    ElmahIoApi.Log(e, HttpContext);
    +

    +

    The Ship-method uses ElmahIoApi underneath why both methods will give the same end result.

    + +

    See Logging breadcrumbs from ASP.NET Core.

    +

    Additional options

    +

    Setting application name

    +

    If logging to the same log from multiple web apps it is a good idea to set unique application names from each app. This will let you search and filter errors on the elmah.io UI. To set an application name, add the following code to the options:

    +

    builder.Services.AddElmahIo(o =>
    +{
    +    // ...
    +    o.Application = "MyApp";
    +});
    +

    +

    The application name can also be configured through appsettings.json:

    +

    {
    +  // ...
    +  "ElmahIo": {
    +    // ...
    +    "Application": "MyApp"
    +  }
    +}
    +

    +

    Hooks

    +

    elmah.io for ASP.NET Core supports a range of actions for hooking into the process of logging messages. Hooks are registered as actions when installing the elmah.io middleware:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnMessage = message =>
    +    {
    +        message.Version = "42";
    +    };
    +    options.OnError = (message, exception) =>
    +    {
    +        logger.LogError(1, exception, "Error during log to elmah.io");
    +    };
    +});
    +

    +

    The actions provide a mechanism for hooking into the log process. The action registered in the OnMessage property is called by elmah.io just before logging a new message to the API. Use this action to decorate/enrich your log messages with additional data, like a version number. The OnError action is called if communication with the elmah.io API failed. If this happens, you should log the message to a local log (through Microsoft.Extensions.Logging, Serilog or similar).

    +
    +

    Do not log to elmah.io in your OnError action, since that could cause an infinite loop in your code.

    +
    +

    While elmah.io supports ignore rules serverside, you may want to filter out errors without even hitting the elmah.io API. Using the OnFilter function on the options object, filtering is easy:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnFilter = message =>
    +    {
    +        return message.Type == "System.NullReferenceException";
    +    };
    +});
    +

    +

    The example above, ignores all messages of type System.NullReferenceException.

    +

    Decorate from HTTP context

    +

    When implementing the OnMessage action as shown above you don't have access to the current HTTP context. Elmah.Io.AspNetCore already tries to fill in as many fields as possible from the current context, but you may want to tweak something from time to time. In this case, you can implement a custom decorator like this:

    +

    public class DecorateElmahIoMessages : IConfigureOptions<ElmahIoOptions>
    +{
    +    private readonly IHttpContextAccessor httpContextAccessor;
    +
    +    public DecorateElmahIoMessages(IHttpContextAccessor httpContextAccessor)
    +    {
    +        this.httpContextAccessor = httpContextAccessor;
    +    }
    +
    +    public void Configure(ElmahIoOptions options)
    +    {
    +        options.OnMessage = msg =>
    +        {
    +            var context = httpContextAccessor.HttpContext;
    +            msg.User = context?.User?.Identity?.Name;
    +        };
    +    }
    +}
    +

    + + +
    +
    +

    Then register IHttpContextAccessor and the new class in the ConfigureServices method in the Startup.cs file:

    +

    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.AddHttpContextAccessor();
    +    services.AddSingleton<IConfigureOptions<ElmahIoOptions>, DecorateElmahIoMessages>();
    +    // ...
    +}
    +

    +
    +
    +

    Then register IHttpContextAccessor and the new class in the in the Program.cs file:

    +

    builder.Services.AddHttpContextAccessor();
    +builder.Services.AddSingleton<IConfigureOptions<ElmahIoOptions>, DecorateElmahIoMessages>();
    +

    +
    +
    +
    +

    Decorating messages using IConfigureOptions requires Elmah.Io.AspNetCore version 4.1.37 or newer.

    +
    +

    Include source code

    +

    You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it.

    +

    There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnMessage = msg =>
    +    {
    +        msg.WithSourceCodeFromPdb();
    +    };
    +});
    +

    +

    Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.

    +
    +

    Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.

    +
    +

    Remove sensitive form data

    +

    The OnMessage event can be used to filter sensitive form data as well. In the following example, we remove the server variable named Secret-Key from all messages, before sending them to elmah.io.

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnMessage = msg =>
    +    {
    +        var item = msg.ServerVariables.FirstOrDefault(x => x.Key == "Secret-Key"); 
    +        if (item != null)
    +        {
    +            msg.ServerVariables.Remove(item);
    +        }
    +    };
    +});
    +

    +

    Formatting exceptions

    +

    A default exception formatter is used to format any exceptions, before sending them off to the elmah.io API. To override the format of the details field in elmah.io, set a new IExceptionFormatter in the ExceptionFormatter property on the ElmahIoOptions object:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...
    +    options.ExceptionFormatter = new DefaultExceptionFormatter();
    +}
    +

    +

    Besides the default exception formatted (DefaultExceptionFormatter), Elmah.Io.AspNetCore comes with a formatter called YellowScreenOfDeathExceptionFormatter. This formatter, outputs an exception and its inner exceptions as a list of exceptions, much like on the ASP.NET yellow screen of death. If you want, implementing your own exception formatter, requires you to implement a single method.

    +

    Logging responses not throwing an exception

    +

    As default, uncaught exceptions (500's) and 404's are logged automatically. Let's say you have a controller returning a Bad Request and want to log that as well. Since returning a 400 from a controller doesn't trigger an exception, you will need to configure this status code:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...
    +    options.HandledStatusCodesToLog = new List<int> { 400 };
    +}
    +

    +

    The list can also be configured through appsettings.json:

    +

    {
    +  // ...
    +  "ElmahIo": {
    +    // ...
    +    "HandledStatusCodesToLog": [ 400 ],
    +  }
    +}
    +

    +

    When configuring status codes through the appsettings.json file, 404s will always be logged. To avoid this, configure the list in C# as shown above.

    +

    Logging through a proxy

    +

    Since ASP.NET Core no longer support proxy configuration through web.config, you can log to elmah.io by configuring a proxy manually:

    +

    builder.Services.AddElmahIo(options =>
    +{
    +    // ...
    +    options.WebProxy = new System.Net.WebProxy("localhost", 8888);
    +}
    +

    +

    In this example, the elmah.io client routes all traffic through http://localhost:8000.

    +

    Logging health check results

    +

    Check out Logging heartbeats from ASP.NET Core for details.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-aspnet-mvc/index.html b/logging-to-elmah-io-from-aspnet-mvc/index.html new file mode 100644 index 0000000000..68cd55b884 --- /dev/null +++ b/logging-to-elmah-io-from-aspnet-mvc/index.html @@ -0,0 +1,668 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from ASP.NET MVC + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from ASP.NET MVC

    +

    Even though ELMAH works out of the box with ASP.NET MVC, ELMAH and MVC provide some features which interfere with one another. As usual, the great community around ELMAH has done something to fix this, by using the Elmah.Mvc NuGet package. We've built a package for ASP.NET MVC exclusively, which installs all the necessary packages.

    +

    To start logging exceptions from ASP.NET MVC, install the Elmah.Io.Mvc NuGet package:

    +
    Install-Package Elmah.Io.Mvc
    +
    dotnet add package Elmah.Io.Mvc
    +
    <PackageReference Include="Elmah.Io.Mvc" Version="5.*" />
    +
    paket add Elmah.Io.Mvc
    +
    +

    During the installation, you will be asked for your API key (Where is my API key?) and log ID (Where is my log ID?). That's it. Every unhandled exception in ASP.NET MVC is logged to elmah.io.

    +

    As part of the installation, we also installed Elmah.MVC, which adds some interesting logic around routing and authentication. Take a look in the web.config for application settings with the elmah.mvc. prefix. For documentation about these settings, check out the Elmah.MVC project on GitHub.

    +

    Since Elmah.MVC configures an URL for accessing the ELMAH UI (just /elmah and not /elmah.axd), you can remove the location element in web.config, added by the Elmah.Io.Mvc NuGet package installer.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-aws-beanstalk/index.html b/logging-to-elmah-io-from-aws-beanstalk/index.html new file mode 100644 index 0000000000..f138786f98 --- /dev/null +++ b/logging-to-elmah-io-from-aws-beanstalk/index.html @@ -0,0 +1,692 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from AWS Beanstalk + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from AWS Beanstalk

    + +

    Logging to elmah.io from .NET applications deployed on AWS Beanstalk is as easy as with other cloud hosting services. Since Beanstalk runs normal ASP.NET, MVC, Web API, and Core applications, setting up elmah.io almost follows the guides already available in the elmah.io documentation. There are a few things to notice when needing to configure elmah.io, which will be explained in this document.

    +

    ASP.NET / MVC / Web API

    +

    To install elmah.io in ASP.NET, MVC, and/or Web API, please follow the guidelines for each framework. You can specify one set of API key and log ID in the Web.config file and another set in the Web.release.config file as explained here: Use multiple logs for different environments.

    +

    If you want to include your production API key and log ID on AWS only (to avoid having sensitive information in source control), you can do this using Environment properties on AWS. Go to your environment on the AWS console and click the Configuration tab. Click the Edit button beneath the Software category and scroll to the bottom. There you will see a section named Environment properties. Input your API key and log ID:

    +

    AWS Environment Properties

    +

    AWS inserts the properties as application settings in the Web.config file. To make sure that elmah.io uses API key and log ID from appSettings, change the <elmah> element to reference the keys specified on AWS:

    +

    <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKeyKey="elmahio-apikey" logIdKey="elmahio-logid" />
    +

    +

    The apiKeyKey and logIdKey attributes reference the app settings keys.

    +

    Finally, if you have an API key and/or log ID specified as part of the appSettings> element in Web.config, you will need to remove those when running in production. The reason for this is that AWS only insert missing keys. To do so, modify your Web.release.config file:

    +

    <appSettings>
    +  <add key="elmahio-logid" xdt:Transform="Remove" xdt:Locator="Match(key)" />
    +  <add key="elmahio-apikey" xdt:Transform="Remove" xdt:Locator="Match(key)" />
    +</appSettings>
    +

    +

    ASP.NET Core

    +

    To install elmah.io in ASP.NET Core, follow this guide: Logging to elmah.io from ASP.NET Core.

    +

    If you want to include your production API key and log ID on AWS only (to avoid having sensitive information in source control), you can do this using Environment properties on AWS. Go to your environment on the AWS console and click the Configuration tab. Click the Edit button beneath the Software category and scroll to the bottom. There you will see a section named Environment properties. Input your API key and log ID:

    +

    AWS Environment Properties

    +

    This example uses the double underscore syntax to set the ApiKey and LogId properties in the appsettings.json file:

    +

    {
    +  "ElmahIo": {
    +    "ApiKey": "API_KEY",
    +    "LogId": "LOG_ID"
    +  }
    +}
    +
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-aws-lambdas/index.html b/logging-to-elmah-io-from-aws-lambdas/index.html new file mode 100644 index 0000000000..e34b360834 --- /dev/null +++ b/logging-to-elmah-io-from-aws-lambdas/index.html @@ -0,0 +1,705 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from AWS Lambdas + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from AWS Lambdas

    + +

    Since AWS now supports .NET Core, logging to elmah.io from a lambda is easy.

    +

    Logging to elmah.io from AWS Serverless Application

    +

    AWS Serverless Applications are running on ASP.NET Core. The configuration matches our documentation for ASP.NET Core. Check out Logging from ASP.NET Core for details on how to log all uncaught exceptions from an AWS Serverless Application.

    +

    The .NET SDK for AWS comes with native support for logging to CloudWatch. We recommend using Microsoft.Extensions.Logging for logging everything to CloudWatch and warnings and errors to elmah.io. The configuration follows that of Logging from Microsoft.Extensions.Logging.

    +

    AWS Serverless Applications doesn't have a Program.cs file. To configure logging, you will need to modify either LambdaEntryPoint.cs, LocalEntryPoint.cs or both:

    +

    public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
    +{
    +    protected override void Init(IWebHostBuilder builder)
    +    {
    +        builder
    +            .UseStartup<Startup>()
    +            .ConfigureLogging((ctx, logging) =>
    +            {
    +                logging.AddElmahIo(options =>
    +                {
    +                    options.ApiKey = "API_KEY";
    +                    options.LogId = new Guid("LOG_ID");
    +                });
    +                logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning);
    +            });
    +    }
    +}
    +

    +

    The same configuration would go into LocalEntryPoint.cs, if you want to log from localhost as well.

    +

    Logging when using Amazon.Lambda.AspNetCoreServer.Hosting

    +

    AWS supports running ASP.NET Core applications as Lambdas using the Amazon.Lambda.AspNetCoreServer.Hosting package. This can serve as an easy way for .NET developers like us to create minimal API-based endpoints and deploy them as Lambda functions on AWS. There's a downside to deploying ASP.NET Core this way since AWS will kill the process when it decides that it is no longer needed. The Elmah.Io.Extensions.Logging package runs an internal message queue and stores log messages asynchronously to better handle a large workload. When AWS kills the process without disposing of configured loggers, log messages queued for processing are left unhandled.

    +

    To solve this, Elmah.Io.Extensions.Logging supports a property named Synchronous that disables the internal message queue and stores log messages in a synchronous way. You may still experience log messages not being stored, but that's a consequence of AWS's choice of killing the process rather than shutting it down nicely (like ASP.NET Core).

    +

    To log messages synchronously, include the following code in your logging setup:

    +

    builder.Services.AddLogging(logging =>
    +{
    +    logging.AddElmahIo(options =>
    +    {
    +        // ...
    +        options.Synchronous = true;
    +    });
    +});
    +

    +

    Be aware that logging a large number of log messages synchronously, may slow down your application and/or cause thread exhaustion. We recommend only logging errors this way and not debug, information, and similar.

    +

    Logging from AWS Lambda Project

    +

    AWS Lambda Project comes with native support for CloudWatch too. In our experience, it's not possible to configure multiple destinations on LambdaLogger, why you would want to use another framework when logging to elmah.io from an AWS Lambda Project. We recommend using a logging framework like Serilog, Microsoft.Extensions.Logging, NLog, or log4net.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-azure-functions/index.html b/logging-to-elmah-io-from-azure-functions/index.html new file mode 100644 index 0000000000..8e471f26be --- /dev/null +++ b/logging-to-elmah-io-from-azure-functions/index.html @@ -0,0 +1,872 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Azure Functions + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Azure Functions

    + +

    Logging errors from Azure Functions requires only a few lines of code. We've created a client specifically for Azure Functions. If your are looking for logging from Isolated Azure Functions (out of process) check out Logging to elmah.io from Isolated Azure Functions.

    +

    Install the newest Elmah.Io.Functions package in your Azure Functions project:

    +
    Install-Package Elmah.Io.Functions
    +
    dotnet add package Elmah.Io.Functions
    +
    <PackageReference Include="Elmah.Io.Functions" Version="5.*" />
    +
    paket add Elmah.Io.Functions
    +
    +

    The elmah.io integration for Azure Functions uses function filters and dependency injection part of the Microsoft.Azure.Functions.Extensions package. To configure elmah.io, open the Startup.cs file or create a new one if not already there. In the Configure-method, add the elmah.io options and exception filter:

    +

    using Elmah.Io.Functions;
    +using Microsoft.Azure.Functions.Extensions.DependencyInjection;
    +using Microsoft.Azure.WebJobs.Host;
    +using Microsoft.Extensions.Configuration;
    +using Microsoft.Extensions.DependencyInjection;
    +using System;
    +
    +[assembly: FunctionsStartup(typeof(MyFunction.Startup))]
    +
    +namespace MyFunction
    +{
    +    public class Startup : FunctionsStartup
    +    {
    +        public override void Configure(IFunctionsHostBuilder builder)
    +        {
    +            var config = new ConfigurationBuilder()
    +                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
    +                .AddEnvironmentVariables()
    +                .Build();
    +
    +            builder.Services.Configure<ElmahIoFunctionOptions>(o =>
    +            {
    +                o.ApiKey = config["apiKey"];
    +                o.LogId = new Guid(config["logId"]);
    +            });
    +
    +            builder.Services.AddSingleton<IFunctionFilter, ElmahIoExceptionFilter>();
    +        }
    +    }
    +}
    +

    +

    Notice how API key and log ID are configured through the ElmahIoFunctionOptions object. In the last line of the Configure-method, the ElmahIoExceptionFilter-filter is configured. This filter will automatically catch any exception caused by your filter and log it to elmah.io.

    +

    A quick comment about the obsolete warning showed when using the package. Microsoft marked IFunctionFilter as obsolete. Not because it will be removed, but because they may change the way attributes work in functions in the future. For now, you can suppress this warning with the following code:

    +

    #pragma warning disable CS0618 // Type or member is obsolete
    +builder.Services.AddSingleton<IFunctionFilter, ElmahIoExceptionFilter>();
    +#pragma warning restore CS0618 // Type or member is obsolete
    +

    +

    In your settings, add the apiKey and logId variables:

    +

    {
    +  // ...
    +  "Values": {
    +    // ...
    +    "apiKey": "API_KEY",
    +    "logId": "LOG_ID"
    +  }
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with your log ID. When running on Azure or similar, you can overwrite apiKey and logId with application settings or environment variables as already thoroughly documented on Microsoft's documentation.

    +

    Application name

    +

    To set the application name on all errors, set the Application property during initialization:

    +

    builder.Services.Configure<ElmahIoFunctionOptions>(o =>
    +{
    +    o.ApiKey = config["apiKey"];
    +    o.LogId = new Guid(config["logId"]);
    +    o.Application = "MyFunction";
    +});
    +

    +

    Message hooks

    +

    Elmah.Io.Functions provide message hooks similar to the integrations with ASP.NET and ASP.NET Core.

    +

    Decorating log messages

    +

    To include additional information on log messages, you can use the OnMessage event when initializing ElmahIoFunctionOptions:

    +

    builder.Services.Configure<ElmahIoFunctionOptions>(o =>
    +{
    +    o.ApiKey = config["apiKey"];
    +    o.LogId = new Guid(config["logId"]);
    +    o.OnMessage = msg =>
    +    {
    +        msg.Version = "1.0.0";
    +    };
    +});
    +

    +

    The example above includes a version number on all errors.

    +

    Include source code

    +

    You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it.

    +

    There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action:

    +

    builder.Services.Configure<ElmahIoFunctionOptions>(o =>
    +{
    +    o.OnMessage = msg =>
    +    {
    +        msg.WithSourceCodeFromPdb();
    +    };
    +});
    +

    +

    Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.

    +

    Handle errors

    +

    To handle any errors happening while processing a log message, you can use the OnError event when initializing ElmahIoFunctionOptions:

    +

    builder.Services.Configure<ElmahIoFunctionOptions>(o =>
    +{
    +    o.ApiKey = config["apiKey"];
    +    o.LogId = new Guid(config["logId"]);
    +    o.OnError = (msg, ex) =>
    +    {
    +        logger.LogError(ex, ex.Message);
    +    };
    +});
    +

    +

    The example above logs any errors during communication with elmah.io to a local log.

    +

    Error filtering

    +

    To ignore specific errors based on their content, you can use the OnFilter event when initializing ElmahIoFunctionOptions:

    +

    builder.Services.Configure<ElmahIoFunctionOptions>(o =>
    +{
    +    o.ApiKey = config["apiKey"];
    +    o.LogId = new Guid(config["logId"]);
    +    o.OnFilter = msg =>
    +    {
    +        return msg.Method == "GET";
    +    };
    +});
    +

    +

    The example above ignores any errors generated during an HTTP GET request.

    +

    Logging through ILogger

    +

    Azure Functions can log through Microsoft.Extensions.Logging (MEL) too. By adding the filter, as shown above, all uncaught exceptions are automatically logged. But when configuring your Function app to log through MEL, custom messages can be logged through the ILogger interface. Furthermore, you will get detailed log messages from within the Function host. To set this up, install the Elmah.Io.Extensions.Logging NuGet package:

    +
    Install-Package Elmah.Io.Extensions.Logging
    +
    dotnet add package Elmah.Io.Extensions.Logging
    +
    <PackageReference Include="Elmah.Io.Extensions.Logging" Version="5.*" />
    +
    paket add Elmah.Io.Extensions.Logging
    +
    +

    Then extend your Startup.cs file like this:

    +

    builder.Services.AddLogging(logging =>
    +{
    +    logging.AddElmahIo(o =>
    +    {
    +        o.ApiKey = config["apiKey"];
    +        o.LogId = new Guid(config["logId"]);
    +    });
    +    logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning);
    +});
    +

    +

    In the example, only warning messages and above are logged to elmah.io. You can remove the filter or set another log level if you want to log more. Jump to Log filtering to learn how to configure filters from config.

    +

    Either pass an ILogger to your function method:

    +

    public class MyFunction
    +{
    +    public static void Run([TimerTrigger("...")]TimerInfo myTimer, ILogger log)
    +    {
    +        log.LogWarning("This is a warning");
    +    }
    +}
    +

    +

    Or inject an ILoggerFactory and create a logger as part of the constructor:

    +

    public class MyFunction
    +{
    +    private readonly ILogger log;
    +
    +    public Function1(ILoggerFactory loggerFactory)
    +    {
    +        this.log = loggerFactory.CreateLogger("MyFunction");
    +    }
    +
    +    public void Run([TimerTrigger("...")]TimerInfo myTimer)
    +    {
    +        log.LogWarning("This is a warning");
    +    }
    +}
    +

    +

    Log filtering

    +

    The code above filters out all log messages with a severity lower than Warning. You can use all of the log filtering capabilities of Microsoft.Extensions.Logging to enable and disable various log levels from multiple categories. A common requirement is to only log Warning and more severe originating from the Azure Functions runtime, but log Information messages from your function code. This can be enabled through a custom category:

    +

    public class MyFunction
    +{
    +    private readonly ILogger log;
    +
    +    public Function1(ILoggerFactory loggerFactory)
    +    {
    +        this.log = loggerFactory.CreateLogger("MyFunction");
    +    }
    +
    +    public void Run([TimerTrigger("...")]TimerInfo myTimer)
    +    {
    +        log.LogInformation("This is an information message");
    +    }
    +}
    +

    +

    The MyFunction category will need configuration in either C# or in the host.json file:

    +

    {
    +  // ...
    +  "logging": {
    +    "logLevel": {
    +      "default": "Warning",
    +      "MyFunction": "Information"
    +    }
    +  }
    +}
    +

    +

    Azure Functions v1

    +

    The recent Elmah.Io.Functions package no longer supports Azure Functions v1. You can still log from Functions v1 using an older version of the package. Check out Logging to elmah.io from Azure WebJobs for details. The guide is for Azure WebJobs but installation for Functions v1 is identical.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-azure-webjobs/index.html b/logging-to-elmah-io-from-azure-webjobs/index.html new file mode 100644 index 0000000000..d852f979a5 --- /dev/null +++ b/logging-to-elmah-io-from-azure-webjobs/index.html @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Azure WebJobs + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Azure WebJobs

    +

    Logging errors from Azure WebJobs requires only a few lines of code. We've created a client specifically for Azure WebJobs.

    +
    +

    Support for Azure WebJobs has been stopped on version 3.1.23 of the Elmah.Io.Functions package. The newer versions only work with Azure Functions.

    +
    +

    Install the Elmah.Io.Functions package:

    +
    Install-Package Elmah.Io.Functions -Version 3.1.23
    +
    dotnet add package Elmah.Io.Functions --version 3.1.23
    +
    <PackageReference Include="Elmah.Io.Functions" Version="3.1.23" />
    +
    paket add Elmah.Io.Functions --version 3.1.23
    +
    +

    Log all uncaught exceptions using the ElmahIoExceptionFilter attribute:

    +

    [ElmahIoExceptionFilter("API_KEY", "LOG_ID")]
    +public class Functions
    +{
    +    public static void ProcessQueueMessage([QueueTrigger("queue")] string msg, TextWriter log)
    +    {
    +        throw new Exception("Some exception");
    +    }
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with your log ID.

    +
    +

    If your WebJob method is declared as async, remember to change the return type to Task. Without it, ElmahIoExceptionFilter is never invoked.

    +
    +

    The filter also supports config variables:

    +

    [ElmahIoExceptionFilter("%apiKey%", "%logId%")]
    +

    +

    The variables above, would require you to add your API key and log ID to your App.config:

    +

    <configuration>
    +  <appSettings>
    +    <add key="apiKey" value="API_KEY"/>
    +    <add key="logId" value="LOG_ID"/>
    +  </appSettings>
    +</configuration>
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-blazor/index.html b/logging-to-elmah-io-from-blazor/index.html new file mode 100644 index 0000000000..df7c3c28eb --- /dev/null +++ b/logging-to-elmah-io-from-blazor/index.html @@ -0,0 +1,794 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Blazor + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Blazor

    + +

    Blazor Server App

    +

    To start logging to elmah.io from a Blazor Server App, install the Elmah.Io.Extensions.Logging NuGet package:

    +
    Install-Package Elmah.Io.Extensions.Logging
    +
    dotnet add package Elmah.Io.Extensions.Logging
    +
    <PackageReference Include="Elmah.Io.Extensions.Logging" Version="5.*" />
    +
    paket add Elmah.Io.Extensions.Logging
    +
    +

    In the Program.cs file, add elmah.io logging configuration:

    +

    builder.Logging.AddElmahIo(options =>
    +{
    +    options.ApiKey = "API_KEY";
    +    options.LogId = new Guid("LOG_ID");
    +});
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the ID of the log you want messages sent to (Where is my log ID?).

    +

    All uncaught exceptions are automatically logged to elmah.io. Exceptions can be logged manually, by injecting an ILogger into your view and adding try/catch:

    +

    @using Microsoft.Extensions.Logging
    +@inject ILogger<FetchData> logger
    +
    +<!-- ... -->
    +
    +@functions {
    +    WeatherForecast[] forecasts;
    +
    +    protected override async Task OnInitAsync()
    +    {
    +        try
    +        {
    +            forecasts = await Http
    +                .GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts-nonexisting");
    +        }
    +        catch (Exception e)
    +        {
    +            logger.LogError(e, e.Message);
    +        }
    +    }
    +}
    +

    +

    Information and other severities can be logged as well:

    +

    @using Microsoft.Extensions.Logging
    +@inject ILogger<Counter> logger
    +
    +<!-- ... -->
    +
    +@functions {
    +    int currentCount = 0;
    +
    +    void IncrementCount()
    +    {
    +        currentCount++;
    +        logger.LogInformation("Incremented count to {currentCount}", currentCount);
    +    }
    +}
    +

    +

    Include details from the HTTP context

    +

    Microsoft.Extensions.Logging doesn't know that it is running inside a web server. That is why Elmah.Io.Extensions.Logging doesn't include HTTP contextual information like URL and status code as default. To do so, install the Elmah.Io.AspNetCore.ExtensionsLogging NuGet package:

    +
    Install-Package Elmah.Io.AspNetCore.ExtensionsLogging
    +
    dotnet add package Elmah.Io.AspNetCore.ExtensionsLogging
    +
    <PackageReference Include="Elmah.Io.AspNetCore.ExtensionsLogging" Version="5.*" />
    +
    paket add Elmah.Io.AspNetCore.ExtensionsLogging
    +
    +

    And add the following code to the Program.cs file:

    +

    app.UseElmahIoExtensionsLogging();
    +

    +

    Make sure to call this method just before the call to UseRouting and UseEndpoints. This will include some of the information you are looking for.

    +

    There's a problem when running Blazor Server where you will see some of the URLs logged as part of errors on elmah.io having the value /_blazor. This is because Blazor doesn't work like traditional websites where the client requests the server and returns an HTML or JSON response. When navigating the UI, parts of the UI are loaded through SignalR, which causes the URL to be /_blazor. Unfortunately, we haven't found a good way to fix this globally. You can include the current URL on manual log statements by injecting a NavigationManager in the top of your .razor file:

    +

    @inject NavigationManager navigationManager
    +

    +

    Then wrap your logging code in a new scope:

    +

    Uri.TryCreate(navigationManager.Uri, UriKind.Absolute, out Uri url);
    +using (Logger.BeginScope(new Dictionary<string, object> 
    +{
    +    { "url", url.AbsolutePath }
    +}))
    +{
    +    logger.LogError(exception, "An error happened");
    +}
    +

    +

    The code uses the current URL from the injected NavigationManager object.

    +

    Blazor WebAssembly App (wasm)

    +

    To start logging to elmah.io from a Blazor Wasm App, install the Elmah.Io.Blazor.Wasm NuGet package:

    +
    Install-Package Elmah.Io.Blazor.Wasm
    +
    dotnet add package Elmah.Io.Blazor.Wasm
    +
    <PackageReference Include="Elmah.Io.Blazor.Wasm" Version="4.*" />
    +
    paket add Elmah.Io.Blazor.Wasm
    +
    +

    In the Program.cs file, add elmah.io logging configuration:

    +

    builder.Logging.AddElmahIo(options =>
    +{
    +    options.ApiKey = "API_KEY";
    +    options.LogId = new Guid("LOG_ID");
    +});
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the ID of the log you want messages sent to (Where is my log ID?).

    +

    All uncaught exceptions are automatically logged to elmah.io after calling AddElmahIo. Errors and other severities can be logged manually, by injecting an ILogger into your view and adding try/catch or by implementing error boundaries:

    +

    @page "/"
    +@inject ILogger<Index> logger
    +
    +@code {
    +    protected override void OnInitialized()
    +    {
    +        logger.LogInformation("Initializing index view");
    +
    +        try
    +        {
    +            object text = "Text";
    +            var cast = (int)text;
    +        }
    +        catch (InvalidCastException e)
    +        {
    +            logger.LogError(e, "An error happened");
    +        }
    +    }
    +}
    +

    +

    The following may be implemented by the package later:

    +
      +
    • Additional information about the HTTP context (like cookies, URL, and user).
    • +
    • Internal message queue and/or batch processing like Microsoft.Extensions.Logging.
    • +
    • Support for logging scopes.
    • +
    +

    Blazor (United) App

    +

    .NET 8 introduces a new approach to developing Blazor applications, formerly known as Blazor United. We have started experimenting a bit with Blazor Apps which have the option of rendering both server-side and client-side from within the same Blazor application. As shown in the sections above, using server-side rendering needs Elmah.Io.Extensions.Logging while client-side rendering needs Elmah.Io.Blazor.Wasm. You cannot have both packages installed and configured in the same project so you need to stick to one of them for Blazor (United) Apps. Since the Elmah.Io.Extensions.Logging package doesn't work with Blazor WebAssembly, we recommend installing the Elmah.Io.Blazor.Wasm package if you want to log from both server-side and client-side. Once the new Blazor App framework matures, we will probably consolidate features from both packages into an Elmah.Io.Blazor package or similar.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-blogengine-net/index.html b/logging-to-elmah-io-from-blogengine-net/index.html new file mode 100644 index 0000000000..fe42c23500 --- /dev/null +++ b/logging-to-elmah-io-from-blogengine-net/index.html @@ -0,0 +1,675 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from BlogEngine.NET + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from BlogEngine.NET

    +

    Because BlogEngine.NET is written in ASP.NET, it doesn't need any custom code to use ELMAH and elmah.io. ELMAH works out of the box for most web frameworks by Microsoft. If you are building and deploying the code yourself, installing elmah.io is achieved using our NuGet package:

    +
    Install-Package Elmah.Io
    +
    dotnet add package Elmah.Io
    +
    <PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add Elmah.Io
    +
    +

    During the installation, you will be asked for your API key (Where is my API key?) and log ID (Where is my log ID?).

    +

    When installed, BlogEngine.NET starts reporting errors to elmah.io. To check it out, force an internal server error or similar, and visit /elmah.axd or the search area of your log at elmah.io.

    +

    Some of you may use the BlogEngine.NET binaries or even installed it using a one-click installer. In this case you will need to add elmah.io manually. To do that, use a tool like NuGet Package Explorer to download the most recent versions of ELMAH and elmah.io. Copy Elmah.dll and Elmah.Io.dll to the bin directory of your BlogEngine.NET installation. Also modify your web.config to include the ELMAH config as shown in the config example. Last but not least, remember to add the elmah.io error logger configuration as a child node to the <elmah> element:

    +

    <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="API_KEY" logId="LOG_ID" />
    +

    +

    Where API_KEY is your API key and LOG_ID is your log ID.

    +

    To wrap this up, you may have noticed that there's a NuGet package to bring ELMAH support into BlogEngine.NET. This package adds the ELMAH assembly and config as well as adds a nice BlogEngine.NET compliant URL for browsing errors. Feel free to use this package, but remember to add it after the elmah.io package. Also, make sure to clean up the dual error log configuration:

    +

    <elmah>
    +  <security allowRemoteAccess="false" />
    +  <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="APIKEY" logId="LOGID" />
    +  <security allowRemoteAccess="true" />
    +  <errorLog type="Elmah.SqlServerCompactErrorLog, Elmah" connectionStringName="elmah-sqlservercompact" />
    +</elmah>
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-console-application/index.html b/logging-to-elmah-io-from-console-application/index.html new file mode 100644 index 0000000000..a5facbe261 --- /dev/null +++ b/logging-to-elmah-io-from-console-application/index.html @@ -0,0 +1,861 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from C# and console applications + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from C# and console applications

    + +

    If you need to log to elmah.io and you cannot use one of the integrations we provide, logging through the Elmah.Io.Client NuGet package is dead simple.

    +

    To start logging, install the Elmah.Io.Client package:

    +
    Install-Package Elmah.Io.Client
    +
    dotnet add package Elmah.Io.Client
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +
    +

    Create a new ElmahioAPI:

    +

    var logger = ElmahioAPI.Create("API_KEY");
    +

    +

    Replace API_KEY with your API key (Where is my API key?).

    +

    The elmah.io client supports logging in different log levels much like other logging frameworks for .NET:

    +

    var logId = new Guid("LOG_ID");
    +logger.Messages.Fatal(logId, new ApplicationException("A fatal exception"), "Fatal message");
    +logger.Messages.Error(logId, new ApplicationException("An exception"), "Error message");
    +logger.Messages.Warning(logId, "A warning");
    +logger.Messages.Information(logId, "An info message");
    +logger.Messages.Debug(logId, "A debug message");
    +logger.Messages.Verbose(logId, "A verbose message");
    +

    +

    Replace LOG_ID with your log ID from elmah.io (Where is my log ID?).

    +

    To have 100% control of how the message is logged to elmah.io, you can use the CreateAndNotify-method:

    +

    logger.Messages.CreateAndNotify(logId, new CreateMessage
    +{
    +    Title = "Hello World",
    +    Application = "Elmah.Io.Client sample",
    +    Detail = "This is a long description of the error. Maybe even a stack trace",
    +    Severity = Severity.Error.ToString(),
    +    Data = new List<Item>
    +    {
    +        new Item {Key = "Username", Value = "Man in black"}
    +    },
    +    Form = new List<Item>
    +    {
    +        new Item {Key = "Password", Value = "SecretPassword"},
    +        new Item {Key = "pwd", Value = "Other secret value"},
    +        new Item {Key = "visible form item", Value = "With a value"}
    +    }
    +});
    +

    +

    Structured logging

    +

    Like the integrations for Serilog, NLog and, Microsoft.Extensions.Logging, the elmah.io client supports structured logging:

    +

    logger.Messages.CreateAndNotify(logId, new CreateMessage
    +{
    +    Title = "Thomas says Hello",
    +    TitleTemplate = "{User} says Hello",
    +});
    +

    + +

    You can log one or more breadcrumbs as part of a log message. Breadcrumbs indicate steps happening just before a log message (typically an error). Breadcrumbs are supported through the Breadcrumbs property on the CreateMessage class:

    +

    logger.Messages.CreateAndNotify(logId, new CreateMessage
    +{
    +    Title = "Oh no, an error happened",
    +    Severity = "Error",
    +    Breadcrumbs = new List<Breadcrumb>
    +    {
    +        new Breadcrumb
    +        {
    +            DateTime = DateTime.UtcNow.AddSeconds(-10),
    +            Action = "Navigation",
    +            Message = "Navigate from / to /signin",
    +            Severity = "Information"
    +        },
    +        new Breadcrumb
    +        {
    +            DateTime = DateTime.UtcNow.AddSeconds(-3),
    +            Action = "Click",
    +            Message = "#submit",
    +            Severity = "Information"
    +        },
    +        new Breadcrumb
    +        {
    +            DateTime = DateTime.UtcNow.AddSeconds(-2),
    +            Action = "Submit",
    +            Message = "#loginform",
    +            Severity = "Information"
    +        },
    +        new Breadcrumb
    +        {
    +            DateTime = DateTime.UtcNow.AddSeconds(-1),
    +            Action = "Request",
    +            Message = "/save",
    +            Severity = "Information"
    +        }
    +    }
    +});
    +

    +

    Breadcrumbs will be ordered by the DateTime field on the elmah.io API why the order you add them to the Breadcrumbs property isn't that important. Be aware that only the 10 most recent breadcrumbs and breadcrumbs with a date less than or equal to the logged message are stored.

    +

    In the example above, only Information breadcrumbs are added. The Severity property accepts the same severities as on the log message itself.

    +

    Events

    +

    The elmah.io client supports two different events: OnMessage and OnMessageFail.

    +

    OnMessage

    +

    To get a callback every time a new message is being logged to elmah.io, you can implement the OnMessage event. This is a great chance to decorate all log messages with a specific property or similar.

    +

    logger.Messages.OnMessage += (sender, eventArgs) =>
    +{
    +    eventArgs.Message.Version = "1.0.0";
    +};
    +

    +

    Include source code

    +

    You can use the OnMessage event to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it.

    +

    There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage event handler:

    +

    logger.Messages.OnMessage += (sender, eventArgs) =>
    +{
    +    eventArgs.Message.WithSourceCodeFromPdb();
    +};
    +

    +

    Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.

    +
    +

    Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.

    +
    +

    OnMessageFail

    +

    Logging to elmah.io can fail if the network connection is down, if elmah.io experiences downtime, or something third. To make sure you log an error elsewhere if this happens, you can implement the OnMessageFail event:

    +

    logger.Messages.OnMessageFail += (sender, eventArgs) =>
    +{
    +    System.Console.Error.WriteLine("Error when logging to elmah.io");
    +};
    +

    +

    Bulk upload

    +

    If logging many messages to elmah.io, bulk upload can be a way to optimize performance. The elmah.io client supports bulk upload using the CreateBulkAndNotify-method:

    +

    logger.Messages.CreateBulkAndNotify(logId, new[]
    +{
    +    new CreateMessage { Title = "This is a bulk message" },
    +    new CreateMessage { Title = "This is another bulk message" },
    +}.ToList());
    +

    +

    Options

    +

    The elmah.io client contains a set of default options that you can override.

    +

    Proxy

    +

    To log through a HTTP proxy, set the WebProxy property:

    +

    var logger = ElmahioAPI.Create("API_KEY", new ElmahIoOptions
    +{
    +    WebProxy = new WebProxy("localhost", 8888)
    +});
    +

    +

    A proxy needs to be specified as part of the options sent to the ElmahioAPI.Create method to make sure that the underlying HttpClient is properly initialized.

    +

    Obfuscate form values

    +

    When logging POSTs with form values, you don't want users' passwords and similar logged to elmah.io. The elmah.io client automatically filters form keys named password and pwd. Using the FormKeysToObfuscate you can tell the client to obfuscate additional form entries:

    +

    var logger = ElmahioAPI.Create("API_KEY");
    +logger.Options.FormKeysToObfuscate.Add("secret_key");
    +

    +

    Full example

    +

    Here's a full example of how to catch all exceptions in a console application and log as many information as possible to elmah.io:

    +

    class Program
    +{
    +    private static IElmahioAPI elmahIo;
    +
    +    static void Main(string[] args)
    +    {
    +        try
    +        {
    +            AppDomain.CurrentDomain.UnhandledException +=
    +                (sender, e) => LogException(e.ExceptionObject as Exception);
    +
    +            // Run some code
    +        }
    +        catch (Exception e)
    +        {
    +            LogException(e);
    +        }
    +    }
    +
    +    private static void LogException(Exception e)
    +    {
    +        if (elmahIo == null) elmahIo = ElmahioAPI.Create("API_KEY");
    +
    +        var baseException = e?.GetBaseException();
    +
    +        elmahIo.Messages.CreateAndNotify(new Guid("LOG_ID"), new CreateMessage
    +        {
    +            Data = e?.ToDataList(),
    +            Detail = e?.ToString(),
    +            Hostname = Environment.MachineName,
    +            Severity = Severity.Error.ToString(),
    +            Source = baseException?.Source,
    +            Title = baseException?.Message ?? "An error happened",
    +            Type = baseException?.GetType().FullName,
    +            User = Environment.UserName,
    +        });
    +    }
    +}
    +

    +

    The code will catch all exceptions both from the catch block and exceptions reported through the UnhandledException event.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-corewcf/index.html b/logging-to-elmah-io-from-corewcf/index.html new file mode 100644 index 0000000000..ee0579091b --- /dev/null +++ b/logging-to-elmah-io-from-corewcf/index.html @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from CoreWCF + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from CoreWCF

    +

    elmah.io supports CoreWCF using our integration with Microsoft.Extensions.Logging. Start by installing the Elmah.Io.Extensions.Logging NuGet package:

    +
    Install-Package Elmah.Io.Extensions.Logging
    +
    dotnet add package Elmah.Io.Extensions.Logging
    +
    <PackageReference Include="Elmah.Io.Extensions.Logging" Version="5.*" />
    +
    paket add Elmah.Io.Extensions.Logging
    +
    +

    Configure logging as part of the configuration (typically in the Program.cs file):

    +

    builder.Logging.AddElmahIo(options =>
    +{
    +    options.ApiKey = "API_KEY";
    +    options.LogId = new Guid("LOG_ID");
    +});
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the id of the log (Where is my log ID?) where you want messages logged.

    +

    CoreWCF will now send all messages logged from your application to elmah.io. CoreWCF doesn't log uncaught exceptions happening in WCF services to Microsoft.Extensions.Logging as you'd expect if coming from ASP.NET Core. To do this, you will need to add a custom error logger by including the following class:

    +

    public class ElmahIoErrorHandler : IErrorHandler
    +{
    +    private readonly ILogger<ElmahIoErrorHandler> logger;
    +
    +    public ElmahIoErrorHandler(ILogger<ElmahIoErrorHandler> logger)
    +    {
    +        this.logger = logger;
    +    }
    +
    +    public bool HandleError(Exception error) => false;
    +
    +    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    +    {
    +        if (error == null) return;
    +
    +        logger.LogError(error, error.Message);
    +    }
    +}
    +

    +

    The ElmahIoErrorHandler class will be called by CoreWCF when exceptions are thrown and log the to the configure ILogger. To invoke the error handler, add the following service behavior:

    +

    public class ElmahIoErrorBehavior : IServiceBehavior
    +{
    +    private readonly ILogger<ElmahIoErrorHandler> logger;
    +
    +    public ElmahIoErrorBehavior(ILogger<ElmahIoErrorHandler> logger)
    +    {
    +        this.logger = logger;
    +    }
    +
    +    public void Validate(ServiceDescription description, ServiceHostBase serviceHostBase)
    +    {
    +    }
    +
    +    public void AddBindingParameters(ServiceDescription description, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters)
    +    {
    +    }
    +
    +    public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
    +    {
    +        var errorHandler = (IErrorHandler)Activator.CreateInstance(typeof(ElmahIoErrorHandler), logger);
    +
    +        foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers)
    +        {
    +            ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher;
    +            channelDispatcher.ErrorHandlers.Add(errorHandler);
    +        }
    +    }
    +}
    +

    +

    The service behavior will look up the ElmahIoErrorHandler and register it with CoreWCF. The code above is hardcoded to work with the elmah.io error handler only. If you have multiple error handlers, you will need to register all of them.

    +

    Finally, register the service behavior in the Program.cs file:

    +

    builder.Services.AddSingleton<IServiceBehavior, ElmahIoErrorBehavior>();
    +

    +

    Uncaught errors will now be logged to elmah.io.

    +

    All of the settings from Elmah.Io.Extensions.Logging not mentioned on this page work with CoreWCF. Check out Logging to elmah.io from Microsoft.Extensions.Logging for details.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-devexpress/index.html b/logging-to-elmah-io-from-devexpress/index.html new file mode 100644 index 0000000000..1f1c6560f5 --- /dev/null +++ b/logging-to-elmah-io-from-devexpress/index.html @@ -0,0 +1,671 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from DevExpress (eXpressApp Framework) + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from DevExpress (eXpressApp Framework)

    +

    eXpressApp Framework (XAF) is built on top of ASP.NET. Installing elmah.io corresponds to any other ASP.NET site:

    +
    Install-Package Elmah.Io
    +
    dotnet add package Elmah.Io
    +
    <PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add Elmah.Io
    +
    +

    During the installation, you will be asked for your API key (Where is my API key?) and log ID (Where is my log ID?).

    +

    To verify the integration, throw a new exception in Default.aspx or similar:

    +

    <body class="VerticalTemplate">
    +    <% throw new Exception("Test exception"); %>
    +    <form id="form2" runat="server">
    +        <!-- ... -->
    +    </form>
    +</body>
    +

    +

    Launch the project and see the test exception flow into elmah.io.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-elmah/index.html b/logging-to-elmah-io-from-elmah/index.html new file mode 100644 index 0000000000..ba5420ed8d --- /dev/null +++ b/logging-to-elmah-io-from-elmah/index.html @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from ASP.NET / WebForms + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from ASP.NET / WebForms

    +

    This article will explain the steps necessary to log errors from your web application into elmah.io. We also offer more specific guides on ASP.NET MVC, Web API, and a lot of other web- and logging-frameworks. Read through this tutorial and head over to a tutorial specific for your choice of framework afterwards.

    +

    This guide is also available as a short video tutorial here:

    +

    + elmah.io Introduction - Installation + +

    +

    Create a new ASP.NET Web Application in Visual Studio :

    +

    Create ASP.NET Web Application

    +

    Select a project template of your choice:

    +

    Select Project Template

    +

    Navigate to elmah.io and login using username/password or your favorite social provider. When logged in, elmah.io redirects you to the dashboard. If you just signed up, you will be guided through the process of creating an organization and a log.

    +

    When the log has been created, elmah.io shows you the install instructions. If you are currently on the dashboard, click the gears icon on the lower right corner of the log box. Don't pay too much attention to the install steps, because the rest of this tutorial will guide you through the installation. Keep the page open in order to copy your API key and log ID at a later step:

    +

    Copy your log id

    +

    Navigate back to your web project, right click References and select Manage NuGet Packages:

    +

    Open Manage NuGet Packages

    +

    In the NuGet dialog, search for elmah.io:

    +

    Search for elmah.io

    +

    Select the elmah.io package and click Install. Input your API key and log ID in the dialog appearing during installation of the NuGet package:

    +

    Insert your log id

    +

    You're ready to rock and roll. Either add throw new Exception("Test"); somewhere or hit F5 and input a URL you know doesn't exist (like http://localhost:64987/notfound). To verify that the installation of elmah.io is successful, navigate back to the elmah.io dashboard and select the Search tab of your newly created log:

    +

    Error Details

    +

    Congrats! Every error on your application is now logged to elmah.io.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-entity-framework-core/index.html b/logging-to-elmah-io-from-entity-framework-core/index.html new file mode 100644 index 0000000000..7ee6d6a2ab --- /dev/null +++ b/logging-to-elmah-io-from-entity-framework-core/index.html @@ -0,0 +1,679 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Entity Framework Core + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Entity Framework Core

    +

    Both elmah.io and Entity Framework Core supports logging through Microsoft.Extensions.Logging. To log all errors happening inside Entity Framework Core, install the Elmah.Io.Extensions.Logging NuGet package:

    +
    Install-Package Elmah.Io.Extensions.Logging
    +
    dotnet add package Elmah.Io.Extensions.Logging
    +
    <PackageReference Include="Elmah.Io.Extensions.Logging" Version="5.*" />
    +
    paket add Elmah.Io.Extensions.Logging
    +
    +

    Then add elmah.io to a new or existing LoggerFactory:

    +

    var loggerFactory = new LoggerFactory()
    +    .AddElmahIo("API_KEY", new Guid("LOG_ID"));
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the log ID (Where is my log ID?) that should receive errors from Entity Framework.

    +
    +

    When using Entity Framework Core from ASP.NET Core, you never create a LoggerFactory. Factories are provided through DI by ASP.NET Core. Check out this sample for details.

    +
    +

    Finally, enable logging in Entity Framework Core:

    +

    optionsBuilder
    +    .UseLoggerFactory(loggerFactory)
    +    .UseSqlServer(/*...*/);
    +

    +

    (UseSqlServer included for illustration purposes only - elmah.io works with any provider)

    +

    That's it! All errors happening in Entity Framework Core, are now logged in elmah.io.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-express/index.html b/logging-to-elmah-io-from-express/index.html new file mode 100644 index 0000000000..eef9278fd3 --- /dev/null +++ b/logging-to-elmah-io-from-express/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/logging-to-elmah-io-from-google-cloud-functions/index.html b/logging-to-elmah-io-from-google-cloud-functions/index.html new file mode 100644 index 0000000000..397f5d3eb0 --- /dev/null +++ b/logging-to-elmah-io-from-google-cloud-functions/index.html @@ -0,0 +1,704 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Google Cloud Functions + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from Google Cloud Functions

    +

    Logging to elmah.io from Google Cloud Functions uses our integration with Microsoft.Extensions.Logging. To start logging, install the Elmah.Io.Extensions.Logging NuGet package through the Cloud Shell or locally:

    +
    Install-Package Elmah.Io.Extensions.Logging
    +
    dotnet add package Elmah.Io.Extensions.Logging
    +
    <PackageReference Include="Elmah.Io.Extensions.Logging" Version="5.*" />
    +
    paket add Elmah.Io.Extensions.Logging
    +
    +

    In the root of your Function app create a new file named Startup.cs:

    +

    public class Startup : FunctionsStartup
    +{
    +    public override void ConfigureServices(WebHostBuilderContext ctx, IServiceCollection services)
    +    {
    +        services.AddLogging(logging =>
    +        {
    +            logging.AddElmahIo(o =>
    +            {
    +                o.ApiKey = "API_KEY";
    +                o.LogId = new Guid("LOG_ID");
    +            });
    +            logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning);
    +        });
    +    }
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the ID of the log to store messages in (Where is my log ID?).

    +

    The filter tells Google Cloud Functions to log warnings and above to elmah.io only. If you want to log detailed information about what goes on inside Google Cloud Functions, you can lower the log level.

    +

    Decorate your function with a FunctionsStartup attribute:

    +

    [FunctionsStartup(typeof(Startup))]    
    +public class Function : IHttpFunction
    +{
    +    // ...
    +}
    +

    +

    All uncaught exceptions happening in your function as well as log messages sent from Google Cloud Functions are now stored in elmah.io.

    +

    To log messages manually, you can inject an ILogger in your function:

    +

    public class Function : IHttpFunction
    +{
    +    private ILogger<Function> _logger;
    +
    +    public Function(ILogger<Function> logger)
    +    {
    +        _logger = logger;
    +    }
    +
    +    // ...
    +}
    +

    +

    Then log messages using the injected logger:

    +

    _logger.LogWarning("Your log message goes here");
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-isolated-azure-functions/index.html b/logging-to-elmah-io-from-isolated-azure-functions/index.html new file mode 100644 index 0000000000..2865282ca8 --- /dev/null +++ b/logging-to-elmah-io-from-isolated-azure-functions/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Isolated Azure Functions + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Isolated Azure Functions

    + +

    Logging errors from Isolated Azure Functions requires only a few lines of code. We've created clients specifically for Isolated Azure Functions. If your are looking for logging from Azure Functions (in process) check out Logging to elmah.io from Azure Functions.

    +

    Install the Elmah.Io.Functions.Isolated package in your project to get started:

    +
    Install-Package Elmah.Io.Functions.Isolated
    +
    dotnet add package Elmah.Io.Functions.Isolated
    +
    <PackageReference Include="Elmah.Io.Functions.Isolated" Version="5.*" />
    +
    paket add Elmah.Io.Functions.Isolated
    +
    +

    Next, call the AddElmahIo method inside ConfigureFunctionsWorkerDefaults:

    +

    .ConfigureFunctionsWorkerDefaults((context, app) =>
    +{
    +    app.AddElmahIo(options =>
    +    {
    +        options.ApiKey = "API_KEY";
    +        options.LogId = new Guid("LOG_ID");
    +    });
    +})
    +

    +

    Also, include a using of the Elmah.Io.Functions.Isolated namespace. elmah.io now automatically identifies any uncaught exceptions and logs them to the specified log. Check out the samples for more ways to configure elmah.io.

    +

    Application name

    +

    To set the application name on all errors, set the Application property:

    +

    app.AddElmahIo(options =>
    +{
    +    // ...
    +    options.Application = "MyFunction";
    +});
    +

    +

    Message hooks

    +

    Elmah.Io.Functions.Isolated provide message hooks similar to the integrations with ASP.NET and ASP.NET Core.

    +

    Decorating log messages

    +

    To include additional information on log messages, you can use the OnMessage action:

    +

    app.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnMessage = msg =>
    +    {
    +        msg.Version = "1.0.0";
    +    };
    +});
    +

    +

    The example above includes a version number on all errors.

    +

    Include source code

    +

    You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it.

    +

    There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action:

    +

    app.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnMessage = msg =>
    +    {
    +        msg.WithSourceCodeFromPdb();
    +    };
    +});
    +

    +

    Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.

    +

    Handle errors

    +

    To handle any errors happening while processing a log message, you can use the OnError action:

    +

    app.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnError = (msg, ex) =>
    +    {
    +        logger.LogError(ex, ex.Message);
    +    };
    +});
    +

    +

    The example above logs any errors during communication with elmah.io to a local log.

    +

    Error filtering

    +

    To ignore specific errors based on their content, you can use the OnFilter action:

    +

    app.AddElmahIo(options =>
    +{
    +    // ...
    +    options.OnFilter = msg =>
    +    {
    +        return msg.Method == "GET";
    +    };
    +});
    +

    +

    The example above ignores any errors generated during an HTTP GET request.

    +

    Logging through ILogger

    +

    Isolated Azure Functions can log through Microsoft.Extensions.Logging (MEL) too. When configuring your Function app to log through MEL, custom messages can be logged through the ILogger interface. Furthermore, you will get detailed log messages from within the Function host. To set this up, install the Elmah.Io.Extensions.Logging NuGet package:

    +
    Install-Package Elmah.Io.Extensions.Logging
    +
    dotnet add package Elmah.Io.Extensions.Logging
    +
    <PackageReference Include="Elmah.Io.Extensions.Logging" Version="5.*" />
    +
    paket add Elmah.Io.Extensions.Logging
    +
    +

    Then extend your Program.cs file like this:

    +

    var host = new HostBuilder()
    +    // ...
    +    .ConfigureLogging(logging =>
    +    {
    +        logging.AddElmahIo(options =>
    +        {
    +            options.ApiKey = "API_KEY";
    +            options.LogId = new Guid("LOG_ID");
    +        });
    +        logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning);
    +    })
    +    // ...
    +    .Build();
    +

    +

    In the example, only warning messages and above are logged to elmah.io. You can remove the filter or set another log level if you want to log more. Jump to Log filtering to learn how to configure filters from config.

    +

    Either pass an ILogger to your function method:

    +

    public class MyFunction
    +{
    +    public static void Run([TimerTrigger("...")]TimerInfo myTimer, ILogger<MyFunction> logger)
    +    {
    +        logger.LogWarning("This is a warning");
    +    }
    +}
    +

    +

    Or inject an ILoggerFactory and create a logger as part of the constructor:

    +

    public class MyFunction
    +{
    +    private readonly ILogger<MyFunction> logger;
    +
    +    public Function1(ILoggerFactory loggerFactory)
    +    {
    +        this.logger = loggerFactory.CreateLogger<MyFunction>();
    +    }
    +
    +    public void Run([TimerTrigger("...")]TimerInfo myTimer)
    +    {
    +        logger.LogWarning("This is a warning");
    +    }
    +}
    +

    +

    Log filtering

    +

    The code above filters out all log messages with a severity lower than Warning. You can use all of the log filtering capabilities of Microsoft.Extensions.Logging to enable and disable various log levels from multiple categories. A common requirement is to only log Warning and more severe originating from the Azure Functions runtime, but log Information messages from your function code. This can be enabled through a custom category:

    +

    public class MyFunction
    +{
    +    public void Run([TimerTrigger("...")]TimerInfo myTimer, ILogger<MyFunction> logger)
    +    {
    +        logger.LogInformation("This is an information message");
    +    }
    +}
    +

    +

    The MyFunction category will need configuration in either C# or in the host.json file:

    +

    {
    +  // ...
    +  "logging": {
    +    "logLevel": {
    +      "default": "Warning",
    +      "MyFunction": "Information"
    +    }
    +  }
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-javascript/index.html b/logging-to-elmah-io-from-javascript/index.html new file mode 100644 index 0000000000..54c5193f2a --- /dev/null +++ b/logging-to-elmah-io-from-javascript/index.html @@ -0,0 +1,974 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from JavaScript + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    NuGet +npm +Samples

    +

    Logging to elmah.io from JavaScript

    + +

    elmah.io doesn't only support server-side .NET logging. We also log JavaScript errors happening on your website. Logging client-side errors require nothing more than installing the elmahio.js script on your website.

    +
    +

    Organizations created end 2023 and forward will have an API key named JavaScript automatically generated. Remember to either use this or generate a new API key with messages_write permission only. This makes it easy to revoke the API key if someone starts sending messages to your log with your key.

    +
    +

    elmahio.js supports all modern browsers like Chrome, Edge, Firefox, and Safari. Internet Explorer 10 and 11 are supported too, but because of internal dependencies on the stacktrace-gps library, nothing older than IE10 is supported.

    +

    If you want to see elmahio.js in action before installing it on your site, feel free to play on the Playground.

    +

    Installation

    +

    Pick an installation method of your choice:

    + + +
    +
    +

    Download the latest release as a zip: https://github.com/elmahio/elmah.io.javascript/releases

    +

    Unpack and copy elmahio.min.js to the Scripts folder or whatever folder you use to store JavaScript files.

    +

    Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site:

    +

    <script src="~/Scripts/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID" type="text/javascript"></script>
    +

    +
    +
    +

    Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site:

    +

    <script src="https://cdn.jsdelivr.net/gh/elmahio/elmah.io.javascript@latest/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID" type="text/javascript"></script>
    +

    +
    +
    +

    Install the elmah.io.javascript npm package:

    +

    npm install elmah.io.javascript
    +

    +

    Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site:

    +

    <script src="~/node_modules/elmah.io.javascript/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID" type="text/javascript"></script>
    +

    +
    +
    +

    Since Bower is no longer maintained, installing elmah.io.javascript through Bower, is supported using bower-npm-resolver. Install the resolver:

    +

    npm install bower-npm-resolver --save
    +

    +

    Add the resolver in your .bowerrc file:

    +

    {
    +  "resolvers": [
    +    "bower-npm-resolver"
    +  ]
    +}
    +

    +

    Install the elmah.io.javascript npm package:

    +

    bower install npm:elmah.io.javascript --save
    +

    +

    Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site:

    +

    <script src="~/bower_components/elmah.io.javascript/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID" type="text/javascript"></script>
    +

    +
    +
    +

    Add the elmah.io.javascript library in your libman.json file:

    +

    {
    +  // ...
    +  "libraries": [
    +    // ...
    +    {
    +      "provider": "filesystem",
    +      "library": "https://raw.githubusercontent.com/elmahio/elmah.io.javascript/3.7.1/dist/elmahio.min.js",
    +      "destination": "wwwroot/lib/elmahio"
    +    }
    +  ]
    +}
    +

    +

    or using the LibMan CLI:

    +

    libman install https://raw.githubusercontent.com/elmahio/elmah.io.javascript/3.7.1/dist/elmahio.min.js --provider filesystem --destination wwwroot\lib\elmahio
    +

    +

    Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site:

    +

    <script src="~/lib/elmahio/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID" type="text/javascript"></script>
    +

    +
    +
    +

    Install the elmah.io.javascript NuGet package:

    +
    Install-Package elmah.io.javascript
    +
    dotnet add package elmah.io.javascript
    +
    <PackageReference Include="elmah.io.javascript" Version="4.*" />
    +
    paket add elmah.io.javascript
    +
    +

    Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site:

    +

    <script src="~/Scripts/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID" type="text/javascript"></script>
    +

    +
    +
    +

    If not already configured, follow the guide installing elmah.io in ASP.NET Core.

    +

    Install the Elmah.Io.AspNetCore.TagHelpers NuGet package:

    +
    Install-Package Elmah.Io.AspNetCore.TagHelpers
    +
    dotnet add package Elmah.Io.AspNetCore.TagHelpers
    +
    <PackageReference Include="Elmah.Io.AspNetCore.TagHelpers" Version="5.*" />
    +
    paket add Elmah.Io.AspNetCore.TagHelpers
    +
    +

    Copy and paste the following line to the top of the _Layout.cshtml file:

    +

    @addTagHelper *, Elmah.Io.AspNetCore.TagHelpers
    +

    +

    In the bottom of the file (but before referencing other JavaScript files), add the following tag helper:

    +

    <elmah-io/>
    +

    +

    If you want to log JavaScript errors from production only, make sure to move the elmah-io element inside the tag <environment exclude="Development">.

    +

    elmah.io automatically pulls your API key and log ID from the options specified as part of the installation for logging serverside errors from ASP.NET Core.

    +
    +
    +

    That's it. All uncaught errors on your website, are now logged to elmah.io.

    +

    Options

    +

    If you prefer configuring in code (or need to access the options for something else), API key and log ID can be configured by referencing the elmahio.min.js script without parameters:

    +

    <script src="~/scripts/elmahio.min.js" type="text/javascript"></script>
    +

    +

    Then initialize the logger in JavaScript:

    +

    new Elmahio({
    +    apiKey: 'API_KEY',
    +    logId: 'LOG_ID'
    +});
    +

    +
    Application name
    +

    The application property on elmah.io can be set on all log messages by setting the application option:

    +

    new Elmahio({
    +    apiKey: 'API_KEY',
    +    logId: 'LOG_ID',
    +    application: 'My application name'
    +});
    +

    +
    Debug output
    +

    For debug purposes, debug output from the logger to the console can be enabled using the debug option:

    +

    new Elmahio({
    +    apiKey: 'API_KEY',
    +    logId: 'LOG_ID',
    +    debug: true
    +});
    +

    +
    Message filtering
    +

    Log messages can be filtered, by adding a filter handler in options:

    +

    new Elmahio({
    +    // ...
    +    filter: function(msg) {
    +        return msg.severity === 'Verbose';
    +    }
    +});
    +

    +

    In the example, all log messages with a severity of Verbose, are not logged to elmah.io.

    +

    Events

    +
    Enriching log messages
    +

    Log messages can be enriched by subscribing to the message event:

    +

    new Elmahio({
    +    // ...
    +}).on('message', function(msg) {
    +    if (!msg.data) msg.data = [];
    +    msg.data.push({key: 'MyCustomKey', value: 'MyCustomValue'});
    +});
    +

    +

    In the example, all log messages are enriched with a data variable with the key MyCustomKey and value MyCustomValue.

    +
    Handling errors
    +

    To react to errors happening in elmah.io.javascript, subscribe to the error event:

    +

    new Elmahio({
    +    // ...
    +}).on('error', function(status, text) {
    +    console.log('An error happened in elmah.io.javascript', status, text);
    +});
    +

    +

    In the example, all errors are written to the console.

    + +

    Breadcrumbs can be used to decorate errors with events or actions happening just before logging the error. Breadcrumbs can be added manually:

    +

    logger.addBreadcrumb('User clicked button x', 'Information', 'click');
    +

    +

    You would want to enrich your code with a range of different breadcrumbs depending on important user actions in your application.

    +

    As default, a maximum of 10 breadcrumbs are stored in memory at all times. The list acts as first in first out, where adding a new breadcrumb to a full list will automatically remove the oldest breadcrumb in the list. The allowed number of breadcrumbs in the list can be changed using the breadcrumbsNumber option:

    +

    var logger = new Elmahio({
    +    // ...
    +    breadcrumbsNumber: 15
    +});
    +

    +

    This will store a maximum of 15 breadcrumbs. Currently, we allow 25 as the highest possible value.

    +

    elmah.io.javascript can also be configured to automatically generate breadcrumbs from important actions like click events and xhr:

    +

    var logger = new Elmahio({
    +    // ...
    +    breadcrumbs: true
    +});
    +

    +

    We are planning to enable automatic breadcrumbs in the future but for now, it's an opt-in feature. Automatic breadcrumbs will be included in the same list as manually added breadcrumbs why the breadcrumbsNumber option is still valid.

    +

    Logging manually

    +

    You may want to log errors manually or even log information messages from JavaScript. To do so, Elmahio is a logging framework too:

    +

    var logger = new Elmahio({
    +    apiKey: 'API_KEY',
    +    logId: 'LOG_ID'
    +});
    +
    +logger.verbose('This is verbose');
    +logger.verbose('This is verbose', new Error('A JavaScript error object'));
    +
    +logger.debug('This is debug');
    +logger.debug('This is debug', new Error('A JavaScript error object'));
    +
    +logger.information('This is information');
    +logger.information('This is information', new Error('A JavaScript error object'));
    +
    +logger.warning('This is warning');
    +logger.warning('This is warning', new Error('A JavaScript error object'));
    +
    +logger.error('This is error');
    +logger.error('This is error', new Error('A JavaScript error object'));
    +
    +logger.fatal('This is fatal');
    +logger.fatal('This is fatal', new Error('A JavaScript error object'));
    +
    +logger.log({
    +  title: 'This is a custom log message',
    +  type: 'Of some type',
    +  severity: 'Error'
    +});
    +
    +var msg = logger.message(); // Get a prefilled message
    +msg.title = "This is a custom log message";
    +logger.log(msg);
    +
    +var msg = logger.message(new Error('A JavaScript error object')); // Get a prefilled message including error details
    +// Set custom variables. If not needed, simply use logger.error(...) instead.
    +logger.log(msg);
    +

    +

    The Error object used, should be a JavaScript Error object.

    +

    As for the log-function, check out message reference.

    +
    +

    Manual logging only works when initializing the elmah.io logger from code.

    +
    +

    Logging from console

    +

    If you don't like to share the Elmahio logger or you want to hook elmah.io logging up to existing code, you can capture log messages from console. To do so, set the captureConsoleMinimumLevel option:

    +

    var log = new Elmahio({
    +  apiKey: 'API_KEY',
    +  logId: 'LOG_ID',
    +  captureConsoleMinimumLevel: 'error'
    +});
    +
    +console.error('This is an %s message.', 'error');
    +

    +

    captureConsoleMinimumLevel can be set to one of the following values:

    +
      +
    • none: will disable logging from console (default).
    • +
    • debug: will capture logging from console.debug, console.info, console.warn, console.error.
    • +
    • info: will capture logging from console.info, console.warn, console.error.
    • +
    • warn: will capture logging from console.warn, console.error.
    • +
    • error: will capture logging from console.error.
    • +
    +
    +

    Capturing the console only works when initializing the elmah.io logger from code. Also, console.log is not captured.

    +
    +

    IntelliSense

    +

    If installing through npm or similar, Visual Studio should pick up the TypeScript mappings from the elmah.io.javascript package. If not, add the following line at the top of the JavaScript file where you want elmah.io.javascript IntelliSense:

    +

    /// <reference path="/path/to/elmahio.d.ts" />
    +

    +

    Message reference

    +

    This is an example of the elmah.io.javascript Message object that is used in various callbacks, etc.:

    +

    {
    +  title: 'this.remove is not a function',
    +  detail: 'TypeError: this.remove is not a function\n  at (undefined:12:14)',
    +  source: 'index.js',
    +  severity: 'Error',
    +  type: 'TypeError',
    +  url: '/current/page',
    +  method: 'GET',
    +  application: 'MyApp',
    +  hostname: 'WOPR',
    +  statusCode: 400,
    +  user: 'DavidLightman',
    +  version: '1.0.0',
    +  cookies: [
    +    { key: '_ga', value: 'GA1.3.1580453215.1783132008' }
    +  ],
    +  queryString: [
    +    { key: 'id', value: '42' }
    +  ],
    +  data: [
    +    { key: 'User-Language', value: 'en-US' },
    +    { key: 'Color-Depth', value: '24' }
    +  ],
    +  serverVariables: [
    +    { key: 'User-Agent', value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' }
    +  ],
    +  breadcrumbs: [
    +    { severity: 'Information', event: 'click', message: 'A user clicked' }
    +  ]
    +}
    +

    +

    For a complete definition, check out the Message interface in the elmah.io.javascript TypeScript mappings.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-jsnlog/index.html b/logging-to-elmah-io-from-jsnlog/index.html new file mode 100644 index 0000000000..38d58e9afa --- /dev/null +++ b/logging-to-elmah-io-from-jsnlog/index.html @@ -0,0 +1,695 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from JSNLog + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from JSNLog

    +
    +

    While logging through JSNLog still works, we recommend using our native integration with JavaScript: Logging to elmah.io from JavaScript

    +
    +

    Using JSNLog you will be able to log JavaScript errors to elmah.io. In this sample, we will focus on logging JavaScript errors from an ASP.NET MVC web application, but you can use JSNLog to log anything to elmah.io, so please check out their documentation.

    +

    Start by installing the JSNLog.Elmah package:

    +
    Install-Package JSNLog.Elmah
    +
    dotnet add package JSNLog.Elmah
    +
    <PackageReference Include="JSNLog.Elmah" Version="2.*" />
    +
    paket add JSNLog.Elmah
    +
    +

    This installs and setup JSNLog into your project, using ELMAH as an appender. Then, install Elmah.Io:

    +
    Install-Package Elmah.Io
    +
    dotnet add package Elmah.Io
    +
    <PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add Elmah.Io
    +
    +

    During the installation, you will be asked for your API key (Where is my API key?) and log ID (Where is my log ID?).

    +

    Add the JSNLog code before any script imports in your _Layout.cshtml file:

    +

    @Html.Raw(JSNLog.JavascriptLogging.Configure())
    +

    +

    You are ready to log errors from JavaScript to elmah.io. To test that everything is installed correctly, launch your web application and execute the following JavaScript using Chrome Developer Tools or similar:

    +

    JL().fatal("log message");
    +

    +

    Navigate to your log at elmah.io and observe the new error. As you can see, logging JavaScript errors is now extremely simple and can be built into any try-catch, jQuery fail handlers, and pretty much anywhere else. To log every JavaScript error, add the following to the bottom of the _Layout.cshtml file:

    +

    <script>
    +window.onerror = function (errorMsg, url, lineNumber, column, errorObj) {
    +
    +    // Send object with all data to server side log, using severity fatal,
    +    // from logger "onerrorLogger"
    +    JL("onerrorLogger").fatalException({
    +        "msg": "Exception!",
    +        "errorMsg": errorMsg, "url": url,
    +        "line number": lineNumber, "column": column
    +    }, errorObj);
    +
    +    // Tell browser to run its own error handler as well  
    +    return false;
    +}
    +</script>
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-log4net/index.html b/logging-to-elmah-io-from-log4net/index.html new file mode 100644 index 0000000000..a1ce2041d8 --- /dev/null +++ b/logging-to-elmah-io-from-log4net/index.html @@ -0,0 +1,889 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from log4net + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from log4net

    + +

    In this tutorial we'll add logging to elmah.io from a .NET application with log4net. Install the elmah.io appender:

    +
    Install-Package Elmah.Io.Log4Net
    +
    dotnet add package Elmah.Io.Log4Net
    +
    <PackageReference Include="Elmah.Io.Log4Net" Version="5.*" />
    +
    paket add Elmah.Io.Log4Net
    +
    +

    Add the following to your AssemblyInfo.cs file:

    +

    [assembly: log4net.Config.XmlConfigurator(Watch = true)]
    +

    +

    Add the following config section to your web/app.config file:

    +

    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
    +

    +

    Finally, add the log4net configuration element to web/app.config:

    +

    <log4net>
    +  <appender name="ElmahIoAppender" type="elmah.io.log4net.ElmahIoAppender, elmah.io.log4net">
    +    <logId value="LOG_ID" />
    +    <apiKey value="API_KEY" />
    +  </appender>
    +  <root>
    +    <level value="Info" />
    +    <appender-ref ref="ElmahIoAppender" />
    +  </root>
    +</log4net>
    +

    +

    That’s it! log4net is now configured and log messages to elmah.io. Remember to replace API_KEY(Where is my API key?) and LOG_ID (Where is my log ID?) with your actual log Id. To start logging, write your usual log4net log statements:

    +

    var log = log4net.LogManager.GetLogger(typeof(HomeController));
    +try
    +{
    +    log.Info("Trying something");
    +    throw new ApplicationException();
    +}
    +catch (ApplicationException ex)
    +{
    +    log.Error("Error happening", ex);
    +}
    +

    +

    Logging custom properties

    +

    log4net offers a feature called context properties. With context properties, you can log additional key/value pairs with every log message. The elmah.io appender for log4net, supports context properties as well. Context properties are handled like custom properties in the elmah.io UI.

    +

    Let's utilize two different hooks in log4net, to add context properties to elmah.io:

    +

    log4net.GlobalContext.Properties["ApplicationIdentifier"] = "MyCoolApp";
    +log4net.ThreadContext.Properties["ThreadId"] = Thread.CurrentThread.ManagedThreadId;
    +
    +log.Info("This is a message with custom properties");
    +

    +

    Basically, we set two custom properties on contextual classes provided by log4net. To read more about the choices in log4net, check out the log4net manual.

    +

    When looking up the log message in elmah.io, we see the context properties in the Data tab. Besides the two custom variables that we set through GlobalContext and ThreadContext, we see a couple of build-in properties in log4net, both prefixed with log4net:.

    +

    In addition, Elmah.Io.Log4Net provides a range of reserved property names, that can be used to fill in data in the correct fields on the elmah.io UI. Let's say you want to fill the User field:

    +

    var properties = new PropertiesDictionary();
    +properties["User"] = "Arnold Schwarzenegger";
    +log.Logger.Log(new LoggingEvent(new LoggingEventData
    +{
    +    Level = Level.Error,
    +    TimeStampUtc = DateTime.UtcNow,
    +    Properties = properties,
    +    Message = "Hasta la vista, baby",
    +}));
    +

    +

    This will fill in the value Arnold Schwarzenegger in the User field, as well as add a key/value pair to the Data tab on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage.

    +

    Setting category

    +

    elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to log4net's LoggerName field automatically when using Elmah.Io.Log4Net. The category field can be overwritten using one of the context features available in log4net:

    +

    log4net.ThreadContext.Properties["Category"] = "The category";
    +log.Info("This is an information message with custom category");
    +

    +

    Message hooks

    +

    Decorating log messages

    +

    In case you want to set one or more core properties on each elmah.io message logged, using message hooks may be a better solution. In that case you will need to add a bit of log4net magic. An example could be setting the Version property on all log messages. In the following code, we set a hard-coded version number on all log messages, but the value could come from assembly info, a text file, or similar:

    +

    Hierarchy hier = log4net.LogManager.GetRepository(Assembly.GetEntryAssembly()) as Hierarchy;
    +var elmahIoAppender = (ElmahIoAppender)(hier?.GetAppenders())
    +    .FirstOrDefault(appender => appender.Name
    +        .Equals("ElmahIoAppender", StringComparison.InvariantCultureIgnoreCase));
    +
    +elmahIoAppender.ActivateOptions();
    +elmahIoAppender.Client.Messages.OnMessage += (sender, a) =>
    +{
    +    a.Message.Version = "1.0.0";
    +};
    +

    +

    This rather ugly piece of code would go into an initalization block, depending on the project type. The code starts by getting the configured elmah.io appender (typically set up in web/app.config or log4net.config). With the appender, you can access the underlying elmah.io client and subscribe to the OnMessage event. This let you trigger a small piece of code, just before sending log messages to elmah.io. In this case, we set the Version property to 1.0.0. Remember to call the ActiveOptions method, to make sure that the Client property is initialized.

    +

    Include source code

    +

    You can use the OnMessage event to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it.

    +

    There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage event handler:

    +

    elmahIoAppender.Client.Messages.OnMessage += (sender, a) =>
    +{
    +    a.Message.WithSourceCodeFromPdb();
    +};
    +

    +

    Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.

    +
    +

    Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.

    +
    +

    Specify API key and log ID in appSettings

    +

    You may prefer storing the API key and log ID in the appSettings element over having the values embedded into the appender element. This can be the case for easy config transformation, overwriting values on Azure, or similar. log4net provides a feature named pattern strings to address just that:

    +

    <?xml version="1.0" encoding="utf-8"?>
    +<configuration>
    +  <configSections>
    +    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
    +  </configSections>
    +  <appSettings>
    +    <add key="logId" value="LOG_ID"/>
    +    <add key="apiKey" value="API_KEY"/>
    +  </appSettings>
    +  <log4net>
    +    <root>
    +      <level value="ALL" />
    +      <appender-ref ref="ElmahIoAppender" />
    +    </root>
    +    <appender name="ElmahIoAppender" type="elmah.io.log4net.ElmahIoAppender, elmah.io.log4net">
    +      <logId type="log4net.Util.PatternString" value="%appSetting{logId}" />
    +      <apiKey type="log4net.Util.PatternString" value="%appSetting{apiKey}" />
    +    </appender>
    +  </log4net>
    +</configuration>
    +

    +

    The logId and apiKey elements underneath the elmah.io appender have been extended to include type="log4net.Util.PatternString". This allows for complex patterns in the value attribute. In this example, I reference an app setting from its name, by adding a value of %appSetting{logId} where logId is a reference to the app setting key specified above.

    +

    ASP.NET Core

    +

    Like other logging frameworks, logging through log4net from ASP.NET Core is also supported. We have a sample to show you how to set it up. The required NuGet packages and configuration are documented in this section.

    +

    To start logging to elmah.io from Microsoft.Extensions.Logging (through log4net), install the Elmah.Io.Log4Net and Microsoft.Extensions.Logging.Log4Net.AspNetCore NuGet packages:

    +
    Install-Package Elmah.Io.Log4Net
    +Install-Package Microsoft.Extensions.Logging.Log4Net.AspNetCore
    +
    dotnet add package Elmah.Io.Log4Net
    +dotnet add package Microsoft.Extensions.Logging.Log4Net.AspNetCore
    +
    <PackageReference Include="Elmah.Io.Log4Net" Version="5.*" />
    +<PackageReference Include="Microsoft.Extensions.Logging.Log4Net.AspNetCore" Version="6.*" />
    +
    paket add Elmah.Io.Log4Net
    +paket add Microsoft.Extensions.Logging.Log4Net.AspNetCore
    +
    +

    The version of Microsoft.Extensions.Logging.Log4Net.AspNetCore should match the version of .NET you are targeting.

    +

    Include a log4net config file to the root of the project:

    +

    <?xml version="1.0" encoding="utf-8" ?>
    +<log4net>
    +  <root>
    +    <level value="WARN" />
    +    <appender-ref ref="ElmahIoAppender" />
    +    <appender-ref ref="ConsoleAppender" />
    +  </root>
    +  <appender name="ElmahIoAppender" type="elmah.io.log4net.ElmahIoAppender, elmah.io.log4net">
    +    <logId value="LOG_ID" />
    +    <apiKey value="API_KEY" />
    +    <!--<application value="My app" />-->
    +  </appender>
    +  <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
    +    <layout type="log4net.Layout.PatternLayout">
    +      <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
    +    </layout>
    +  </appender>
    +</log4net>
    +

    +

    In the Program.cs file, make sure to set up log4net:

    +

    builder.Logging.AddLog4Net();
    +

    +

    All internal logging from ASP.NET Core, as well as manual logging you create through the ILogger interface, now goes directly into elmah.io.

    +

    A common request is to include all of the HTTP contextual information you usually get logged when using a package like Elmah.Io.AspNetCore. We have developed a specialized NuGet package to include cookies, server variables, etc. when logging through log4net from ASP.NET Core. To set it up, install the Elmah.Io.AspNetCore.Log4Net NuGet package:

    +
    Install-Package Elmah.Io.AspNetCore.Log4Net
    +
    dotnet add package Elmah.Io.AspNetCore.Log4Net
    +
    <PackageReference Include="Elmah.Io.AspNetCore.Log4Net" Version="5.*" />
    +
    paket add Elmah.Io.AspNetCore.Log4Net
    +
    +

    Finally, make sure to call the UseElmahIoLog4Net method in the Program.cs file:

    +

    // ... Exception handling middleware
    +app.UseElmahIoLog4Net();
    +// ... UseMvc etc.
    +

    +

    Troubleshooting

    +

    Here are some things to try out if logging from log4net to elmah.io doesn't work:

    +
      +
    • Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation.
    • +
    • Make sure that you have the newest Elmah.Io.Log4Net and Elmah.Io.Client packages installed.
    • +
    • Make sure to include all of the configuration from the example above. That includes both the <root> and <appender> element.
    • +
    • Make sure that the API key is valid and allow the Messages | Write permission.
    • +
    • Make sure to include a valid log ID.
    • +
    • Make sure that you have sufficient log messages in your subscription and that you didn't disable logging to the log or include any ignore filters/rules.
    • +
    • Enable and inspect log4net's internal debug log by including the following code in your web/app.config file to reveal any exceptions happening inside the log4net engine room or one of the appenders:
    • +
    +

    <?xml version="1.0" encoding="utf-8"?>
    +<configuration>
    +  <appSettings>
    +    <add key="log4net.Internal.Debug" value="true"/>
    +  </appSettings>
    +  <system.diagnostics>
    +    <trace autoflush="true">
    +      <listeners>
    +        <add
    +          name="textWriterTraceListener"
    +          type="System.Diagnostics.TextWriterTraceListener"
    +          initializeData="C:\temp\log4net-debug.log" />
    +      </listeners>
    +    </trace>
    +  </system.diagnostics>
    +</configuration>
    +

    +

    System.IO.FileLoadException: Could not load file or assembly 'log4net ...

    +

    In case you get the following exception while trying to log messages to elmah.io:

    +

    System.IO.FileLoadException: Could not load file or assembly 'log4net, Version=2.0.8.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
    +

    +

    This indicates they either the log4net.dll file is missing or there's a problem with assembly bindings. You can include a binding redirect to the newest version of log4net by including the following code in your web/app.config file:

    +

    <?xml version="1.0" encoding="utf-8"?>
    +<configuration>
    +  <runtime>
    +    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    +      <dependentAssembly>
    +        <assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral"/>
    +        <bindingRedirect oldVersion="0.0.0.0-2.0.12.0" newVersion="2.0.12.0"/>
    +      </dependentAssembly>
    +    </assemblyBinding>
    +  </runtime>
    +</configuration>
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-logary/index.html b/logging-to-elmah-io-from-logary/index.html new file mode 100644 index 0000000000..fc2934c490 --- /dev/null +++ b/logging-to-elmah-io-from-logary/index.html @@ -0,0 +1,695 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Logary + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    NuGet

    +

    Logging to elmah.io from Logary

    +

    Logary is a semantic logging framework like Serilog and Microsoft Semantic Logging. Combining semantic logs with elmah.io is a perfect fit since elmah.io has been designed with semantics from the ground up.

    +

    In this tutorial, we’ll add Logary to a Console application, but the process is almost identical to other project types. Create a new console application and add the elmah.io target for Logary:

    +
    Install-Package Logary.Targets.ElmahIO
    +
    dotnet add package Logary.Targets.ElmahIO
    +
    <PackageReference Include="Logary.Targets.ElmahIO" Version="5.*" />
    +
    paket add Logary.Targets.ElmahIO
    +
    +

    Configuration in F#

    +

    Configure elmah.io just like you would any normal target:

    +

    withTargets [
    +  // ...
    +  ElmahIO.create { logId = Guid.Parse "LOG_ID"; apiKey = "API_KEY" } "elmah.io"
    +] >>
    +withRules [
    + // ...
    + Rule.createForTarget "elmah.io"
    +]
    +

    +

    where LOG_ID is the id of your log (Where is my log ID?).

    +

    Configuration in C#

    +

    Configuration in C# is just as easy:

    +

    .Target<ElmahIO.Builder>(
    +  "elmah.io",
    +  conf => conf.Target.SendTo(logId: "LOG_ID", apiKey: "API_KEY"))
    +

    +

    where API_KEY is your API key and LOG_ID is the ID of your log.

    +

    Logging

    +

    To start logging messages to elmah.io, you can use the following F# code:

    +

    let logger = logary.getLogger (PointName [| "Logary"; "Samples"; "main" |])
    +
    +Message.event Info "User logged in"
    +  |> Message.setField "userName" "haf"
    +  |> Logger.logSimple logger
    +
    +

    +

    or in C#:

    +

    var logger = logary.GetLogger("Logary.CSharpExample");
    +logger.LogEventFormat(LogLevel.Fatal, "Unhandled {exception}!", e);
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-maui/index.html b/logging-to-elmah-io-from-maui/index.html new file mode 100644 index 0000000000..a65ff09803 --- /dev/null +++ b/logging-to-elmah-io-from-maui/index.html @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from MAUI + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from MAUI

    +

    We just started looking into .NET MAUI. If you want to go for the stable choice, check out the integration with Xamarin (the NuGet package is also in prerelease but more stable than the integration described on this page).

    +

    A quick way to get started with logging from MAUI to elmah.io is by installing the Elmah.Io.Blazor.Wasm NuGet package:

    +
    Install-Package Elmah.Io.Blazor.Wasm -IncludePrerelease
    +
    dotnet add package Elmah.Io.Blazor.Wasm --prerelease
    +
    <PackageReference Include="Elmah.Io.Blazor.Wasm" Version="4.*" />
    +
    paket add Elmah.Io.Blazor.Wasm
    +
    +

    The name is, admittedly, misleading here. The Elmah.Io.Blazor.Wasm package contains a simplified version of the Elmah.Io.Extensions.Logging package that doesn't make use of the Elmah.Io.Client package to communicate with the elmah.io API.

    +

    Finally, configure the elmah.io logger in the MauiProgram.cs file:

    +

    public static MauiApp CreateMauiApp()
    +{
    +    var builder = MauiApp.CreateBuilder();
    +    builder
    +        .UseMauiApp<App>()
    +        // ...
    +        .Logging
    +            .AddElmahIo(options =>
    +            {
    +                options.ApiKey = "API_KEY";
    +                options.LogId = new Guid("LOG_ID");
    +            });
    +
    +    return builder.Build();
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the ID of the log you want messages sent to (Where is my log ID?).

    +

    The logging initialization code will log all exceptions logged through ILogger both manually and by MAUI itself. MAUI supports dependency injection of the ILogger like most other web and mobile frameworks built on top of .NET.

    +

    Some exceptions are not logged through Microsoft.Extensions.Logging why you need to handle these manually. If you have worked with Xamarin, you probably know about the various exceptions-related events (like UnhandledException). Which event you need to subscribe to depends on the current platform. An easy approach is to use Matt Johnson-Pint's MauiExceptions class available here. When the class is added, logging uncaught exceptions is using a few lines of code:

    +

    // Replace with an instance of the ILogger
    +ILogger logger;
    +
    +// Subscribe to the UnhandledException event and log it through ILogger
    +MauiExceptions.UnhandledException += (sender, args) =>
    +{
    +    var exception = (Exception)args.ExceptionObject;
    +    logger.LogError(exception, exception.GetBaseException().Message);
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-microsoft-extensions-logging/index.html b/logging-to-elmah-io-from-microsoft-extensions-logging/index.html new file mode 100644 index 0000000000..587e2f4714 --- /dev/null +++ b/logging-to-elmah-io-from-microsoft-extensions-logging/index.html @@ -0,0 +1,963 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Microsoft.Extensions.Logging + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Microsoft.Extensions.Logging

    + +

    Microsoft.Extensions.Logging is both a logging framework itself and a logging abstraction on top of other logging frameworks like log4net and Serilog.

    +

    Start by installing the Elmah.Io.Extensions.Logging package:

    +
    Install-Package Elmah.Io.Extensions.Logging
    +
    dotnet add package Elmah.Io.Extensions.Logging
    +
    <PackageReference Include="Elmah.Io.Extensions.Logging" Version="5.*" />
    +
    paket add Elmah.Io.Extensions.Logging
    +
    +

    Locate your API key (Where is my API key?) and log ID. The two values will be referenced as API_KEY and LOG_ID (Where is my log ID?) in the following.

    +

    Logging from ASP.NET Core

    +

    In the Program.cs file, add a new using statement:

    +

    using Elmah.Io.Extensions.Logging;
    +

    +

    Then call the AddElmahIo-method as shown here:

    +

    builder.Logging.AddElmahIo(options =>
    +{
    +    options.ApiKey = "API_KEY";
    +    options.LogId = new Guid("LOG_ID");
    +});
    +builder.Logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning);
    +

    +

    By calling, the AddFilter-method, you ensure that only warnings and up are logged to elmah.io.

    +

    The API key and log ID can also be configured in appsettings.json:

    +

    {
    +  // ...
    +  "ElmahIo": {
    +    "ApiKey": "API_KEY",
    +    "LogId": "LOG_ID"
    +  }
    +}
    +

    +

    To tell Microsoft.Extensions.Logging to use configuration from the appsettings.json file, include the following code in Program.cs:

    +

    builder.Services.Configure<ElmahIoProviderOptions>(builder.Configuration.GetSection("ElmahIo"));
    +builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
    +builder.Logging.AddElmahIo();
    +

    +

    The first line fetches elmah.io configuration from the ElmahIo object in the appsettings.json file. The second line configures log levels in Microsoft.Extensions.Logging from the Logging object in appsettings.json. The third line adds the elmah.io logger to Microsoft.Extensions.Logging. Notice how the overload without any options object is called since options are already loaded from appsettings.json.

    +

    Start logging messages by injecting an ILogger in your controllers:

    +

    public class HomeController : Controller
    +{
    +    private readonly ILogger<HomeController> logger;
    +
    +    public HomeController(ILogger<HomeController> logger)
    +    {
    +        this.logger = logger;
    +    }
    +
    +    public IActionResult Index()
    +    {
    +        logger.LogWarning("Request to index");
    +        return View();
    +    }
    +}
    +

    +

    For an example of configuring elmah.io with ASP.NET Core 3.1, check out this sample.

    +

    Include HTTP context

    +

    A common use case for using Microsoft.Extensions.Logging is part of an ASP.NET Core project. When combining the two, you would expect the log messages to contain relevant information from the HTTP context (like URL, status code, cookies, etc.). This is not the case out of the box, since Microsoft.Extensions.Logging doesn't know which project type includes it.

    +
    +

    Logging HTTP context requires Elmah.Io.Extensions.Logging version 3.6.x or newer.

    +
    +

    To add HTTP context properties to log messages when logging from ASP.NET Core, install the Elmah.Io.AspNetCore.ExtensionsLogging NuGet package:

    +
    Install-Package Elmah.Io.AspNetCore.ExtensionsLogging
    +
    dotnet add package Elmah.Io.AspNetCore.ExtensionsLogging
    +
    <PackageReference Include="Elmah.Io.AspNetCore.ExtensionsLogging" Version="5.*" />
    +
    paket add Elmah.Io.AspNetCore.ExtensionsLogging
    +
    +

    Then call the UseElmahIoExtensionsLogging method in the Program.cs file:

    +

    // ... Exception handling middleware
    +app.UseElmahIoExtensionsLogging();
    +// ... UseMvc etc.
    +

    +

    It's important to call the UseElmahIoExtensionsLogging method after any calls to UseElmahIo, UseAuthentication, and other exception handling middleware but before UseMvc and UseEndpoints. If you experience logged errors without the HTTP context, try moving the UseElmahIoExtensionsLogging method as the first call in the Configure method.

    +

    Logging from a console application

    +

    Choose the right framework version:

    +
    + +
    + +
    +
    +

    Configure logging to elmah.io using a new or existing ServiceCollection:

    +

    var services = new ServiceCollection();
    +services.AddLogging(logging => logging.AddElmahIo(options =>
    +{
    +    options.ApiKey = "API_KEY";
    +    options.LogId = new Guid("LOG_ID");
    +}));
    +

    +

    Get a reference to the LoggerFactory:

    +

    using var serviceProvider = services.BuildServiceProvider();
    +var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
    +

    +
    +
    +

    Create a new LoggerFactory and configure it to use elmah.io:

    +

    using var loggerFactory = LoggerFactory.Create(builder => builder
    +    .AddElmahIo(options =>
    +    {
    +        options.ApiKey = "API_KEY";
    +        options.LogId = new Guid("LOG_ID");
    +    }));
    +

    +
    +
    +

    Adding the using keyword is important to let elmah.io store messages before exiting the application.

    +

    Finally, create a new logger and start logging exceptions:

    +

    var logger = factory.CreateLogger("MyLog");
    +logger.LogError(ex, "Unexpected error");
    +

    +

    Logging custom properties

    +

    Elmah.Io.Extensions.Logging support Microsoft.Extensions.Logging scopes from version 3.6.x. In short, scopes are a way to decorate your log messages like enrichers in Serilog and context in NLog and log4net. By including properties in a scope, these properties automatically go into the Data tab on elmah.io.

    +

    To define a new scope, wrap your logging code in a using:

    +

    using (logger.BeginScope(new Dictionary<string, object> { { "UserId", 42 } }))
    +{
    +    logger.LogInformation("Someone says hello");
    +}
    +

    +

    In the example above, the UserId key will be added on the Data tab with the value of 42.

    +

    Like the other logging framework integrations, Elmah.Io.Extensions.Logging supports a range of known keys that can be used to insert value in the correct fields on the elmah.io UI.

    +

    using (logger.BeginScope(new Dictionary<string, object>
    +    { { "statuscode", 500 }, { "method", "GET" } }))
    +{
    +    logger.LogError("Request to {url} caused an error", "/profile");
    +}
    +

    +

    In this example, a log message with the template Request to {url} caused an error to be logged. The use of the variable names statuscode, method, and url will fill in those values in the correct fields on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage.

    +

    An alternative is to use the OnMessage action. As an example, we'll add a version number to all messages:

    +

    logging
    +    .AddElmahIo(options =>
    +    {
    +        // ...
    +        options.OnMessage = msg =>
    +        {
    +            msg.Version = "2.0.0";
    +        };
    +    });
    +

    +

    You can even access the current HTTP context in the OnMessage action. To do so, start by creating a new class named DecorateElmahIoMessages:

    +

    public class DecorateElmahIoMessages : IConfigureOptions<ElmahIoProviderOptions>
    +{
    +    private readonly IHttpContextAccessor httpContextAccessor;
    +
    +    public DecorateElmahIoMessages(IHttpContextAccessor httpContextAccessor)
    +    {
    +        this.httpContextAccessor = httpContextAccessor;
    +    }
    +
    +    public void Configure(ElmahIoProviderOptions options)
    +    {
    +        options.OnMessage = msg =>
    +        {
    +            var context = httpContextAccessor.HttpContext;
    +            if (context == null) return;
    +            msg.User = context.User?.Identity?.Name;
    +        };
    +    }
    +}
    +

    +

    Then register IHttpContextAccessor and the new class in the Program.cs file:

    +

    builder.Services.AddHttpContextAccessor();
    +builder.Services.AddSingleton<IConfigureOptions<ElmahIoProviderOptions>, DecorateElmahIoMessages>();
    +

    +

    Setting category

    +

    elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to Microsoft.Extensions.Logging field of the same name. The category field is automatically set when using a typed logger:

    +

    ILogger<MyType> logger = ...;
    +logger.LogInformation("This is an information message with category");
    +

    +

    The category can be overwritten using a property named category on either the log message or a new scope:

    +

    using (logger.BeginScope(new Dictionary<string, object> { { "category", "The category" } }))
    +{
    +    logger.LogInformation("This is an information message with category");
    +}
    +

    +

    Include source code

    +

    You can use the OnMessage action already described to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it.

    +

    There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action:

    +

    logging
    +    .AddElmahIo(options =>
    +    {
    +        // ...
    +        options.OnMessage = msg =>
    +        {
    +            msg.WithSourceCodeFromPdb();
    +        };
    +    });
    +

    +

    Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.

    +
    +

    Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.

    +
    +

    Options

    +

    Setting application name

    +

    If logging to the same log from multiple applications it is a good idea to set unique application names from each app. This will let you search and filter errors on the elmah.io UI. To set an application name, add the following code to the options:

    +

    logging.AddElmahIo(options =>
    +{
    +    // ...
    +    options.Application = "MyApp";
    +});
    +

    +

    The application name can also be configured through appsettings.json:

    +

    {
    +  // ...
    +  "ElmahIo": {
    +    // ...
    +    "Application": "MyApp"
    +  }
    +}
    +

    +

    appsettings.json configuration

    +

    Some of the configuration for Elmah.Io.Extensions.Logging can be done through the appsettings.json file when using ASP.NET Core 2.x or above. To configure the minimum log level, add a new logger named ElmahIo to the settings file:

    +

    {
    +  "Logging": {
    +    // ...
    +    "ElmahIo": {
    +      "LogLevel": {
    +        "Default": "Warning"
    +      }
    +    }
    +  }
    +}
    +

    +

    Finally, tell the logger to look for this information, by adding a bit of code to Program.cs:

    +

    builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
    +

    +

    Filtering log messages

    +

    As default, the elmah.io logger for Microsoft.Extensions.Logging only logs warnings, errors, and fatals. The rationale behind this is that we build an error management system and doesn't do much to support millions of debug messages from your code. Sometimes you may want to log non-exception messages, though. To do so, use filters in Microsoft.Extensions.Logging.

    +

    To log everything from log level Information and up, do the following:

    +

    Inside Program.cs change the minimum level:

    +

    builder.Logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Information);
    +

    +

    In the code sample, every log message with a log level of Information and up will be logged to elmah.io.

    +

    Logging through a proxy

    +
    +

    Proxy configuration requires 3.5.49 or newer.

    +
    +

    You can log through a proxy using options:

    +

    logging.AddElmahIo(options =>
    +{
    +    // ...
    +    options.WebProxy = new WebProxy("localhost", 8000);
    +});
    +

    +

    In this example, the elmah.io client routes all traffic through http://localhost:8000.

    +

    Troubleshooting

    +

    Here are some things to try out if logging from Microsoft.Extensions.Logging to elmah.io doesn't work:

    + +

    x message(s) dropped because of queue size limit

    +

    If you see this message in your log, it means that you are logging a large number of messages to elmah.io through Microsoft.Extensions.Logging within a short period of time. Either turn down the volume using filters:

    +

    logging.AddElmahIo(options => { /*...*/ });
    +logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning);
    +

    +

    or increase the queue size of Elmah.Io.Extensions.Logging:

    +

    logging.AddElmahIo(options =>
    +{
    +    // ...
    +    options.BackgroundQueueSize = 5000;
    +});
    +

    +

    Uncaught errors are logged twice

    +

    If you have both Elmah.Io.Extensions.Logging and Elmah.Io.AspNetCore installed, you may see a pattern of uncaught errors being logged twice. This is because a range of middleware from Microsoft (and others) log uncaught exceptions through ILogger. When you have Elmah.Io.AspNetCore installed you typically don't want other pieces of middleware to log the same error but with fewer details.

    +

    To ignore duplicate errors you need to figure out which middleware class that trigger the logging and in which namespace it is located. One or more of the following ignore filters can be added to your Program.cs file:

    +

    logging.AddFilter<ElmahIoLoggerProvider>(
    +    "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware", LogLevel.None);
    +logging.AddFilter<ElmahIoLoggerProvider>(
    +    "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware", LogLevel.None);
    +logging.AddFilter<ElmahIoLoggerProvider>(
    +    "Microsoft.AspNetCore.Server.IIS.Core", LogLevel.None);
    +

    +

    Be aware that these lines will ignore all messages from the specified class or namespace. To ignore specific errors you can implement the OnFilter action as shown previously in this document. Ignoring uncaught errors from IIS would look like this:

    +

    options.OnFilter = msg =>
    +{
    +    return msg.TitleTemplate == "Connection ID \"{ConnectionId}\", Request ID \"{TraceIdentifier}\": An unhandled exception was thrown by the application.";
    +};
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-nancy/index.html b/logging-to-elmah-io-from-nancy/index.html new file mode 100644 index 0000000000..6706ccf22a --- /dev/null +++ b/logging-to-elmah-io-from-nancy/index.html @@ -0,0 +1,691 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Nancy + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from Nancy

    +
    +

    Nancy is no longer maintained. While the installation steps on this page (probably) still work, we no longer test the Nancy.Elmah package.

    +
    +

    As with MVC and WebAPI, Nancy already provides ELMAH support out of the box. Start by installing the Elmah.Io NuGet package:

    +
    Install-Package Elmah.Io
    +
    dotnet add package Elmah.Io
    +
    <PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add Elmah.Io
    +
    +

    During the installation, you will be asked for your API key (Where is my API key?) and log ID (Where is my log ID?).

    +

    To integrate Nancy and ELMAH, Christian Westman already did a great job with his Nancy.Elmah package. Install it using NuGet:

    +
    Install-Package Nancy.Elmah
    +
    dotnet add package Nancy.Elmah
    +
    <PackageReference Include="Nancy.Elmah" Version="0.*" />
    +
    paket add Nancy.Elmah
    +
    +

    You must install the Elmah.Io package before Nancy.Elmah, because both packages like to add the ELMAH configuration to the web.config file. If you install it the other way around, you will need to add the elmah.io ErrorLog element manually.

    +

    For Nancy to know how to log errors to Elmah, you need to add an override of the DefaultNancyBootstrapper. Create a new class in the root named Bootstrapper:

    +

    using Nancy;
    +using Nancy.Bootstrapper;
    +using Nancy.Elmah;
    +using Nancy.TinyIoc;
    +
    +namespace Elmah.Io.NancyExample
    +{
    +    public class Bootstrapper : DefaultNancyBootstrapper
    +    {
    +        protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
    +        {
    +            base.ApplicationStartup(container, pipelines);
    +            Elmahlogging.Enable(pipelines, "elmah");
    +        }
    +    }
    +}
    +

    +

    The important thing in the code sample is line 13, where we tell Nancy.Elmah to hook into the pipeline of Nancy for it to catch and log HTTP errors. The second parameter for the Enable-method, lets us define a URL for the ELMAH error page, which can be used as an alternative to elmah.io for quick viewing of errors.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-nlog/index.html b/logging-to-elmah-io-from-nlog/index.html new file mode 100644 index 0000000000..8c871dd4ac --- /dev/null +++ b/logging-to-elmah-io-from-nlog/index.html @@ -0,0 +1,1022 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from NLog + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from NLog

    + +

    NLog is one of the most popular logging frameworks for .NET. With an active history of almost 10 years, the possibilities with NLog are many and it’s easy to find documentation on how to use it.

    +

    To start logging messages from NLog to elmah.io, you need to install the Elmah.Io.NLog NuGet package:

    +
    Install-Package Elmah.Io.NLog
    +
    dotnet add package Elmah.Io.NLog
    +
    <PackageReference Include="Elmah.Io.NLog" Version="5.*" />
    +
    paket add Elmah.Io.NLog
    +
    +
    +

    Please don't use NLog 4.6.0 since that version contains a bug that causes the elmah.io target to not load correctly. 4.5.11, 4.6.1, or newer.

    +
    +

    Configuration in .NET

    +

    To configure the elmah.io target, add the following configuration to your app.config/web.config/nlog.config depending on what kind of project you’ve created:

    + + +
    +
    +

    <extensions>
    +  <add assembly="Elmah.Io.NLog"/>
    +</extensions>
    +
    +<targets>
    +  <target name="elmahio" type="elmah.io" apiKey="API_KEY" logId="LOG_ID"/>
    +</targets>
    +
    +<rules>
    +  <logger name="*" minlevel="Info" writeTo="elmahio" />
    +</rules>
    +

    +
    +
    +

    <targets>
    +  <target name="elmahio" type="elmah.io, Elmah.Io.NLog" apiKey="API_KEY" logId="LOG_ID"/>
    +</targets>
    +
    +<rules>
    +  <logger name="*" minlevel="Info" writeTo="elmahio" />
    +</rules>
    +

    +
    +
    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the ID of the log you want messages sent to (Where is my log ID?).

    +

    In the example, we specify the level minimum as Info. This tells NLog to log only information, warning, error, and fatal messages. You may adjust this, but be aware that your elmah.io log may run full pretty fast, especially if you log thousands and thousands of trace and debug messages.

    +

    Log messages to elmah.io, just as with every other target and NLog:

    +

    log.Warn("This is a warning message");
    +log.Error(new Exception(), "This is an error message");
    +

    +

    Specify API key and log ID in appSettings

    +

    If you are already using elmah.io, you may have your API key and log ID in the appSettings element already. To use these settings from withing the NLog target configuration you can use an NLog layout formatter:

    +

    <targets>
    +  <target name="elmahio" type="elmah.io" apiKey="${appsetting:item=apiKey}" logId="${appsetting:item=logId}"/>
    +</targets>
    +

    +

    By using the layout ${appsetting:item=apiKey} you tell NLog that the value for this attribute is in an appSettings element named elmahKey:

    +

    <appSettings>
    +  <add key="apiKey" value="API_KEY" />
    +  <add key="logId" value="LOG_ID" />
    +</appSettings>
    +

    +
    +

    The appSettings layout formatter only works when targeting .NET Full Framework and requires Elmah.Io.NLog version 3.3.x or above and NLog version 4.6.x or above.

    +
    +

    IntelliSense

    +

    There is support for adding IntelliSense in Visual Studio for the NLog.config file. Extend the nlog root element like this:

    +

    <?xml version="1.0" encoding="utf-8" ?>
    +<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
    +      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +      xmlns:elmahio="http://www.nlog-project.org/schemas/NLog.Targets.Elmah.Io.xsd"
    +      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.Targets.Elmah.Io.xsd http://www.nlog-project.org/schemas/NLog.Targets.Elmah.Io.xsd">
    +  <!-- ... -->
    +</nlog>
    +

    +

    Then change the type attribute in the target to xsi:type:

    +

    <target xsi:type="elmahio:elmah.io" />
    +

    +

    Configuration in appsettings.json

    +

    .NET 5 (previously Core) and newer switched from declaring XML configuration in app/web/nlog.config files to JSON configuration in an appsettings.json file. To configure elmah.io in JSON, install the NLog.Extensions.Logging NuGet package:

    +
    Install-Package NLog.Extensions.Logging
    +
    dotnet add package NLog.Extensions.Logging
    +
    <PackageReference Include="NLog.Extensions.Logging" Version="1.*" />
    +
    paket add NLog.Extensions.Logging
    +
    +

    Extend the appsettings.json file with a new NLog section:

    + + +
    +
    +

    {
    +  "NLog": {
    +    "throwConfigExceptions": true,
    +    "extensions": [
    +      { "assembly": "Elmah.Io.NLog" }
    +    ],
    +    "targets": {
    +      "elmahio": {
    +        "type": "elmah.io",
    +        "apiKey": "API_KEY",
    +        "logId": "LOG_ID"
    +      }
    +    },
    +    "rules": [
    +      {
    +        "logger": "*",
    +        "minLevel": "Info",
    +        "writeTo": "elmahio"
    +      }
    +    ]
    +  }
    +}
    +

    +
    +
    +

    {
    +  "NLog": {
    +    "throwConfigExceptions": true,
    +    "targets": {
    +      "elmahio": {
    +        "type": "elmah.io, Elmah.Io.NLog",
    +        "apiKey": "API_KEY",
    +        "logId": "LOG_ID"
    +      }
    +    },
    +    "rules": [
    +      {
    +        "logger": "*",
    +        "minLevel": "Info",
    +        "writeTo": "elmahio"
    +      }
    +    ]
    +  }
    +}
    +

    +
    +
    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the ID of the log you want messages sent to (Where is my log ID?).

    +

    If you haven't already loaded the configuration in your application, make sure to do so:

    +

    var config = new ConfigurationBuilder()
    +    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    +    .Build();
    +

    +

    Finally, tell NLog how to load the NLog configuration section:

    +

    LogManager.Configuration = new NLogLoggingConfiguration(config.GetSection("NLog"));
    +

    +

    elmah.io configuration outside the NLog section

    +

    You might not want the elmah.io API key and log Id inside the NLog section or already have an ElmahIo section defined and want to reuse that. Splitting up configuration like that is supported through NLog layout renderers:

    +

    {
    +  "NLog": {
    +    // ...
    +    "targets": {
    +      "elmahio": {
    +        "type": "elmah.io",
    +        "apiKey": "${configsetting:item=ElmahIo.ApiKey}",
    +        "logId": "${configsetting:item=ElmahIo.LogId}"
    +      }
    +    },
    +    // ...
    +  },
    +  "ElmahIo": {
    +    "ApiKey": "API_KEY",
    +    "LogId": "LOG_ID"
    +  }
    +}
    +

    +

    Notice how the value of the apiKey and logId parameters have been replaced with ${configsetting:item=ElmahIo.*}. At the bottom, the ElmahIo section wraps the API key and log Id.

    +

    To make this work, you will need an additional line of C# when setting up NLog logging:

    +

    var config = new ConfigurationBuilder()
    +    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    +    .Build();
    +
    +ConfigSettingLayoutRenderer.DefaultConfiguration = config; // 👈 add this line
    +
    +LogManager.Configuration = new NLogLoggingConfiguration(config.GetSection("NLog"));
    +

    +

    IntelliSense

    +

    There is support for adding IntelliSense in Visual Studio for the NLog section in the appsettings.json file. Copy and paste the following link into the Schema textbox above the file content:

    +

    https://nlog-project.org/schemas/appsettings.schema.json
    +

    +

    Configuration in code

    +

    The elmah.io target can be configured from C# code if you prefer or need to access the built-in events (see more later). The following adds logging to elmah.io:

    +

    var config = new LoggingConfiguration();
    +var elmahIoTarget = new ElmahIoTarget();
    +elmahIoTarget.Name = "elmahio";
    +elmahIoTarget.ApiKey = "API_KEY";
    +elmahIoTarget.LogId = "LOG_ID";
    +config.AddTarget(elmahIoTarget);
    +config.AddRuleForAllLevels(elmahIoTarget);
    +LogManager.Configuration = config;
    +

    +

    The example will log all log levels to elmah.io. For more information about how to configure individual log levels, check out the NLog documentation on GitHub.

    +

    Logging custom properties

    +

    NLog supports logging custom properties in multiple ways. If you want to include a property (like a version number) in all log messages, you might want to look into the OnMessage feature on Elmah.Io.NLog.

    +

    With custom properties, you can log additional key/value pairs with every log message. The elmah.io target for NLog supports custom properties as well. Properties are persisted alongside every log message in elmah.io and searchable if named correctly.

    +

    One way to log custom properties with NLog and elmah.io is to use the overload of each logging-method that takes a LogEventInfo object as a parameter:

    +

    var infoMessage = new LogEventInfo(LogLevel.Info, "", "This is an information message");
    +infoMessage.Properties.Add("Some Property Key", "Some Property Value");
    +log.Info(infoMessage);
    +

    +

    This saves the information message in elmah.io with a custom property with key Some Property Key and value Some Property Value.

    +

    As of NLog 4.5, structured logging is supported as well. To log a property as part of the log message, use the new syntax as shown here:

    +

    log.Warn("Property named {FirstName}", "Donald");
    +

    +

    In the example, NLog will log the message Property named "Donald", but the key (FirstName) and value (Donald), will also be available in the Data tab inside elmah.io.

    +

    Elmah.Io.NLog provides a range of reserved property names, that can be used to fill in data in the correct fields on the elmah.io UI. Let's say you want to fill the User field using structured logging only:

    +

    log.Info("{Quote} from {User}", "Hasta la vista, baby", "T-800");
    +

    +

    This will fill in the value T-800 in the User field, as well as add two key/value pairs (Quote and User) to the Data tab on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage.

    +

    NLog also provides a fluent API (available in the NLog.Fluent namespace) that some might find more readable:

    +

    logger.Info()
    +      .Message("I'll be back")
    +      .Property("User", "T-800")
    +      .Write();
    +

    +

    If you want to use the normal logging methods like Info and Error, you can do so injunction with the MappedDiagnosticsLogicalContext class, also provided by NLog:

    +

    using (MappedDiagnosticsLogicalContext.SetScoped("User", "T-800"))
    +{
    +   logger.Info("I'll be back");
    +}
    +

    +

    This will create the same result as the example above.

    +

    In NLog 5 MappedDiagnosticsLogicalContext has been deprecated in favor of a scope context:

    +

    using (logger.PushScopeProperty("User", "T-800"))
    +{
    +   logger.Info("I'll be back");
    +}
    +

    +

    Setting application name

    +

    The application field on elmah.io can be set globally using NLog's global context:

    +

    GlobalDiagnosticsContext.Set("Application", "My application name");
    +

    +

    Setting version number

    +

    The version field on elmah.io can be set globally using NLog's global context:

    +

    GlobalDiagnosticsContext.Set("Version", "1.2.3");
    +

    +

    Setting category

    +

    elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to NLog's logger field automatically when using Elmah.Io.NLog. The category field can be overwritten using a scoped property (NLog 5):

    +

    using (logger.PushScopeProperty("category", "The category"))
    +{
    +    logger.Info("This is an information message with category");
    +}
    +

    +

    Message hooks

    +

    Elmah.Io.NLog provides message hooks similar to the integrations with ASP.NET and ASP.NET Core. Message hooks need to be implemented in C#. Either configure the elmah.io target in C# or fetch the target already configured in XML:

    +

    var elmahIoTarget = (ElmahIoTarget)LogManager.Configuration.FindTargetByName("elmahio");
    +

    +

    You also need to install the most recent version of the Elmah.Io.Client NuGet package to use message hooks.

    +

    Decorating log messages

    +

    To include additional information on log messages, you can use the OnMessage action:

    +

    elmahIoTarget.OnMessage = msg =>
    +{
    +    msg.Version = "1.0.0";
    +};
    +

    +

    The example above includes a version number on all errors.

    +

    Include source code

    +

    You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it.

    +

    There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action:

    +

    elmahIoTarget.OnMessage = msg =>
    +{
    +    msg.WithSourceCodeFromPdb();
    +};
    +

    +

    Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.

    +
    +

    Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.

    +
    +

    Handle errors

    +

    To handle any errors happening while processing a log message, you can use the OnError event when initializing the elmah.io target:

    +

    elmahIoTarget.OnError = (msg, err) =>
    +{
    +    // Do something here
    +};
    +

    +

    The example implements a callback if logging to elmah.io fails. How you choose to implement this is entirely up to your application and tech stack.

    +

    Error filtering

    +

    To ignore specific errors based on their content, you can use the OnFilter event when initializing the elmah.io target:

    +

    elmahIoTarget.OnFilter = msg =>
    +{
    +    return msg.Title.Contains("trace");
    +};
    +

    +

    The example above ignores any log messages with the word trace in the title.

    +

    Include HTTP context in ASP.NET and ASP.NET Core

    +

    When logging through NLog from a web application, you may want to include HTTP contextual information like the current URL, status codes, server variables, etc. NLog provides two web-packages to include this information. For ASP.NET, MVC, and Web API you can install the NLog.Web NuGet package and include the following code in web.config:

    +

    <system.webServer>
    +  <modules runAllManagedModulesForAllRequests="true">
    +    <add name="NLog" type="NLog.Web.NLogHttpModule, NLog.Web" />
    +  </modules>
    +</system.webServer>
    +

    +

    For ASP.NET Core you can install the NLog.Web.AspNetCore NuGet package. When installed, the elmah.io NLog target automatically picks up the HTTP context and fills in all possible fields on messages sent to elmah.io.

    +

    Troubleshooting

    +

    Here are some things to try out if logging from NLog to elmah.io doesn't work:

    +
      +
    • Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation.
    • +
    • Make sure that you have the newest Elmah.Io.NLog and Elmah.Io.Client packages installed.
    • +
    • Make sure to include all of the configuration from the example above. That includes both the <extensions> (Only needed in Elmah.Io.NLog 3 and 4), <targets>, and <rules> element.
    • +
    • Make sure that the API key is valid and allow the Messages | Write permission.
    • +
    • Make sure to include a valid log ID.
    • +
    • Make sure that you have sufficient log messages in your subscription and that you didn't disable logging to the log or include any ignore filters/rules.
    • +
    • Always make sure to call LogManager.Shutdown() before exiting the application to make sure that all log messages are flushed.
    • +
    • Extend the nlog element with internalLogLevel="Warn" internalLogFile="c:\temp\nlog-internal.log and inspect that log file for any internal NLog errors.
    • +
    +

    System.IO.FileLoadException: Could not load file or assembly 'Newtonsoft.Json'

    +

    If you see this error in the internal NLog file it means that there's a problem with multiple assemblies referencing different versions of Newtonsoft.Json (also known as NuGet dependency hell). This can be fixed by redirecting to the installed version in the App.config or Web.config file:

    +

    <runtime>
    +  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    +    <dependentAssembly>
    +      <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
    +      <bindingRedirect oldVersion="0.0.0.0-13.0.1.0" newVersion="13.0.1.0"/>
    +    </dependentAssembly>
    +  </assemblyBinding>
    +</runtime>
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-orchard/index.html b/logging-to-elmah-io-from-orchard/index.html new file mode 100644 index 0000000000..8c61fa6514 --- /dev/null +++ b/logging-to-elmah-io-from-orchard/index.html @@ -0,0 +1,717 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Orchard CMS + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from Orchard CMS

    +

    Orchard CMS is a free, open-source community-focused content management system built on the ASP.NET MVC and ASP.NET Core platforms. This tutorial is written for the ASP.NET Core version of Orchard. If you want to log to elmah.io from the MVC version, you should follow our tutorial for MVC.

    +

    To start logging to elmah.io, install the Elmah.Io.Client and Elmah.Io.AspNetCore NuGet packages:

    +
    Install-Package Elmah.Io.Client
    +Install-Package Elmah.Io.AspNetCore
    +
    dotnet add package Elmah.Io.Client
    +dotnet add package Elmah.Io.AspNetCore
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +<PackageReference Include="Elmah.Io.AspNetCore" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +paket add Elmah.Io.AspNetCore
    +
    +

    Then modify your Startup.cs file:

    +

    public class Startup
    +{
    +    public void ConfigureServices(IServiceCollection services)
    +    {
    +        // ...
    +        services.AddElmahIo(o =>
    +        {
    +            o.ApiKey = "API_KEY";
    +            o.LogId = new Guid("LOG_ID");
    +        });
    +    }
    +
    +    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    +    {
    +        // ...
    +        app.UseElmahIo();
    +        // ...
    +    }
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the id of the log (Where is my log ID?) where you want errors logged.

    +

    Like with any other ASP.NET Core application, it's important to call the UseElmahIo-method after setting up other middleware handling exceptions (like UseDeveloperExceptionPage).

    +

    Orchard uses NLog as the internal logging framework. Hooking into this pipeline is a great way to log warnings and errors through NLog to elmah.io as well.

    +

    Install the Elmah.Io.Nlog NuGet package:

    +
    Install-Package Elmah.Io.NLog
    +
    dotnet add package Elmah.Io.NLog
    +
    <PackageReference Include="Elmah.Io.NLog" Version="5.*" />
    +
    paket add Elmah.Io.NLog
    +
    +

    Add the elmah.io target to the NLog.config-file:

    +

    <?xml version="1.0" encoding="utf-8" ?>
    +<nlog>
    +
    +  <extensions>
    +    <!-- ... -->
    +    <add assembly="Elmah.Io.NLog"/>
    +  </extensions>
    +
    +  <targets>
    +    <!-- ... -->
    +    <target name="elmahio" type="elmah.io" apiKey="API_KEY" logId="LOG_ID"/>
    +  </targets>
    +
    +  <rules>
    +    <!-- ... -->
    +    <logger name="*" minlevel="Warn" writeTo="elmahio" />
    +  </rules>
    +</nlog>
    +

    +

    Make sure not to log Trace and Debug messages to elmah.io, which will quickly use up the included storage.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-piranha-cms/index.html b/logging-to-elmah-io-from-piranha-cms/index.html new file mode 100644 index 0000000000..22f577dab2 --- /dev/null +++ b/logging-to-elmah-io-from-piranha-cms/index.html @@ -0,0 +1,686 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Piranha CMS + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from Piranha CMS

    +

    Piranha CMS is a popular headless CMS written in ASP.NET Core. elmah.io works with Piranha CMS out of the box. This document contains a quick installation guide for setting up elmah.io logging in Piranha CMS. For the full overview of logging from ASP.NET Core, check out Logging to elmah.io from ASP.NET Core.

    +

    To start logging to elmah.io, install the Elmah.Io.AspNetCore NuGet package:

    +
    Install-Package Elmah.Io.AspNetCore
    +
    dotnet add package Elmah.Io.AspNetCore
    +
    <PackageReference Include="Elmah.Io.AspNetCore" Version="5.*" />
    +
    paket add Elmah.Io.AspNetCore
    +
    +

    Then modify your Startup.cs file:

    +

    public class Startup
    +{
    +    public void ConfigureServices(IServiceCollection services)
    +    {
    +        // ...
    +        services.AddElmahIo(o =>
    +        {
    +            o.ApiKey = "API_KEY";
    +            o.LogId = new Guid("LOG_ID");
    +        });
    +    }
    +
    +    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    +    {
    +        // ...
    +        app.UseElmahIo();
    +        // ...
    +    }
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the id of the log (Where is my log ID?) where you want errors logged.

    +

    Make sure to call the UseElmahIo-method after setting up other middleware handling exceptions (like UseDeveloperExceptionPage), but before the call to UsePiranha.

    +

    To use structured logging and the ILogger interface with Piranha CMS and elmah.io, set up Microsoft.Extensions.Logging as explained here: Logging to elmah.io from Microsoft.Extensions.Logging.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-powershell/index.html b/logging-to-elmah-io-from-powershell/index.html new file mode 100644 index 0000000000..aafc29c1e0 --- /dev/null +++ b/logging-to-elmah-io-from-powershell/index.html @@ -0,0 +1,767 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from PowerShell + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from PowerShell

    + +

    There are a couple of options for logging to elmah.io from PowerShell. If you need to log a few messages, using the API is the easiest.

    +

    Log through the API

    +

    Logging to elmah.io from PowerShell is easy using built-in cmdlets:

    +

    $apiKey = "API_KEY"
    +$logId = "LOG_ID"
    +$url = "https://api.elmah.io/v3/messages/$logId/?api_key=$apiKey"
    +
    +$body = @{
    +    title = "Error from PowerShell"
    +    severity = "Error"
    +    detail = "This is an error message logged from PowerShell"
    +    hostname = hostname
    +}
    +Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType "application/json-patch+json"
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the ID of the log you want messages sent to (Where is my log ID?).

    +

    Log through PoShLog.Sinks.ElmahIo

    +

    PoShLog is a PowerShell logging module built on top of Serilog. To log to elmah.io using PoShLog, install the following packages:

    +

    Install-Module -Name PoShLog
    +Install-Module -Name PoShLog.Sinks.ElmahIo
    +

    +

    Logging messages can now be done using Write-*Log:

    +

    Import-Module PoShLog
    +Import-Module PoShLog.Sinks.ElmahIo
    +
    +New-Logger |
    +    Add-SinkElmahIo -ApiKey 'API_KEY' -LogId 'LOG_ID' |
    +    Start-Logger
    +
    +Write-ErrorLog 'Say My Name'
    +
    +# Don't forget to close the logger
    +Close-Logger
    +

    +

    Log through Elmah.Io.Client

    +

    If you prefer to use the Elmah.Io.Client NuGet package, you can do this in PowerShell too. First of all, you will need to include elmah.io.client.dll. How you do this is entirely up to you. You can include this assembly in your script location or you can download it through NuGet on every execution. To download the package through NuGet, you will need nuget.exe:

    +

    $source = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
    +$target = ".\nuget.exe"
    +Invoke-WebRequest $source -OutFile $target
    +Set-Alias nuget $target -Scope Global
    +

    +

    This script will download the latest version of the NuGet command-line tool and make it available through the command nuget.

    +

    To install Elmah.Io.Client run nuget.exe:

    +

    nuget install Elmah.Io.Client
    +

    +

    This will create an Elmah.Io.Client-version folder containing the latest stable version of the Elmah.Io.Client package.

    +

    You now have Elmah.Io.Client.dll loaded into your shell and everything is set up to log to elmah.io. To do so, add try-catch around critical code:

    +

    $logger = [Elmah.Io.Client.ElmahioAPI]::Create("API_KEY")
    +Try {
    +    # some code that may throw exceptions
    +}
    +Catch {
    +    $logger.Messages.Error([guid]::new("LOG_ID"), $_.Exception, "Oh no")
    +}
    +

    +

    In the first line, we create a new logger object. Then, in the Catch block, the catched exception is shipped off to the elmah.io log specified in LOG_ID together with a custom message.

    +

    Examples

    +

    For inspiration, here's a list of examples of common scenarios where you'd want to log to elmah.io from PowerShell.

    +

    Log error on low remaining disk

    +

    You can monitor when a server is running low on disk space like this:

    +

    $cdrive = Get-Volume -DriveLetter C
    +$sizeremainingingb = $cdrive.SizeRemaining/1024/1024/1024
    +if ($sizeremainingingb -lt 10) {
    +    $apiKey = "API_KEY"
    +    $logId = "LOG_ID"
    +    $url = "https://api.elmah.io/v3/messages/$logId/?api_key=$apiKey"
    +
    +    $body = @{
    +        title = "Disk storage less than 10 gb"
    +        severity = "Error"
    +        detail = "Remaining storage in gb: $sizeremainingingb"
    +        hostname = hostname
    +    }
    +    Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType "application/json-patch+json"
    +}
    +

    +

    Troubleshooting

    +

    Elmah.Io.Client.ElmahioAPI cannot be loaded

    +

    If PowerShell complains about Elmah.Io.Client.ElmahioAPI not being found, try adding the following lines to the script after installing the Elmah.Io.Client NuGet package:

    +

    $elmahIoClientPath = Get-ChildItem -Path . -Filter Elmah.Io.Client.dll -Recurse `
    +  | Where-Object {$_.FullName -match "net45"}
    +[Reflection.Assembly]::LoadFile($elmahIoClientPath.FullName)
    +
    +$jsonNetPath = Get-ChildItem -Path . -Filter Newtonsoft.Json.dll -Recurse `
    +  | Where-Object {$_.FullName -match "net45" -and $_.FullName -notmatch "portable"}
    +[Reflection.Assembly]::LoadFile($jsonNetPath.FullName)
    +

    +

    You may need to include additional assemblies if PowerShell keeps complaining.

    +

    The catch block is not invoked even though a cmdlet failed

    +

    Most errors in PowerShell are non-terminating meaning that they are handled internally in the cmdlet. To force a cmdlet to use terminating errors use the -ErrorAction parameter:

    +

    Try {
    +    My-Cmdlet -ErrorAction Stop
    +}
    +Catch {
    +    // Log to elmah.io
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-react/index.html b/logging-to-elmah-io-from-react/index.html new file mode 100644 index 0000000000..b7de7563bb --- /dev/null +++ b/logging-to-elmah-io-from-react/index.html @@ -0,0 +1,674 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from React + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    NuGet +npm +Samples

    +

    Logging to elmah.io from React

    +

    To log all errors from a React application, install the elmah.io.javascript npm package as described in Logging from JavaScript. Then modify the index.js or index.tsx file:

    +

    // ...
    +import Elmahio from 'elmah.io.javascript'; 
    +
    +new Elmahio({
    +  apiKey: 'API_KEY',
    +  logId: 'LOG_ID'
    +});
    +
    +// After this the ReactDOM.render etc. will be included
    +

    +

    When launching your React app, elmah.io is configured and all errors happening in the application are logged.

    +

    Check out the elmahio-react and elmahio-react-typescript samples for some real working code.

    +
    +

    React have a known bug/feature in DEV mode where errors are submitted twice. For better error handling in React, you should look into Error Boundaries.

    +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-serilog/index.html b/logging-to-elmah-io-from-serilog/index.html new file mode 100644 index 0000000000..bdaed6379e --- /dev/null +++ b/logging-to-elmah-io-from-serilog/index.html @@ -0,0 +1,992 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Serilog + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Serilog

    + +

    Serilog is a great addition to the flowering .NET logging community, described as “A no-nonsense logging library for the NoSQL era” on their project page. Serilog works just like other logging frameworks such as log4net and NLog but offers a great fluent API and the concept of sinks (a bit like appenders in log4net). Sinks are superior to appenders because they threat errors as objects rather than strings, a perfect fit for elmah.io which itself is built on NoSQL. Serilog already comes with native support for elmah.io, which makes it easy to integrate with any application using Serilog.

    +

    Adding this type of logging to your windows and console applications is just as easy. Add the Serilog.Sinks.ElmahIo NuGet package to your project:

    +
    Install-Package Serilog.Sinks.ElmahIo
    +
    dotnet add package Serilog.Sinks.ElmahIo
    +
    <PackageReference Include="Serilog.Sinks.ElmahIo" Version="5.*" />
    +
    paket add Serilog.Sinks.ElmahIo
    +
    +

    To configure Serilog, add the following code to the Application_Start method in global.asax.cs:

    +

    var log =
    +    new LoggerConfiguration()
    +        .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID")))
    +        .CreateLogger();
    +Log.Logger = log;
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the ID of the log you want messages sent to (Where is my log ID?).

    +

    First, we create a new LoggerConfiguration and tell it to write to elmah.io. The log object can be used to log errors and you should register this in your IoC container. In this case, we don't use IoC, that is why the log object is set as the public static Logger property, which makes it accessible from everywhere.

    +

    To log exceptions to elmah.io through Serilog use the Log class provided by Serilog:

    +

    try
    +{
    +    // Do some stuff that may cause an exception
    +}
    +catch (Exception e)
    +{
    +    Log.Error(e, "The actual error message");
    +}
    +

    +

    The Error method tells Serilog to log the error in the configured sinks, which in our case logs to elmah.io. Simple and beautiful.

    +
    +

    Always call Log.CloseAndFlush(); before your program terminates.

    +
    +

    Logging custom properties

    +

    Serilog supports logging custom properties in three ways: As part of the log message, through enrichers, and using LogContext. All three types of properties are implemented in the elmah.io sink as part of the Data dictionary to elmah.io.

    +

    The following example shows how to log all three types of properties:

    +

    var logger =
    +    new LoggerConfiguration()
    +        .Enrich.WithProperty("ApplicationIdentifier", "MyCoolApp")
    +        .Enrich.FromLogContext()
    +        .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID")))
    +        .CreateLogger();
    +
    +using (LogContext.PushProperty("ThreadId", Thread.CurrentThread.ManagedThreadId))
    +{
    +    logger.Error("This is a message with {Type} properties", "custom");
    +}
    +

    +

    Beneath the Data tab on the logged message details, the ApplicationIdentifier, ThreadId, and Type properties can be found.

    +

    Serilog.Sinks.ElmahIo provides a range of reserved property names, that can be used to fill in data in the correct fields on the elmah.io UI. Let's say you want to fill the User field using structured logging only:

    +

    logger.Information("{Quote} from {User}", "Hasta la vista, baby", "Arnold Schwarzenegger");
    +

    +

    This will fill in the value Arnold Schwarzenegger in the User field, as well as add two key/value pairs (Quote and User) to the Data tab on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage.

    +

    Message hooks

    +

    Serilog.Sinks.ElmahIo provides message hooks similar to the integrations with ASP.NET and ASP.NET Core.

    +
    +

    Message hooks require Serilog.Sinks.ElmahIo version 3.3.0 or newer.

    +
    +

    Decorating log messages

    +

    To include additional information on log messages, you can use the OnMessage event when initializing the elmah.io target:

    +

    Log.Logger =
    +    new LoggerConfiguration()
    +        .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID"))
    +        {
    +            OnMessage = msg =>
    +            {
    +                msg.Version = "1.0.0";
    +            }
    +        })
    +        .CreateLogger();
    +

    +

    The example above includes a version number on all errors. Since the elmah.io sink also picks up enrichers specified with Serilog, this example could be implemented by enriching all log messages with a field named version.

    +

    Include source code

    +

    You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it.

    +

    There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action:

    +

    Log.Logger =
    +    new LoggerConfiguration()
    +        .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID"))
    +        {
    +            OnMessage = msg =>
    +            {
    +                msg.WithSourceCodeFromPdb();
    +            }
    +        })
    +        .CreateLogger();
    +

    +

    Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.

    +
    +

    Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.

    +
    +

    Handle errors

    +

    To handle any errors happening while processing a log message, you can use the OnError event when initializing the elmah.io sink:

    +

    Log.Logger =
    +    new LoggerConfiguration()
    +        .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID"))
    +        {
    +            OnError = (msg, ex) =>
    +            {
    +                Console.Error.WriteLine(ex.Message);
    +            }
    +        })
    +        .CreateLogger();
    +

    +

    The example implements a callback if logging to elmah.io fails. How you choose to implement this is entirely up to your application and tech stack.

    +

    Error filtering

    +

    To ignore specific messages based on their content, you can use the OnFilter event when initializing the elmah.io sink:

    +

    Log.Logger =
    +    new LoggerConfiguration()
    +        .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID"))
    +        {
    +            OnFilter = msg =>
    +            {
    +                return msg.Title.Contains("trace");
    +            }
    +        })
    +        .CreateLogger();
    +

    +

    The example above ignores any log message with the word trace in the title.

    +

    ASP.NET Core

    +

    Serilog provides a package for ASP.NET Core, that routes log messages from inside the framework through Serilog. We recommend using this package together with the elmah.io sink, in order to capture warnings and errors happening inside ASP.NET Core.

    +

    To use this, install the following packages:

    +
    Install-Package Serilog.AspNetCore
    +Install-Package Serilog.Sinks.ElmahIo
    +
    dotnet add package Serilog.AspNetCore
    +dotnet add package Serilog.Sinks.ElmahIo
    +
    <PackageReference Include="Serilog.AspNetCore" Version="6.*" />
    +<PackageReference Include="Serilog.Sinks.ElmahIo" Version="5.*" />
    +
    paket add Serilog.AspNetCore
    +paket add Serilog.Sinks.ElmahIo
    +
    +

    Configure Serilog using the UseSerilog method in the Program.cs file:

    +

    builder.Host.UseSerilog((ctx, lc) => lc
    +    .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID"))
    +    {
    +        MinimumLogEventLevel = LogEventLevel.Information,
    +    }));
    +}
    +

    +

    Now, all warnings, errors, and fatals happening inside ASP.NET Core are logged to elmah.io.

    +

    A common request is to include all of the HTTP contextual information you usually get logged when using a package like Elmah.Io.AspNetCore. We have developed a specialized NuGet package to include cookies, server variables, etc. when logging through Serilog from ASP.NET Core. To set it up, install the Elmah.Io.AspNetCore.Serilog NuGet package:

    +
    Install-Package Elmah.Io.AspNetCore.Serilog
    +
    dotnet add package Elmah.Io.AspNetCore.Serilog
    +
    <PackageReference Include="Elmah.Io.AspNetCore.Serilog" Version="5.*" />
    +
    paket add Elmah.Io.AspNetCore.Serilog
    +
    +

    Then, call the UseElmahIoSerilog method in Program.cs file:

    +

    // ... Exception handling middleware
    +app.UseElmahIoSerilog();
    +// ... UseMvc etc.
    +

    +

    The middleware uses Serilog's LogContext feature to enrich each log message with additional properties. To turn on the log context, extend your Serilog config:

    +

    builder.Host.UseSerilog((ctx, lc) => lc
    +    .WriteTo.ElmahIo(/*...*/)
    +    .Enrich.FromLogContext() // <-- add this line
    +);
    +

    +

    There's a problem with this approach when an endpoint throws an uncaught exception. Microsoft.Extensions.Logging logs all uncaught exceptions as errors, but the LogContext is already popped when doing so. The recommended approach is to ignore these errors in the elmah.io sink and install the Elmah.Io.AspNetCore package to log uncaught errors to elmah.io (as explained in Logging from ASP.NET Core). The specific error message can be ignored in the sink by providing the following filter during initialization of Serilog:

    +

    .WriteTo.ElmahIo(/*...*/)
    +{
    +    // ...
    +    OnFilter = msg =>
    +    {
    +        return
    +            msg != null
    +            && msg.TitleTemplate != null
    +            && msg.TitleTemplate.Equals(
    +                "An unhandled exception has occurred while executing the request.",
    +                StringComparison.InvariantCultureIgnoreCase);
    +    }
    +})
    +

    +

    ASP.NET

    +

    Messages logged through Serilog in an ASP.NET WebForms, MVC, or Web API application can be enriched with a range of HTTP contextual information using the SerilogWeb.Classic NuGet package. Start by installing the package:

    +
    Install-Package SerilogWeb.Classic
    +
    dotnet add package SerilogWeb.Classic
    +
    <PackageReference Include="SerilogWeb.Classic" Version="5.*" />
    +
    paket add SerilogWeb.Classic
    +
    +

    The package includes automatic HTTP request and response logging as well as some Serilog enrichers. Unless you are trying to debug a specific problem with your website, we recommend disabling HTTP logging since that will produce a lot of messages (depending on the traffic on your website). HTTP logging can be disabled by including the following code in the Global.asax.cs file:

    +

    protected void Application_Start()
    +{
    +    SerilogWebClassic.Configure(cfg => cfg
    +        .Disable()
    +    );
    +
    +    // ...
    +}
    +

    +

    To enrich log messages with HTTP contextual information you can configure one or more enrichers in the same place as you configure the elmah.io sink:

    +

    Log.Logger = new LoggerConfiguration()
    +    .WriteTo.ElmahIo(new ElmahIoSinkOptions("API_KEY", new Guid("LOG_ID")))
    +    .Enrich.WithHttpRequestClientHostIP()
    +    .Enrich.WithHttpRequestRawUrl()
    +    .Enrich.WithHttpRequestType()
    +    .Enrich.WithHttpRequestUrl()
    +    .Enrich.WithHttpRequestUserAgent()
    +    .Enrich.WithUserName(anonymousUsername:null)
    +    .CreateLogger();
    +

    +

    This will automatically fill in fields on elmah.io like URL, method, client IP, and UserAgent.

    +

    Check out this full sample for more details.

    +

    Config using appsettings.json

    +

    While Serilog provides a great fluent C# API, some prefer to configure Serilog using an appsettings.json file. To configure the elmah.io sink this way, you will need to install the Serilog.Settings.Configuration NuGet package. Then configure elmah.io in your appsettings.json file:

    +

    {
    +    // ...
    +    "Serilog":{
    +        "Using":[
    +            "Serilog.Sinks.ElmahIo"
    +        ],
    +        "MinimumLevel": "Warning",
    +        "WriteTo":[
    +            {
    +                "Name": "ElmahIo",
    +                "Args":{
    +                    "apiKey": "API_KEY",
    +                    "logId": "LOG_ID"
    +                }
    +            }
    +        ]
    +    }
    +}
    +

    +
    +

    Make sure to specify the apiKey and logId arguments with the first character in lowercase.

    +
    +

    Finally, tell Serilog to read the configuration from the appsettings.json file:

    +

    var configuration = new ConfigurationBuilder()
    +    .AddJsonFile("appsettings.json")
    +    .Build();
    +
    +var logger = new LoggerConfiguration()
    +    .ReadFrom.Configuration(configuration)
    +    .CreateLogger();
    +

    +

    Extended exception details with Serilog.Exceptions

    +

    The more information you have on an error, the easier it is to find out what went wrong. Muhammad Rehan Saeed made a nice enrichment package for Serilog named Serilog.Exceptions. The package uses reflection on a logged exception to log additional information depending on the concrete exception type. You can install the package through NuGet:

    +
    Install-Package Serilog.Exceptions
    +
    dotnet add package Serilog.Exceptions
    +
    <PackageReference Include="Serilog.Exceptions" Version="5.*" />
    +
    paket add Serilog.Exceptions
    +
    +

    And configure it in C# code:

    +

    var logger = new LoggerConfiguration()
    +    .Enrich.WithExceptionDetails()
    +    .WriteTo.ElmahIo(/*...*/)
    +    .CreateLogger();
    +

    +

    The elmah.io sink will automatically pick up the additional information and show them in the extended message details overlay. To navigate to this view, click an error on the search view. Then click the button in the upper right corner to open extended message details. The information logged by Serilog.Exceptions are available beneath the Data tab.

    +

    Remove sensitive data

    +

    Structured logging with Serilog is a great way to store a lot of contextual information about a log message. In some cases, it may result in sensitive data being stored in your log, though. We recommend you remove any sensitive data from your log messages before storing them on elmah.io and anywhere else. To implement this, you can use the OnMessage event as already shown previously in the document:

    +

    OnMessage = msg =>
    +{
    +    foreach (var d in msg.Data)
    +    {
    +        if (d.Key.Equals("Password"))
    +        {
    +            d.Value = "****";
    +        }
    +    }
    +}
    +

    +

    An alternative to replacing sensitive values manually is to use a custom destructuring package for Serilog. The following example shows how to achieve this using the Destructurama.Attributed package:

    +
    Install-Package Destructurama.Attributed
    +
    dotnet add package Destructurama.Attributed
    +
    <PackageReference Include="Destructurama.Attributed" Version="2.*" />
    +
    paket add Destructurama.Attributed
    +
    +

    Set up destructuring from attributes:

    +

    Log.Logger = new LoggerConfiguration()
    +    .Destructure.UsingAttributes()
    +    .WriteTo.ElmahIo(/*...*/)
    +    .CreateLogger();
    +

    +

    Make sure to decorate any properties including sensitive data with the NotLogged attribute:

    +

    public class LoginModel
    +{
    +    public string Username { get; set; }
    +
    +    [NotLogged]
    +    public string Password { get; set; }
    +}
    +

    +

    Setting a category

    +

    elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to Serilog's SourceContext automatically when using Serilog.Sinks.ElmahIo. To make sure that the category is correctly populated, either use the Forcontext method:

    +

    Log.ForContext<Program>().Information("This is an information message with context");
    +Log.ForContext("The context").Information("This is another information message with context");
    +

    +

    Or set a SourceContext or Category property using LogContext:

    +

    using (LogContext.PushProperty("SourceContext", "The context"))
    +    Log.Information("This is an information message with context");
    +
    +using (LogContext.PushProperty("Category", "The context"))
    +    Log.Information("This is another information message with context");
    +

    +

    When using Serilog through Microsoft.Extensions.Logging's ILogger<T> interface, the source context will automatically be set by Serilog.

    +

    Troubleshooting

    +

    Here are some things to try out if logging from Serilog to elmah.io doesn't work:

    +
      +
    • Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation.
    • +
    • Make sure that you have the newest Serilog.Sinks.ElmahIo and Elmah.Io.Client packages installed.
    • +
    • Make sure to include all of the configuration from the example above.
    • +
    • Make sure that the API key is valid and allow the Messages | Write permission.
    • +
    • Make sure to include a valid log ID.
    • +
    • Make sure that you have sufficient log messages in your subscription and that you didn't disable logging to the log or include any ignore filters/rules.
    • +
    • Always make sure to call Log.CloseAndFlush() before exiting the application to make sure that all log messages are flushed.
    • +
    • Set up Serilog's SelfLog to inspect any errors happening inside Serilog or the elmah.io sink: Serilog.Debugging.SelfLog.Enable(msg => Debug.WriteLine(msg));.
    • +
    • Implement the OnError action and put a breakpoint in the handler to inspect if any errors are thrown while logging to the elmah.io API.
    • +
    +

    PeriodicBatchingSink is marked as sealed

    +

    If you get a runtime error stating that the PeriodicBatchingSink class is sealed and cannot be extended, make sure to manually install version 3.1.0 or newer of the Serilog.Sinks.PeriodicBatching NuGet package. The bug has also been fixed in the Serilog.Sinks.ElmahIo NuGet package from version 4.3.29 and forward.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-servicestack/index.html b/logging-to-elmah-io-from-servicestack/index.html new file mode 100644 index 0000000000..d284a29e74 --- /dev/null +++ b/logging-to-elmah-io-from-servicestack/index.html @@ -0,0 +1,671 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from ServiceStack + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from ServiceStack

    +

    Logging errors to elmah.io from ServiceStack is almost as easy as installing in MVC and Web API. The folks over at ServiceStack provide you with a NuGet package named ServiceStack.Logging.Elmah. Like Web API you need to tell ServiceStack to use ELMAH as the logging framework for errors, besides adding the standard ELMAH configuration in web.config. Start by installing both ServiceStack.Logging.Elmah and Elmah.Io into your ServiceStack web project:

    +
    Install-Package ServiceStack.Logging.Elmah
    +Install-Package Elmah.Io
    +
    dotnet add package ServiceStack.Logging.Elmah
    +dotnet add package Elmah.Io
    +
    <PackageReference Include="ServiceStack.Logging.Elmah" Version="5.*" />
    +<PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add ServiceStack.Logging.Elmah
    +paket add Elmah.Io
    +
    +

    During the installation, you will be asked for your API key (Where is my API key?) and log ID (Where is my log ID?).

    +

    Once installed, add the following line to your AppHost:

    +

    LogManager.LogFactory = new ElmahLogFactory(new NLogFactory());
    +

    +

    The above example assumes that you are already using NLog as the existing framework for logging. Wrapping different logger factories in each other and makes it possible to log errors through ELMAH and other types of messages like warnings and info messages through another logging framework. If you don’t need anything other than ELMAH logging, use the NullLogFactory instead of NLogFactory.

    +

    That’s it! By installing both the ServiceStack.Logging.Elmah and elmah.io packages, you should have sufficient configuration in your web.config to start logging errors.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-signalr/index.html b/logging-to-elmah-io-from-signalr/index.html new file mode 100644 index 0000000000..cffde379de --- /dev/null +++ b/logging-to-elmah-io-from-signalr/index.html @@ -0,0 +1,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from SignalR + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from SignalR

    +

    Logging from SignalR is supported through our Elmah.Io.Extensions.Logging package. For details not included in this article, check out Logging from Microsoft.Extensions.Logging.

    +

    Start by installing the Elmah.Io.Extensions.Logging package:

    +
    Install-Package Elmah.Io.Extensions.Logging
    +
    dotnet add package Elmah.Io.Extensions.Logging
    +
    <PackageReference Include="Elmah.Io.Extensions.Logging" Version="5.*" />
    +
    paket add Elmah.Io.Extensions.Logging
    +
    +

    In the Program.cs file, add a new using statement:

    +

    using Elmah.Io.Extensions.Logging;
    +

    +

    Then configure elmah.io like shown here:

    +

    builder.Logging.AddElmahIo(options =>
    +{
    +    options.ApiKey = "API_KEY";
    +    options.LogId = new Guid("LOG_ID");
    +});
    +builder.Logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning);
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the log Id of the log you want to log to.

    +

    The code only logs Warning, Error, and Fatal messages. To change that you can change the LogLevel filter specified in the line calling the AddFilter method. You may also need to change log levels for SignalR itself:

    +

    logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Debug);
    +logging.AddFilter("Microsoft.AspNetCore.Http.Connections", LogLevel.Debug);
    +

    +

    Be aware that changing log levels to Debug or lower will cause a lot of messages to be stored in elmah.io. Log levels can be specified through the appsettings.json file as with any other ASP.NET Core application. Check out appsettings.json configuration for more details.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-sitefinity/index.html b/logging-to-elmah-io-from-sitefinity/index.html new file mode 100644 index 0000000000..a1a062b543 --- /dev/null +++ b/logging-to-elmah-io-from-sitefinity/index.html @@ -0,0 +1,671 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Sitefinity + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from Sitefinity

    +

    Sitefinity is a CMS from Telerik, implemented on top of ASP.NET. Like other content management systems build on top of ASP.NET, ELMAH is supported out of the box.

    +

    To install elmah.io in a Sitefinity website, start by opening the website in Visual Studio by selecting File | Open Web Site... and navigate to the Sitefinity projects folder (something similar to this: C:\Program Files (x86)\Telerik\Sitefinity\Projects\Default).

    +

    Right-click the website and install the Elmah.Io NuGet package:

    +
    Install-Package Elmah.Io
    +
    dotnet add package Elmah.Io
    +
    <PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add Elmah.Io
    +
    +

    During installation, you will be prompted for your API key (Where is my API key?) and log ID (Where is my log ID?).

    +

    That's it! Uncaught errors in Sitefinity are logged to your elmah.io log. To test that the integration works, right-click the website and add a new Web Form named ELMAH.aspx. In the code-behind file add the following code:

    +

    protected void Page_Load(object sender, EventArgs e)
    +{
    +    throw new ApplicationException();
    +}
    +

    +

    Start the website and navigate to the ELMAH.aspx page. If everything works as intended, you will see the yellow screen of death, and a new error will pop up on elmah.io.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-sveltekit/index.html b/logging-to-elmah-io-from-sveltekit/index.html new file mode 100644 index 0000000000..30cd5fe0a2 --- /dev/null +++ b/logging-to-elmah-io-from-sveltekit/index.html @@ -0,0 +1,676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from SvelteKit + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    NuGet +npm +Samples

    +

    Logging to elmah.io from SvelteKit

    +

    To log all errors from a SvelteKit application, install the elmah.io.javascript npm package as described in Logging from JavaScript. Then add the following code to the hooks.client.js file. If the file does not exist, make sure to create it in the src folder since SvelteKit will automatically load it from there.

    +

    import Elmahio from 'elmah.io.javascript';
    +var logger = new Elmahio({
    +    apiKey: 'API_KEY',
    +    logId: 'LOG_ID'
    +});
    +
    +/** @type {import('@sveltejs/kit').HandleClientError} */
    +export function handleError({ error, event }) {
    +    if (error && error.message) {
    +        logger.error(error.message, error);
    +    } else {
    +        logger.error('Error in application', error);
    +    }
    +}
    +

    +

    When launching your SvelteKit app, elmah.io is configured and all errors happening in the application are logged. For now, elmah.io.javascript only supports SvelteKit apps running inside the browser, why implementing the HandleServerError is not supported.

    +

    Check out the elmahio-sveltekit sample for some real working code.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-system-diagnostics/index.html b/logging-to-elmah-io-from-system-diagnostics/index.html new file mode 100644 index 0000000000..b730ab115a --- /dev/null +++ b/logging-to-elmah-io-from-system-diagnostics/index.html @@ -0,0 +1,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from System.Diagnostics + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from System.Diagnostics

    +
    +

    Logging through System.Diagnostics have been deprecated. Please use the Elmah.Io.Client package to log trace messages to elmah.io.

    +
    +

    .NET comes with its own tracing/logging feature located in the System.Diagnostics namespaces. A core part of System.Diagnostics is the Trace class, but that namespace contains utilities for performance counters, working with the event log, and a lot of other features. In this article, we will focus on logging to elmah.io from System.Diagnostics.Trace.

    +

    To start logging, install the Elmah.Io.Trace package:

    +
    Install-Package Elmah.Io.Trace
    +
    dotnet add package Elmah.Io.Trace
    +
    <PackageReference Include="Elmah.Io.Trace" Version="3.*" />
    +
    paket add Elmah.Io.Trace
    +
    +

    As default, Trace logs to the Win32 OutputDebugString function, but it is possible to log to multiple targets (like appenders in log4net). To do so, tell Trace about elmah.io:

    +

    System.Diagnostics.Trace.Listeners.Add(
    +    new ElmahIoTraceListener("API_KEY", new Guid("LOG_ID")));
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with your log id (Where is my log ID?).

    +

    To start logging, call the Trace API:

    +

    try
    +{
    +    System.Diagnostics.Trace.Write("Starting something dangerous");
    +    // ...
    +}
    +catch (Exception e)
    +{
    +    System.Diagnostics.Trace.Fail(e.Message, e.ToString());
    +}
    +

    +

    In the example, we write an information message with the message Starting something dangerous and log any thrown exception to elmah.io.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-umbraco/index.html b/logging-to-elmah-io-from-umbraco/index.html new file mode 100644 index 0000000000..f1591fe523 --- /dev/null +++ b/logging-to-elmah-io-from-umbraco/index.html @@ -0,0 +1,791 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Umbraco + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Umbraco

    + +

    elmah.io offer great support for all newer Umbraco versions. Umbraco has been in rapid development in the last few years, so the installation instructions are very different depending on which major version you are using. Make sure to select the right version below since newer versions of the Elmah.Io.Umbraco package don't work with older versions of Umbraco and vice versa.

    +
    +
    +
    +
    +
    +
    To learn more about the elmah.io integration with Umbraco and an overall introduction to the included features, make sure to check out the elmah.io and Umbraco page.
    +
    +
    + +

    During the installation steps described below, you will need your API key (Where is my API key?) and log ID (Where is my log ID?).

    +

    elmah.io integrates with Umbraco's Health Checks feature too. To learn more about how to set it up, visit Logging heartbeats from Umbraco.

    +

    Umbraco >= 9

    +
    +

    Umbraco 9 is targeting .NET 5.0 which is no longer supported by Microsoft. This is why we have chosen to support Umbraco 10 and up only.

    +
    +

    To install elmah.io in your Umbraco >= v10 site, install the Elmah.Io.Umbraco NuGet package:

    +
    Install-Package Elmah.Io.Umbraco
    +
    dotnet add package Elmah.Io.Umbraco
    +
    <PackageReference Include="Elmah.Io.Umbraco" Version="5.*" />
    +
    paket add Elmah.Io.Umbraco
    +
    +

    After installing the NuGet package add the following to the Startup.cs file:

    +

    public class Startup
    +{
    +    // ...
    +
    +    public void ConfigureServices(IServiceCollection services)
    +    {
    +        services.AddElmahIo(options =>
    +        {
    +            options.ApiKey = "API_KEY";
    +            options.LogId = new Guid("LOG_ID");
    +        });
    +        // ...
    +    }
    +
    +    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    +    {
    +        // ...
    +        app.UseElmahIo();
    +        // ...
    +    }
    +}
    +

    +
    +

    Make sure to call the UseElmahIo-method after installation of other pieces of middleware handling exceptions and auth (like UseDeveloperExceptionPage, UseExceptionHandler, UseAuthentication, and UseAuthorization), but before the call to UseUmbraco.

    +
    +

    This will log all uncaught errors to elmah.io. If you want to hook into Umbraco's logging through Serilog, extend the configuration in the appsettings.json file with the following JSON:

    +

    {
    +  "Serilog": {
    +    ...
    +    "WriteTo": [
    +      {
    +        "Name": "ElmahIo",
    +        "Args": {
    +          "apiKey": "API_KEY",
    +          "logId": "LOG_ID"
    +        }
    +      }
    +    ]
    +  },
    +  ...
    +}
    +

    +

    This will configure elmah.io's Serilog sink in Umbraco. You may experience logging not coming through when running locally. In this case, it might help to remove the WriteTo action from the appsettings.Development.json file.

    +

    Umbraco 8

    +

    To install elmah.io in your Umbraco v8 site, install the Elmah.Io.Umbraco v4 package:

    +
    Install-Package Elmah.Io.Umbraco -Version 4.2.21
    +
    dotnet add package Elmah.Io.Umbraco --version 4.2.21
    +
    <PackageReference Include="Elmah.Io.Umbraco" Version="4.2.21" />
    +
    paket add Elmah.Io.Umbraco --version 4.2.21
    +
    +

    During the installation, you will be presented with a dialog asking for your API key and log ID. Hit F5 and watch messages start flowing into elmah.io.

    +
    +

    Unless serious security issues in the Elmah.Io.Umbraco v4 package are found, new features will be added to the v5 package only (supporting Umbraco 10 and newer).

    +
    +

    Configuration

    +

    If you are running on the default Umbraco template, all necessary configuration is added during the installation of the Elmah.Io.Umbraco NuGet package. If your web.config file for some reason isn't updated during installation, you can configure elmah.io manually: Configure elmah.io manually. Likewise, the installer configures the elmah.io sink for Serilog in your config\serilog.user.config file.

    +

    Different environments

    +

    You may have different environments like Staging and Production. At least you have two: Localhost and Production. If you want to log to different error logs depending on the current environment, check out Use multiple logs for different environments. Web.config transformations work on the Web.config file only but you may have other config files that need transformation as well. In terms of elmah.io, the serilog.user.config file also includes elmah.io configuration that you may want to disable on localhost and include on production. If you are running on Umbraco Cloud this is natively supported as explained here: Config Transforms. Even in self-hosted environments, you can achieve something similar using the SlowCheetah extension. Check out this question on Our for details: Deploying different umbracoSettings.config for different environments.

    +

    Umbraco 7

    +

    We still support Umbraco 7 through the Elmah.Io.Umbraco package version 3.2.35:

    +
    Install-Package Elmah.Io.Umbraco -Version 3.2.35
    +
    dotnet add package Elmah.Io.Umbraco --version 3.2.35
    +
    <PackageReference Include="Elmah.Io.Umbraco" Version="3.2.35" />
    +
    paket add Elmah.Io.Umbraco --version 3.2.35
    +
    +
    +

    New features will be added to the updated package for Umbraco 10 and newer only.

    +
    +

    Umbraco Cloud

    +

    When using Umbraco Cloud, you may not have a local clone of the source code. To install elmah.io on Umbraco Cloud, follow these steps:

    +
      +
    • +

      Clone your Umbraco Cloud project to a local folder as explained here: Working with a Local Clone.

      +
    • +
    • +

      Where you need to install and configure the Elmah.Io.Umbraco package depends on the Umbraco major version you are running on Umbraco Cloud. For Umbraco 7-8, all changes should be made in the *.Web project only and all commits from within that folder as well. Don't commit and push anything in the root folder. For Umbraco versions above 8, all changes should be made in the src\UmbracoProject folder.

      +
    • +
    • +

      Follow the installation steps for your Umbraco version as specified in the beginning of this document.

      +
    • +
    • +

      Commit and push all changes to the git repository. This will add elmah.io logging to your remote Umbraco Cloud project.

      +
    • +
    +

    In case you want logging to different elmah.io logs from each Umbraco Cloud environment, please check out Umbraco's support for config transformations here: Config transforms.

    +

    Umbraco Uno

    +
    +

    Umbraco Uno has been discontinued.

    +
    +

    Installing elmah.io in Umbraco Uno follows the process of installing it onto Umbraco Cloud. To modify code and configuration in Uno you will need a Umbraco Uno Standard plan or higher. Also, you need to enable Custom Code to clone the code locally. This can be done from Uno by clicking the Enable custom code button:

    +

    Enable custom code

    +

    After enabling Custom Code you can create a Development environment and follow the steps in the Umbraco Cloud documentation.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-uno/index.html b/logging-to-elmah-io-from-uno/index.html new file mode 100644 index 0000000000..ce75b38baa --- /dev/null +++ b/logging-to-elmah-io-from-uno/index.html @@ -0,0 +1,684 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Uno + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Uno

    +
    +

    The Uno integration for elmah.io is currently in prerelease.

    +
    +

    Integrating Uno with elmah.io is done by installing the Elmah.Io.Uno NuGet package:

    +
    Install-Package Elmah.Io.Uno -IncludePrerelease
    +
    dotnet add package Elmah.Io.Uno --prerelease
    +
    <PackageReference Include="Elmah.Io.Uno" Version="4.0.19-pre" />
    +
    paket add Elmah.Io.Uno
    +
    +

    While configured in the shared project, the NuGet package will need to be installed in all platform projects.

    +

    Elmah.Io.Uno comes with a logger for Microsoft.Extensions.Logging. To configure the logger, open the App.xaml.cs file and locate the InitializeLogging method. Here you will see the logging configuration for your application. Include logging to elmah.io by calling the AddElmahIo method:

    +

    var factory = LoggerFactory.Create(builder =>
    +{
    +    // ...
    +    builder.AddElmahIo("API_KEY", new Guid("LOG_ID"));
    +    // ...
    +});
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the log Id of the log you want to log to.

    +

    elmah.io will now automatically log all warning, error, and fatal messages to elmah.io. Uno log messages internally, but you can also do manual logging like this:

    +

    this.Log().LogWarning("Oh no");
    +

    +

    Logging with Uno's log helper will require you to add the following usings:

    +

    using Microsoft.Extensions.Logging;
    +using Uno.Extensions;
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-vue/index.html b/logging-to-elmah-io-from-vue/index.html new file mode 100644 index 0000000000..66109c4df3 --- /dev/null +++ b/logging-to-elmah-io-from-vue/index.html @@ -0,0 +1,675 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Vue + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    NuGet +npm +Samples

    +

    Logging to elmah.io from Vue

    +

    To log all errors from a Vue.js application, install the elmah.io.javascript npm package as described in Logging from JavaScript or include it with a direct <script> include:

    +

    <script src="https://cdn.jsdelivr.net/gh/elmahio/elmah.io.javascript@latest/dist/elmahio.min.js" type="text/javascript"></script>
    +

    +

    Before initializing the application, include the following code:

    +

    var logger = new Elmahio({
    +  apiKey: "API_KEY",
    +  logId: "LOG_ID"
    +});
    +Vue.config.errorHandler = function (err, vm, info) {
    +  logger.error(err.message, err);
    +};
    +Vue.config.warnHandler = function (msg, vm, trace) {
    +  logger.warning(msg);
    +};
    +

    +

    elmah.io.javascript will automatically log all errors raised through window.onerrorand log additional errors and warnings from Vue.js through the errorHandler and warnHandler functions. If you want to exclude warnings, simply remove the warnHandler function.

    +

    Check out the Elmah.Io.JavaScript.VueJs sample for some real working code.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-wcf/index.html b/logging-to-elmah-io-from-wcf/index.html new file mode 100644 index 0000000000..3a8cf96ee1 --- /dev/null +++ b/logging-to-elmah-io-from-wcf/index.html @@ -0,0 +1,720 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from WCF + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from WCF

    +

    ELMAH (the open-source project) and WCF aren't exactly known to go hand in hand. But, with a bit of custom code, logging exceptions from WCF to elmah.io is possible.

    +

    Let's get started. Install elmah.io into your WCF project using NuGet:

    +
    Install-Package Elmah.Io
    +
    dotnet add package Elmah.Io
    +
    <PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add Elmah.Io
    +
    +

    During the installation, you will be asked for your API key (Where is my API key?) and log ID (Where is my log ID?).

    +

    Add a new class named HttpErrorHandler:

    +

    public class HttpErrorHandler : IErrorHandler
    +{
    +    public bool HandleError(Exception error)
    +    {
    +        return false;
    +    }
    +
    +    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    +    {
    +        if (error != null)
    +        {
    +            Elmah.ErrorSignal.FromCurrentContext().Raise(error);
    +        }
    +    }
    +}
    +

    +

    This is an implementation of WCF's IErrorHandler that instructs WCF to log any errors to ELMAH, using the Raise-method on ErrorSignal.

    +

    Then create an attribute named ServiceErrorBehaviourAttribute:

    +

    public class ServiceErrorBehaviourAttribute : Attribute, IServiceBehavior
    +{
    +    Type errorHandlerType;
    +
    +    public ServiceErrorBehaviourAttribute(Type errorHandlerType)
    +    {
    +        this.errorHandlerType = errorHandlerType;
    +    }
    +
    +    public void Validate(ServiceDescription description, ServiceHostBase serviceHostBase)
    +    {
    +    }
    +
    +    public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
    +        BindingParameterCollection bindingParameters)
    +    {
    +    }
    +
    +    public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
    +    {
    +        IErrorHandler errorHandler;
    +        errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType);
    +        foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers)
    +        {
    +            ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher;
    +            channelDispatcher.ErrorHandlers.Add(errorHandler);
    +        }
    +    }
    +}
    +

    +

    We'll use the ServiceErrorBehaviourAttribute class for decorating endpoints which we want logging uncaught errors to ELMAH. Add the new attribute to your service implementation like this:

    +

    [ServiceErrorBehaviour(typeof(HttpErrorHandler))]
    +public class Service1 : IService1
    +{
    +    // ...
    +}
    +

    +

    That's it. Services decorated with the ServiceErrorBehaviourAttribute now logs exceptions to ELMAH.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-web-api/index.html b/logging-to-elmah-io-from-web-api/index.html new file mode 100644 index 0000000000..f377332ded --- /dev/null +++ b/logging-to-elmah-io-from-web-api/index.html @@ -0,0 +1,716 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from ASP.NET Web API + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Web API

    +

    Web API provides its own mechanism for handling errors, why ELMAH’s modules and handlers don't work there. Luckily, Richard Dingwall created the Elmah.Contrib.WebApi NuGet package to fix this. We've built a package for ASP.NET Web API exclusively, which installs all the necessary packages.

    +

    To start logging exceptions from Web API, install the Elmah.Io.WebApi NuGet package:

    +
    Install-Package Elmah.Io.WebApi
    +
    dotnet add package Elmah.Io.WebApi
    +
    <PackageReference Include="Elmah.Io.WebApi" Version="5.*" />
    +
    paket add Elmah.Io.WebApi
    +
    +

    During the installation, you will be asked for your API key (Where is my API key?) and log ID (Where is my log ID?).

    +
    + +
    + +
    +
    +

    Add the following code to your WebApiConfig.cs file:

    +

    public static class WebApiConfig
    +{
    +    public static void Register(HttpConfiguration config)
    +    {
    +        // ...
    +        config.Services.Add(typeof(IExceptionLogger), new ElmahExceptionLogger());
    +        // ...
    +    }
    +}
    +

    +

    The registered IExceptionLogger intercepts all thrown exceptions, even errors in controller constructors and routing errors.

    +
    +
    +

    Add the following code to your Global.asax.cs file:

    +

    protected void Application_Start()
    +{
    +    // ...
    +    GlobalConfiguration.Configuration.Filters.Add(new ElmahHandleErrorApiAttribute());
    +    // ...
    +}
    +

    +

    In this case you register a new global filter with Web API. The downside of this approach is, that only errors thrown in controller actions are logged.

    +
    +
    +

    All uncaught exceptions in ASP.NET Web API are now logged to elmah.io

    +

    Logging from exception/action filters

    +

    It's a widely used Web API approach, to handle all exceptions in a global exception/action filter and return a nicely formatted JSON/XML error response to the client. This is a nice approach to avoid throwing internal server errors, but it also puts ELMAH out of the game. When catching any exception manually and converting it to a response message, errors won't be logged in elmah.io.

    +

    To overcome this, errors should be logged manually from your global exception/action filter:

    +

    public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute 
    +{
    +    public override void OnException(HttpActionExecutedContext context)
    +    {
    +        ErrorSignal.FromCurrentContext().Raise(context.Exception);
    +
    +        // Now generate the result to the client
    +    }
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-web-pages/index.html b/logging-to-elmah-io-from-web-pages/index.html new file mode 100644 index 0000000000..2fbc27d546 --- /dev/null +++ b/logging-to-elmah-io-from-web-pages/index.html @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Web Pages + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet

    +

    Logging to elmah.io from Web Pages

    +

    Logging from ASP.NET Web Pages uses our integration with ELMAH (the open-source project). This means that the installation is identical to installing elmah.io in ASP.NET Web Forms.

    +

    For a full guide to installing the Elmah.Io NuGet package in your Web Pages project go to: Logging to elmah.io from ASP.NET / WebForms.

    +

    If the dialog requesting you to input an API key and a Log ID is not shown during the installation of the NuGet package, ELMAH configuration needs to be checked manually as explained here: Configure elmah.io manually.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-winforms/index.html b/logging-to-elmah-io-from-winforms/index.html new file mode 100644 index 0000000000..45e64e52cf --- /dev/null +++ b/logging-to-elmah-io-from-winforms/index.html @@ -0,0 +1,705 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Windows Forms + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to elmah.io from Windows Forms

    +

    elmah.io logging can be easily added to Windows Forms applications. We don't provide a package specific for WinForms, but the Elmah.Io.Client package, combined with a bit of code, will achieve just the same.

    +

    To start logging to elmah.io, install the Elmah.Io.Client NuGet package:

    +
    Install-Package Elmah.Io.Client
    +
    dotnet add package Elmah.Io.Client
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +
    +

    Add the following usings to the Program.cs file:

    +

    using Elmah.Io.Client;
    +using System.Security.Principal;
    +using System.Threading;
    +

    +

    Add an event handler to the ThreadException event in the Main method:

    +

    Application.ThreadException += Application_ThreadException;
    +

    +

    Finally, add the Application_ThreadException method:

    +

    static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    +{
    +    var logger = ElmahioAPI.Create("API_KEY");
    +    var exception = e.Exception;
    +    var baseException = exception.GetBaseException();
    +    logger.Messages.Create("LOG_ID", new CreateMessage
    +    {
    +        DateTime = DateTime.UtcNow,
    +        Detail = exception?.ToString(),
    +        Type = baseException?.GetType().FullName,
    +        Title = baseException?.Message ?? "An error occurred",
    +        Data = exception.ToDataList(),
    +        Severity = "Error",
    +        Source = baseException?.Source,
    +        User = WindowsIdentity.GetCurrent().Name,
    +    });
    +
    +    Application.Exit();
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the id of the log (Where is my log ID?) where you want errors logged.

    +

    This example closes the application when an error occurs. If you only want to log the error, make sure to re-use the logger object:

    +

    private static IElmahioAPI logger;
    +
    +static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    +{
    +    if (logger == null)
    +    {
    +        logger = ElmahioAPI.Create("API_KEY");
    +    }
    +
    +    // ...
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-wpf/index.html b/logging-to-elmah-io-from-wpf/index.html new file mode 100644 index 0000000000..0a92e21f55 --- /dev/null +++ b/logging-to-elmah-io-from-wpf/index.html @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from WPF + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from WPF

    + +

    elmah.io logging can be easily added to WPF applications. To start logging to elmah.io, install the Elmah.Io.Wpf NuGet package:

    +
    Install-Package Elmah.Io.Wpf
    +
    dotnet add package Elmah.Io.Wpf
    +
    <PackageReference Include="Elmah.Io.Wpf" Version="5.*" />
    +
    paket add Elmah.Io.Wpf
    +
    +

    Next, initialize elmah.io in the App.xaml.cs file:

    +

    public partial class App : Application
    +{
    +    public App()
    +    {
    +        ElmahIoWpf.Init(new ElmahIoWpfOptions
    +        {
    +            ApiKey = "API_KEY",
    +            LogId = new Guid("LOG_ID")
    +        });
    +    }
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the id of the log (Where is my log ID?) where you want errors logged.

    +
    +

    Remember to generate a new API key with messages_write permission only. This makes it easy to revoke the API key if someone starts sending messages to your log with your key.

    +
    +

    That's it. All uncaught exceptions are now logged to elmah.io.

    +

    Logging exceptions manually

    +

    Once initialized using the Init call, exceptions can be logged manually:

    +

    ElmahIoWpf.Log(new Exception());
    +

    + +

    The Elmah.Io.Wpf package automatically records breadcrumbs when clicking buttons and opening/closing windows. To manually include a breadcrumb you can include the following code:

    +

    ElmahIoWpf.AddBreadcrumb(new Client.Breadcrumb(DateTime.UtcNow, severity:"Information", action:"Save", message:"Record save"));
    +

    +

    severity can be set to Verbose, Debug, Information, Warning, Error, or Fatal. The value of action is a string of your choice. If using one of the following values, the action will get a special icon in the elmah.io UI: click, submit, navigation, request, error, warning, fatal. The message field can be used to describe the breadcrumb in more detail and/or include IDs or similar related to the breadcrumb.

    +

    The number of breadcrumbs to store in memory is 10 as a default. If you want to lower or increase this number, set the MaximumBreadcrumbs property during initialization:

    +

    ElmahIoWpf.Init(new ElmahIoWpfOptions
    +{
    +    // ...
    +    MaximumBreadcrumbs = 20,
    +});
    +

    +

    Additional options

    +

    Setting application name

    +

    The application name can be set on all logged messages by setting the Application property on ElmahIoWpfOptions during initialization:

    +

    ElmahIoWpf.Init(new ElmahIoWpfOptions
    +{
    +    // ...
    +    Application = "WPF on .NET 6",    
    +});
    +

    +

    Hooks

    +

    The ElmahIoWpfOptions class also supports a range of actions to hook into various stages of logging errors. Hooks are registered as actions when installing Elmah.Io.Wpf:

    +

    ElmahIoWpf.Init(new ElmahIoWpfOptions
    +{
    +    // ...
    +    OnFilter = msg =>
    +    {
    +        return msg.Type.Equals("System.NullReferenceException");
    +    },
    +    OnMessage = msg =>
    +    {
    +        msg.Version = "42";
    +    },
    +    OnError = (msg, ex) =>
    +    {
    +        // Log somewhere else
    +    }
    +});
    +

    +

    The OnFilter action can be used to ignore/filter specific errors. In this example, all errors of type System.NullReferenceException is ignored. The OnMessage action can be used to decorate/enrich all errors with different information. In this example, all errors get a version number of 42. The OnError action can be used to handle if the elmah.io API is down. While this doesn't happen frequently, you might want to log errors elsewhere.

    +

    Legacy

    +

    Before the Elmah.Io.Wpf package was developed, this was the recommended way of installing elmah.io in WPF.

    +

    To start logging to elmah.io, install the Elmah.Io.Client NuGet package:

    +
    Install-Package Elmah.Io.Client
    +
    dotnet add package Elmah.Io.Client
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +
    +

    Add the following usings to the App.xaml.cs file:

    +

    using Elmah.Io.Client;
    +using System.Diagnostics;
    +using System.Security.Principal;
    +using System.Threading.Tasks;
    +

    +

    Add the following code:

    +

    private IElmahioAPI logger;
    +
    +public App()
    +{
    +    logger = ElmahioAPI.Create("API_KEY");
    +
    +    AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
    +        LogException(args.ExceptionObject as Exception);
    +
    +    TaskScheduler.UnobservedTaskException += (sender, args) =>
    +        LogException(args.Exception);
    +
    +    Dispatcher.UnhandledException += (sender, args) =>
    +    {
    +        if (!Debugger.IsAttached)
    +            LogException(args.Exception);
    +    };
    +}
    +
    +private void LogException(Exception exception)
    +{
    +    var baseException = exception.GetBaseException();
    +    logger.Messages.Create("LOG_ID", new CreateMessage
    +    {
    +        DateTime = DateTime.UtcNow,
    +        Detail = exception?.ToString(),
    +        Type = baseException?.GetType().FullName,
    +        Title = baseException?.Message ?? "An error occurred",
    +        Data = exception.ToDataList(),
    +        Severity = "Error",
    +        Source = baseException?.Source,
    +        User = WindowsIdentity.GetCurrent().Name,
    +    });
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID with the id of the log (Where is my log ID?) where you want errors logged.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-elmah-io-from-xamarin/index.html b/logging-to-elmah-io-from-xamarin/index.html new file mode 100644 index 0000000000..037d0cdf44 --- /dev/null +++ b/logging-to-elmah-io-from-xamarin/index.html @@ -0,0 +1,972 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to elmah.io from Xamarin + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Build status +NuGet +Samples

    +

    Logging to elmah.io from Xamarin

    + +
    +

    The Xamarin integration for elmah.io is currently in prerelease.

    +
    +

    Integrating Xamarin with elmah.io is done by installing the Elmah.Io.Xamarin NuGet package:

    +
    Install-Package Elmah.Io.Xamarin -IncludePrerelease
    +
    dotnet add package Elmah.Io.Xamarin --prerelease
    +
    <PackageReference Include="Elmah.Io.Xamarin" Version="4.0.30-pre" />
    +
    paket add Elmah.Io.Xamarin
    +
    +

    For each platform (Android and iOS) you will need to set up elmah.io as illustrated in the following sections. The code is the same for both Xamarin and Xamarin.Forms.

    +
    +
    + +
    +
    + +
    +
    +

    Open the MainActivity.cs file and add the following using statements:

    +

    using System;
    +using Elmah.Io.Xamarin;
    +

    +

    Locate the OnCreate method and add the following code before all other lines:

    +

    ElmahIoXamarin.Init(new ElmahIoXamarinOptions
    +{
    +    ApiKey = "API_KEY",
    +    LogId = new Guid("LOG_ID"),
    +});
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the log Id of the log you want to log to.

    +

    Calling the Init method will initialize elmah.io. For more configuration options see the Additional configuration section.

    +
    +
    +

    Open the Main.cs file and add the following using statements:

    +

    using System;
    +using Elmah.Io.Xamarin;
    +

    +

    Locate the Main method and add the following code after the call to UIApplication.Main:

    +

    ElmahIoXamarin.Init(new ElmahIoXamarinOptions
    +{
    +    ApiKey = "API_KEY",
    +    LogId = new Guid("LOG_ID"),
    +});
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the log Id of the log you want to log to.

    +

    Calling the Init method will initialize elmah.io. For more configuration options see the Additional configuration section.

    +
    +
    +

    Log exceptions manually

    +

    Once the ElmahIoXamarin.Init method has been configured during initialization of the app, any exception can be logged manually using the Log methods available in the Elmah.Io.Xamarin namespace:

    +

    try
    +{
    +    // Code that may break
    +}
    +catch (Exception e)
    +{
    +    // Log the exception with the Log extension method:
    +
    +    e.Log();
    +
    +    // or use the Log method on ElmahIoXamarin:
    +
    +    ElmahIoXamarin.Log(e);
    +}
    +

    + +

    Breadcrumbs can be a great help when needing to figure out how a user ended up with an error. To log breadcrumbs, you can use the AddBreadcrumb method on ElmahIoXamarin. The following is a sample for Android which will log breadcrumbs on interesting events:

    +

    public class MainActivity : AppCompatActivity, BottomNavigationView.IOnNavigationItemSelectedListener
    +{
    +    public override void OnBackPressed()
    +    {
    +        ElmahIoXamarin.AddBreadcrumb("OnBackPressed", DateTime.UtcNow, action: "Navigation");
    +        base.OnBackPressed();
    +    }
    +
    +    protected override void OnPause()
    +    {
    +        ElmahIoXamarin.AddBreadcrumb("OnPause", DateTime.UtcNow);
    +        base.OnPause();
    +    }
    +
    +    // ...
    +
    +    public bool OnNavigationItemSelected(IMenuItem item)
    +    {
    +        switch (item.ItemId)
    +        {
    +            case Resource.Id.navigation_home:
    +                ElmahIoXamarin.AddBreadcrumb("Navigate to Home", DateTime.UtcNow, action: "Navigation");
    +                textMessage.SetText(Resource.String.title_home);
    +                return true;
    +            case Resource.Id.navigation_dashboard:
    +                ElmahIoXamarin.AddBreadcrumb("Navigate to Dashboard", DateTime.UtcNow, action: "Navigation");
    +                textMessage.SetText(Resource.String.title_dashboard);
    +                return true;
    +            case Resource.Id.navigation_notifications:
    +                ElmahIoXamarin.AddBreadcrumb("Navigate to Notifications", DateTime.UtcNow, action: "Navigation");
    +                textMessage.SetText(Resource.String.title_notifications);
    +                return true;
    +        }
    +        return false;
    +    }
    +}
    +

    +

    Additional configuration

    +

    Besides the mandatory properties ApiKey and LogId the ElmahIoXamarinOptions provide a range of other configuration parameters.

    +

    Application

    +

    The elmah.io integration for Xamarin will automatically use the package name for the Application field. To override this you can set the application name in settings:

    +

    ElmahIoXamarin.Init(new ElmahIoXamarinOptions
    +{
    +    // ...
    +    Application = "MyApp"
    +});
    +

    +

    Version

    +

    The elmah.io integration for Xamarin will automatically use the package version for the Version field. To override this you can set the version string in settings:

    +

    ElmahIoXamarin.Init(new ElmahIoXamarinOptions
    +{
    +    // ...
    +    Version = "1.0.2"
    +});
    +

    +

    Decorating all messages

    +

    All log messages logged through this integration can be decorated with the OnMessage action:

    +

    ElmahIoXamarin.Init(new ElmahIoXamarinOptions
    +{
    +    // ...
    +    OnMessage = msg =>
    +    {
    +        msg.Source = "Custom source";
    +    }
    +});
    +

    +

    Filtering log messages

    +

    Log messages can be filtered directly on the device to avoid specific log messages from being sent to elmah.io with the OnFilter function:

    +

    ElmahIoXamarin.Init(new ElmahIoXamarinOptions
    +{
    +    // ...
    +    OnFilter = msg => msg.Title.Contains("foo")
    +});
    +

    +

    This code will automatically ignore all log messages with the text foo in the title.

    +

    Handling errors

    +

    You may want to handle the scenario where the device cannot communicate with the elmah.io API. You can use the OnError action:

    +

    ElmahIoXamarin.Init(new ElmahIoXamarinOptions
    +{
    +    // ...
    +    OnError = (msg, ex) =>
    +    {
    +        // Do something with ex
    +    }
    +});
    +

    +

    Legacy integration

    +

    If you prefer you can configure elmah.io manually without the use of the Elmah.Io.Xamarin package. This is not the recommended way to integrate with elmah.io from Xamarin and this approach will be discontinued.

    +

    Start by installing the Elmah.Io.Client NuGet package:

    +
    Install-Package Elmah.Io.Client
    +
    dotnet add package Elmah.Io.Client
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +
    +

    If you are targeting a single platform, you can install the package directly in the startup project. If you are targeting multiple platforms, you can either install the package in all platform-specific projects or a shared project.

    +

    Additional steps will vary from platform to platform.

    +
    +
    + +
    +
    + +
    +
    +

    Locate your main activity class and look for the OnCreate method. Here, you'd want to set up event handlers for when uncaught exceptions happen:

    +

    protected override void OnCreate(Bundle savedInstanceState)
    +{
    +    // ...
    +
    +    AndroidEnvironment.UnhandledExceptionRaiser += AndroidEnvironment_UnhandledExceptionRaiser;
    +    AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
    +    TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
    +
    +    // ... LoadApplication(new App()); etc.
    +}
    +

    +

    Next, implement a method that can log an exception to elmah.io:

    +

    private void LogExceptionToElmahIo(Exception exception)
    +{
    +    if (exception == null) return;
    +
    +    if (elmahIoClient == null)
    +    {
    +        elmahIoClient = ElmahioAPI.Create("API_KEY");
    +    }
    +
    +    var packageInfo = PackageManager.GetPackageInfo(PackageName, PackageInfoFlags.MetaData);
    +
    +    var baseException = exception?.GetBaseException();
    +    var errorMessage = baseException?.Message ?? "Unhandled exception";
    +    try
    +    {
    +        elmahIoClient.Messages.Create("LOG_ID", new CreateMessage
    +        {
    +            Data = exception?.ToDataList(),
    +            DateTime = DateTime.UtcNow,
    +            Detail = exception?.ToString(),
    +            Severity = "Error",
    +            Source = baseException?.Source,
    +            Title = errorMessage,
    +            Type = baseException?.GetType().FullName,
    +            Version = packageInfo.VersionName,
    +            Application = packageInfo.PackageName,
    +        });
    +    }
    +    catch (Exception inner)
    +    {
    +        Android.Util.Log.Error("elmahio", inner.Message);
    +    }
    +
    +    // Log to Android Device Logging.
    +    Android.Util.Log.Error("crash", errorMessage);
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the log Id of the log you want to log to.

    +

    Finally, implement the three event handlers that we added in the first step:

    +

    private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
    +{
    +    LogExceptionToElmahIo(e.Exception);
    +    e.SetObserved();
    +}
    +
    +private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    +{
    +    LogExceptionToElmahIo(e.ExceptionObject as Exception);
    +}
    +
    +private void AndroidEnvironment_UnhandledExceptionRaiser(object sender, RaiseThrowableEventArgs e)
    +{
    +    LogExceptionToElmahIo(e.Exception);
    +    e.Handled = true;
    +}
    +

    +
    +
    +

    Locate your main application class and look for the Main method. Here, you'd want to set up event handlers for when uncaught exceptions happen:

    +

    static void Main(string[] args)
    +{
    +    // ...
    +
    +    AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
    +    TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
    +}
    +

    +

    Next, implement a method that can log an exception to elmah.io:

    +

    private static void LogExceptionToElmahIo(Exception exception)
    +{
    +    if (exception == null) return;
    +
    +    if (elmahIoClient == null)
    +    {
    +        elmahIoClient = ElmahioAPI.Create("API_KEY");
    +    }
    +
    +    var baseException = exception?.GetBaseException();
    +    elmahIoClient.Messages.Create("LOG_ID", new CreateMessage
    +    {
    +        Data = exception?.ToDataList(),
    +        DateTime = DateTime.UtcNow,
    +        Detail = exception?.ToString(),
    +        Severity = "Error",
    +        Source = baseException?.Source,
    +        Title = baseException?.Message ?? "Unhandled exception",
    +        Type = baseException?.GetType().FullName,
    +    });
    +}
    +

    +

    Replace API_KEY with your API key (Where is my API key?) and LOG_ID (Where is my log ID?) with the log Id of the log you want to log to.

    +

    Finally, implement the two event handlers that we added in the first step:

    +

    private static void TaskScheduler_UnobservedTaskException(
    +    object sender, UnobservedTaskExceptionEventArgs e)
    +{
    +    LogExceptionToElmahIo(e.Exception);
    +    e.SetObserved();
    +}
    +
    +private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    +{
    +    LogExceptionToElmahIo(e.ExceptionObject as Exception);
    +}
    +

    +
    +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging-to-multiple-elmah-logs/index.html b/logging-to-multiple-elmah-logs/index.html new file mode 100644 index 0000000000..7f339c5fd4 --- /dev/null +++ b/logging-to-multiple-elmah-logs/index.html @@ -0,0 +1,679 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Logging to multiple ELMAH logs + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Logging to multiple ELMAH logs

    +

    Unfortunately, ELMAH (the open source project) doesn't support multiple log targets like other logging frameworks like Serilog. This makes logging to multiple logs a bit tricky but in no way impossible. Let's say that you're using ELMAH in your web application and configured it to log everything in SQL Server. If you look through your web.config file, you will have code looking like this somewhere:

    +

    <elmah>
    +    <errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="elmah"/>
    +</elmah>
    +

    +

    As you probably know, this tells ELMAH to log all unhandled errors in SQL Server with the connection string “elmah”. You cannot add more <errorLog> elements, why logging into a second log seems impossible. Meet ELMAH's Logged event, which is a great hook to log to multiple targets. Install the Elmah.Io NuGet package and add the following code to your global.asax.cs file:

    +

    void ErrorLog_Logged(object sender, Elmah.ErrorLoggedEventArgs args)
    +{
    +    var elmahIoLog = new Elmah.Io.ErrorLog(ElmahioAPI.Create("API_KEY"), new Guid("LOG_ID"));
    +    elmahIoLog.Log(args.Entry.Error);
    +}
    +

    +

    In the above code, we listen for the Logged event by simply declaring a method named ErrorLog_Logged. When called, we create a new (Elmah.Io.)ErrorLog instance with an IElmahioAPI object and the log ID. Remember to replace API_KEY with your API key (Where is my API key?) and LOG_ID with your log ID (Where is my log ID?). You may want to share the ElmahioAPI object between requests by declaring it as a private member. Next, we simply call the Log method with a new Error object. Bam! The error is logged both in SQL Server and in elmah.io.

    +

    If you only want to log certain types of errors in elmah.io, but everything to your normal log, you can extend your code like this:

    +

    void ErrorLog_Logged(object sender, Elmah.ErrorLoggedEventArgs args)
    +{
    +    if (args.Entry.Error.StatusCode == 500)
    +    {
    +        var elmahIoLog = new Elmah.Io.ErrorLog(/*...*/);
    +        elmahIoLog.Log(args.Entry.Error);
    +    }
    +}
    +

    +

    This time we only begin logging to elmah.io, if the thrown exception is of type HttpException and contains an HTTP status code of 500. This example only logs errors with status code 500 in elmah.io and all errors in your normal error log. If you want to create this filter on all logs, you should use the ErrorLog_Filtering method instead. This method is called before ErrorLog_Logged and before actually logging the error to your normal error log.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/managing-environments/index.html b/managing-environments/index.html new file mode 100644 index 0000000000..0640feff5e --- /dev/null +++ b/managing-environments/index.html @@ -0,0 +1,668 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Managing Environments + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Managing Environments

    +

    Environments is a grouping feature on elmah.io that will help you group similar logs. By default you will have four environments in your organization: Development, Test, Staging, and Production. While this is the default view, additional environments can be added or environments named differently. The names don't need to match environments if you prefer a different naming scheme. Other ways to use environments would be to group logs by customers, departments, or servers.

    +

    When creating a new log from the elmah.io Dashboard, you can pick an environment in the dropdown:

    +

    Pick environment

    +

    Once logs are added to environments, they are nicely grouped in the left menu and on the list of log boxes on the dashboard:

    +

    Environments

    +

    Logs can be ordered using the drag-and-drop button at the top but only inside the same environment. If you need to move a log to another environment, click Edit (the pencil icon on the log box) and select the new environment from the dropdown.

    +

    If you want to show logs within one or more specific environments on the dashboard, you can use the Environments dropdown:

    +

    Environments dropdown

    +

    The list of environments can be managed from your organization settings page. To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard:

    +

    Organization settings

    +

    On the organization settings page, select the Environments tab:

    +

    Manage environments

    +

    The order of environments can be changed using drag and drop. New environments can be created and existing ones deleted.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/managing-organisations-and-users/index.html b/managing-organisations-and-users/index.html new file mode 100644 index 0000000000..d8fcbd9fa9 --- /dev/null +++ b/managing-organisations-and-users/index.html @@ -0,0 +1,707 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Managing Organizations and Users + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Managing Organizations and Users

    + +

    Chances are that you are not the only one needing to access your logs. Luckily, elmah.io offers great features to manage the users in your organization and to specify who should be allowed access to what.

    +

    This guide is also available as a short video tutorial here:

    +

    + user-administration + +

    +

    To manage access, you will need to know about the concepts of users and organizations.

    +

    A user represents a person wanting to access one or more logs. Each user logs in using a username/password or a social provider of choice. A user can be added to one or more organizations. Each user has an access level within the organization as well as an access level on each log. The access level on the organization and the logs doesn't need to be the same.

    +

    An organization is a collection of users and their roles inside the organization. You will typically only need a single organization, representing all of the users in your company needing to access one or more logs on elmah.io. Your elmah.io subscription is attached to your organization and everyone with administrator access to the organization will be able to manage the subscription.

    +

    Adding existing users to an organization

    +

    To assign users to a log, you will need to add them to the organization first. When hovering the organization name in either the left menu or on the dashboard, you will see a small gear icon. When clicking the icon, you will be taken to the organization settings page:

    +

    Organization Settings

    +

    At first, the user creating the organization will be the only one on the list. To add a new user to the list, click the Add user button and input the user's email or name in the textbox. The dropdown will show a list of users on elmah.io matching your query.

    +
    +

    Each user needs to sign up on elmah.io before being visible through Add user. Jump to Invite new users to an organization to learn how to invite new users.

    +
    +

    When the new user is visible in the dropdown, click the user and select an access level. The chosen access level decides what the new user is allowed to do inside the organization. Read users are only allowed to view the organization, while Administrator users are allowed to add new users and delete the entire organization and all logs beneath it. The access level set for the user in the organization will become the user's access level on all new logs inside that organization as well.

    +

    To change the access level on an added user, click one of the grouped buttons to the right of the user's name. Changing a user's access level on the organization won't change the user's access level on each log. To delete a user from the organization, click the red delete button to the far right.

    +

    When a user is added to an organization, the user will automatically have access to all new logs created in that organization. For security reasons, a new user added to the organization, will not have access to existing logs in the organization. To assign the new user to existing logs, assign an access level on each log by clicking the settings button to the right of the user:

    +

    Manage log(s) access

    +
    +

    Awarding a user Administrator on a log doesn't give them Administrator rights to the organization.

    +
    +

    To assign a user to all logs, click the None, Read, Write, or Administrator buttons in the table header above the list of logs.

    +

    Likewise, organization administrators can assign users to different emails for each log in the organization:

    +

    Manage email access

    +

    This view is similar to the one the user has on the profile when signing into elmah.io. Make sure to notify users when you assign emails to them to avoid them marking emails as spam.

    +

    Invite new users to an organization

    +

    If someone not already created as a user on elmah.io needs access to your organization, you can use the Invite feature. Inviting users will send them an email telling them to sign up for elmah.io and automatically add them to your organization.

    +

    To invite a user click the Invite user button and input the new user's email. Select an organization access level and click the green Invite user button. This will add the new user to the organization and display it as "Invited" until the user signs up.

    +

    Control security

    +

    You may have requirements of using two-factor authentication or against using social sign-ins in your company. These requirements can be configured on elmah.io as well. Click the Security button above the user's list to set it up:

    +

    Users security

    +

    Using this view you can allow or disallow sign-ins using:

    +
      +
    • An elmah.io username and password
    • +
    • Twitter
    • +
    • Facebook
    • +
    • Microsoft
    • +
    • Google
    • +
    +

    Notice that disallowing different sign-in types will still allow users in your organization to sign into elmah.io. As soon as a disallowed user type is trying to access pages inside the organization and/or logs a page telling them which sign-in type or required settings is shown.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/missing-server-side-information-on-uptime-errors/index.html b/missing-server-side-information-on-uptime-errors/index.html new file mode 100644 index 0000000000..b750f4440f --- /dev/null +++ b/missing-server-side-information-on-uptime-errors/index.html @@ -0,0 +1,661 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Missing server- side information on uptime errors + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Missing server-side information on uptime errors

    +

    To decorate uptime errors with server-side error information, you will need a few things:

    +
      +
    1. The monitored website should be configured to log errors to elmah.io.
    2. +
    3. The uptime check needs to be created on the same log as the monitored website.
    4. +
    5. When installed in a previous version, errors generated by Uptime Monitoring are ignored by the BotBuster app (since Uptime Monitoring is also a bot). Uninstall the BotBuster app and enable the Filter Crawlers filter instead.
    6. +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/query-messages-using-full-text-search/index.html b/query-messages-using-full-text-search/index.html new file mode 100644 index 0000000000..ca57723334 --- /dev/null +++ b/query-messages-using-full-text-search/index.html @@ -0,0 +1,941 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Query messages using full-text search + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Query messages using full-text search

    +

    All messages sent to elmah.io, are indexed in Elasticsearch. Storing messages in a database like Elasticsearch opens up a world of possibilities. This article explains how to query your log messages using full-text search, Search Filters, and Lucene Query Syntax.

    + + +

    The easiest approach to start searching your log messages is by inputting search terms in the Search field on elmah.io:

    +

    Full-text query

    +

    We don't want to get into too much detail on how full-text works in Elasticsearch. In short, Elasticsearch breaks the query into the terms nominavi and voluptatibus and tries to match all log messages including those terms. Full-text search work on analyzed fields in Elasticsearch, which means that wildcards and other constructs are fully supported.

    +

    Full-text queries work great. when you want to do a quick search for some keywords like part of an exception message or stack trace. Remember that the entire log message is search, why a search for 500 would hit both log messages with status code 500 and the term 500 in the stack trace.

    +

    Search Filters

    +

    Search filters are built exclusively for elmah.io. They are built on top of Lucene Query Syntax (which we'll discuss in a minute), but much easier to write. Search filters are available through either the Add filter button below the search field or using various links and icons on the elmah.io UI.

    +

    Let's say we want to find all errors with a status code of 500:

    +

    Search filters

    +

    Adding the two filters is possible using a few clicks.

    +

    As mentioned previously, search filters are available throughout the UI too. In this example, a filter is used to find messages not matching a specified URL:

    +

    Search filter by URL

    +

    Search filters can be used in combination with full-text queries for greater flexibility.

    +

    Lucene Query Syntax

    +

    Elasticsearch is implemented on top of Lucene; a high-performance search engine, written entirely in Java. While Elasticsearch supports a lot of nice abstractions on top of Lucene, sometimes you just want close to the metal. This is when we need to introduce you to Lucene Query Syntax. The query syntax is a query language similar to the WHERE part of a SQL statement. Unlike SQL, the query syntax supports both filters (similar to SQL) and full-text queries.

    +

    All Lucene queries are made up of strings containing full-text search strings and/or terms combined with operators. A simple full-text query simply looks like this:

    +

    values to search for
    +

    +

    This will search log messages for 'values', 'to', 'search', and 'for'. For exact searches you can use quotes:

    +

    "values to search for"
    +

    +

    This will only find log messages where that exact string is present somewhere.

    +

    Queries can also search inside specific fields:

    +

    field:value
    +

    +

    This is similar to the WHERE part in SQL and will, in this example, search for the term 'value' inside the field named 'field'.

    +

    Both full-text queries and field-based queries can be combined with other queries using AND, OR, and NOT:

    +

    field1:value1 AND field2:value2 AND NOT field3:value3
    +

    +

    You can use operators known from C# if you prefer that syntax:

    +

    field1:value1 && field2:value2 && !field3:value3
    +

    +

    Full-text and field-based queries can be combined into complex queries:

    +

    field1:value1 && field2:"exact value" || (field1:value2 && "a full text query")
    +

    +

    Examples are worth a thousand words, why the rest of this document is examples of frequently used queries. If you think that examples are missing or have a problem with custom queries, let us know. We will extend this tutorial with the examples you need.

    +

    Find messages with type

    +

    type:"System.Web.HttpException"
    +

    +

    Find messages with status codes

    +

    statusCode:[500 TO 599]
    +

    +

    Find messages with URL and method

    +

    url:"/tester/" AND method:get
    +

    +

    Find messages with URL starting with

    +

    url:\/.well-known*
    +

    +

    The forward slash, in the beginning, needs to be escaped, since Lucene will understand it as the start of a regex otherwise.

    +

    Find messages by IP

    +

    remoteAddr:192.168.0.1
    +

    +

    Find messages by IP's

    +

    remoteAddr:192.68.0.*
    +

    +

    The examples above can be achieved using Search Filters as well. We recommend using Search Filters where possible and falling back to Lucene Query Syntax when something isn't supported through filters. An example is using OR which currently isn't possible using filters.

    +

    Field specification

    +

    As already illustrated through multiple examples in this document, all log messages consist of a range of fields. Here's a full overview of all fields, data types, etc. The .raw column requires a bit of explanation if you are not familiar with Elasticsearch. Fields that are marked as having .raw are indexed for full-text queries. This means that the values are tokenized and optimized for full-text search and not querying by exact values. In this case, a special raw field is created for supporting SQL like WHERE queries. To illustrate, searching for a part of a user agent would look like this:

    +

    userAgent:chrome
    +

    +

    And searching for a specific user agent would look like this:

    +

    userAgent.raw:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36"
    +

    +

    Here is the full set of fields:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameType.rawDescription
    applicationNamestringUsed to identify which application logged this message. You can use this if you have multiple applications and services logging into the same log.
    assignedTostringThe id of the user assigned to the log message. elmah.io user IDs are not something that is published on the UI anywhere why this field is intended for the My errors dashboard only.
    browserstringA short string classifying each log message to a browser. The value can be one of the following: chrome, safari, edge, firefox, opera, ie, other.
    categorystringThe log message category. Category can be a string of choice but typically contain a logging category set by a logging framework like NLog or Serilog. When logging through a logging framework, this field will be provided by the framework and not something that needs to be set manually.
    correlationIdstringCorrelationId can be used to group similar log messages into a single discoverable batch. A correlation ID could be a session ID from ASP.NET Core, a unique string spanning multiple microservices handling the same request, or similar.
    countrystringAn ISO 3166 two-letter country code in case we could resolve a country from the log message.
    detailstringA longer description of the message. For errors, this could be a stack trace, but it's really up to you what to log in there.
    domainstringThe domain name if it could be resolved from the server variables.
    hashstringA unique hash for a log message. The hash is used for multiple things on elmah.io like the new detection.
    hiddenbooleanA boolean indicating if a log message has been hidden through the UI, the API, or a hide rule.
    hostNamestringThe hostname of the server logging the message.
    isBotbooleanA boolean indicating is a log message is generated by an automated bot or crawler. This flag is set manually through the UI.
    isBotSuggestionbooleanA boolean indicating if the log message looks to be generated by an automated bot or crawler. Unlike the isBot field, this field is automatically set using machine learning and is available on the Enterprise plan only.
    isBurstbooleanA boolean indicating if the log message is a burst. Log messages are automatically marked as burst if we have seen it more than 50 times during the retention period of the purchased plan.
    isFixedbooleanA boolean indicating if the log message has been marked as fixed. Log messages can be marked as fixed from the UI.
    isHeartbeatbooleanA boolean indicating if the log message is logged from the elmah.io Heartbeats feature.
    isNewbooleanA boolean indicating if we have seen this unique log message before.
    isSpikebooleanA boolean indicating if the log message is logged from the elmah.io Spike feature.
    isUptimebooleanA boolean indicating if the log message is logged from the elmah.io Uptime Monitoring feature.
    messagestringThe textual title or headline of the message to log.
    messageTemplatestringThe title template of the message to log. This property can be used from logging frameworks that support structured logging like: "{user} says {quote}". In the example, titleTemplate will be this string and the title will be "Gilfoyle says It's not magic. It's talent and sweat".
    methodstringIf the log message relates to an HTTP request, you may send the HTTP method of that request. If you don't provide us with a method, we will try to find a key named REQUEST_METHOD in serverVariables.
    osstringA short string classifying each log message to an operating system. The value can be one of the following: ios, windows, android, macos, linux, other.
    remoteAddrstringThe IP address of the user generating this log message if it can be resolved from server variables.
    severitystringAn enum value representing the severity of this message. The following values are allowed: Verbose, Debug, Information, Warning, Error, Fatal.
    sourcestringThe source of the code logging the message. This could be the assembly name.
    statusCodenumberIf the message logged relates to an HTTP status code, you can put the code in this property. This would probably only be relevant for errors but could be used for logging successful status codes as well.
    timedateThe date and time in UTC of the message. If you don't provide us with a value, this will be set the current date and time in UTC.
    typestringThe type of message. If logging an error, the type of the exception would go into type but you can put anything in there, that makes sense for your domain.
    urlstringIf the log message relates to an HTTP request, you may send the URL of that request. If you don't provide us with an URL, we will try to find a key named URL in serverVariables.
    userstringAn identification of the user triggering this message. You can put the user's email address or your user key into this property.
    userAgentstringThe user agent of the user causing the log message if it can be resolved from server variables.
    versionstringVersions can be used to distinguish messages from different versions of your software. The value of the version can be a SemVer compliant string or any other syntax that you are using as your version numbering scheme.
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/remove-sensitive-form-data/index.html b/remove-sensitive-form-data/index.html new file mode 100644 index 0000000000..d23768552b --- /dev/null +++ b/remove-sensitive-form-data/index.html @@ -0,0 +1,668 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Remove sensitive form data from log messages + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Remove sensitive form data

    +

    You may have something like usernames and passwords in form posts on your website. Since elmah.io automatically logs the content of a failing form POST, sensitive data potentially ends up in your log. No one else but you and your company should get to look inside your log, but remember that everyone connected to the Internet, is a potential hacking victim.

    +

    In this example, we hide the value of a form value named SomeSecretFormField. Add the following code in the Application_Start method in the global.asax.cs file:

    +

    Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client
    +var logger = Elmah.Io.ErrorLog.Client;
    +logger.OnMessage += (sender, args) =>
    +{
    +    var form = args.Message.Form.FirstOrDefault(f => f.Key == "SomeSecretFormField");
    +    if (form != null)
    +    {
    +        form.Value = "***hidden***";
    +    }
    +};
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000000..5975a417a1 --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /cdn-cgi/ \ No newline at end of file diff --git a/roslyn-analyzers-for-elmah-io-and-aspnet-core/index.html b/roslyn-analyzers-for-elmah-io-and-aspnet-core/index.html new file mode 100644 index 0000000000..24a439b69e --- /dev/null +++ b/roslyn-analyzers-for-elmah-io-and-aspnet-core/index.html @@ -0,0 +1,718 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Roslyn analyzers for elmah.io and ASP.NET Core + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Roslyn analyzers for elmah.io and ASP.NET Core

    + +
    +

    The Roslyn analyzers for elmah.io and ASP.NET Core has reached end of life. The analyzers are no longer updated and won't work for top-level statements or when configuring Elmah.Io.AspNetCore in the Program.cs file. To validate the installation we recommend running the diagnose command as explained here: Diagnose potential problems with an elmah.io installation.

    +
    +

    To help to install elmah.io in ASP.NET Core (by using the Elmah.Io.AspNetCore NuGet package) we have developed a range of Roslyn analyzers. Analyzers run inside Visual Studio and make it possible to validate your Startup.cs file during development.

    +

    Installation and usage

    +

    The analyzers can be installed in two ways. As a NuGet package or a Visual Studio extension. To install it from NuGet:

    +
    Install-Package Elmah.Io.AspNetCore.Analyzers
    +
    dotnet add package Elmah.Io.AspNetCore.Analyzers
    +
    <PackageReference Include="Elmah.Io.AspNetCore.Analyzers" Version="0.*" />
    +
    paket add Elmah.Io.AspNetCore.Analyzers
    +
    +

    The package is installed as a private asset, which means that it is not distributed as part of your build. You can keep the package installed after you have used it to inspect any warnings generated or uninstall it.

    +

    To install it as a Visual Studio extension, navigate to Extensions | Manage extensions | Online and search for Elmah.Io.AspNetCore.Analyzers. Then click the Download button and restart Visual Studio. As an alternative, you can download the extension directly from the Visual Studio Marketplace.

    +

    Once installed, analyzers will help you add or move elmah.io-related setup code:

    +

    Roslyn analyzers

    +

    All issues are listed as warnings in the Error list as well. The following is an explanation of possible warnings.

    +

    EIO1000 ConfigureServices must call AddElmahIo

    +

    AddElmahIo needs to be added as part of the ConfigureServices method:

    +

    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.AddElmahIo(/*...*/); //👈
    +}
    +

    +

    EIO1001 Configure must call UseElmahIo

    +

    UseElmahIo needs to be added as part of the Configure method:

    +

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    +{
    +    app.UseElmahIo(); //👈
    +}
    +

    +

    EIO1002 UseElmahIo must be called before/after Use*

    +

    UseElmahIo needs to be called after any calls to UseDeveloperExceptionPage, UseExceptionHandler, UseAuthorization, and UseAuthentication but before any calls to UseEndpoints and UseMvc:

    +

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    +{
    +    if (env.IsDevelopment())
    +    {
    +        app.UseDeveloperExceptionPage();
    +    }
    +    else
    +    {
    +        app.UseExceptionHandler(/*...*/);
    +    }
    +
    +    app.UseAuthentication();
    +    app.UseAuthorization();
    +
    +    app.UseElmahIo(); //👈
    +
    +    app.UseEndpoints();
    +    app.UseMvc(/*...*/);
    +}
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/search/lunr.js b/search/lunr.js new file mode 100644 index 0000000000..aca0a167f3 --- /dev/null +++ b/search/lunr.js @@ -0,0 +1,3475 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ + +;(function(){ + +/** + * A convenience function for configuring and constructing + * a new lunr Index. + * + * A lunr.Builder instance is created and the pipeline setup + * with a trimmer, stop word filter and stemmer. + * + * This builder object is yielded to the configuration function + * that is passed as a parameter, allowing the list of fields + * and other builder parameters to be customised. + * + * All documents _must_ be added within the passed config function. + * + * @example + * var idx = lunr(function () { + * this.field('title') + * this.field('body') + * this.ref('id') + * + * documents.forEach(function (doc) { + * this.add(doc) + * }, this) + * }) + * + * @see {@link lunr.Builder} + * @see {@link lunr.Pipeline} + * @see {@link lunr.trimmer} + * @see {@link lunr.stopWordFilter} + * @see {@link lunr.stemmer} + * @namespace {function} lunr + */ +var lunr = function (config) { + var builder = new lunr.Builder + + builder.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + builder.searchPipeline.add( + lunr.stemmer + ) + + config.call(builder, builder) + return builder.build() +} + +lunr.version = "2.3.9" +/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A namespace containing utils for the rest of the lunr library + * @namespace lunr.utils + */ +lunr.utils = {} + +/** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf lunr.utils + * @function + */ +lunr.utils.warn = (function (global) { + /* eslint-disable no-console */ + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } + /* eslint-enable no-console */ +})(this) + +/** + * Convert an object to a string. + * + * In the case of `null` and `undefined` the function returns + * the empty string, in all other cases the result of calling + * `toString` on the passed object is returned. + * + * @param {Any} obj The object to convert to a string. + * @return {String} string representation of the passed object. + * @memberOf lunr.utils + */ +lunr.utils.asString = function (obj) { + if (obj === void 0 || obj === null) { + return "" + } else { + return obj.toString() + } +} + +/** + * Clones an object. + * + * Will create a copy of an existing object such that any mutations + * on the copy cannot affect the original. + * + * Only shallow objects are supported, passing a nested object to this + * function will cause a TypeError. + * + * Objects with primitives, and arrays of primitives are supported. + * + * @param {Object} obj The object to clone. + * @return {Object} a clone of the passed object. + * @throws {TypeError} when a nested object is passed. + * @memberOf Utils + */ +lunr.utils.clone = function (obj) { + if (obj === null || obj === undefined) { + return obj + } + + var clone = Object.create(null), + keys = Object.keys(obj) + + for (var i = 0; i < keys.length; i++) { + var key = keys[i], + val = obj[key] + + if (Array.isArray(val)) { + clone[key] = val.slice() + continue + } + + if (typeof val === 'string' || + typeof val === 'number' || + typeof val === 'boolean') { + clone[key] = val + continue + } + + throw new TypeError("clone is not deep and does not support nested objects") + } + + return clone +} +lunr.FieldRef = function (docRef, fieldName, stringValue) { + this.docRef = docRef + this.fieldName = fieldName + this._stringValue = stringValue +} + +lunr.FieldRef.joiner = "/" + +lunr.FieldRef.fromString = function (s) { + var n = s.indexOf(lunr.FieldRef.joiner) + + if (n === -1) { + throw "malformed field ref string" + } + + var fieldRef = s.slice(0, n), + docRef = s.slice(n + 1) + + return new lunr.FieldRef (docRef, fieldRef, s) +} + +lunr.FieldRef.prototype.toString = function () { + if (this._stringValue == undefined) { + this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef + } + + return this._stringValue +} +/*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A lunr set. + * + * @constructor + */ +lunr.Set = function (elements) { + this.elements = Object.create(null) + + if (elements) { + this.length = elements.length + + for (var i = 0; i < this.length; i++) { + this.elements[elements[i]] = true + } + } else { + this.length = 0 + } +} + +/** + * A complete set that contains all elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.complete = { + intersect: function (other) { + return other + }, + + union: function () { + return this + }, + + contains: function () { + return true + } +} + +/** + * An empty set that contains no elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.empty = { + intersect: function () { + return this + }, + + union: function (other) { + return other + }, + + contains: function () { + return false + } +} + +/** + * Returns true if this set contains the specified object. + * + * @param {object} object - Object whose presence in this set is to be tested. + * @returns {boolean} - True if this set contains the specified object. + */ +lunr.Set.prototype.contains = function (object) { + return !!this.elements[object] +} + +/** + * Returns a new set containing only the elements that are present in both + * this set and the specified set. + * + * @param {lunr.Set} other - set to intersect with this set. + * @returns {lunr.Set} a new set that is the intersection of this and the specified set. + */ + +lunr.Set.prototype.intersect = function (other) { + var a, b, elements, intersection = [] + + if (other === lunr.Set.complete) { + return this + } + + if (other === lunr.Set.empty) { + return other + } + + if (this.length < other.length) { + a = this + b = other + } else { + a = other + b = this + } + + elements = Object.keys(a.elements) + + for (var i = 0; i < elements.length; i++) { + var element = elements[i] + if (element in b.elements) { + intersection.push(element) + } + } + + return new lunr.Set (intersection) +} + +/** + * Returns a new set combining the elements of this and the specified set. + * + * @param {lunr.Set} other - set to union with this set. + * @return {lunr.Set} a new set that is the union of this and the specified set. + */ + +lunr.Set.prototype.union = function (other) { + if (other === lunr.Set.complete) { + return lunr.Set.complete + } + + if (other === lunr.Set.empty) { + return this + } + + return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements))) +} +/** + * A function to calculate the inverse document frequency for + * a posting. This is shared between the builder and the index + * + * @private + * @param {object} posting - The posting for a given term + * @param {number} documentCount - The total number of documents. + */ +lunr.idf = function (posting, documentCount) { + var documentsWithTerm = 0 + + for (var fieldName in posting) { + if (fieldName == '_index') continue // Ignore the term index, its not a field + documentsWithTerm += Object.keys(posting[fieldName]).length + } + + var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5) + + return Math.log(1 + Math.abs(x)) +} + +/** + * A token wraps a string representation of a token + * as it is passed through the text processing pipeline. + * + * @constructor + * @param {string} [str=''] - The string token being wrapped. + * @param {object} [metadata={}] - Metadata associated with this token. + */ +lunr.Token = function (str, metadata) { + this.str = str || "" + this.metadata = metadata || {} +} + +/** + * Returns the token string that is being wrapped by this object. + * + * @returns {string} + */ +lunr.Token.prototype.toString = function () { + return this.str +} + +/** + * A token update function is used when updating or optionally + * when cloning a token. + * + * @callback lunr.Token~updateFunction + * @param {string} str - The string representation of the token. + * @param {Object} metadata - All metadata associated with this token. + */ + +/** + * Applies the given function to the wrapped string token. + * + * @example + * token.update(function (str, metadata) { + * return str.toUpperCase() + * }) + * + * @param {lunr.Token~updateFunction} fn - A function to apply to the token string. + * @returns {lunr.Token} + */ +lunr.Token.prototype.update = function (fn) { + this.str = fn(this.str, this.metadata) + return this +} + +/** + * Creates a clone of this token. Optionally a function can be + * applied to the cloned token. + * + * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token. + * @returns {lunr.Token} + */ +lunr.Token.prototype.clone = function (fn) { + fn = fn || function (s) { return s } + return new lunr.Token (fn(this.str, this.metadata), this.metadata) +} +/*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A function for splitting a string into tokens ready to be inserted into + * the search index. Uses `lunr.tokenizer.separator` to split strings, change + * the value of this property to change how strings are split into tokens. + * + * This tokenizer will convert its parameter to a string by calling `toString` and + * then will split this string on the character in `lunr.tokenizer.separator`. + * Arrays will have their elements converted to strings and wrapped in a lunr.Token. + * + * Optional metadata can be passed to the tokenizer, this metadata will be cloned and + * added as metadata to every token that is created from the object to be tokenized. + * + * @static + * @param {?(string|object|object[])} obj - The object to convert into tokens + * @param {?object} metadata - Optional metadata to associate with every token + * @returns {lunr.Token[]} + * @see {@link lunr.Pipeline} + */ +lunr.tokenizer = function (obj, metadata) { + if (obj == null || obj == undefined) { + return [] + } + + if (Array.isArray(obj)) { + return obj.map(function (t) { + return new lunr.Token( + lunr.utils.asString(t).toLowerCase(), + lunr.utils.clone(metadata) + ) + }) + } + + var str = obj.toString().toLowerCase(), + len = str.length, + tokens = [] + + for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) { + var char = str.charAt(sliceEnd), + sliceLength = sliceEnd - sliceStart + + if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) { + + if (sliceLength > 0) { + var tokenMetadata = lunr.utils.clone(metadata) || {} + tokenMetadata["position"] = [sliceStart, sliceLength] + tokenMetadata["index"] = tokens.length + + tokens.push( + new lunr.Token ( + str.slice(sliceStart, sliceEnd), + tokenMetadata + ) + ) + } + + sliceStart = sliceEnd + 1 + } + + } + + return tokens +} + +/** + * The separator used to split a string into tokens. Override this property to change the behaviour of + * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens. + * + * @static + * @see lunr.tokenizer + */ +lunr.tokenizer.separator = /[\s\-]+/ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ +lunr.Pipeline = function () { + this._stack = [] +} + +lunr.Pipeline.registeredFunctions = Object.create(null) + +/** + * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token + * string as well as all known metadata. A pipeline function can mutate the token string + * or mutate (or add) metadata for a given token. + * + * A pipeline function can indicate that the passed token should be discarded by returning + * null, undefined or an empty string. This token will not be passed to any downstream pipeline + * functions and will not be added to the index. + * + * Multiple tokens can be returned by returning an array of tokens. Each token will be passed + * to any downstream pipeline functions and all will returned tokens will be added to the index. + * + * Any number of pipeline functions may be chained together using a lunr.Pipeline. + * + * @interface lunr.PipelineFunction + * @param {lunr.Token} token - A token from the document being processed. + * @param {number} i - The index of this token in the complete list of tokens for this document/field. + * @param {lunr.Token[]} tokens - All tokens for this document/field. + * @returns {(?lunr.Token|lunr.Token[])} + */ + +/** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @param {String} label - The label to register this function with + */ +lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn +} + +/** + * Warns if the function is not registered as a Pipeline function. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @private + */ +lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } +} + +/** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised - The serialised pipeline to load. + * @returns {lunr.Pipeline} + */ +lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error('Cannot load unregistered function: ' + fnName) + } + }) + + return pipeline +} + +/** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline. + */ +lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) +} + +/** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + pos = pos + 1 + this._stack.splice(pos, 0, newFn) +} + +/** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + this._stack.splice(pos, 0, newFn) +} + +/** + * Removes a function from the pipeline. + * + * @param {lunr.PipelineFunction} fn The function to remove from the pipeline. + */ +lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + if (pos == -1) { + return + } + + this._stack.splice(pos, 1) +} + +/** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + */ +lunr.Pipeline.prototype.run = function (tokens) { + var stackLength = this._stack.length + + for (var i = 0; i < stackLength; i++) { + var fn = this._stack[i] + var memo = [] + + for (var j = 0; j < tokens.length; j++) { + var result = fn(tokens[j], j, tokens) + + if (result === null || result === void 0 || result === '') continue + + if (Array.isArray(result)) { + for (var k = 0; k < result.length; k++) { + memo.push(result[k]) + } + } else { + memo.push(result) + } + } + + tokens = memo + } + + return tokens +} + +/** + * Convenience method for passing a string through a pipeline and getting + * strings out. This method takes care of wrapping the passed string in a + * token and mapping the resulting tokens back to strings. + * + * @param {string} str - The string to pass through the pipeline. + * @param {?object} metadata - Optional metadata to associate with the token + * passed to the pipeline. + * @returns {string[]} + */ +lunr.Pipeline.prototype.runString = function (str, metadata) { + var token = new lunr.Token (str, metadata) + + return this.run([token]).map(function (t) { + return t.toString() + }) +} + +/** + * Resets the pipeline by removing any existing processors. + * + */ +lunr.Pipeline.prototype.reset = function () { + this._stack = [] +} + +/** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + */ +lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) +} +/*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A vector is used to construct the vector space of documents and queries. These + * vectors support operations to determine the similarity between two documents or + * a document and a query. + * + * Normally no parameters are required for initializing a vector, but in the case of + * loading a previously dumped vector the raw elements can be provided to the constructor. + * + * For performance reasons vectors are implemented with a flat array, where an elements + * index is immediately followed by its value. E.g. [index, value, index, value]. This + * allows the underlying array to be as sparse as possible and still offer decent + * performance when being used for vector calculations. + * + * @constructor + * @param {Number[]} [elements] - The flat list of element index and element value pairs. + */ +lunr.Vector = function (elements) { + this._magnitude = 0 + this.elements = elements || [] +} + + +/** + * Calculates the position within the vector to insert a given index. + * + * This is used internally by insert and upsert. If there are duplicate indexes then + * the position is returned as if the value for that index were to be updated, but it + * is the callers responsibility to check whether there is a duplicate at that index + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @returns {Number} + */ +lunr.Vector.prototype.positionForIndex = function (index) { + // For an empty vector the tuple can be inserted at the beginning + if (this.elements.length == 0) { + return 0 + } + + var start = 0, + end = this.elements.length / 2, + sliceLength = end - start, + pivotPoint = Math.floor(sliceLength / 2), + pivotIndex = this.elements[pivotPoint * 2] + + while (sliceLength > 1) { + if (pivotIndex < index) { + start = pivotPoint + } + + if (pivotIndex > index) { + end = pivotPoint + } + + if (pivotIndex == index) { + break + } + + sliceLength = end - start + pivotPoint = start + Math.floor(sliceLength / 2) + pivotIndex = this.elements[pivotPoint * 2] + } + + if (pivotIndex == index) { + return pivotPoint * 2 + } + + if (pivotIndex > index) { + return pivotPoint * 2 + } + + if (pivotIndex < index) { + return (pivotPoint + 1) * 2 + } +} + +/** + * Inserts an element at an index within the vector. + * + * Does not allow duplicates, will throw an error if there is already an entry + * for this index. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + */ +lunr.Vector.prototype.insert = function (insertIdx, val) { + this.upsert(insertIdx, val, function () { + throw "duplicate index" + }) +} + +/** + * Inserts or updates an existing index within the vector. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + * @param {function} fn - A function that is called for updates, the existing value and the + * requested value are passed as arguments + */ +lunr.Vector.prototype.upsert = function (insertIdx, val, fn) { + this._magnitude = 0 + var position = this.positionForIndex(insertIdx) + + if (this.elements[position] == insertIdx) { + this.elements[position + 1] = fn(this.elements[position + 1], val) + } else { + this.elements.splice(position, 0, insertIdx, val) + } +} + +/** + * Calculates the magnitude of this vector. + * + * @returns {Number} + */ +lunr.Vector.prototype.magnitude = function () { + if (this._magnitude) return this._magnitude + + var sumOfSquares = 0, + elementsLength = this.elements.length + + for (var i = 1; i < elementsLength; i += 2) { + var val = this.elements[i] + sumOfSquares += val * val + } + + return this._magnitude = Math.sqrt(sumOfSquares) +} + +/** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector - The vector to compute the dot product with. + * @returns {Number} + */ +lunr.Vector.prototype.dot = function (otherVector) { + var dotProduct = 0, + a = this.elements, b = otherVector.elements, + aLen = a.length, bLen = b.length, + aVal = 0, bVal = 0, + i = 0, j = 0 + + while (i < aLen && j < bLen) { + aVal = a[i], bVal = b[j] + if (aVal < bVal) { + i += 2 + } else if (aVal > bVal) { + j += 2 + } else if (aVal == bVal) { + dotProduct += a[i + 1] * b[j + 1] + i += 2 + j += 2 + } + } + + return dotProduct +} + +/** + * Calculates the similarity between this vector and another vector. + * + * @param {lunr.Vector} otherVector - The other vector to calculate the + * similarity with. + * @returns {Number} + */ +lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / this.magnitude() || 0 +} + +/** + * Converts the vector to an array of the elements within the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toArray = function () { + var output = new Array (this.elements.length / 2) + + for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) { + output[j] = this.elements[i] + } + + return output +} + +/** + * A JSON serializable representation of the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toJSON = function () { + return this.elements +} +/* eslint-disable */ +/*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartarus.org/~martin + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token - The string to stem + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + * @function + */ +lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return function (token) { + return token.update(porterStemmer); + } +})(); + +lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') +/*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.generateStopWordFilter builds a stopWordFilter function from the provided + * list of stop words. + * + * The built in lunr.stopWordFilter is built using this generator and can be used + * to generate custom stopWordFilters for applications or non English languages. + * + * @function + * @param {Array} token The token to pass through the filter + * @returns {lunr.PipelineFunction} + * @see lunr.Pipeline + * @see lunr.stopWordFilter + */ +lunr.generateStopWordFilter = function (stopWords) { + var words = stopWords.reduce(function (memo, stopWord) { + memo[stopWord] = stopWord + return memo + }, {}) + + return function (token) { + if (token && words[token.toString()] !== token.toString()) return token + } +} + +/** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @function + * @implements {lunr.PipelineFunction} + * @params {lunr.Token} token - A token to check for being a stop word. + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + */ +lunr.stopWordFilter = lunr.generateStopWordFilter([ + 'a', + 'able', + 'about', + 'across', + 'after', + 'all', + 'almost', + 'also', + 'am', + 'among', + 'an', + 'and', + 'any', + 'are', + 'as', + 'at', + 'be', + 'because', + 'been', + 'but', + 'by', + 'can', + 'cannot', + 'could', + 'dear', + 'did', + 'do', + 'does', + 'either', + 'else', + 'ever', + 'every', + 'for', + 'from', + 'get', + 'got', + 'had', + 'has', + 'have', + 'he', + 'her', + 'hers', + 'him', + 'his', + 'how', + 'however', + 'i', + 'if', + 'in', + 'into', + 'is', + 'it', + 'its', + 'just', + 'least', + 'let', + 'like', + 'likely', + 'may', + 'me', + 'might', + 'most', + 'must', + 'my', + 'neither', + 'no', + 'nor', + 'not', + 'of', + 'off', + 'often', + 'on', + 'only', + 'or', + 'other', + 'our', + 'own', + 'rather', + 'said', + 'say', + 'says', + 'she', + 'should', + 'since', + 'so', + 'some', + 'than', + 'that', + 'the', + 'their', + 'them', + 'then', + 'there', + 'these', + 'they', + 'this', + 'tis', + 'to', + 'too', + 'twas', + 'us', + 'wants', + 'was', + 'we', + 'were', + 'what', + 'when', + 'where', + 'which', + 'while', + 'who', + 'whom', + 'why', + 'will', + 'with', + 'would', + 'yet', + 'you', + 'your' +]) + +lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') +/*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the beginning and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token The token to pass through the filter + * @returns {lunr.Token} + * @see lunr.Pipeline + */ +lunr.trimmer = function (token) { + return token.update(function (s) { + return s.replace(/^\W+/, '').replace(/\W+$/, '') + }) +} + +lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') +/*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A token set is used to store the unique list of all tokens + * within an index. Token sets are also used to represent an + * incoming query to the index, this query token set and index + * token set are then intersected to find which tokens to look + * up in the inverted index. + * + * A token set can hold multiple tokens, as in the case of the + * index token set, or it can hold a single token as in the + * case of a simple query token set. + * + * Additionally token sets are used to perform wildcard matching. + * Leading, contained and trailing wildcards are supported, and + * from this edit distance matching can also be provided. + * + * Token sets are implemented as a minimal finite state automata, + * where both common prefixes and suffixes are shared between tokens. + * This helps to reduce the space used for storing the token set. + * + * @constructor + */ +lunr.TokenSet = function () { + this.final = false + this.edges = {} + this.id = lunr.TokenSet._nextId + lunr.TokenSet._nextId += 1 +} + +/** + * Keeps track of the next, auto increment, identifier to assign + * to a new tokenSet. + * + * TokenSets require a unique identifier to be correctly minimised. + * + * @private + */ +lunr.TokenSet._nextId = 1 + +/** + * Creates a TokenSet instance from the given sorted array of words. + * + * @param {String[]} arr - A sorted array of strings to create the set from. + * @returns {lunr.TokenSet} + * @throws Will throw an error if the input array is not sorted. + */ +lunr.TokenSet.fromArray = function (arr) { + var builder = new lunr.TokenSet.Builder + + for (var i = 0, len = arr.length; i < len; i++) { + builder.insert(arr[i]) + } + + builder.finish() + return builder.root +} + +/** + * Creates a token set from a query clause. + * + * @private + * @param {Object} clause - A single clause from lunr.Query. + * @param {string} clause.term - The query clause term. + * @param {number} [clause.editDistance] - The optional edit distance for the term. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromClause = function (clause) { + if ('editDistance' in clause) { + return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance) + } else { + return lunr.TokenSet.fromString(clause.term) + } +} + +/** + * Creates a token set representing a single string with a specified + * edit distance. + * + * Insertions, deletions, substitutions and transpositions are each + * treated as an edit distance of 1. + * + * Increasing the allowed edit distance will have a dramatic impact + * on the performance of both creating and intersecting these TokenSets. + * It is advised to keep the edit distance less than 3. + * + * @param {string} str - The string to create the token set from. + * @param {number} editDistance - The allowed edit distance to match. + * @returns {lunr.Vector} + */ +lunr.TokenSet.fromFuzzyString = function (str, editDistance) { + var root = new lunr.TokenSet + + var stack = [{ + node: root, + editsRemaining: editDistance, + str: str + }] + + while (stack.length) { + var frame = stack.pop() + + // no edit + if (frame.str.length > 0) { + var char = frame.str.charAt(0), + noEditNode + + if (char in frame.node.edges) { + noEditNode = frame.node.edges[char] + } else { + noEditNode = new lunr.TokenSet + frame.node.edges[char] = noEditNode + } + + if (frame.str.length == 1) { + noEditNode.final = true + } + + stack.push({ + node: noEditNode, + editsRemaining: frame.editsRemaining, + str: frame.str.slice(1) + }) + } + + if (frame.editsRemaining == 0) { + continue + } + + // insertion + if ("*" in frame.node.edges) { + var insertionNode = frame.node.edges["*"] + } else { + var insertionNode = new lunr.TokenSet + frame.node.edges["*"] = insertionNode + } + + if (frame.str.length == 0) { + insertionNode.final = true + } + + stack.push({ + node: insertionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str + }) + + // deletion + // can only do a deletion if we have enough edits remaining + // and if there are characters left to delete in the string + if (frame.str.length > 1) { + stack.push({ + node: frame.node, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // deletion + // just removing the last character from the str + if (frame.str.length == 1) { + frame.node.final = true + } + + // substitution + // can only do a substitution if we have enough edits remaining + // and if there are characters left to substitute + if (frame.str.length >= 1) { + if ("*" in frame.node.edges) { + var substitutionNode = frame.node.edges["*"] + } else { + var substitutionNode = new lunr.TokenSet + frame.node.edges["*"] = substitutionNode + } + + if (frame.str.length == 1) { + substitutionNode.final = true + } + + stack.push({ + node: substitutionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // transposition + // can only do a transposition if there are edits remaining + // and there are enough characters to transpose + if (frame.str.length > 1) { + var charA = frame.str.charAt(0), + charB = frame.str.charAt(1), + transposeNode + + if (charB in frame.node.edges) { + transposeNode = frame.node.edges[charB] + } else { + transposeNode = new lunr.TokenSet + frame.node.edges[charB] = transposeNode + } + + if (frame.str.length == 1) { + transposeNode.final = true + } + + stack.push({ + node: transposeNode, + editsRemaining: frame.editsRemaining - 1, + str: charA + frame.str.slice(2) + }) + } + } + + return root +} + +/** + * Creates a TokenSet from a string. + * + * The string may contain one or more wildcard characters (*) + * that will allow wildcard matching when intersecting with + * another TokenSet. + * + * @param {string} str - The string to create a TokenSet from. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromString = function (str) { + var node = new lunr.TokenSet, + root = node + + /* + * Iterates through all characters within the passed string + * appending a node for each character. + * + * When a wildcard character is found then a self + * referencing edge is introduced to continually match + * any number of any characters. + */ + for (var i = 0, len = str.length; i < len; i++) { + var char = str[i], + final = (i == len - 1) + + if (char == "*") { + node.edges[char] = node + node.final = final + + } else { + var next = new lunr.TokenSet + next.final = final + + node.edges[char] = next + node = next + } + } + + return root +} + +/** + * Converts this TokenSet into an array of strings + * contained within the TokenSet. + * + * This is not intended to be used on a TokenSet that + * contains wildcards, in these cases the results are + * undefined and are likely to cause an infinite loop. + * + * @returns {string[]} + */ +lunr.TokenSet.prototype.toArray = function () { + var words = [] + + var stack = [{ + prefix: "", + node: this + }] + + while (stack.length) { + var frame = stack.pop(), + edges = Object.keys(frame.node.edges), + len = edges.length + + if (frame.node.final) { + /* In Safari, at this point the prefix is sometimes corrupted, see: + * https://github.com/olivernn/lunr.js/issues/279 Calling any + * String.prototype method forces Safari to "cast" this string to what + * it's supposed to be, fixing the bug. */ + frame.prefix.charAt(0) + words.push(frame.prefix) + } + + for (var i = 0; i < len; i++) { + var edge = edges[i] + + stack.push({ + prefix: frame.prefix.concat(edge), + node: frame.node.edges[edge] + }) + } + } + + return words +} + +/** + * Generates a string representation of a TokenSet. + * + * This is intended to allow TokenSets to be used as keys + * in objects, largely to aid the construction and minimisation + * of a TokenSet. As such it is not designed to be a human + * friendly representation of the TokenSet. + * + * @returns {string} + */ +lunr.TokenSet.prototype.toString = function () { + // NOTE: Using Object.keys here as this.edges is very likely + // to enter 'hash-mode' with many keys being added + // + // avoiding a for-in loop here as it leads to the function + // being de-optimised (at least in V8). From some simple + // benchmarks the performance is comparable, but allowing + // V8 to optimize may mean easy performance wins in the future. + + if (this._str) { + return this._str + } + + var str = this.final ? '1' : '0', + labels = Object.keys(this.edges).sort(), + len = labels.length + + for (var i = 0; i < len; i++) { + var label = labels[i], + node = this.edges[label] + + str = str + label + node.id + } + + return str +} + +/** + * Returns a new TokenSet that is the intersection of + * this TokenSet and the passed TokenSet. + * + * This intersection will take into account any wildcards + * contained within the TokenSet. + * + * @param {lunr.TokenSet} b - An other TokenSet to intersect with. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.prototype.intersect = function (b) { + var output = new lunr.TokenSet, + frame = undefined + + var stack = [{ + qNode: b, + output: output, + node: this + }] + + while (stack.length) { + frame = stack.pop() + + // NOTE: As with the #toString method, we are using + // Object.keys and a for loop instead of a for-in loop + // as both of these objects enter 'hash' mode, causing + // the function to be de-optimised in V8 + var qEdges = Object.keys(frame.qNode.edges), + qLen = qEdges.length, + nEdges = Object.keys(frame.node.edges), + nLen = nEdges.length + + for (var q = 0; q < qLen; q++) { + var qEdge = qEdges[q] + + for (var n = 0; n < nLen; n++) { + var nEdge = nEdges[n] + + if (nEdge == qEdge || qEdge == '*') { + var node = frame.node.edges[nEdge], + qNode = frame.qNode.edges[qEdge], + final = node.final && qNode.final, + next = undefined + + if (nEdge in frame.output.edges) { + // an edge already exists for this character + // no need to create a new node, just set the finality + // bit unless this node is already final + next = frame.output.edges[nEdge] + next.final = next.final || final + + } else { + // no edge exists yet, must create one + // set the finality bit and insert it + // into the output + next = new lunr.TokenSet + next.final = final + frame.output.edges[nEdge] = next + } + + stack.push({ + qNode: qNode, + output: next, + node: node + }) + } + } + } + } + + return output +} +lunr.TokenSet.Builder = function () { + this.previousWord = "" + this.root = new lunr.TokenSet + this.uncheckedNodes = [] + this.minimizedNodes = {} +} + +lunr.TokenSet.Builder.prototype.insert = function (word) { + var node, + commonPrefix = 0 + + if (word < this.previousWord) { + throw new Error ("Out of order word insertion") + } + + for (var i = 0; i < word.length && i < this.previousWord.length; i++) { + if (word[i] != this.previousWord[i]) break + commonPrefix++ + } + + this.minimize(commonPrefix) + + if (this.uncheckedNodes.length == 0) { + node = this.root + } else { + node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child + } + + for (var i = commonPrefix; i < word.length; i++) { + var nextNode = new lunr.TokenSet, + char = word[i] + + node.edges[char] = nextNode + + this.uncheckedNodes.push({ + parent: node, + char: char, + child: nextNode + }) + + node = nextNode + } + + node.final = true + this.previousWord = word +} + +lunr.TokenSet.Builder.prototype.finish = function () { + this.minimize(0) +} + +lunr.TokenSet.Builder.prototype.minimize = function (downTo) { + for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) { + var node = this.uncheckedNodes[i], + childKey = node.child.toString() + + if (childKey in this.minimizedNodes) { + node.parent.edges[node.char] = this.minimizedNodes[childKey] + } else { + // Cache the key for this node since + // we know it can't change anymore + node.child._str = childKey + + this.minimizedNodes[childKey] = node.child + } + + this.uncheckedNodes.pop() + } +} +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * An index contains the built index of all documents and provides a query interface + * to the index. + * + * Usually instances of lunr.Index will not be created using this constructor, instead + * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be + * used to load previously built and serialized indexes. + * + * @constructor + * @param {Object} attrs - The attributes of the built search index. + * @param {Object} attrs.invertedIndex - An index of term/field to document reference. + * @param {Object} attrs.fieldVectors - Field vectors + * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens. + * @param {string[]} attrs.fields - The names of indexed document fields. + * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms. + */ +lunr.Index = function (attrs) { + this.invertedIndex = attrs.invertedIndex + this.fieldVectors = attrs.fieldVectors + this.tokenSet = attrs.tokenSet + this.fields = attrs.fields + this.pipeline = attrs.pipeline +} + +/** + * A result contains details of a document matching a search query. + * @typedef {Object} lunr.Index~Result + * @property {string} ref - The reference of the document this result represents. + * @property {number} score - A number between 0 and 1 representing how similar this document is to the query. + * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match. + */ + +/** + * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple + * query language which itself is parsed into an instance of lunr.Query. + * + * For programmatically building queries it is advised to directly use lunr.Query, the query language + * is best used for human entered text rather than program generated text. + * + * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported + * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello' + * or 'world', though those that contain both will rank higher in the results. + * + * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can + * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding + * wildcards will increase the number of documents that will be found but can also have a negative + * impact on query performance, especially with wildcards at the beginning of a term. + * + * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term + * hello in the title field will match this query. Using a field not present in the index will lead + * to an error being thrown. + * + * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term + * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported + * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2. + * Avoid large values for edit distance to improve query performance. + * + * Each term also supports a presence modifier. By default a term's presence in document is optional, however + * this can be changed to either required or prohibited. For a term's presence to be required in a document the + * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and + * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not + * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'. + * + * To escape special characters the backslash character '\' can be used, this allows searches to include + * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead + * of attempting to apply a boost of 2 to the search term "foo". + * + * @typedef {string} lunr.Index~QueryString + * @example Simple single term query + * hello + * @example Multiple term query + * hello world + * @example term scoped to a field + * title:hello + * @example term with a boost of 10 + * hello^10 + * @example term with an edit distance of 2 + * hello~2 + * @example terms with presence modifiers + * -foo +bar baz + */ + +/** + * Performs a search against the index using lunr query syntax. + * + * Results will be returned sorted by their score, the most relevant results + * will be returned first. For details on how the score is calculated, please see + * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}. + * + * For more programmatic querying use lunr.Index#query. + * + * @param {lunr.Index~QueryString} queryString - A string containing a lunr query. + * @throws {lunr.QueryParseError} If the passed query string cannot be parsed. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.search = function (queryString) { + return this.query(function (query) { + var parser = new lunr.QueryParser(queryString, query) + parser.parse() + }) +} + +/** + * A query builder callback provides a query object to be used to express + * the query to perform on the index. + * + * @callback lunr.Index~queryBuilder + * @param {lunr.Query} query - The query object to build up. + * @this lunr.Query + */ + +/** + * Performs a query against the index using the yielded lunr.Query object. + * + * If performing programmatic queries against the index, this method is preferred + * over lunr.Index#search so as to avoid the additional query parsing overhead. + * + * A query object is yielded to the supplied function which should be used to + * express the query to be run against the index. + * + * Note that although this function takes a callback parameter it is _not_ an + * asynchronous operation, the callback is just yielded a query object to be + * customized. + * + * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.query = function (fn) { + // for each query clause + // * process terms + // * expand terms from token set + // * find matching documents and metadata + // * get document vectors + // * score documents + + var query = new lunr.Query(this.fields), + matchingFields = Object.create(null), + queryVectors = Object.create(null), + termFieldCache = Object.create(null), + requiredMatches = Object.create(null), + prohibitedMatches = Object.create(null) + + /* + * To support field level boosts a query vector is created per + * field. An empty vector is eagerly created to support negated + * queries. + */ + for (var i = 0; i < this.fields.length; i++) { + queryVectors[this.fields[i]] = new lunr.Vector + } + + fn.call(query, query) + + for (var i = 0; i < query.clauses.length; i++) { + /* + * Unless the pipeline has been disabled for this term, which is + * the case for terms with wildcards, we need to pass the clause + * term through the search pipeline. A pipeline returns an array + * of processed terms. Pipeline functions may expand the passed + * term, which means we may end up performing multiple index lookups + * for a single query term. + */ + var clause = query.clauses[i], + terms = null, + clauseMatches = lunr.Set.empty + + if (clause.usePipeline) { + terms = this.pipeline.runString(clause.term, { + fields: clause.fields + }) + } else { + terms = [clause.term] + } + + for (var m = 0; m < terms.length; m++) { + var term = terms[m] + + /* + * Each term returned from the pipeline needs to use the same query + * clause object, e.g. the same boost and or edit distance. The + * simplest way to do this is to re-use the clause object but mutate + * its term property. + */ + clause.term = term + + /* + * From the term in the clause we create a token set which will then + * be used to intersect the indexes token set to get a list of terms + * to lookup in the inverted index + */ + var termTokenSet = lunr.TokenSet.fromClause(clause), + expandedTerms = this.tokenSet.intersect(termTokenSet).toArray() + + /* + * If a term marked as required does not exist in the tokenSet it is + * impossible for the search to return any matches. We set all the field + * scoped required matches set to empty and stop examining any further + * clauses. + */ + if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = lunr.Set.empty + } + + break + } + + for (var j = 0; j < expandedTerms.length; j++) { + /* + * For each term get the posting and termIndex, this is required for + * building the query vector. + */ + var expandedTerm = expandedTerms[j], + posting = this.invertedIndex[expandedTerm], + termIndex = posting._index + + for (var k = 0; k < clause.fields.length; k++) { + /* + * For each field that this query term is scoped by (by default + * all fields are in scope) we need to get all the document refs + * that have this term in that field. + * + * The posting is the entry in the invertedIndex for the matching + * term from above. + */ + var field = clause.fields[k], + fieldPosting = posting[field], + matchingDocumentRefs = Object.keys(fieldPosting), + termField = expandedTerm + "/" + field, + matchingDocumentsSet = new lunr.Set(matchingDocumentRefs) + + /* + * if the presence of this term is required ensure that the matching + * documents are added to the set of required matches for this clause. + * + */ + if (clause.presence == lunr.Query.presence.REQUIRED) { + clauseMatches = clauseMatches.union(matchingDocumentsSet) + + if (requiredMatches[field] === undefined) { + requiredMatches[field] = lunr.Set.complete + } + } + + /* + * if the presence of this term is prohibited ensure that the matching + * documents are added to the set of prohibited matches for this field, + * creating that set if it does not yet exist. + */ + if (clause.presence == lunr.Query.presence.PROHIBITED) { + if (prohibitedMatches[field] === undefined) { + prohibitedMatches[field] = lunr.Set.empty + } + + prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet) + + /* + * Prohibited matches should not be part of the query vector used for + * similarity scoring and no metadata should be extracted so we continue + * to the next field + */ + continue + } + + /* + * The query field vector is populated using the termIndex found for + * the term and a unit value with the appropriate boost applied. + * Using upsert because there could already be an entry in the vector + * for the term we are working with. In that case we just add the scores + * together. + */ + queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b }) + + /** + * If we've already seen this term, field combo then we've already collected + * the matching documents and metadata, no need to go through all that again + */ + if (termFieldCache[termField]) { + continue + } + + for (var l = 0; l < matchingDocumentRefs.length; l++) { + /* + * All metadata for this term/field/document triple + * are then extracted and collected into an instance + * of lunr.MatchData ready to be returned in the query + * results + */ + var matchingDocumentRef = matchingDocumentRefs[l], + matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field), + metadata = fieldPosting[matchingDocumentRef], + fieldMatch + + if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) { + matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata) + } else { + fieldMatch.add(expandedTerm, field, metadata) + } + + } + + termFieldCache[termField] = true + } + } + } + + /** + * If the presence was required we need to update the requiredMatches field sets. + * We do this after all fields for the term have collected their matches because + * the clause terms presence is required in _any_ of the fields not _all_ of the + * fields. + */ + if (clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = requiredMatches[field].intersect(clauseMatches) + } + } + } + + /** + * Need to combine the field scoped required and prohibited + * matching documents into a global set of required and prohibited + * matches + */ + var allRequiredMatches = lunr.Set.complete, + allProhibitedMatches = lunr.Set.empty + + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i] + + if (requiredMatches[field]) { + allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field]) + } + + if (prohibitedMatches[field]) { + allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field]) + } + } + + var matchingFieldRefs = Object.keys(matchingFields), + results = [], + matches = Object.create(null) + + /* + * If the query is negated (contains only prohibited terms) + * we need to get _all_ fieldRefs currently existing in the + * index. This is only done when we know that the query is + * entirely prohibited terms to avoid any cost of getting all + * fieldRefs unnecessarily. + * + * Additionally, blank MatchData must be created to correctly + * populate the results. + */ + if (query.isNegated()) { + matchingFieldRefs = Object.keys(this.fieldVectors) + + for (var i = 0; i < matchingFieldRefs.length; i++) { + var matchingFieldRef = matchingFieldRefs[i] + var fieldRef = lunr.FieldRef.fromString(matchingFieldRef) + matchingFields[matchingFieldRef] = new lunr.MatchData + } + } + + for (var i = 0; i < matchingFieldRefs.length; i++) { + /* + * Currently we have document fields that match the query, but we + * need to return documents. The matchData and scores are combined + * from multiple fields belonging to the same document. + * + * Scores are calculated by field, using the query vectors created + * above, and combined into a final document score using addition. + */ + var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]), + docRef = fieldRef.docRef + + if (!allRequiredMatches.contains(docRef)) { + continue + } + + if (allProhibitedMatches.contains(docRef)) { + continue + } + + var fieldVector = this.fieldVectors[fieldRef], + score = queryVectors[fieldRef.fieldName].similarity(fieldVector), + docMatch + + if ((docMatch = matches[docRef]) !== undefined) { + docMatch.score += score + docMatch.matchData.combine(matchingFields[fieldRef]) + } else { + var match = { + ref: docRef, + score: score, + matchData: matchingFields[fieldRef] + } + matches[docRef] = match + results.push(match) + } + } + + /* + * Sort the results objects by score, highest first. + */ + return results.sort(function (a, b) { + return b.score - a.score + }) +} + +/** + * Prepares the index for JSON serialization. + * + * The schema for this JSON blob will be described in a + * separate JSON schema file. + * + * @returns {Object} + */ +lunr.Index.prototype.toJSON = function () { + var invertedIndex = Object.keys(this.invertedIndex) + .sort() + .map(function (term) { + return [term, this.invertedIndex[term]] + }, this) + + var fieldVectors = Object.keys(this.fieldVectors) + .map(function (ref) { + return [ref, this.fieldVectors[ref].toJSON()] + }, this) + + return { + version: lunr.version, + fields: this.fields, + fieldVectors: fieldVectors, + invertedIndex: invertedIndex, + pipeline: this.pipeline.toJSON() + } +} + +/** + * Loads a previously serialized lunr.Index + * + * @param {Object} serializedIndex - A previously serialized lunr.Index + * @returns {lunr.Index} + */ +lunr.Index.load = function (serializedIndex) { + var attrs = {}, + fieldVectors = {}, + serializedVectors = serializedIndex.fieldVectors, + invertedIndex = Object.create(null), + serializedInvertedIndex = serializedIndex.invertedIndex, + tokenSetBuilder = new lunr.TokenSet.Builder, + pipeline = lunr.Pipeline.load(serializedIndex.pipeline) + + if (serializedIndex.version != lunr.version) { + lunr.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + lunr.version + "' does not match serialized index '" + serializedIndex.version + "'") + } + + for (var i = 0; i < serializedVectors.length; i++) { + var tuple = serializedVectors[i], + ref = tuple[0], + elements = tuple[1] + + fieldVectors[ref] = new lunr.Vector(elements) + } + + for (var i = 0; i < serializedInvertedIndex.length; i++) { + var tuple = serializedInvertedIndex[i], + term = tuple[0], + posting = tuple[1] + + tokenSetBuilder.insert(term) + invertedIndex[term] = posting + } + + tokenSetBuilder.finish() + + attrs.fields = serializedIndex.fields + + attrs.fieldVectors = fieldVectors + attrs.invertedIndex = invertedIndex + attrs.tokenSet = tokenSetBuilder.root + attrs.pipeline = pipeline + + return new lunr.Index(attrs) +} +/*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Builder performs indexing on a set of documents and + * returns instances of lunr.Index ready for querying. + * + * All configuration of the index is done via the builder, the + * fields to index, the document reference, the text processing + * pipeline and document scoring parameters are all set on the + * builder before indexing. + * + * @constructor + * @property {string} _ref - Internal reference to the document reference field. + * @property {string[]} _fields - Internal reference to the document fields to index. + * @property {object} invertedIndex - The inverted index maps terms to document fields. + * @property {object} documentTermFrequencies - Keeps track of document term frequencies. + * @property {object} documentLengths - Keeps track of the length of documents added to the index. + * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing. + * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing. + * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index. + * @property {number} documentCount - Keeps track of the total number of documents indexed. + * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75. + * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2. + * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space. + * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index. + */ +lunr.Builder = function () { + this._ref = "id" + this._fields = Object.create(null) + this._documents = Object.create(null) + this.invertedIndex = Object.create(null) + this.fieldTermFrequencies = {} + this.fieldLengths = {} + this.tokenizer = lunr.tokenizer + this.pipeline = new lunr.Pipeline + this.searchPipeline = new lunr.Pipeline + this.documentCount = 0 + this._b = 0.75 + this._k1 = 1.2 + this.termIndex = 0 + this.metadataWhitelist = [] +} + +/** + * Sets the document field used as the document reference. Every document must have this field. + * The type of this field in the document should be a string, if it is not a string it will be + * coerced into a string by calling toString. + * + * The default ref is 'id'. + * + * The ref should _not_ be changed during indexing, it should be set before any documents are + * added to the index. Changing it during indexing can lead to inconsistent results. + * + * @param {string} ref - The name of the reference field in the document. + */ +lunr.Builder.prototype.ref = function (ref) { + this._ref = ref +} + +/** + * A function that is used to extract a field from a document. + * + * Lunr expects a field to be at the top level of a document, if however the field + * is deeply nested within a document an extractor function can be used to extract + * the right field for indexing. + * + * @callback fieldExtractor + * @param {object} doc - The document being added to the index. + * @returns {?(string|object|object[])} obj - The object that will be indexed for this field. + * @example Extracting a nested field + * function (doc) { return doc.nested.field } + */ + +/** + * Adds a field to the list of document fields that will be indexed. Every document being + * indexed should have this field. Null values for this field in indexed documents will + * not cause errors but will limit the chance of that document being retrieved by searches. + * + * All fields should be added before adding documents to the index. Adding fields after + * a document has been indexed will have no effect on already indexed documents. + * + * Fields can be boosted at build time. This allows terms within that field to have more + * importance when ranking search results. Use a field boost to specify that matches within + * one field are more important than other fields. + * + * @param {string} fieldName - The name of a field to index in all documents. + * @param {object} attributes - Optional attributes associated with this field. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this field. + * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document. + * @throws {RangeError} fieldName cannot contain unsupported characters '/' + */ +lunr.Builder.prototype.field = function (fieldName, attributes) { + if (/\//.test(fieldName)) { + throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'") + } + + this._fields[fieldName] = attributes || {} +} + +/** + * A parameter to tune the amount of field length normalisation that is applied when + * calculating relevance scores. A value of 0 will completely disable any normalisation + * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b + * will be clamped to the range 0 - 1. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.b = function (number) { + if (number < 0) { + this._b = 0 + } else if (number > 1) { + this._b = 1 + } else { + this._b = number + } +} + +/** + * A parameter that controls the speed at which a rise in term frequency results in term + * frequency saturation. The default value is 1.2. Setting this to a higher value will give + * slower saturation levels, a lower value will result in quicker saturation. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.k1 = function (number) { + this._k1 = number +} + +/** + * Adds a document to the index. + * + * Before adding fields to the index the index should have been fully setup, with the document + * ref and all fields to index already having been specified. + * + * The document must have a field name as specified by the ref (by default this is 'id') and + * it should have all fields defined for indexing, though null or undefined values will not + * cause errors. + * + * Entire documents can be boosted at build time. Applying a boost to a document indicates that + * this document should rank higher in search results than other documents. + * + * @param {object} doc - The document to add to the index. + * @param {object} attributes - Optional attributes associated with this document. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this document. + */ +lunr.Builder.prototype.add = function (doc, attributes) { + var docRef = doc[this._ref], + fields = Object.keys(this._fields) + + this._documents[docRef] = attributes || {} + this.documentCount += 1 + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i], + extractor = this._fields[fieldName].extractor, + field = extractor ? extractor(doc) : doc[fieldName], + tokens = this.tokenizer(field, { + fields: [fieldName] + }), + terms = this.pipeline.run(tokens), + fieldRef = new lunr.FieldRef (docRef, fieldName), + fieldTerms = Object.create(null) + + this.fieldTermFrequencies[fieldRef] = fieldTerms + this.fieldLengths[fieldRef] = 0 + + // store the length of this field for this document + this.fieldLengths[fieldRef] += terms.length + + // calculate term frequencies for this field + for (var j = 0; j < terms.length; j++) { + var term = terms[j] + + if (fieldTerms[term] == undefined) { + fieldTerms[term] = 0 + } + + fieldTerms[term] += 1 + + // add to inverted index + // create an initial posting if one doesn't exist + if (this.invertedIndex[term] == undefined) { + var posting = Object.create(null) + posting["_index"] = this.termIndex + this.termIndex += 1 + + for (var k = 0; k < fields.length; k++) { + posting[fields[k]] = Object.create(null) + } + + this.invertedIndex[term] = posting + } + + // add an entry for this term/fieldName/docRef to the invertedIndex + if (this.invertedIndex[term][fieldName][docRef] == undefined) { + this.invertedIndex[term][fieldName][docRef] = Object.create(null) + } + + // store all whitelisted metadata about this token in the + // inverted index + for (var l = 0; l < this.metadataWhitelist.length; l++) { + var metadataKey = this.metadataWhitelist[l], + metadata = term.metadata[metadataKey] + + if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) { + this.invertedIndex[term][fieldName][docRef][metadataKey] = [] + } + + this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata) + } + } + + } +} + +/** + * Calculates the average document length for this index + * + * @private + */ +lunr.Builder.prototype.calculateAverageFieldLengths = function () { + + var fieldRefs = Object.keys(this.fieldLengths), + numberOfFields = fieldRefs.length, + accumulator = {}, + documentsWithField = {} + + for (var i = 0; i < numberOfFields; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + field = fieldRef.fieldName + + documentsWithField[field] || (documentsWithField[field] = 0) + documentsWithField[field] += 1 + + accumulator[field] || (accumulator[field] = 0) + accumulator[field] += this.fieldLengths[fieldRef] + } + + var fields = Object.keys(this._fields) + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i] + accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName] + } + + this.averageFieldLength = accumulator +} + +/** + * Builds a vector space model of every document using lunr.Vector + * + * @private + */ +lunr.Builder.prototype.createFieldVectors = function () { + var fieldVectors = {}, + fieldRefs = Object.keys(this.fieldTermFrequencies), + fieldRefsLength = fieldRefs.length, + termIdfCache = Object.create(null) + + for (var i = 0; i < fieldRefsLength; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + fieldName = fieldRef.fieldName, + fieldLength = this.fieldLengths[fieldRef], + fieldVector = new lunr.Vector, + termFrequencies = this.fieldTermFrequencies[fieldRef], + terms = Object.keys(termFrequencies), + termsLength = terms.length + + + var fieldBoost = this._fields[fieldName].boost || 1, + docBoost = this._documents[fieldRef.docRef].boost || 1 + + for (var j = 0; j < termsLength; j++) { + var term = terms[j], + tf = termFrequencies[term], + termIndex = this.invertedIndex[term]._index, + idf, score, scoreWithPrecision + + if (termIdfCache[term] === undefined) { + idf = lunr.idf(this.invertedIndex[term], this.documentCount) + termIdfCache[term] = idf + } else { + idf = termIdfCache[term] + } + + score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf) + score *= fieldBoost + score *= docBoost + scoreWithPrecision = Math.round(score * 1000) / 1000 + // Converts 1.23456789 to 1.234. + // Reducing the precision so that the vectors take up less + // space when serialised. Doing it now so that they behave + // the same before and after serialisation. Also, this is + // the fastest approach to reducing a number's precision in + // JavaScript. + + fieldVector.insert(termIndex, scoreWithPrecision) + } + + fieldVectors[fieldRef] = fieldVector + } + + this.fieldVectors = fieldVectors +} + +/** + * Creates a token set of all tokens in the index using lunr.TokenSet + * + * @private + */ +lunr.Builder.prototype.createTokenSet = function () { + this.tokenSet = lunr.TokenSet.fromArray( + Object.keys(this.invertedIndex).sort() + ) +} + +/** + * Builds the index, creating an instance of lunr.Index. + * + * This completes the indexing process and should only be called + * once all documents have been added to the index. + * + * @returns {lunr.Index} + */ +lunr.Builder.prototype.build = function () { + this.calculateAverageFieldLengths() + this.createFieldVectors() + this.createTokenSet() + + return new lunr.Index({ + invertedIndex: this.invertedIndex, + fieldVectors: this.fieldVectors, + tokenSet: this.tokenSet, + fields: Object.keys(this._fields), + pipeline: this.searchPipeline + }) +} + +/** + * Applies a plugin to the index builder. + * + * A plugin is a function that is called with the index builder as its context. + * Plugins can be used to customise or extend the behaviour of the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied when building the index. + * + * The plugin function will be called with the index builder as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index builder as its context. + * + * @param {Function} plugin The plugin to apply. + */ +lunr.Builder.prototype.use = function (fn) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + fn.apply(this, args) +} +/** + * Contains and collects metadata about a matching document. + * A single instance of lunr.MatchData is returned as part of every + * lunr.Index~Result. + * + * @constructor + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + * @property {object} metadata - A cloned collection of metadata associated with this document. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData = function (term, field, metadata) { + var clonedMetadata = Object.create(null), + metadataKeys = Object.keys(metadata || {}) + + // Cloning the metadata to prevent the original + // being mutated during match data combination. + // Metadata is kept in an array within the inverted + // index so cloning the data can be done with + // Array#slice + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + clonedMetadata[key] = metadata[key].slice() + } + + this.metadata = Object.create(null) + + if (term !== undefined) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = clonedMetadata + } +} + +/** + * An instance of lunr.MatchData will be created for every term that matches a + * document. However only one instance is required in a lunr.Index~Result. This + * method combines metadata from another instance of lunr.MatchData with this + * objects metadata. + * + * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData.prototype.combine = function (otherMatchData) { + var terms = Object.keys(otherMatchData.metadata) + + for (var i = 0; i < terms.length; i++) { + var term = terms[i], + fields = Object.keys(otherMatchData.metadata[term]) + + if (this.metadata[term] == undefined) { + this.metadata[term] = Object.create(null) + } + + for (var j = 0; j < fields.length; j++) { + var field = fields[j], + keys = Object.keys(otherMatchData.metadata[term][field]) + + if (this.metadata[term][field] == undefined) { + this.metadata[term][field] = Object.create(null) + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k] + + if (this.metadata[term][field][key] == undefined) { + this.metadata[term][field][key] = otherMatchData.metadata[term][field][key] + } else { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key]) + } + + } + } + } +} + +/** + * Add metadata for a term/field pair to this instance of match data. + * + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + */ +lunr.MatchData.prototype.add = function (term, field, metadata) { + if (!(term in this.metadata)) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = metadata + return + } + + if (!(field in this.metadata[term])) { + this.metadata[term][field] = metadata + return + } + + var metadataKeys = Object.keys(metadata) + + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + + if (key in this.metadata[term][field]) { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key]) + } else { + this.metadata[term][field][key] = metadata[key] + } + } +} +/** + * A lunr.Query provides a programmatic way of defining queries to be performed + * against a {@link lunr.Index}. + * + * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method + * so the query object is pre-initialized with the right index fields. + * + * @constructor + * @property {lunr.Query~Clause[]} clauses - An array of query clauses. + * @property {string[]} allFields - An array of all available fields in a lunr.Index. + */ +lunr.Query = function (allFields) { + this.clauses = [] + this.allFields = allFields +} + +/** + * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause. + * + * This allows wildcards to be added to the beginning and end of a term without having to manually do any string + * concatenation. + * + * The wildcard constants can be bitwise combined to select both leading and trailing wildcards. + * + * @constant + * @default + * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour + * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists + * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with trailing wildcard + * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING }) + * @example query term with leading and trailing wildcard + * query.term('foo', { + * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING + * }) + */ + +lunr.Query.wildcard = new String ("*") +lunr.Query.wildcard.NONE = 0 +lunr.Query.wildcard.LEADING = 1 +lunr.Query.wildcard.TRAILING = 2 + +/** + * Constants for indicating what kind of presence a term must have in matching documents. + * + * @constant + * @enum {number} + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with required presence + * query.term('foo', { presence: lunr.Query.presence.REQUIRED }) + */ +lunr.Query.presence = { + /** + * Term's presence in a document is optional, this is the default value. + */ + OPTIONAL: 1, + + /** + * Term's presence in a document is required, documents that do not contain + * this term will not be returned. + */ + REQUIRED: 2, + + /** + * Term's presence in a document is prohibited, documents that do contain + * this term will not be returned. + */ + PROHIBITED: 3 +} + +/** + * A single clause in a {@link lunr.Query} contains a term and details on how to + * match that term against a {@link lunr.Index}. + * + * @typedef {Object} lunr.Query~Clause + * @property {string[]} fields - The fields in an index this clause should be matched against. + * @property {number} [boost=1] - Any boost that should be applied when matching this clause. + * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be. + * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline. + * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended. + * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents. + */ + +/** + * Adds a {@link lunr.Query~Clause} to this query. + * + * Unless the clause contains the fields to be matched all fields will be matched. In addition + * a default boost of 1 is applied to the clause. + * + * @param {lunr.Query~Clause} clause - The clause to add to this query. + * @see lunr.Query~Clause + * @returns {lunr.Query} + */ +lunr.Query.prototype.clause = function (clause) { + if (!('fields' in clause)) { + clause.fields = this.allFields + } + + if (!('boost' in clause)) { + clause.boost = 1 + } + + if (!('usePipeline' in clause)) { + clause.usePipeline = true + } + + if (!('wildcard' in clause)) { + clause.wildcard = lunr.Query.wildcard.NONE + } + + if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) { + clause.term = "*" + clause.term + } + + if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) { + clause.term = "" + clause.term + "*" + } + + if (!('presence' in clause)) { + clause.presence = lunr.Query.presence.OPTIONAL + } + + this.clauses.push(clause) + + return this +} + +/** + * A negated query is one in which every clause has a presence of + * prohibited. These queries require some special processing to return + * the expected results. + * + * @returns boolean + */ +lunr.Query.prototype.isNegated = function () { + for (var i = 0; i < this.clauses.length; i++) { + if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) { + return false + } + } + + return true +} + +/** + * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause} + * to the list of clauses that make up this query. + * + * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion + * to a token or token-like string should be done before calling this method. + * + * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an + * array, each term in the array will share the same options. + * + * @param {object|object[]} term - The term(s) to add to the query. + * @param {object} [options] - Any additional properties to add to the query clause. + * @returns {lunr.Query} + * @see lunr.Query#clause + * @see lunr.Query~Clause + * @example adding a single term to a query + * query.term("foo") + * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard + * query.term("foo", { + * fields: ["title"], + * boost: 10, + * wildcard: lunr.Query.wildcard.TRAILING + * }) + * @example using lunr.tokenizer to convert a string to tokens before using them as terms + * query.term(lunr.tokenizer("foo bar")) + */ +lunr.Query.prototype.term = function (term, options) { + if (Array.isArray(term)) { + term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this) + return this + } + + var clause = options || {} + clause.term = term.toString() + + this.clause(clause) + + return this +} +lunr.QueryParseError = function (message, start, end) { + this.name = "QueryParseError" + this.message = message + this.start = start + this.end = end +} + +lunr.QueryParseError.prototype = new Error +lunr.QueryLexer = function (str) { + this.lexemes = [] + this.str = str + this.length = str.length + this.pos = 0 + this.start = 0 + this.escapeCharPositions = [] +} + +lunr.QueryLexer.prototype.run = function () { + var state = lunr.QueryLexer.lexText + + while (state) { + state = state(this) + } +} + +lunr.QueryLexer.prototype.sliceString = function () { + var subSlices = [], + sliceStart = this.start, + sliceEnd = this.pos + + for (var i = 0; i < this.escapeCharPositions.length; i++) { + sliceEnd = this.escapeCharPositions[i] + subSlices.push(this.str.slice(sliceStart, sliceEnd)) + sliceStart = sliceEnd + 1 + } + + subSlices.push(this.str.slice(sliceStart, this.pos)) + this.escapeCharPositions.length = 0 + + return subSlices.join('') +} + +lunr.QueryLexer.prototype.emit = function (type) { + this.lexemes.push({ + type: type, + str: this.sliceString(), + start: this.start, + end: this.pos + }) + + this.start = this.pos +} + +lunr.QueryLexer.prototype.escapeCharacter = function () { + this.escapeCharPositions.push(this.pos - 1) + this.pos += 1 +} + +lunr.QueryLexer.prototype.next = function () { + if (this.pos >= this.length) { + return lunr.QueryLexer.EOS + } + + var char = this.str.charAt(this.pos) + this.pos += 1 + return char +} + +lunr.QueryLexer.prototype.width = function () { + return this.pos - this.start +} + +lunr.QueryLexer.prototype.ignore = function () { + if (this.start == this.pos) { + this.pos += 1 + } + + this.start = this.pos +} + +lunr.QueryLexer.prototype.backup = function () { + this.pos -= 1 +} + +lunr.QueryLexer.prototype.acceptDigitRun = function () { + var char, charCode + + do { + char = this.next() + charCode = char.charCodeAt(0) + } while (charCode > 47 && charCode < 58) + + if (char != lunr.QueryLexer.EOS) { + this.backup() + } +} + +lunr.QueryLexer.prototype.more = function () { + return this.pos < this.length +} + +lunr.QueryLexer.EOS = 'EOS' +lunr.QueryLexer.FIELD = 'FIELD' +lunr.QueryLexer.TERM = 'TERM' +lunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE' +lunr.QueryLexer.BOOST = 'BOOST' +lunr.QueryLexer.PRESENCE = 'PRESENCE' + +lunr.QueryLexer.lexField = function (lexer) { + lexer.backup() + lexer.emit(lunr.QueryLexer.FIELD) + lexer.ignore() + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexTerm = function (lexer) { + if (lexer.width() > 1) { + lexer.backup() + lexer.emit(lunr.QueryLexer.TERM) + } + + lexer.ignore() + + if (lexer.more()) { + return lunr.QueryLexer.lexText + } +} + +lunr.QueryLexer.lexEditDistance = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.EDIT_DISTANCE) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexBoost = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.BOOST) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexEOS = function (lexer) { + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } +} + +// This matches the separator used when tokenising fields +// within a document. These should match otherwise it is +// not possible to search for some tokens within a document. +// +// It is possible for the user to change the separator on the +// tokenizer so it _might_ clash with any other of the special +// characters already used within the search string, e.g. :. +// +// This means that it is possible to change the separator in +// such a way that makes some words unsearchable using a search +// string. +lunr.QueryLexer.termSeparator = lunr.tokenizer.separator + +lunr.QueryLexer.lexText = function (lexer) { + while (true) { + var char = lexer.next() + + if (char == lunr.QueryLexer.EOS) { + return lunr.QueryLexer.lexEOS + } + + // Escape character is '\' + if (char.charCodeAt(0) == 92) { + lexer.escapeCharacter() + continue + } + + if (char == ":") { + return lunr.QueryLexer.lexField + } + + if (char == "~") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexEditDistance + } + + if (char == "^") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexBoost + } + + // "+" indicates term presence is required + // checking for length to ensure that only + // leading "+" are considered + if (char == "+" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + // "-" indicates term presence is prohibited + // checking for length to ensure that only + // leading "-" are considered + if (char == "-" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + if (char.match(lunr.QueryLexer.termSeparator)) { + return lunr.QueryLexer.lexTerm + } + } +} + +lunr.QueryParser = function (str, query) { + this.lexer = new lunr.QueryLexer (str) + this.query = query + this.currentClause = {} + this.lexemeIdx = 0 +} + +lunr.QueryParser.prototype.parse = function () { + this.lexer.run() + this.lexemes = this.lexer.lexemes + + var state = lunr.QueryParser.parseClause + + while (state) { + state = state(this) + } + + return this.query +} + +lunr.QueryParser.prototype.peekLexeme = function () { + return this.lexemes[this.lexemeIdx] +} + +lunr.QueryParser.prototype.consumeLexeme = function () { + var lexeme = this.peekLexeme() + this.lexemeIdx += 1 + return lexeme +} + +lunr.QueryParser.prototype.nextClause = function () { + var completedClause = this.currentClause + this.query.clause(completedClause) + this.currentClause = {} +} + +lunr.QueryParser.parseClause = function (parser) { + var lexeme = parser.peekLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.type) { + case lunr.QueryLexer.PRESENCE: + return lunr.QueryParser.parsePresence + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expected either a field or a term, found " + lexeme.type + + if (lexeme.str.length >= 1) { + errorMessage += " with value '" + lexeme.str + "'" + } + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } +} + +lunr.QueryParser.parsePresence = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.str) { + case "-": + parser.currentClause.presence = lunr.Query.presence.PROHIBITED + break + case "+": + parser.currentClause.presence = lunr.Query.presence.REQUIRED + break + default: + var errorMessage = "unrecognised presence operator'" + lexeme.str + "'" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term or field, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term or field, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseField = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + if (parser.query.allFields.indexOf(lexeme.str) == -1) { + var possibleFields = parser.query.allFields.map(function (f) { return "'" + f + "'" }).join(', '), + errorMessage = "unrecognised field '" + lexeme.str + "', possible fields: " + possibleFields + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.fields = [lexeme.str] + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseTerm = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + parser.currentClause.term = lexeme.str.toLowerCase() + + if (lexeme.str.indexOf("*") != -1) { + parser.currentClause.usePipeline = false + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseEditDistance = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var editDistance = parseInt(lexeme.str, 10) + + if (isNaN(editDistance)) { + var errorMessage = "edit distance must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.editDistance = editDistance + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseBoost = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var boost = parseInt(lexeme.str, 10) + + if (isNaN(boost)) { + var errorMessage = "boost must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.boost = boost + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like environments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})(); diff --git a/search/main.js b/search/main.js new file mode 100644 index 0000000000..a5e469d7c8 --- /dev/null +++ b/search/main.js @@ -0,0 +1,109 @@ +function getSearchTermFromLocation() { + var sPageURL = window.location.search.substring(1); + var sURLVariables = sPageURL.split('&'); + for (var i = 0; i < sURLVariables.length; i++) { + var sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] == 'q') { + return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20')); + } + } +} + +function joinUrl (base, path) { + if (path.substring(0, 1) === "/") { + // path starts with `/`. Thus it is absolute. + return path; + } + if (base.substring(base.length-1) === "/") { + // base ends with `/` + return base + path; + } + return base + "/" + path; +} + +function escapeHtml (value) { + return value.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +} + +function formatResult (location, title, summary) { + return ''; +} + +function displayResults (results) { + var search_results = document.getElementById("mkdocs-search-results"); + while (search_results.firstChild) { + search_results.removeChild(search_results.firstChild); + } + if (results.length > 0){ + for (var i=0; i < results.length; i++){ + var result = results[i]; + var html = formatResult(result.location, result.title, result.summary); + search_results.insertAdjacentHTML('beforeend', html); + } + } else { + var noResultsText = search_results.getAttribute('data-no-results-text'); + if (!noResultsText) { + noResultsText = "No results found"; + } + search_results.insertAdjacentHTML('beforeend', '

    ' + noResultsText + '

    '); + } +} + +function doSearch () { + var query = document.getElementById('mkdocs-search-query').value; + if (query.length > min_search_length) { + if (!window.Worker) { + displayResults(search(query)); + } else { + searchWorker.postMessage({query: query}); + } + } else { + // Clear results for short queries + displayResults([]); + } +} + +function initSearch () { + var search_input = document.getElementById('mkdocs-search-query'); + if (search_input) { + search_input.addEventListener("keyup", doSearch); + } + var term = getSearchTermFromLocation(); + if (term) { + search_input.value = term; + doSearch(); + } +} + +function onWorkerMessage (e) { + if (e.data.allowSearch) { + initSearch(); + } else if (e.data.results) { + var results = e.data.results; + displayResults(results); + } else if (e.data.config) { + min_search_length = e.data.config.min_search_length-1; + } +} + +if (!window.Worker) { + console.log('Web Worker API not supported'); + // load index in main thread + $.getScript(joinUrl(base_url, "search/worker.js")).done(function () { + console.log('Loaded worker'); + init(); + window.postMessage = function (msg) { + onWorkerMessage({data: msg}); + }; + }).fail(function (jqxhr, settings, exception) { + console.error('Could not load worker.js'); + }); +} else { + // Wrap search in a web worker + var searchWorker = new Worker(joinUrl(base_url, "search/worker.js")); + searchWorker.postMessage({init: true}); + searchWorker.onmessage = onWorkerMessage; +} diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 0000000000..37f11c2109 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"elmah.io Installation Quick Start Welcome to the quick-start installation guide. Here you will find a quick introduction to installing elmah.io. For the full overview, read through the individual guides by clicking a technology below: ASP.NET ASP.NET MVC ASP.NET Web API ASP.NET Core Extensions.Logging Functions Serilog log4net NLog Logary Umbraco JavaScript ASP.NET / MVC / Web API Install the Elmah.Io NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io paket add Elmah.Io During the installation, you will be asked for your API key and log ID. For more information, check out the installation guides for WebForms , MVC , and Web API . There is a short video tutorial available here: ASP.NET Core Install the Elmah.Io.AspNetCore NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore dotnet add package Elmah.Io.AspNetCore paket add Elmah.Io.AspNetCore Once installed, call AddElmahIo and UseElmahIo in the Program.cs file: var builder = WebApplication.CreateBuilder(args); // ... builder.Services.AddElmahIo(options => // \ud83d\udc48 { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); // ... var app = builder.Build(); // ... app.UseElmahIo(); // \ud83d\udc48 // ... app.Run(); Make sure to insert your API key and log ID. For more information, check out the installation guides for ASP.NET Core and Microsoft.Extensions.Logging . JavaScript Install the elmah.io.javascript npm package: npm install elmah.io.javascript Reference the installed script and include your API key and log ID as part of the URL: For more information, check out the installation guide for JavaScript .","title":"Quick start"},{"location":"#elmahio-installation-quick-start","text":"Welcome to the quick-start installation guide. Here you will find a quick introduction to installing elmah.io. For the full overview, read through the individual guides by clicking a technology below: ASP.NET ASP.NET MVC ASP.NET Web API ASP.NET Core Extensions.Logging Functions Serilog log4net NLog Logary Umbraco JavaScript","title":"elmah.io Installation Quick Start"},{"location":"#aspnet-mvc-web-api","text":"Install the Elmah.Io NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io paket add Elmah.Io During the installation, you will be asked for your API key and log ID. For more information, check out the installation guides for WebForms , MVC , and Web API . There is a short video tutorial available here:","title":"ASP.NET / MVC / Web API"},{"location":"#aspnet-core","text":"Install the Elmah.Io.AspNetCore NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore dotnet add package Elmah.Io.AspNetCore paket add Elmah.Io.AspNetCore Once installed, call AddElmahIo and UseElmahIo in the Program.cs file: var builder = WebApplication.CreateBuilder(args); // ... builder.Services.AddElmahIo(options => // \ud83d\udc48 { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); // ... var app = builder.Build(); // ... app.UseElmahIo(); // \ud83d\udc48 // ... app.Run(); Make sure to insert your API key and log ID. For more information, check out the installation guides for ASP.NET Core and Microsoft.Extensions.Logging .","title":"ASP.NET Core"},{"location":"#javascript","text":"Install the elmah.io.javascript npm package: npm install elmah.io.javascript Reference the installed script and include your API key and log ID as part of the URL: For more information, check out the installation guide for JavaScript .","title":"JavaScript"},{"location":"adding-version-information/","text":"Adding Version Information Adding Version Information Version Numbers on the UI Adding Version Numbers ASP.NET Core ASP.NET Microsoft.Extensions.Logging log4net NLog Serilog Almost every piece of software has some sort of version. Whether it's a nice-looking SemVer string or a simple timestamp, being able to distinguish one version from the other is important. elmah.io supports sending version information from your application in every message logged in two ways: By adding the version manually (as explained in this document). By using the Deployment Tracking feature (as explained in Set Up Deployment Tracking ). Version Numbers on the UI Let's start by looking at how version numbers are represented in the elmah.io UI. Every message contains a version property as illustrated below: The error is logged by an application with version number 1.0.0. This way, you will be able to see which version of your software logged each message. Having the version number on the message opens up some interesting search possibilities. Imagine that you want to search for every message logged by 1.0.* versions of your software, including release candidates, etc. Simply search in the search box like this: The example above finds every message logged from 1.0.0, 1.0.0-rc1, 1.0.1, etc. Adding Version Numbers How you choose to represent version numbers in your system is really up to you. elmah.io doesn't require SemVer, even though we strongly recommend you use it. Version is a simple string in our API , which means that you can send anything from SemVer to a stringified timestamp. Adding a version number to every message logged in elmah.io is easy as 1-2-3. If you're using our API, there's a property named version where you can put the version of your application. Chances are that you are using one of the integrations for ASP.NET Core, log4net, or Serilog. There are multiple ways to send a version number to elmah.io. ASP.NET Core Adding version information to all errors logged from Elmah.Io.AspNetCore can be achieved using the OnMessage action when initializing logging to elmah.io: builder.Services.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.Version = \"1.2.3\"; }; }); ASP.NET You probably want to attach the same version number on every message logged in elmah.io. The easiest way to achieve that is to create a global event handler for the OnMessage event, which is triggered every time the elmah.io client logs a message to elmah.io: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = ErrorLog.Client; logger.OnMessage += (sender, args) => { args.Message.Version = \"1.2.3\"; } In the example, the message send off to elmah.io is decorated with the version number 1.2.3 You will need to replace this with the value of an app setting, the assembly info, or whatever strategy you've used to make the version number available to your code. If you're logging errors to elmah.io in catch blocks, logging the version number can be done using a similar approach to the above: try { CallSomeBusinessLogic(inputValue); } catch (Exception e) { e.Data.Add(\"X-ELMAHIO-VERSION\", \"1.2.3\"); ErrorSignal.FromCurrentContext().Raise(e); } In this case, the code at this point doesn't know anything about elmah.io. Luckily, there's an alternative to the Version property, by putting a custom element in the Data dictionary on Exception. The exact name of the key must be X-ELMAHIO-VERSION for elmah.io to interpret this as the version number. Microsoft.Extensions.Logging When using the Elmah.Io.Extensions.Logging package there are multiple ways of enriching log messages with version information. If you want the same version number on all log messages you can use the OnMessage action: builder.Logging.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.Version = \"1.2.3\"; }; }); As an alternative, you can push a version property on individual log messages using either a structured property: logger.LogInformation(\"Message from {version}\", \"1.2.3\"); Or using scopes: using (logger.BeginScope(new { Version = \"1.2.3\" })) { logger.LogInformation(\"A message inside a logging scope\"); } log4net log4net supports the concept of customer properties in various ways. Since log4net properties are converted to custom properties in elmah.io, the easiest way to add a version number of every message logged through log4net is by configuring a global property somewhere in your initialization code: log4net.GlobalContext.Properties[\"version\"] = \"1.2.3\"; log4net supports custom properties in the context of a log call as well. To do that, put the version property in the ThreadContext before logging to log4net: log4net.ThreadContext.Properties[\"version\"] = \"1.2.3\"; log4net.Error(\"This is an error message\"); NLog NLog supports structured properties as well as various context objects. To set a version number on all log messages you can include the following code after initializing NLog: GlobalDiagnosticsContext.Set(\"version\", \"1.2.3\"); If the elmah.io NLog target is initialized from C# code you can also use the OnMessage action: var elmahIoTarget = new ElmahIoTarget(); // ... elmahIoTarget.OnMessage = msg => { msg.Version = \"1.2.3\"; }; To set a version number on a single log message, you can include it as a property: log.Info(\"Message from {version}\", \"1.2.3\"); Serilog Serilog can decorate all log messages using enrichers: var logger = new LoggerConfiguration() .Enrich.WithProperty(\"version\", \"1.2.3\") .Enrich.FromLogContext() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\"))) .CreateLogger(); You can also enrich a single log message with a version number using a structured property: Log.Information(\"Meesage from {version}\", \"1.2.3\"); Or using the LogContext : using (LogContext.PushProperty(\"version\", \"1.2.3\")) { Log.Error(\"An error message\"); }","title":"Adding version information"},{"location":"adding-version-information/#adding-version-information","text":"Adding Version Information Version Numbers on the UI Adding Version Numbers ASP.NET Core ASP.NET Microsoft.Extensions.Logging log4net NLog Serilog Almost every piece of software has some sort of version. Whether it's a nice-looking SemVer string or a simple timestamp, being able to distinguish one version from the other is important. elmah.io supports sending version information from your application in every message logged in two ways: By adding the version manually (as explained in this document). By using the Deployment Tracking feature (as explained in Set Up Deployment Tracking ).","title":"Adding Version Information"},{"location":"adding-version-information/#version-numbers-on-the-ui","text":"Let's start by looking at how version numbers are represented in the elmah.io UI. Every message contains a version property as illustrated below: The error is logged by an application with version number 1.0.0. This way, you will be able to see which version of your software logged each message. Having the version number on the message opens up some interesting search possibilities. Imagine that you want to search for every message logged by 1.0.* versions of your software, including release candidates, etc. Simply search in the search box like this: The example above finds every message logged from 1.0.0, 1.0.0-rc1, 1.0.1, etc.","title":"Version Numbers on the UI"},{"location":"adding-version-information/#adding-version-numbers","text":"How you choose to represent version numbers in your system is really up to you. elmah.io doesn't require SemVer, even though we strongly recommend you use it. Version is a simple string in our API , which means that you can send anything from SemVer to a stringified timestamp. Adding a version number to every message logged in elmah.io is easy as 1-2-3. If you're using our API, there's a property named version where you can put the version of your application. Chances are that you are using one of the integrations for ASP.NET Core, log4net, or Serilog. There are multiple ways to send a version number to elmah.io.","title":"Adding Version Numbers"},{"location":"adding-version-information/#aspnet-core","text":"Adding version information to all errors logged from Elmah.Io.AspNetCore can be achieved using the OnMessage action when initializing logging to elmah.io: builder.Services.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.Version = \"1.2.3\"; }; });","title":"ASP.NET Core"},{"location":"adding-version-information/#aspnet","text":"You probably want to attach the same version number on every message logged in elmah.io. The easiest way to achieve that is to create a global event handler for the OnMessage event, which is triggered every time the elmah.io client logs a message to elmah.io: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = ErrorLog.Client; logger.OnMessage += (sender, args) => { args.Message.Version = \"1.2.3\"; } In the example, the message send off to elmah.io is decorated with the version number 1.2.3 You will need to replace this with the value of an app setting, the assembly info, or whatever strategy you've used to make the version number available to your code. If you're logging errors to elmah.io in catch blocks, logging the version number can be done using a similar approach to the above: try { CallSomeBusinessLogic(inputValue); } catch (Exception e) { e.Data.Add(\"X-ELMAHIO-VERSION\", \"1.2.3\"); ErrorSignal.FromCurrentContext().Raise(e); } In this case, the code at this point doesn't know anything about elmah.io. Luckily, there's an alternative to the Version property, by putting a custom element in the Data dictionary on Exception. The exact name of the key must be X-ELMAHIO-VERSION for elmah.io to interpret this as the version number.","title":"ASP.NET"},{"location":"adding-version-information/#microsoftextensionslogging","text":"When using the Elmah.Io.Extensions.Logging package there are multiple ways of enriching log messages with version information. If you want the same version number on all log messages you can use the OnMessage action: builder.Logging.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.Version = \"1.2.3\"; }; }); As an alternative, you can push a version property on individual log messages using either a structured property: logger.LogInformation(\"Message from {version}\", \"1.2.3\"); Or using scopes: using (logger.BeginScope(new { Version = \"1.2.3\" })) { logger.LogInformation(\"A message inside a logging scope\"); }","title":"Microsoft.Extensions.Logging"},{"location":"adding-version-information/#log4net","text":"log4net supports the concept of customer properties in various ways. Since log4net properties are converted to custom properties in elmah.io, the easiest way to add a version number of every message logged through log4net is by configuring a global property somewhere in your initialization code: log4net.GlobalContext.Properties[\"version\"] = \"1.2.3\"; log4net supports custom properties in the context of a log call as well. To do that, put the version property in the ThreadContext before logging to log4net: log4net.ThreadContext.Properties[\"version\"] = \"1.2.3\"; log4net.Error(\"This is an error message\");","title":"log4net"},{"location":"adding-version-information/#nlog","text":"NLog supports structured properties as well as various context objects. To set a version number on all log messages you can include the following code after initializing NLog: GlobalDiagnosticsContext.Set(\"version\", \"1.2.3\"); If the elmah.io NLog target is initialized from C# code you can also use the OnMessage action: var elmahIoTarget = new ElmahIoTarget(); // ... elmahIoTarget.OnMessage = msg => { msg.Version = \"1.2.3\"; }; To set a version number on a single log message, you can include it as a property: log.Info(\"Message from {version}\", \"1.2.3\");","title":"NLog"},{"location":"adding-version-information/#serilog","text":"Serilog can decorate all log messages using enrichers: var logger = new LoggerConfiguration() .Enrich.WithProperty(\"version\", \"1.2.3\") .Enrich.FromLogContext() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\"))) .CreateLogger(); You can also enrich a single log message with a version number using a structured property: Log.Information(\"Meesage from {version}\", \"1.2.3\"); Or using the LogContext : using (LogContext.PushProperty(\"version\", \"1.2.3\")) { Log.Error(\"An error message\"); }","title":"Serilog"},{"location":"allowing-elmah-io-uptime-agents/","text":"Allowing elmah.io uptime agents elmah.io Uptime Monitoring runs in 9 different regions. Depending on which regions you enabled on each uptime check, your network infrastructure/firewall, or security settings, you may need to allow requests from elmah.io uptime agents. You can allow requests by allowing all requests with the following user-agent: User-Agent: elmahio-uptimebot/2.0 Allowing requests by possible inbound IP addresses is also an option. Since we run on Azure these changes over time, but here's a list by region as of time of writing this article: United States West 23.100.40.244,23.100.40.246,23.100.40.2,23.100.32.186,40.83.253.229,138.91.92.9,138.91.172.82,138.91.230.36,138.91.228.104,23.100.46.198,40.112.243.45,20.190.0.233,20.190.1.8,20.190.1.21,20.190.1.32,20.190.1.42,20.190.1.61,20.190.1.72,20.190.0.33,20.190.1.162,20.190.1.177,20.190.1.187,20.190.1.191,52.149.26.109,52.149.26.223,52.149.26.248,52.149.27.21,52.149.27.137,52.149.28.241,51.143.61.29,52.137.93.170,52.149.29.95,52.149.29.238,52.149.30.76,52.149.30.96,20.190.1.217,20.190.2.43,20.190.2.66,52.143.80.96,52.156.145.74,52.156.145.106 United States Central 13.86.38.119,13.89.106.62,40.89.243.166,52.143.244.58,52.154.168.87,52.154.169.9,52.154.169.57,52.154.169.86,52.154.169.106,52.154.169.204,52.154.170.25,52.154.170.83,52.154.170.158,52.154.171.81,52.154.171.88,52.154.173.21,40.89.244.137,52.154.173.79,52.154.173.101,52.154.173.180,52.154.174.243,52.154.175.58,52.154.240.147,52.154.240.170,52.154.241.97,52.154.241.132,52.154.241.246,52.154.43.251,52.154.44.21,52.154.44.95 United States East 13.92.137.62,13.92.138.72,13.92.137.49,13.92.143.167,13.68.182.254,13.82.23.172,13.82.23.182,13.82.18.16,13.82.18.62,13.92.139.214,40.71.11.179,52.247.85.151,52.247.85.207,52.247.85.217,52.247.85.218,52.247.85.228,52.247.85.242,52.247.74.244,52.247.75.195,52.247.76.11,52.247.76.21,52.247.76.72,52.247.76.155,52.247.85.252,52.247.86.14,52.247.86.22,52.167.77.53,52.177.247.146,52.184.204.69,20.49.97.1 United Kingdom South 51.140.180.76,51.140.191.115,51.140.176.159,51.140.176.11,51.140.185.39,51.140.177.87,51.140.185.119,51.140.188.39,51.140.188.241,51.140.183.68,51.140.184.173 United Kingdom West 51.140.210.96,51.140.252.30,51.140.251.198,51.141.123.138,51.140.248.129,51.140.252.164,51.140.248.213,52.142.164.59,51.140.248.195,51.140.250.153 Australia South East 191.239.187.197,191.239.187.149,191.239.184.74,191.239.188.25,52.189.210.149,52.189.209.158,52.189.215.220,52.189.208.40,40.127.95.32,40.115.76.80,40.115.76.83,40.115.76.93,40.115.76.94,40.115.76.101,40.115.76.109,40.115.76.129,40.115.76.143,191.239.188.11,13.77.50.103 Europe North 40.127.186.114,40.127.183.46,40.127.189.87,40.127.183.64,40.115.126.151,137.135.216.255,40.85.101.214,40.85.101.216,40.85.101.219,40.127.139.252,13.69.228.38 Europe West 20.71.75.100,20.71.75.133,20.71.75.198,20.71.75.237,20.71.76.1,20.71.76.23,20.71.76.57,20.71.76.171,20.71.76.196,20.71.77.10,20.71.77.83,20.71.77.134,20.71.77.205,20.71.78.20,20.71.78.208,20.71.78.243,20.71.79.77,20.71.79.165,20.71.79.228,20.73.232.14,20.73.232.51,20.73.232.121,20.73.232.130,20.73.232.163,20.73.232.186,20.73.232.217,20.73.232.241,20.73.132.133,20.73.132.167,20.73.133.173 Brazil South 191.232.176.16,191.232.180.88,191.232.177.130,191.232.180.187,191.232.179.236,191.232.177.40,191.232.180.122","title":"Allowing elmah.io uptime agents"},{"location":"allowing-elmah-io-uptime-agents/#allowing-elmahio-uptime-agents","text":"elmah.io Uptime Monitoring runs in 9 different regions. Depending on which regions you enabled on each uptime check, your network infrastructure/firewall, or security settings, you may need to allow requests from elmah.io uptime agents. You can allow requests by allowing all requests with the following user-agent: User-Agent: elmahio-uptimebot/2.0 Allowing requests by possible inbound IP addresses is also an option. Since we run on Azure these changes over time, but here's a list by region as of time of writing this article: United States West 23.100.40.244,23.100.40.246,23.100.40.2,23.100.32.186,40.83.253.229,138.91.92.9,138.91.172.82,138.91.230.36,138.91.228.104,23.100.46.198,40.112.243.45,20.190.0.233,20.190.1.8,20.190.1.21,20.190.1.32,20.190.1.42,20.190.1.61,20.190.1.72,20.190.0.33,20.190.1.162,20.190.1.177,20.190.1.187,20.190.1.191,52.149.26.109,52.149.26.223,52.149.26.248,52.149.27.21,52.149.27.137,52.149.28.241,51.143.61.29,52.137.93.170,52.149.29.95,52.149.29.238,52.149.30.76,52.149.30.96,20.190.1.217,20.190.2.43,20.190.2.66,52.143.80.96,52.156.145.74,52.156.145.106 United States Central 13.86.38.119,13.89.106.62,40.89.243.166,52.143.244.58,52.154.168.87,52.154.169.9,52.154.169.57,52.154.169.86,52.154.169.106,52.154.169.204,52.154.170.25,52.154.170.83,52.154.170.158,52.154.171.81,52.154.171.88,52.154.173.21,40.89.244.137,52.154.173.79,52.154.173.101,52.154.173.180,52.154.174.243,52.154.175.58,52.154.240.147,52.154.240.170,52.154.241.97,52.154.241.132,52.154.241.246,52.154.43.251,52.154.44.21,52.154.44.95 United States East 13.92.137.62,13.92.138.72,13.92.137.49,13.92.143.167,13.68.182.254,13.82.23.172,13.82.23.182,13.82.18.16,13.82.18.62,13.92.139.214,40.71.11.179,52.247.85.151,52.247.85.207,52.247.85.217,52.247.85.218,52.247.85.228,52.247.85.242,52.247.74.244,52.247.75.195,52.247.76.11,52.247.76.21,52.247.76.72,52.247.76.155,52.247.85.252,52.247.86.14,52.247.86.22,52.167.77.53,52.177.247.146,52.184.204.69,20.49.97.1 United Kingdom South 51.140.180.76,51.140.191.115,51.140.176.159,51.140.176.11,51.140.185.39,51.140.177.87,51.140.185.119,51.140.188.39,51.140.188.241,51.140.183.68,51.140.184.173 United Kingdom West 51.140.210.96,51.140.252.30,51.140.251.198,51.141.123.138,51.140.248.129,51.140.252.164,51.140.248.213,52.142.164.59,51.140.248.195,51.140.250.153 Australia South East 191.239.187.197,191.239.187.149,191.239.184.74,191.239.188.25,52.189.210.149,52.189.209.158,52.189.215.220,52.189.208.40,40.127.95.32,40.115.76.80,40.115.76.83,40.115.76.93,40.115.76.94,40.115.76.101,40.115.76.109,40.115.76.129,40.115.76.143,191.239.188.11,13.77.50.103 Europe North 40.127.186.114,40.127.183.46,40.127.189.87,40.127.183.64,40.115.126.151,137.135.216.255,40.85.101.214,40.85.101.216,40.85.101.219,40.127.139.252,13.69.228.38 Europe West 20.71.75.100,20.71.75.133,20.71.75.198,20.71.75.237,20.71.76.1,20.71.76.23,20.71.76.57,20.71.76.171,20.71.76.196,20.71.77.10,20.71.77.83,20.71.77.134,20.71.77.205,20.71.78.20,20.71.78.208,20.71.78.243,20.71.79.77,20.71.79.165,20.71.79.228,20.73.232.14,20.73.232.51,20.73.232.121,20.73.232.130,20.73.232.163,20.73.232.186,20.73.232.217,20.73.232.241,20.73.132.133,20.73.132.167,20.73.133.173 Brazil South 191.232.176.16,191.232.180.88,191.232.177.130,191.232.180.187,191.232.179.236,191.232.177.40,191.232.180.122","title":"Allowing elmah.io uptime agents"},{"location":"asp-net-core-troubleshooting/","text":"ASP.NET Core Troubleshooting So, your ASP.NET Core application doesn't log errors to elmah.io? Here is a list of things to try out: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure to reference the most recent version of the Elmah.Io.AspNetCore NuGet package. Make sure that the Elmah.Io.Client NuGet package is installed and that the major version matches that of Elmah.Io.AspNetCore . Make sure that you are calling both the AddElmahIo - and UseElmahIo -methods in the Program.cs file (or Startup.cs for older applications), as described on Logging to elmah.io from ASP.NET Core . Make sure that you call the UseElmahIo -method after invoking other Use* methods that in any way inspect exceptions (like UseDeveloperExceptionPage and UseExceptionHandler ). Make sure that you call the UseElmahIo -method before invoking UseMvc , UseEndpoints , and similar. Make sure that your server has an outgoing internet connection and that it can communicate with api.elmah.io on port 443 . The integration for ASP.NET Core support setting up an HTTP proxy if your server doesn't allow outgoing traffic. Check out Logging through a proxy for details. Make sure that you didn't enable any Ignore filters or set up any Rules with an ignore action on the log in question. Make sure that you don't have any code catching all exceptions happening in your system and ignoring them (could be a logging filter, a piece of middleware, or similar). Make sure that you haven't reached the message limit included in your current plan. Your current usage can be viewed on the Subscription tab on organization settings. Some of the bullets above have been implemented as Roslyn analyzers. Check out Roslyn analyzers for elmah.io and ASP.NET Core for details. Common problems and how to fix them Here you will a list of common exceptions and how to solve them. InvalidOperationException Exception [InvalidOperationException: Unable to resolve service for type 'Elmah.Io.AspNetCore.IBackgroundTaskQueue' while attempting to activate 'Elmah.Io.AspNetCore.ElmahIoMiddleware'.] Microsoft.Extensions.Internal.ActivatorUtilities+ConstructorMatcher.CreateInstance(IServiceProvider provider) Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass4_0.b__0(RequestDelegate next) Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder.Build() Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication() Solution You forgot to call the AddElmahIo -method in the Program.cs file: builder.Services.AddElmahIo(o => { // ... }); ArgumentException Exception [ArgumentException: Input an API key Parameter name: apiKey] Elmah.Io.AspNetCore.Extensions.StringExtensions.AssertApiKey(string apiKey) Elmah.Io.AspNetCore.ElmahIoMiddleware..ctor(RequestDelegate next, IBackgroundTaskQueue queue, IOptions options) Microsoft.Extensions.Internal.ActivatorUtilities+ConstructorMatcher.CreateInstance(IServiceProvider provider) Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters) Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass4_0.b__0(RequestDelegate next) Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder.Build() Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication() Solution You forgot to call the AddElmahIo -method in the Program.cs file: builder.Services.AddElmahIo(o => { // ... }); or you called AddElmahIo without options and didn't provide these options elsewhere: builder.Services.AddElmahIo(); Even though you configure elmah.io through appsettings.json you still need to call AddElmahIo . In this case, you can register ElmahIoOptions manually and use the empty AddElmahIo overload: builder.Services.Configure(Configuration.GetSection(\"ElmahIo\")); builder.Services.AddElmahIo(); An error occurred while starting the application If you see the error An error occurred while starting the application and the exception isn't logged to elmah.io, the error probably happens before hitting the elmah.io middleware. To help find out what is going on, add the following lines to your Program.cs file: builder.WebHost.UseSetting(WebHostDefaults.DetailedErrorsKey, \"true\"); builder.WebHost.CaptureStartupErrors(true); URL missing when using Map When handling requests with the Map method, ASP.NET Core will remove the path from HttpRequest.Path . In this case, Elmah.Io.AspNetCore will look for an URL in the HttpRequest.PathBase property. This is not already enough and won't always return the right URL. Consider using the MapWhen method instead. Thread pool thread or asynchronous tasks blocked on a synchronous call Azure and other systems with runtime diagnostics and validation may complain with the error Thread pool thread or asynchronous tasks blocked on a synchronous call in the Elmah.Io.AspNetCore package. This can be caused by our implementation of the package using a background worker for processing batches of messages. This background worker runs in a single thread and will never cause thread starvation as suggested by the error. We may want to move the internal implementation from BlockingCollection to Channel at some point.","title":"ASP.NET Core troubleshooting"},{"location":"asp-net-core-troubleshooting/#aspnet-core-troubleshooting","text":"So, your ASP.NET Core application doesn't log errors to elmah.io? Here is a list of things to try out: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure to reference the most recent version of the Elmah.Io.AspNetCore NuGet package. Make sure that the Elmah.Io.Client NuGet package is installed and that the major version matches that of Elmah.Io.AspNetCore . Make sure that you are calling both the AddElmahIo - and UseElmahIo -methods in the Program.cs file (or Startup.cs for older applications), as described on Logging to elmah.io from ASP.NET Core . Make sure that you call the UseElmahIo -method after invoking other Use* methods that in any way inspect exceptions (like UseDeveloperExceptionPage and UseExceptionHandler ). Make sure that you call the UseElmahIo -method before invoking UseMvc , UseEndpoints , and similar. Make sure that your server has an outgoing internet connection and that it can communicate with api.elmah.io on port 443 . The integration for ASP.NET Core support setting up an HTTP proxy if your server doesn't allow outgoing traffic. Check out Logging through a proxy for details. Make sure that you didn't enable any Ignore filters or set up any Rules with an ignore action on the log in question. Make sure that you don't have any code catching all exceptions happening in your system and ignoring them (could be a logging filter, a piece of middleware, or similar). Make sure that you haven't reached the message limit included in your current plan. Your current usage can be viewed on the Subscription tab on organization settings. Some of the bullets above have been implemented as Roslyn analyzers. Check out Roslyn analyzers for elmah.io and ASP.NET Core for details.","title":"ASP.NET Core Troubleshooting"},{"location":"asp-net-core-troubleshooting/#common-problems-and-how-to-fix-them","text":"Here you will a list of common exceptions and how to solve them.","title":"Common problems and how to fix them"},{"location":"asp-net-core-troubleshooting/#invalidoperationexception","text":"Exception [InvalidOperationException: Unable to resolve service for type 'Elmah.Io.AspNetCore.IBackgroundTaskQueue' while attempting to activate 'Elmah.Io.AspNetCore.ElmahIoMiddleware'.] Microsoft.Extensions.Internal.ActivatorUtilities+ConstructorMatcher.CreateInstance(IServiceProvider provider) Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass4_0.b__0(RequestDelegate next) Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder.Build() Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication() Solution You forgot to call the AddElmahIo -method in the Program.cs file: builder.Services.AddElmahIo(o => { // ... });","title":"InvalidOperationException"},{"location":"asp-net-core-troubleshooting/#argumentexception","text":"Exception [ArgumentException: Input an API key Parameter name: apiKey] Elmah.Io.AspNetCore.Extensions.StringExtensions.AssertApiKey(string apiKey) Elmah.Io.AspNetCore.ElmahIoMiddleware..ctor(RequestDelegate next, IBackgroundTaskQueue queue, IOptions options) Microsoft.Extensions.Internal.ActivatorUtilities+ConstructorMatcher.CreateInstance(IServiceProvider provider) Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters) Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass4_0.b__0(RequestDelegate next) Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder.Build() Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication() Solution You forgot to call the AddElmahIo -method in the Program.cs file: builder.Services.AddElmahIo(o => { // ... }); or you called AddElmahIo without options and didn't provide these options elsewhere: builder.Services.AddElmahIo(); Even though you configure elmah.io through appsettings.json you still need to call AddElmahIo . In this case, you can register ElmahIoOptions manually and use the empty AddElmahIo overload: builder.Services.Configure(Configuration.GetSection(\"ElmahIo\")); builder.Services.AddElmahIo();","title":"ArgumentException"},{"location":"asp-net-core-troubleshooting/#an-error-occurred-while-starting-the-application","text":"If you see the error An error occurred while starting the application and the exception isn't logged to elmah.io, the error probably happens before hitting the elmah.io middleware. To help find out what is going on, add the following lines to your Program.cs file: builder.WebHost.UseSetting(WebHostDefaults.DetailedErrorsKey, \"true\"); builder.WebHost.CaptureStartupErrors(true);","title":"An error occurred while starting the application"},{"location":"asp-net-core-troubleshooting/#url-missing-when-using-map","text":"When handling requests with the Map method, ASP.NET Core will remove the path from HttpRequest.Path . In this case, Elmah.Io.AspNetCore will look for an URL in the HttpRequest.PathBase property. This is not already enough and won't always return the right URL. Consider using the MapWhen method instead.","title":"URL missing when using Map"},{"location":"asp-net-core-troubleshooting/#thread-pool-thread-or-asynchronous-tasks-blocked-on-a-synchronous-call","text":"Azure and other systems with runtime diagnostics and validation may complain with the error Thread pool thread or asynchronous tasks blocked on a synchronous call in the Elmah.Io.AspNetCore package. This can be caused by our implementation of the package using a background worker for processing batches of messages. This background worker runs in a single thread and will never cause thread starvation as suggested by the error. We may want to move the internal implementation from BlockingCollection to Channel at some point.","title":"Thread pool thread or asynchronous tasks blocked on a synchronous call"},{"location":"asp-net-troubleshooting/","text":"ASP.NET Troubleshooting You are probably here because your application doesn't log errors to elmah.io, even though you installed the integration. Before contacting support, there are some things you can try out yourself. Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure that you are referencing one of the following NuGet packages: Elmah.Io , Elmah.Io.AspNet , Elmah.Io.Mvc or Elmah.Io.WebApi . Make sure that the Elmah.Io.Client NuGet package is installed and that the major version matches that of Elmah.Io , Elmah.Io.AspNet , Elmah.Io.Mvc or Elmah.Io.WebApi . Make sure that your project reference the following assemblies: Elmah , Elmah.Io , and Elmah.Io.Client . Make sure that your web.config file contains valid config as described here . You can validate your web.config file using this Web.config Validator . When installing the Elmah.Io NuGet package, config is automatically added to your web.config file, as long as your Visual Studio allows for running PowerShell scripts as part of the installation. To check if you have the correct execution policy, go to the Package Manager Console and verify that the result of the following statement is RemoteSigned : Get-ExecutionPolicy Make sure that your server has an outgoing internet connection and that it can communicate with api.elmah.io on port 443 . Most of our integrations support setting up an HTTP proxy if your server doesn't allow outgoing traffic. Make sure that you didn't enable any Ignore filters or set up any Rules with an ignore action on the log in question. Make sure that you don't have any code catching all exceptions happening in your system and ignoring them (could be a logging filter or similar). If you are using custom errors, make sure to configure it correctly. For more details, check out the following posts: Web.config customErrors element with ASP.NET explained and Demystifying ASP.NET MVC 5 Error Pages and Error Logging . Common errors and how to fix them Here you will a list of common errors/exceptions and how to solve them. TypeLoadException Exception [TypeLoadException: Inheritance security rules violated by type: 'System.Net.Http.WebRequestHandler'. Derived types must either match the security accessibility of the base type or be less accessible.] Microsoft.Rest.ServiceClient`1.CreateRootHandler() +0 Microsoft.Rest.ServiceClient`1..ctor(DelegatingHandler[] handlers) +59 Elmah.Io.Client.ElmahioAPI..ctor(DelegatingHandler[] handlers) +96 Elmah.Io.Client.ElmahioAPI..ctor(ServiceClientCredentials credentials, DelegatingHandler[] handlers) +70 Elmah.Io.Client.ElmahioAPI.Create(String apiKey, ElmahIoOptions options) +146 Elmah.Io.Client.ElmahioAPI.Create(String apiKey) +91 Elmah.Io.ErrorLog..ctor(IDictionary config) +109 Solution This is most likely caused by a problem with the System.Net.Http NuGet package. Make sure to upgrade to the newest version ( 4.3.4 as of writing this). The default template for creating a new web application installs version 4.3.0 which is seriously flawed. AmbiguousMatchException Exception [AmbiguousMatchException: Multiple custom attributes of the same type found.] System.Attribute.GetCustomAttribute(Assembly element, Type attributeType, Boolean inherit) +119 System.Reflection.CustomAttributeExtensions.GetCustomAttribute(Assembly element, Type attributeType) +16 Microsoft.Rest.ServiceClient`1.get_FrameworkVersion() +226 Microsoft.Rest.ServiceClient`1.SetDefaultAgentInfo() +93 Microsoft.Rest.ServiceClient`1.InitializeHttpClient(HttpClient httpClient, HttpClientHandler httpClientHandler, DelegatingHandler[] handlers) +386 Microsoft.Rest.ServiceClient`1..ctor(HttpClient serviceHttpClient, HttpClientHandler rootHandler, Boolean disposeHttpClient, DelegatingHandler[] delHandlers) +82 Microsoft.Rest.ServiceClient`1..ctor(HttpClientHandler rootHandler, DelegatingHandler[] handlers) +66 Elmah.Io.Client.ElmahioAPI..ctor(HttpClientHandler rootHandler, DelegatingHandler[] handlers) +104 Elmah.Io.Client.ElmahioAPI..ctor(ServiceClientCredentials credentials, HttpClientHandler rootHandler, DelegatingHandler[] handlers) +78 Elmah.Io.ErrorLog..ctor(IDictionary config) +225 [TargetInvocationException: Exception has been thrown by the target of an invocation.] System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor) +0 System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) +359 System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes, StackCrawlMark& stackMark) +1485 System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes) +298 System.Activator.CreateInstance(Type type, Object[] args) +34 Elmah.ErrorLog.GetDefaultImpl(HttpContext context) +178 Elmah.ServiceCenter.GetService(Object context, Type serviceType) +17 Elmah.ErrorLog.GetDefault(HttpContext context) +34 Elmah.ErrorPageBase.get_ErrorLog() +39 Elmah.ErrorLogPage.OnLoad(EventArgs e) +400 System.Web.UI.Control.LoadRecursive() +154 System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +4082 Solution This is most likely caused by some other software installed on the machine hosting your website. Applications like Microsoft Monitoring Agent (Application Insights) are known for creating problems for other software running on the same machine. Pause or stop any other monitoring service to see if the problem goes away. Exceptions aren't logged to elmah.io when adding the HandleError attribute Much like custom errors, the HandleError attribute can swallow exceptions from your website. This means that ASP.NET MVC catches any exceptions and shows the Error.cshtml view. To log exceptions with this setup, you will need to extend your Error.cshtml file: @model System.Web.Mvc.HandleErrorInfo @{ if (Model.Exception != null) { Elmah.ErrorLog.GetDefault(HttpContext.Current).Log(new Elmah.Error(Model.Exception, HttpContext.Current)); } }
    Your error page content goes here
    Errors are not logged when using update panels ASP.NET Update Panels are great at many things. Unfortunately, one of those things is causing problems when trying to log server-side errors. If errors aren't logged as part of a call made from an update panel, you can try to add the following code to Global.asax.cs : protected void Application_Error(object sender, EventArgs e) { var ex = Server.GetLastError(); var error = new Elmah.Error(ex); error.ServerVariables.Add(\"URL\", HttpContext.Current?.Request?.Url?.AbsolutePath); error.ServerVariables.Add(\"HTTP_USER_AGENT\", HttpContext.Current?.Request?.UserAgent); error.ServerVariables.Add(\"REMOTE_ADDR\", HttpContext.Current?.Request?.UserHostAddress); Elmah.ErrorLog.GetDefault(HttpContext.Current).Log(error); } If you start experiencing errors logged multiple times, you can remove the ELMAH module from web.config : ","title":"ASP.NET troubleshooting"},{"location":"asp-net-troubleshooting/#aspnet-troubleshooting","text":"You are probably here because your application doesn't log errors to elmah.io, even though you installed the integration. Before contacting support, there are some things you can try out yourself. Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure that you are referencing one of the following NuGet packages: Elmah.Io , Elmah.Io.AspNet , Elmah.Io.Mvc or Elmah.Io.WebApi . Make sure that the Elmah.Io.Client NuGet package is installed and that the major version matches that of Elmah.Io , Elmah.Io.AspNet , Elmah.Io.Mvc or Elmah.Io.WebApi . Make sure that your project reference the following assemblies: Elmah , Elmah.Io , and Elmah.Io.Client . Make sure that your web.config file contains valid config as described here . You can validate your web.config file using this Web.config Validator . When installing the Elmah.Io NuGet package, config is automatically added to your web.config file, as long as your Visual Studio allows for running PowerShell scripts as part of the installation. To check if you have the correct execution policy, go to the Package Manager Console and verify that the result of the following statement is RemoteSigned : Get-ExecutionPolicy Make sure that your server has an outgoing internet connection and that it can communicate with api.elmah.io on port 443 . Most of our integrations support setting up an HTTP proxy if your server doesn't allow outgoing traffic. Make sure that you didn't enable any Ignore filters or set up any Rules with an ignore action on the log in question. Make sure that you don't have any code catching all exceptions happening in your system and ignoring them (could be a logging filter or similar). If you are using custom errors, make sure to configure it correctly. For more details, check out the following posts: Web.config customErrors element with ASP.NET explained and Demystifying ASP.NET MVC 5 Error Pages and Error Logging .","title":"ASP.NET Troubleshooting"},{"location":"asp-net-troubleshooting/#common-errors-and-how-to-fix-them","text":"Here you will a list of common errors/exceptions and how to solve them.","title":"Common errors and how to fix them"},{"location":"asp-net-troubleshooting/#typeloadexception","text":"Exception [TypeLoadException: Inheritance security rules violated by type: 'System.Net.Http.WebRequestHandler'. Derived types must either match the security accessibility of the base type or be less accessible.] Microsoft.Rest.ServiceClient`1.CreateRootHandler() +0 Microsoft.Rest.ServiceClient`1..ctor(DelegatingHandler[] handlers) +59 Elmah.Io.Client.ElmahioAPI..ctor(DelegatingHandler[] handlers) +96 Elmah.Io.Client.ElmahioAPI..ctor(ServiceClientCredentials credentials, DelegatingHandler[] handlers) +70 Elmah.Io.Client.ElmahioAPI.Create(String apiKey, ElmahIoOptions options) +146 Elmah.Io.Client.ElmahioAPI.Create(String apiKey) +91 Elmah.Io.ErrorLog..ctor(IDictionary config) +109 Solution This is most likely caused by a problem with the System.Net.Http NuGet package. Make sure to upgrade to the newest version ( 4.3.4 as of writing this). The default template for creating a new web application installs version 4.3.0 which is seriously flawed.","title":"TypeLoadException"},{"location":"asp-net-troubleshooting/#ambiguousmatchexception","text":"Exception [AmbiguousMatchException: Multiple custom attributes of the same type found.] System.Attribute.GetCustomAttribute(Assembly element, Type attributeType, Boolean inherit) +119 System.Reflection.CustomAttributeExtensions.GetCustomAttribute(Assembly element, Type attributeType) +16 Microsoft.Rest.ServiceClient`1.get_FrameworkVersion() +226 Microsoft.Rest.ServiceClient`1.SetDefaultAgentInfo() +93 Microsoft.Rest.ServiceClient`1.InitializeHttpClient(HttpClient httpClient, HttpClientHandler httpClientHandler, DelegatingHandler[] handlers) +386 Microsoft.Rest.ServiceClient`1..ctor(HttpClient serviceHttpClient, HttpClientHandler rootHandler, Boolean disposeHttpClient, DelegatingHandler[] delHandlers) +82 Microsoft.Rest.ServiceClient`1..ctor(HttpClientHandler rootHandler, DelegatingHandler[] handlers) +66 Elmah.Io.Client.ElmahioAPI..ctor(HttpClientHandler rootHandler, DelegatingHandler[] handlers) +104 Elmah.Io.Client.ElmahioAPI..ctor(ServiceClientCredentials credentials, HttpClientHandler rootHandler, DelegatingHandler[] handlers) +78 Elmah.Io.ErrorLog..ctor(IDictionary config) +225 [TargetInvocationException: Exception has been thrown by the target of an invocation.] System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor) +0 System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) +359 System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes, StackCrawlMark& stackMark) +1485 System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes) +298 System.Activator.CreateInstance(Type type, Object[] args) +34 Elmah.ErrorLog.GetDefaultImpl(HttpContext context) +178 Elmah.ServiceCenter.GetService(Object context, Type serviceType) +17 Elmah.ErrorLog.GetDefault(HttpContext context) +34 Elmah.ErrorPageBase.get_ErrorLog() +39 Elmah.ErrorLogPage.OnLoad(EventArgs e) +400 System.Web.UI.Control.LoadRecursive() +154 System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +4082 Solution This is most likely caused by some other software installed on the machine hosting your website. Applications like Microsoft Monitoring Agent (Application Insights) are known for creating problems for other software running on the same machine. Pause or stop any other monitoring service to see if the problem goes away.","title":"AmbiguousMatchException"},{"location":"asp-net-troubleshooting/#exceptions-arent-logged-to-elmahio-when-adding-the-handleerror-attribute","text":"Much like custom errors, the HandleError attribute can swallow exceptions from your website. This means that ASP.NET MVC catches any exceptions and shows the Error.cshtml view. To log exceptions with this setup, you will need to extend your Error.cshtml file: @model System.Web.Mvc.HandleErrorInfo @{ if (Model.Exception != null) { Elmah.ErrorLog.GetDefault(HttpContext.Current).Log(new Elmah.Error(Model.Exception, HttpContext.Current)); } }
    Your error page content goes here
    ","title":"Exceptions aren't logged to elmah.io when adding the HandleError attribute"},{"location":"asp-net-troubleshooting/#errors-are-not-logged-when-using-update-panels","text":"ASP.NET Update Panels are great at many things. Unfortunately, one of those things is causing problems when trying to log server-side errors. If errors aren't logged as part of a call made from an update panel, you can try to add the following code to Global.asax.cs : protected void Application_Error(object sender, EventArgs e) { var ex = Server.GetLastError(); var error = new Elmah.Error(ex); error.ServerVariables.Add(\"URL\", HttpContext.Current?.Request?.Url?.AbsolutePath); error.ServerVariables.Add(\"HTTP_USER_AGENT\", HttpContext.Current?.Request?.UserAgent); error.ServerVariables.Add(\"REMOTE_ADDR\", HttpContext.Current?.Request?.UserHostAddress); Elmah.ErrorLog.GetDefault(HttpContext.Current).Log(error); } If you start experiencing errors logged multiple times, you can remove the ELMAH module from web.config : ","title":"Errors are not logged when using update panels"},{"location":"authentication/","text":"Authentication All of our integrations communicates with the elmah.io API. In order to request endpoints on the API, each client will need to provide a valid API key. API keys are available on the Organization Settings view, as well as on the Install tab on the Log Settings screen. We wrote a guide to help you find your API key here: Where is my API key? . A default API key is created when you create your organization, but new keys can be added, keys revoked, and more. Sending the API key to the elmah.io API, is typically handled by the Elmah.Io.Client NuGet package. All integrations have a dependency to this package, which means that it will be automatically installed through NuGet. How you provide your API key depends on the integration you are installing. Some integrations expect the API key in a config file, while others, accept the key in C#. For details about how to provide the API key for each integration, click the various installation guides in the left menu. Besides a unique string representing an API key, each key can have a set of permissions. As default, API keys only have the Write Messages permission, which means that the key cannot be used to read data from your logs. In 99% of all scenarios, you will browse through errors using the elmah.io UI, which will require you to sign in using username/password or one of the supported social providers. In the case you want to enable one of the native UIs provided by different integrations (like the /elmah.axd endpoint part of the ELMAH package) or you are building a third-party integration to elmah.io, you will need to assign additional permissions to your API key. For details about API key permissions, check out How to configure API key permissions .","title":"Authentication"},{"location":"authentication/#authentication","text":"All of our integrations communicates with the elmah.io API. In order to request endpoints on the API, each client will need to provide a valid API key. API keys are available on the Organization Settings view, as well as on the Install tab on the Log Settings screen. We wrote a guide to help you find your API key here: Where is my API key? . A default API key is created when you create your organization, but new keys can be added, keys revoked, and more. Sending the API key to the elmah.io API, is typically handled by the Elmah.Io.Client NuGet package. All integrations have a dependency to this package, which means that it will be automatically installed through NuGet. How you provide your API key depends on the integration you are installing. Some integrations expect the API key in a config file, while others, accept the key in C#. For details about how to provide the API key for each integration, click the various installation guides in the left menu. Besides a unique string representing an API key, each key can have a set of permissions. As default, API keys only have the Write Messages permission, which means that the key cannot be used to read data from your logs. In 99% of all scenarios, you will browse through errors using the elmah.io UI, which will require you to sign in using username/password or one of the supported social providers. In the case you want to enable one of the native UIs provided by different integrations (like the /elmah.axd endpoint part of the ELMAH package) or you are building a third-party integration to elmah.io, you will need to assign additional permissions to your API key. For details about API key permissions, check out How to configure API key permissions .","title":"Authentication"},{"location":"bot-detection/","text":"Bot detection elmah.io can help you with classifying log messages generated by bots and crawlers. When storing a log message, we run a range of checks to try and identify if a log message is generated by an automated script or a real human visitor of your website/application. In this case, a flag named isBot is set to true on the message. In case we couldn't identify a log message as generated by a bot, the flag is set to false . Besides an automated check, you can also mark a log message as generated by a bot manually. This is done from within the elmah.io UI: The benefit of marking log messages with the isBot flag manually is that elmah.io will then automatically mark new instances of this log message with isBot=true (this feature is available for automatically bot-marked log messages as well). By doing so you get the possibilities listed later in this article. Log messages marked as generated by bots include a small robot icon on the search result: Search by or not by bots If you want to show all log messages generated by bots you can create a search query like this: isBot:true Or include a search filter for the Is Bot field: By reversing the filter, you see a list of log messages NOT generated by a bot, which can make it easier to get an overview of \"real\" errors. Hide or ignore log messages generated by bots By using the isBot field in Hide and Ignore filters, you can let elmah.io automatically hide or ignore future log messages generated by bots. Be aware that creating ignore filters based on the isBot field is only available for users on the Enterprise plan. To create a new filter, navigate to the Rules tab on log settings, and click the Add a new rule button. Input a query including the isBot field and select either the Hide or Ignore action:","title":"Bot detection"},{"location":"bot-detection/#bot-detection","text":"elmah.io can help you with classifying log messages generated by bots and crawlers. When storing a log message, we run a range of checks to try and identify if a log message is generated by an automated script or a real human visitor of your website/application. In this case, a flag named isBot is set to true on the message. In case we couldn't identify a log message as generated by a bot, the flag is set to false . Besides an automated check, you can also mark a log message as generated by a bot manually. This is done from within the elmah.io UI: The benefit of marking log messages with the isBot flag manually is that elmah.io will then automatically mark new instances of this log message with isBot=true (this feature is available for automatically bot-marked log messages as well). By doing so you get the possibilities listed later in this article. Log messages marked as generated by bots include a small robot icon on the search result:","title":"Bot detection"},{"location":"bot-detection/#search-by-or-not-by-bots","text":"If you want to show all log messages generated by bots you can create a search query like this: isBot:true Or include a search filter for the Is Bot field: By reversing the filter, you see a list of log messages NOT generated by a bot, which can make it easier to get an overview of \"real\" errors.","title":"Search by or not by bots"},{"location":"bot-detection/#hide-or-ignore-log-messages-generated-by-bots","text":"By using the isBot field in Hide and Ignore filters, you can let elmah.io automatically hide or ignore future log messages generated by bots. Be aware that creating ignore filters based on the isBot field is only available for users on the Enterprise plan. To create a new filter, navigate to the Rules tab on log settings, and click the Add a new rule button. Input a query including the isBot field and select either the Hide or Ignore action:","title":"Hide or ignore log messages generated by bots"},{"location":"cli-clear/","text":"Clearing log messages from the CLI The clear command is used to delete one or more messages from a log. Be aware that clearing a log does not reset the monthly counter towards log messages included in your current plan. The clear command is intended for cleanup in non-expired log messages you no longer need. Usage > elmahio clear --help Description: Delete one or more messages from a log Usage: elmahio clear [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --logId (REQUIRED) The log ID of the log to clear messages --query (REQUIRED) Clear messages matching this query (use * for all messages) --from Optional date and time to clear messages from --to Optional date and time to clear messages to -?, -h, --help Show help and usage information Examples Simple: elmahio clear --apiKey API_KEY --logId LOG_ID --query \"statusCode:404\" Full: elmahio clear --apiKey API_KEY --logId LOG_ID --query \"statusCode:404\" --from 2022-05-17 --to 2022-05-18","title":"Clear"},{"location":"cli-clear/#clearing-log-messages-from-the-cli","text":"The clear command is used to delete one or more messages from a log. Be aware that clearing a log does not reset the monthly counter towards log messages included in your current plan. The clear command is intended for cleanup in non-expired log messages you no longer need.","title":"Clearing log messages from the CLI"},{"location":"cli-clear/#usage","text":"> elmahio clear --help Description: Delete one or more messages from a log Usage: elmahio clear [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --logId (REQUIRED) The log ID of the log to clear messages --query (REQUIRED) Clear messages matching this query (use * for all messages) --from Optional date and time to clear messages from --to Optional date and time to clear messages to -?, -h, --help Show help and usage information","title":"Usage"},{"location":"cli-clear/#examples","text":"Simple: elmahio clear --apiKey API_KEY --logId LOG_ID --query \"statusCode:404\" Full: elmahio clear --apiKey API_KEY --logId LOG_ID --query \"statusCode:404\" --from 2022-05-17 --to 2022-05-18","title":"Examples"},{"location":"cli-dataloader/","text":"Dataloader loads data from the CLI The dataloader command loads 50 log messages into a specified log. Usage > elmahio dataloader --help Description: Load 50 log messages into the specified log Usage: elmahio dataloader [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --logId (REQUIRED) The log ID of the log to import messages into -?, -h, --help Show help and usage information Example elmahio dataloader --apiKey API_KEY --logId LOG_ID","title":"Dataloader"},{"location":"cli-dataloader/#dataloader-loads-data-from-the-cli","text":"The dataloader command loads 50 log messages into a specified log.","title":"Dataloader loads data from the CLI"},{"location":"cli-dataloader/#usage","text":"> elmahio dataloader --help Description: Load 50 log messages into the specified log Usage: elmahio dataloader [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --logId (REQUIRED) The log ID of the log to import messages into -?, -h, --help Show help and usage information","title":"Usage"},{"location":"cli-dataloader/#example","text":"elmahio dataloader --apiKey API_KEY --logId LOG_ID","title":"Example"},{"location":"cli-deployment/","text":"Create a deployment from the CLI The deployment command is used to create new deployments on elmah.io. Usage > elmahio deployment --help Description: Create a new deployment Usage: elmahio deployment [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --version (REQUIRED) The version number of this deployment --created When was this deployment created in UTC --description Description of this deployment --userName The name of the person responsible for creating this deployment --userEmail The email of the person responsible for creating this deployment --logId The ID of a log if this deployment is specific to a single log -?, -h, --help Show help and usage information Examples Simple: elmahio deployment --apiKey API_KEY --version 1.0.0 Full: elmahio deployment --apiKey API_KEY --version 1.0.0 --created 2022-02-08 --description \"My new cool release\" --userName \"Thomas Ardal\" --userEmail \"thomas@elmah.io\" --logId LOG_ID","title":"Deployment"},{"location":"cli-deployment/#create-a-deployment-from-the-cli","text":"The deployment command is used to create new deployments on elmah.io.","title":"Create a deployment from the CLI"},{"location":"cli-deployment/#usage","text":"> elmahio deployment --help Description: Create a new deployment Usage: elmahio deployment [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --version (REQUIRED) The version number of this deployment --created When was this deployment created in UTC --description Description of this deployment --userName The name of the person responsible for creating this deployment --userEmail The email of the person responsible for creating this deployment --logId The ID of a log if this deployment is specific to a single log -?, -h, --help Show help and usage information","title":"Usage"},{"location":"cli-deployment/#examples","text":"Simple: elmahio deployment --apiKey API_KEY --version 1.0.0 Full: elmahio deployment --apiKey API_KEY --version 1.0.0 --created 2022-02-08 --description \"My new cool release\" --userName \"Thomas Ardal\" --userEmail \"thomas@elmah.io\" --logId LOG_ID","title":"Examples"},{"location":"cli-diagnose/","text":"Diagnose potential problems with an elmah.io installation The diagnose command can be run in the root folder of an elmah.io installation to find potential problems with the configuration. Usage > elmahio diagnose --help Description: Diagnose potential problems with an elmah.io installation Usage: elmahio diagnose [options] Options: --directory The root directory to check [default: C:\\test] --verbose Output verbose diagnostics to help debug problems [default: False] -?, -h, --help Show help and usage information Examples Simple: elmahio diagnose --directory c:\\projects\\my-project Full: elmahio diagnose --directory c:\\projects\\my-project --verbose","title":"Diagnose"},{"location":"cli-diagnose/#diagnose-potential-problems-with-an-elmahio-installation","text":"The diagnose command can be run in the root folder of an elmah.io installation to find potential problems with the configuration.","title":"Diagnose potential problems with an elmah.io installation"},{"location":"cli-diagnose/#usage","text":"> elmahio diagnose --help Description: Diagnose potential problems with an elmah.io installation Usage: elmahio diagnose [options] Options: --directory The root directory to check [default: C:\\test] --verbose Output verbose diagnostics to help debug problems [default: False] -?, -h, --help Show help and usage information","title":"Usage"},{"location":"cli-diagnose/#examples","text":"Simple: elmahio diagnose --directory c:\\projects\\my-project Full: elmahio diagnose --directory c:\\projects\\my-project --verbose","title":"Examples"},{"location":"cli-export/","text":"Exporting log messages from the CLI The export command is used to export one or more log messages from a log to JSON. Usage > elmahio export --help Description: Export log messages from a specified log Usage: elmahio export [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --logId (REQUIRED) The ID of the log to export messages from --dateFrom (REQUIRED) Defines the Date from which the logs start. Ex. \" --dateFrom 2023-03-09\" --dateTo (REQUIRED) Defines the Date from which the logs end. Ex. \" --dateTo 2023-03-16\" --filename Defines the path and filename of the file to export to. Ex. \" --filename C:\\myDirectory\\myFile.json\" [default: C:\\Users\\thoma\\Export-638145521994987555.json] --query Defines the query that is passed to the API [default: *] --includeHeaders Include headers, cookies, etc. in output (will take longer to export) -?, -h, --help Show help and usage information Examples Simple: elmahio export --apiKey API_KEY --logId LOG_ID --dateFrom 2020-08-21 --dateTo 2020-08-28 Full: elmahio export --apiKey API_KEY --logId LOG_ID --dateFrom 2020-08-21 --dateTo 2020-08-28 --filename c:\\temp\\elmahio.json --query \"statusCode: 404\" --includeHeaders","title":"Export"},{"location":"cli-export/#exporting-log-messages-from-the-cli","text":"The export command is used to export one or more log messages from a log to JSON.","title":"Exporting log messages from the CLI"},{"location":"cli-export/#usage","text":"> elmahio export --help Description: Export log messages from a specified log Usage: elmahio export [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --logId (REQUIRED) The ID of the log to export messages from --dateFrom (REQUIRED) Defines the Date from which the logs start. Ex. \" --dateFrom 2023-03-09\" --dateTo (REQUIRED) Defines the Date from which the logs end. Ex. \" --dateTo 2023-03-16\" --filename Defines the path and filename of the file to export to. Ex. \" --filename C:\\myDirectory\\myFile.json\" [default: C:\\Users\\thoma\\Export-638145521994987555.json] --query Defines the query that is passed to the API [default: *] --includeHeaders Include headers, cookies, etc. in output (will take longer to export) -?, -h, --help Show help and usage information","title":"Usage"},{"location":"cli-export/#examples","text":"Simple: elmahio export --apiKey API_KEY --logId LOG_ID --dateFrom 2020-08-21 --dateTo 2020-08-28 Full: elmahio export --apiKey API_KEY --logId LOG_ID --dateFrom 2020-08-21 --dateTo 2020-08-28 --filename c:\\temp\\elmahio.json --query \"statusCode: 404\" --includeHeaders","title":"Examples"},{"location":"cli-import/","text":"Importing log messages from the CLI The import command is used to import log messages from IIS log files and W3C Extended log files to an elmah.io log. Usage > elmahio import --help Description: Import log messages to a specified log Usage: elmahio import [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --logId (REQUIRED) The ID of the log to import messages to --type (REQUIRED) The type of log file to import. Use 'w3c' for W3C Extended Log File Format and 'iis' for IIS Log File Format --filename (REQUIRED) Defines the path and filename of the file to import from. Ex. \" --filename C:\\myDirectory\\log.txt\" --dateFrom Defines the Date from which the logs start. Ex. \" --dateFrom 2023-03-06\" --dateTo Defines the Date from which the logs end. Ex. \" --dateTo 2023-03-13\" -?, -h, --help Show help and usage information Examples IIS: elmahio import --apiKey API_KEY --logId LOG_ID --type iis --filename u_inetsv1.log --dateFrom 2023-03-13T10:14 --dateTo 2023-03-13T10:16 w3c: elmahio import --apiKey API_KEY --logId LOG_ID --type w3c --filename u_extend1.log --dateFrom 2023-03-13T10:14 --dateTo 2023-03-13T10:16","title":"Import"},{"location":"cli-import/#importing-log-messages-from-the-cli","text":"The import command is used to import log messages from IIS log files and W3C Extended log files to an elmah.io log.","title":"Importing log messages from the CLI"},{"location":"cli-import/#usage","text":"> elmahio import --help Description: Import log messages to a specified log Usage: elmahio import [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --logId (REQUIRED) The ID of the log to import messages to --type (REQUIRED) The type of log file to import. Use 'w3c' for W3C Extended Log File Format and 'iis' for IIS Log File Format --filename (REQUIRED) Defines the path and filename of the file to import from. Ex. \" --filename C:\\myDirectory\\log.txt\" --dateFrom Defines the Date from which the logs start. Ex. \" --dateFrom 2023-03-06\" --dateTo Defines the Date from which the logs end. Ex. \" --dateTo 2023-03-13\" -?, -h, --help Show help and usage information","title":"Usage"},{"location":"cli-import/#examples","text":"IIS: elmahio import --apiKey API_KEY --logId LOG_ID --type iis --filename u_inetsv1.log --dateFrom 2023-03-13T10:14 --dateTo 2023-03-13T10:16 w3c: elmahio import --apiKey API_KEY --logId LOG_ID --type w3c --filename u_extend1.log --dateFrom 2023-03-13T10:14 --dateTo 2023-03-13T10:16","title":"Examples"},{"location":"cli-log/","text":"Log a message from the CLI The log command is used to store a log message in a specified log. Usage > elmahio log --help Description: Log a message to the specified log Usage: elmahio log [options] Options: --apiKey (REQUIRED) An API key with permission to execute the command --logId (REQUIRED) The ID of the log to send the log message to --application Used to identify which application logged this message. You can use this if you have multiple applications and services logging to the same log --detail A longer description of the message. For errors this could be a stacktrace, but it's really up to you what to log in there. --hostname The hostname of the server logging the message. --title (REQUIRED) The textual title or headline of the message to log. --titleTemplate <titleTemplate> The title template of the message to log. This property can be used from logging frameworks that supports structured logging like: \"{user} says {quote}\". In the example, titleTemplate will be this string and title will be \"Gilfoyle says It's not magic. It's talent and sweat\". --source <source> The source of the code logging the message. This could be the assembly name. --statusCode <statusCode> If the message logged relates to a HTTP status code, you can put the code in this property. This would probably only be relevant for errors, but could be used for logging successful status codes as well. --dateTime <dateTime> The date and time in UTC of the message. If you don't provide us with a value in dateTime, we will set the current date and time in UTC. --type <type> The type of message. If logging an error, the type of the exception would go into type but you can put anything in there, that makes sense for your domain. --user <user> An identification of the user triggering this message. You can put the users email address or your user key into this property. --severity <severity> An enum value representing the severity of this message. The following values are allowed: Verbose, Debug, Information, Warning, Error, Fatal. --url <url> If message relates to a HTTP request, you may send the URL of that request. If you don't provide us with an URL, we will try to find a key named URL in serverVariables. --method <method> If message relates to a HTTP request, you may send the HTTP method of that request. If you don't provide us with a method, we will try to find a key named REQUEST_METHOD in serverVariables. --version <version> Versions can be used to distinguish messages from different versions of your software. The value of version can be a SemVer compliant string or any other syntax that you are using as your version numbering scheme. --correlationId <correlationId> CorrelationId can be used to group similar log messages together into a single discoverable batch. A correlation ID could be a session ID from ASP.NET Core, a unique string spanning multiple microsservices handling the same request, or similar. --category <category> The category to set on the message. Category can be used to emulate a logger name when created from a logging framework. -?, -h, --help Show help and usage information Examples Simple: elmahio log --apiKey API_KEY --logId LOG_ID --title \"An error happened\" Full: elmahio log --apiKey API_KEY --logId LOG_ID --title \"An error happened\" --application \"My app\" --details \"Some details\" --hostname \"localhost\" --titleTemplate \"An {severity} happened\" --source \"A source\" --statusCode 500 --dateTime 2022-01-13 --type \"The type\" --user \"A user\" --severity \"Error\" --url \"https://elmah.io\" --method \"GET\" --version \"1.0.0\"","title":"Log"},{"location":"cli-log/#log-a-message-from-the-cli","text":"The log command is used to store a log message in a specified log.","title":"Log a message from the CLI"},{"location":"cli-log/#usage","text":"> elmahio log --help Description: Log a message to the specified log Usage: elmahio log [options] Options: --apiKey <apiKey> (REQUIRED) An API key with permission to execute the command --logId <logId> (REQUIRED) The ID of the log to send the log message to --application <application> Used to identify which application logged this message. You can use this if you have multiple applications and services logging to the same log --detail <detail> A longer description of the message. For errors this could be a stacktrace, but it's really up to you what to log in there. --hostname <hostname> The hostname of the server logging the message. --title <title> (REQUIRED) The textual title or headline of the message to log. --titleTemplate <titleTemplate> The title template of the message to log. This property can be used from logging frameworks that supports structured logging like: \"{user} says {quote}\". In the example, titleTemplate will be this string and title will be \"Gilfoyle says It's not magic. It's talent and sweat\". --source <source> The source of the code logging the message. This could be the assembly name. --statusCode <statusCode> If the message logged relates to a HTTP status code, you can put the code in this property. This would probably only be relevant for errors, but could be used for logging successful status codes as well. --dateTime <dateTime> The date and time in UTC of the message. If you don't provide us with a value in dateTime, we will set the current date and time in UTC. --type <type> The type of message. If logging an error, the type of the exception would go into type but you can put anything in there, that makes sense for your domain. --user <user> An identification of the user triggering this message. You can put the users email address or your user key into this property. --severity <severity> An enum value representing the severity of this message. The following values are allowed: Verbose, Debug, Information, Warning, Error, Fatal. --url <url> If message relates to a HTTP request, you may send the URL of that request. If you don't provide us with an URL, we will try to find a key named URL in serverVariables. --method <method> If message relates to a HTTP request, you may send the HTTP method of that request. If you don't provide us with a method, we will try to find a key named REQUEST_METHOD in serverVariables. --version <version> Versions can be used to distinguish messages from different versions of your software. The value of version can be a SemVer compliant string or any other syntax that you are using as your version numbering scheme. --correlationId <correlationId> CorrelationId can be used to group similar log messages together into a single discoverable batch. A correlation ID could be a session ID from ASP.NET Core, a unique string spanning multiple microsservices handling the same request, or similar. --category <category> The category to set on the message. Category can be used to emulate a logger name when created from a logging framework. -?, -h, --help Show help and usage information","title":"Usage"},{"location":"cli-log/#examples","text":"Simple: elmahio log --apiKey API_KEY --logId LOG_ID --title \"An error happened\" Full: elmahio log --apiKey API_KEY --logId LOG_ID --title \"An error happened\" --application \"My app\" --details \"Some details\" --hostname \"localhost\" --titleTemplate \"An {severity} happened\" --source \"A source\" --statusCode 500 --dateTime 2022-01-13 --type \"The type\" --user \"A user\" --severity \"Error\" --url \"https://elmah.io\" --method \"GET\" --version \"1.0.0\"","title":"Examples"},{"location":"cli-overview/","text":"CLI overview The elmah.io CLI lets you execute common tasks against elmah.io. Installing the CLI The elmah.io CLI requires .NET 6 or newer installed. The elmah.io CLI can be installed in several ways. To set up everything automatically, execute the following script from the command line: dotnet tool install --global Elmah.Io.Cli or make sure to run on the latest version if you already have the CLI installed: dotnet tool update --global Elmah.Io.Cli If you prefer downloading the CLI as a zip you can download the latest version from GitHub . To clone and build the CLI manually, check out the instructions below. Run the CLI Clear Dataloader Deployment Diagnose Export Import Log Sourcemap Tail Run the CLI to get help: elmahio --help Help similar to this is outputted to the console: elmahio: CLI for executing various actions against elmah.io Usage: elmahio [options] [command] Options: --nologo Doesn't display the startup banner or the copyright message --version Show version information -?, -h, --help Show help and usage information Commands: clear Delete one or more messages from a log dataloader Load 50 log messages into the specified log deployment Create a new deployment diagnose Diagnose potential problems with an elmah.io installation export Export log messages from a specified log import Import log messages to a specified log log Log a message to the specified log sourcemap Upload a source map and minified JavaScript tail Tail log messages from a specified log Cloning the CLI Create a new folder and git clone the repository: git clone https://github.com/elmahio/Elmah.Io.Cli.git Building the CLI Navigate to the root repository of the code and execute the following command: dotnet build","title":"CLI overview"},{"location":"cli-overview/#cli-overview","text":"The elmah.io CLI lets you execute common tasks against elmah.io.","title":"CLI overview"},{"location":"cli-overview/#installing-the-cli","text":"The elmah.io CLI requires .NET 6 or newer installed. The elmah.io CLI can be installed in several ways. To set up everything automatically, execute the following script from the command line: dotnet tool install --global Elmah.Io.Cli or make sure to run on the latest version if you already have the CLI installed: dotnet tool update --global Elmah.Io.Cli If you prefer downloading the CLI as a zip you can download the latest version from GitHub . To clone and build the CLI manually, check out the instructions below.","title":"Installing the CLI"},{"location":"cli-overview/#run-the-cli","text":"Clear Dataloader Deployment Diagnose Export Import Log Sourcemap Tail Run the CLI to get help: elmahio --help Help similar to this is outputted to the console: elmahio: CLI for executing various actions against elmah.io Usage: elmahio [options] [command] Options: --nologo Doesn't display the startup banner or the copyright message --version Show version information -?, -h, --help Show help and usage information Commands: clear Delete one or more messages from a log dataloader Load 50 log messages into the specified log deployment Create a new deployment diagnose Diagnose potential problems with an elmah.io installation export Export log messages from a specified log import Import log messages to a specified log log Log a message to the specified log sourcemap Upload a source map and minified JavaScript tail Tail log messages from a specified log","title":"Run the CLI"},{"location":"cli-overview/#cloning-the-cli","text":"Create a new folder and git clone the repository: git clone https://github.com/elmahio/Elmah.Io.Cli.git","title":"Cloning the CLI"},{"location":"cli-overview/#building-the-cli","text":"Navigate to the root repository of the code and execute the following command: dotnet build","title":"Building the CLI"},{"location":"cli-sourcemap/","text":"Upload a source map from the CLI The sourcemap command is used to upload source maps and minified JavaScript files to elmah.io. Usage > elmahio sourcemap --help Description: Upload a source map and minified JavaScript Usage: elmahio sourcemap [options] Options: --apiKey <apiKey> (REQUIRED) An API key with permission to execute the command --logId <logId> (REQUIRED) The ID of the log which should contain the minified JavaScript and source map --path <path> (REQUIRED) An URL to the online minified JavaScript file --sourceMap <sourceMap> (REQUIRED) The source map file. Only files with an extension of .map and content type of application/json will be accepted --minifiedJavaScript <minifiedJavaScript> (REQUIRED) The minified JavaScript file. Only files with an extension of .js and content type of text/javascript will be accepted -?, -h, --help Show help and usage information Examples sourcemap --apiKey API_KEY --logId LOG_ID --path \"/bundles/sharedbundle.min.js\" --sourceMap \"c:\\path\\to\\sharedbundle.map\" --minifiedJavaScript \"c:\\path\\to\\sharedbundle.min.js\"","title":"Sourcemap"},{"location":"cli-sourcemap/#upload-a-source-map-from-the-cli","text":"The sourcemap command is used to upload source maps and minified JavaScript files to elmah.io.","title":"Upload a source map from the CLI"},{"location":"cli-sourcemap/#usage","text":"> elmahio sourcemap --help Description: Upload a source map and minified JavaScript Usage: elmahio sourcemap [options] Options: --apiKey <apiKey> (REQUIRED) An API key with permission to execute the command --logId <logId> (REQUIRED) The ID of the log which should contain the minified JavaScript and source map --path <path> (REQUIRED) An URL to the online minified JavaScript file --sourceMap <sourceMap> (REQUIRED) The source map file. Only files with an extension of .map and content type of application/json will be accepted --minifiedJavaScript <minifiedJavaScript> (REQUIRED) The minified JavaScript file. Only files with an extension of .js and content type of text/javascript will be accepted -?, -h, --help Show help and usage information","title":"Usage"},{"location":"cli-sourcemap/#examples","text":"sourcemap --apiKey API_KEY --logId LOG_ID --path \"/bundles/sharedbundle.min.js\" --sourceMap \"c:\\path\\to\\sharedbundle.map\" --minifiedJavaScript \"c:\\path\\to\\sharedbundle.min.js\"","title":"Examples"},{"location":"cli-tail/","text":"Tail log messages from the CLI The tail command is used to tail log messages in a specified log. Usage > elmahio tail --help Description: Tail log messages from a specified log Usage: elmahio tail [options] Options: --apiKey <apiKey> (REQUIRED) An API key with permission to execute the command --logId <logId> (REQUIRED) The ID of the log to send the log message to -?, -h, --help Show help and usage information Example elmahio tail --apiKey API_KEY --logId LOG_ID","title":"Tail"},{"location":"cli-tail/#tail-log-messages-from-the-cli","text":"The tail command is used to tail log messages in a specified log.","title":"Tail log messages from the CLI"},{"location":"cli-tail/#usage","text":"> elmahio tail --help Description: Tail log messages from a specified log Usage: elmahio tail [options] Options: --apiKey <apiKey> (REQUIRED) An API key with permission to execute the command --logId <logId> (REQUIRED) The ID of the log to send the log message to -?, -h, --help Show help and usage information","title":"Usage"},{"location":"cli-tail/#example","text":"elmahio tail --apiKey API_KEY --logId LOG_ID","title":"Example"},{"location":"configure-elmah-io-from-code/","text":"Configure elmah.io from code You typically configure elmah.io in your web.config file. With a little help from some custom code, you will be able to configure everything in code as well: using Elmah; using System.Collections.Generic; using System.ComponentModel.Design; [assembly: WebActivatorEx.PreApplicationStartMethod(typeof(ElmahFromCodeExample.ElmahConfig), \"Start\")] namespace ElmahFromCodeExample { public static class ElmahConfig { public static void Start() { ServiceCenter.Current = CreateServiceProviderQueryHandler(ServiceCenter.Current); HttpApplication.RegisterModule(typeof(ErrorLogModule)); } private static ServiceProviderQueryHandler CreateServiceProviderQueryHandler(ServiceProviderQueryHandler sp) { return context => { var container = new ServiceContainer(sp(context)); var config = new Dictionary<string, string>(); config[\"apiKey\"] = \"API_KEY\"; config[\"logId\"] = \"LOG_ID\"; var log = new Elmah.Io.ErrorLog(config); container.AddService(typeof(Elmah.ErrorLog), log); return container; }; } } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with a log ID ( Where is my log ID? ). Let's look at the code. Our class ElmahConfig is configured as a PreApplicationStartMethod which means, that ASP.NET (MVC) will execute the Start method when the web application starts up. Inside this method, we set the ServiceCenter.Current property to the return type of the CreateServiceProviderQueryHandler method. This method is where the magic happens. Besides creating the new ServiceContainer , we created the Elmah.Io.ErrorLog class normally configured through XML. The Dictionary should contain the API key and log ID as explained earlier. In the second line of the Start -method, we call the RegisterModule -method with ErrorLogModule as parameter. This replaces the need for registering the module in web.config as part of the system.webServer element. That's it! You no longer need the <elmah> element, config sections, or anything else related to ELMAH and elmah.io in your web.config file.","title":"Configure elmah.io from code"},{"location":"configure-elmah-io-from-code/#configure-elmahio-from-code","text":"You typically configure elmah.io in your web.config file. With a little help from some custom code, you will be able to configure everything in code as well: using Elmah; using System.Collections.Generic; using System.ComponentModel.Design; [assembly: WebActivatorEx.PreApplicationStartMethod(typeof(ElmahFromCodeExample.ElmahConfig), \"Start\")] namespace ElmahFromCodeExample { public static class ElmahConfig { public static void Start() { ServiceCenter.Current = CreateServiceProviderQueryHandler(ServiceCenter.Current); HttpApplication.RegisterModule(typeof(ErrorLogModule)); } private static ServiceProviderQueryHandler CreateServiceProviderQueryHandler(ServiceProviderQueryHandler sp) { return context => { var container = new ServiceContainer(sp(context)); var config = new Dictionary<string, string>(); config[\"apiKey\"] = \"API_KEY\"; config[\"logId\"] = \"LOG_ID\"; var log = new Elmah.Io.ErrorLog(config); container.AddService(typeof(Elmah.ErrorLog), log); return container; }; } } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with a log ID ( Where is my log ID? ). Let's look at the code. Our class ElmahConfig is configured as a PreApplicationStartMethod which means, that ASP.NET (MVC) will execute the Start method when the web application starts up. Inside this method, we set the ServiceCenter.Current property to the return type of the CreateServiceProviderQueryHandler method. This method is where the magic happens. Besides creating the new ServiceContainer , we created the Elmah.Io.ErrorLog class normally configured through XML. The Dictionary should contain the API key and log ID as explained earlier. In the second line of the Start -method, we call the RegisterModule -method with ErrorLogModule as parameter. This replaces the need for registering the module in web.config as part of the system.webServer element. That's it! You no longer need the <elmah> element, config sections, or anything else related to ELMAH and elmah.io in your web.config file.","title":"Configure elmah.io from code"},{"location":"configure-elmah-io-manually/","text":"Configure elmah.io manually The Elmah.Io NuGet package normally adds all of the necessary configuration, to get up and running with elmah.io. This is one of our killer features and our customers tell us, that we have the simplest installer on the market. In some cases, you may experience problems with the automatic configuration, though. Different reasons can cause the configuration not to be added automatically. The most common reason is restrictions to executing PowerShell inside Visual Studio. Start by installing the Elmah.Io package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io If a dialog is shown during the installation, input your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Don't worry if the configuration isn't added, since we will verify this later. Add the following to the <configSections> element in your web.config : <sectionGroup name=\"elmah\"> <section name=\"security\" requirePermission=\"false\" type=\"Elmah.SecuritySectionHandler, Elmah\" /> <section name=\"errorLog\" requirePermission=\"false\" type=\"Elmah.ErrorLogSectionHandler, Elmah\" /> <section name=\"errorMail\" requirePermission=\"false\" type=\"Elmah.ErrorMailSectionHandler, Elmah\" /> <section name=\"errorFilter\" requirePermission=\"false\" type=\"Elmah.ErrorFilterSectionHandler, Elmah\" /> </sectionGroup> Add the following to the <httpModules> element (inside <system.web> ) in your web.config : <add name=\"ErrorLog\" type=\"Elmah.ErrorLogModule, Elmah\" /> <add name=\"ErrorMail\" type=\"Elmah.ErrorMailModule, Elmah\" /> <add name=\"ErrorFilter\" type=\"Elmah.ErrorFilterModule, Elmah\"/> Add the following to the <modules> element (inside <system.webServer> ) in your web.config : <add name=\"ErrorLog\" type=\"Elmah.ErrorLogModule, Elmah\" preCondition=\"managedHandler\" /> <add name=\"ErrorMail\" type=\"Elmah.ErrorMailModule, Elmah\" preCondition=\"managedHandler\" /> <add name=\"ErrorFilter\" type=\"Elmah.ErrorFilterModule, Elmah\" preCondition=\"managedHandler\" /> Add the following to the system.webServer element in your web.config : <validation validateIntegratedModeConfiguration=\"false\" /> Add the following as a root element beneath the <configuration> element in your web.config : <elmah> <security allowRemoteAccess=\"false\" /> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> </elmah> Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with your log ID ( Where is my log ID? ). That's it. You managed to install elmah.io manually and you should go to your LinkedIn profile and update with a new certification called \"Certified elmah.io installer\" :) Here's a full example of ELMAH configuration in a web.config file: <configuration> <configSections> <sectionGroup name=\"elmah\"> <section name=\"security\" requirePermission=\"false\" type=\"Elmah.SecuritySectionHandler, Elmah\" /> <section name=\"errorLog\" requirePermission=\"false\" type=\"Elmah.ErrorLogSectionHandler, Elmah\" /> <section name=\"errorMail\" requirePermission=\"false\" type=\"Elmah.ErrorMailSectionHandler, Elmah\" /> <section name=\"errorFilter\" requirePermission=\"false\" type=\"Elmah.ErrorFilterSectionHandler, Elmah\" /> </sectionGroup> </configSections> <system.web> <httpModules> <add name=\"ErrorLog\" type=\"Elmah.ErrorLogModule, Elmah\" /> <add name=\"ErrorMail\" type=\"Elmah.ErrorMailModule, Elmah\" /> <add name=\"ErrorFilter\" type=\"Elmah.ErrorFilterModule, Elmah\"/> </httpModules> </system.web> <system.webServer> <validation validateIntegratedModeConfiguration=\"false\" /> <modules> <add name=\"ErrorLog\" type=\"Elmah.ErrorLogModule, Elmah\" preCondition=\"managedHandler\" /> <add name=\"ErrorMail\" type=\"Elmah.ErrorMailModule, Elmah\" preCondition=\"managedHandler\" /> <add name=\"ErrorFilter\" type=\"Elmah.ErrorFilterModule, Elmah\" preCondition=\"managedHandler\" /> </modules> </system.webServer> <elmah> <security allowRemoteAccess=\"false\" /> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> </elmah> </configuration> In case you need to access your error log on /elmah.axd , you need to add the following to the <configuration> element in your web.config : <location path=\"elmah.axd\" inheritInChildApplications=\"false\"> <system.web> <httpHandlers> <add verb=\"POST,GET,HEAD\" path=\"elmah.axd\" type=\"Elmah.ErrorLogPageFactory, Elmah\" /> </httpHandlers> </system.web> <system.webServer> <handlers> <add name=\"ELMAH\" verb=\"POST,GET,HEAD\" path=\"elmah.axd\" type=\"Elmah.ErrorLogPageFactory, Elmah\" preCondition=\"integratedMode\" /> </handlers> </system.webServer> </location> We don't recommend browsing your error logs through the /elmah.axd endpoint. The elmah.io UI will let you control different levels of access and more.","title":"Configure elmah.io manually"},{"location":"configure-elmah-io-manually/#configure-elmahio-manually","text":"The Elmah.Io NuGet package normally adds all of the necessary configuration, to get up and running with elmah.io. This is one of our killer features and our customers tell us, that we have the simplest installer on the market. In some cases, you may experience problems with the automatic configuration, though. Different reasons can cause the configuration not to be added automatically. The most common reason is restrictions to executing PowerShell inside Visual Studio. Start by installing the Elmah.Io package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io If a dialog is shown during the installation, input your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Don't worry if the configuration isn't added, since we will verify this later. Add the following to the <configSections> element in your web.config : <sectionGroup name=\"elmah\"> <section name=\"security\" requirePermission=\"false\" type=\"Elmah.SecuritySectionHandler, Elmah\" /> <section name=\"errorLog\" requirePermission=\"false\" type=\"Elmah.ErrorLogSectionHandler, Elmah\" /> <section name=\"errorMail\" requirePermission=\"false\" type=\"Elmah.ErrorMailSectionHandler, Elmah\" /> <section name=\"errorFilter\" requirePermission=\"false\" type=\"Elmah.ErrorFilterSectionHandler, Elmah\" /> </sectionGroup> Add the following to the <httpModules> element (inside <system.web> ) in your web.config : <add name=\"ErrorLog\" type=\"Elmah.ErrorLogModule, Elmah\" /> <add name=\"ErrorMail\" type=\"Elmah.ErrorMailModule, Elmah\" /> <add name=\"ErrorFilter\" type=\"Elmah.ErrorFilterModule, Elmah\"/> Add the following to the <modules> element (inside <system.webServer> ) in your web.config : <add name=\"ErrorLog\" type=\"Elmah.ErrorLogModule, Elmah\" preCondition=\"managedHandler\" /> <add name=\"ErrorMail\" type=\"Elmah.ErrorMailModule, Elmah\" preCondition=\"managedHandler\" /> <add name=\"ErrorFilter\" type=\"Elmah.ErrorFilterModule, Elmah\" preCondition=\"managedHandler\" /> Add the following to the system.webServer element in your web.config : <validation validateIntegratedModeConfiguration=\"false\" /> Add the following as a root element beneath the <configuration> element in your web.config : <elmah> <security allowRemoteAccess=\"false\" /> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> </elmah> Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with your log ID ( Where is my log ID? ). That's it. You managed to install elmah.io manually and you should go to your LinkedIn profile and update with a new certification called \"Certified elmah.io installer\" :) Here's a full example of ELMAH configuration in a web.config file: <configuration> <configSections> <sectionGroup name=\"elmah\"> <section name=\"security\" requirePermission=\"false\" type=\"Elmah.SecuritySectionHandler, Elmah\" /> <section name=\"errorLog\" requirePermission=\"false\" type=\"Elmah.ErrorLogSectionHandler, Elmah\" /> <section name=\"errorMail\" requirePermission=\"false\" type=\"Elmah.ErrorMailSectionHandler, Elmah\" /> <section name=\"errorFilter\" requirePermission=\"false\" type=\"Elmah.ErrorFilterSectionHandler, Elmah\" /> </sectionGroup> </configSections> <system.web> <httpModules> <add name=\"ErrorLog\" type=\"Elmah.ErrorLogModule, Elmah\" /> <add name=\"ErrorMail\" type=\"Elmah.ErrorMailModule, Elmah\" /> <add name=\"ErrorFilter\" type=\"Elmah.ErrorFilterModule, Elmah\"/> </httpModules> </system.web> <system.webServer> <validation validateIntegratedModeConfiguration=\"false\" /> <modules> <add name=\"ErrorLog\" type=\"Elmah.ErrorLogModule, Elmah\" preCondition=\"managedHandler\" /> <add name=\"ErrorMail\" type=\"Elmah.ErrorMailModule, Elmah\" preCondition=\"managedHandler\" /> <add name=\"ErrorFilter\" type=\"Elmah.ErrorFilterModule, Elmah\" preCondition=\"managedHandler\" /> </modules> </system.webServer> <elmah> <security allowRemoteAccess=\"false\" /> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> </elmah> </configuration> In case you need to access your error log on /elmah.axd , you need to add the following to the <configuration> element in your web.config : <location path=\"elmah.axd\" inheritInChildApplications=\"false\"> <system.web> <httpHandlers> <add verb=\"POST,GET,HEAD\" path=\"elmah.axd\" type=\"Elmah.ErrorLogPageFactory, Elmah\" /> </httpHandlers> </system.web> <system.webServer> <handlers> <add name=\"ELMAH\" verb=\"POST,GET,HEAD\" path=\"elmah.axd\" type=\"Elmah.ErrorLogPageFactory, Elmah\" preCondition=\"integratedMode\" /> </handlers> </system.webServer> </location> We don't recommend browsing your error logs through the /elmah.axd endpoint. The elmah.io UI will let you control different levels of access and more.","title":"Configure elmah.io manually"},{"location":"create-deployments-from-atlassian-bamboo/","text":"Create deployments from Atlassian Bamboo Setting up elmah.io Deployment Tracking on Bamboo is easy using a bit of PowerShell. Add a new Script Task and select Windows PowerShell in Interpreter . Select Inline in Script location and add the following PowerShell to Script body : $ProgressPreference = \"SilentlyContinue\" Write-Host $bamboo_buildNumber $url = \"https://api.elmah.io/v3/deployments?api_key=API_KEY\" $body = @{ version = $Env:bamboo_buildNumber logId = \"LOG_ID\" } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body Replace API_KEY and LOG_ID and everything is configured. The script uses the build number of the current build as version number ( $Env:bamboo_buildNumber ). If you prefer another scheme, Bamboo offers a range of variables .","title":"Create deployments from Atlassian Bamboo"},{"location":"create-deployments-from-atlassian-bamboo/#create-deployments-from-atlassian-bamboo","text":"Setting up elmah.io Deployment Tracking on Bamboo is easy using a bit of PowerShell. Add a new Script Task and select Windows PowerShell in Interpreter . Select Inline in Script location and add the following PowerShell to Script body : $ProgressPreference = \"SilentlyContinue\" Write-Host $bamboo_buildNumber $url = \"https://api.elmah.io/v3/deployments?api_key=API_KEY\" $body = @{ version = $Env:bamboo_buildNumber logId = \"LOG_ID\" } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body Replace API_KEY and LOG_ID and everything is configured. The script uses the build number of the current build as version number ( $Env:bamboo_buildNumber ). If you prefer another scheme, Bamboo offers a range of variables .","title":"Create deployments from Atlassian Bamboo"},{"location":"create-deployments-from-azure-devops-pipelines/","text":"Create deployments from Azure DevOps Pipelines Create deployments from Azure DevOps Pipelines Using YAML Using Classic editor Notifying elmah.io about new deployments is possible as a build step in Azure DevOps, by adding a bit of PowerShell. Using YAML Edit your build definition YAML file. If not already shown, open the assistant by clicking the Show assistant button. Search for 'powershell'. Click the PowerShell task. Select the Inline radio button and input the following script: $ProgressPreference = \"SilentlyContinue\" $url = \"https://api.elmah.io/v3/deployments?api_key=API_KEY\" $body = @{ version = \"$env:BUILD_BUILDNUMBER\" description = \"$env:BUILD_SOURCEVERSIONMESSAGE\" userName = \"$env:BUILD_REQUESTEDFOR\" userEmail = \"$env:BUILD_REQUESTEDFOREMAIL\" logId = \"LOG_ID\" } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the id of the log representing the application deployed by this build configuration. Click the Add button and the new task will be added to your YAML definition. You typically want to move the deployment task to the last placement in tasks . Using Classic editor Edit the build definition currently building your project(s). Click the Add task button and locate the PowerShell task. Click Add . Fill in the details as shown in the screenshot. ... and here's the code from the screenshot above: $ProgressPreference = \"SilentlyContinue\" $url = \"https://api.elmah.io/v3/deployments?api_key=API_KEY\" $body = @{ version = \"$env:BUILD_BUILDNUMBER\" description = \"$env:BUILD_SOURCEVERSIONMESSAGE\" userName = \"$env:BUILD_REQUESTEDFOR\" userEmail = \"$env:BUILD_REQUESTEDFOREMAIL\" logId = \"LOG_ID\" } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the id of the log representing the application deployed by this build configuration.","title":"Create deployments from Azure DevOps Pipelines"},{"location":"create-deployments-from-azure-devops-pipelines/#create-deployments-from-azure-devops-pipelines","text":"Create deployments from Azure DevOps Pipelines Using YAML Using Classic editor Notifying elmah.io about new deployments is possible as a build step in Azure DevOps, by adding a bit of PowerShell.","title":"Create deployments from Azure DevOps Pipelines"},{"location":"create-deployments-from-azure-devops-pipelines/#using-yaml","text":"Edit your build definition YAML file. If not already shown, open the assistant by clicking the Show assistant button. Search for 'powershell'. Click the PowerShell task. Select the Inline radio button and input the following script: $ProgressPreference = \"SilentlyContinue\" $url = \"https://api.elmah.io/v3/deployments?api_key=API_KEY\" $body = @{ version = \"$env:BUILD_BUILDNUMBER\" description = \"$env:BUILD_SOURCEVERSIONMESSAGE\" userName = \"$env:BUILD_REQUESTEDFOR\" userEmail = \"$env:BUILD_REQUESTEDFOREMAIL\" logId = \"LOG_ID\" } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the id of the log representing the application deployed by this build configuration. Click the Add button and the new task will be added to your YAML definition. You typically want to move the deployment task to the last placement in tasks .","title":"Using YAML"},{"location":"create-deployments-from-azure-devops-pipelines/#using-classic-editor","text":"Edit the build definition currently building your project(s). Click the Add task button and locate the PowerShell task. Click Add . Fill in the details as shown in the screenshot. ... and here's the code from the screenshot above: $ProgressPreference = \"SilentlyContinue\" $url = \"https://api.elmah.io/v3/deployments?api_key=API_KEY\" $body = @{ version = \"$env:BUILD_BUILDNUMBER\" description = \"$env:BUILD_SOURCEVERSIONMESSAGE\" userName = \"$env:BUILD_REQUESTEDFOR\" userEmail = \"$env:BUILD_REQUESTEDFOREMAIL\" logId = \"LOG_ID\" } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the id of the log representing the application deployed by this build configuration.","title":"Using Classic editor"},{"location":"create-deployments-from-azure-devops-releases/","text":"Create deployments from Azure DevOps Releases If you are using Releases in Azure DevOps, you should use our extension to notify elmah.io about new deployments. To install and configure the extension, follow the simple steps below: Go to the elmah.io Deployment Tasks extension on the Azure DevOps Marketplace and click the Get it free button: Select your organization and click the Install button: Go to your Azure DevOps project and add the elmah.io Deployment Notification task. Fill in all fields as shown here: You will need to replace API_KEY with an API key ( Where is my API key? ) with permission ( How to configure API key permissions ) to create deployments. If the deployment is specific to a single log, insert a log ID ( Where is my log ID? ) with the ID of the log instead of LOG_ID . Deployments without a log ID will show on all logs in the organization. That's it! Azure DevOps will now notify elmah.io every time the release pipeline is executed.","title":"Create deployments from Azure DevOps Releases"},{"location":"create-deployments-from-azure-devops-releases/#create-deployments-from-azure-devops-releases","text":"If you are using Releases in Azure DevOps, you should use our extension to notify elmah.io about new deployments. To install and configure the extension, follow the simple steps below: Go to the elmah.io Deployment Tasks extension on the Azure DevOps Marketplace and click the Get it free button: Select your organization and click the Install button: Go to your Azure DevOps project and add the elmah.io Deployment Notification task. Fill in all fields as shown here: You will need to replace API_KEY with an API key ( Where is my API key? ) with permission ( How to configure API key permissions ) to create deployments. If the deployment is specific to a single log, insert a log ID ( Where is my log ID? ) with the ID of the log instead of LOG_ID . Deployments without a log ID will show on all logs in the organization. That's it! Azure DevOps will now notify elmah.io every time the release pipeline is executed.","title":"Create deployments from Azure DevOps Releases"},{"location":"create-deployments-from-bitbucket-pipelines/","text":"Create deployments from Bitbucket Pipelines Pipelines use scripts, embedded in YAML files, to configure the different steps required to build and deploy software. To notify elmah.io as part of a build/deployment, the first you will need to do is to add your API key as a secure environment variable. To do so, go to Settings | Workspace Settings | Workspace variables and add a new variable: Where is my API key? Then add a new script to your build YAML-file after building and deploying your software: pipelines: default: - step: script: # ... - curl -X POST -d \"{\\\"version\\\":\\\"$BITBUCKET_BUILD_NUMBER\\\"}\" -H \"Content-Type:application/json\" https://api.elmah.io/v3/deployments?api_key=$ELMAHIO_APIKEY The script uses curl to invoke the elmah.io Deployments endpoint with the API key ( $ELMAHIO_APIKEY ) and a version number ( $BITBUCKET_BUILD_NUMBER ). The posted JSON can be extended to support additional properties like a changelog and the name of the person triggering the deployment. Check out the API documentation for details.","title":"Create deployments from Bitbucket Pipelines"},{"location":"create-deployments-from-bitbucket-pipelines/#create-deployments-from-bitbucket-pipelines","text":"Pipelines use scripts, embedded in YAML files, to configure the different steps required to build and deploy software. To notify elmah.io as part of a build/deployment, the first you will need to do is to add your API key as a secure environment variable. To do so, go to Settings | Workspace Settings | Workspace variables and add a new variable: Where is my API key? Then add a new script to your build YAML-file after building and deploying your software: pipelines: default: - step: script: # ... - curl -X POST -d \"{\\\"version\\\":\\\"$BITBUCKET_BUILD_NUMBER\\\"}\" -H \"Content-Type:application/json\" https://api.elmah.io/v3/deployments?api_key=$ELMAHIO_APIKEY The script uses curl to invoke the elmah.io Deployments endpoint with the API key ( $ELMAHIO_APIKEY ) and a version number ( $BITBUCKET_BUILD_NUMBER ). The posted JSON can be extended to support additional properties like a changelog and the name of the person triggering the deployment. Check out the API documentation for details.","title":"Create deployments from Bitbucket Pipelines"},{"location":"create-deployments-from-cli/","text":"Create deployments from the elmah.io CLI Deployments can be easily created from either the command-line or a build server using the elmah.io CLI. There's a help page dedicated to the deployment command but here's a quick recap. If not already installed, start by installing the elmah.io CLI: dotnet tool install --global Elmah.Io.Cli Then, create a new deployment using the deployment command: elmahio deployment --apiKey API_KEY --version 1.0.0 In case you are calling the CLI from a build server, you may want to exclude the elmah.io logo and copyright message using the --nologo parameter to reduce log output and to avoid cluttering the build output: elmahio deployment --nologo --apiKey API_KEY --version 1.0.0","title":"Create deployments from CLI"},{"location":"create-deployments-from-cli/#create-deployments-from-the-elmahio-cli","text":"Deployments can be easily created from either the command-line or a build server using the elmah.io CLI. There's a help page dedicated to the deployment command but here's a quick recap. If not already installed, start by installing the elmah.io CLI: dotnet tool install --global Elmah.Io.Cli Then, create a new deployment using the deployment command: elmahio deployment --apiKey API_KEY --version 1.0.0 In case you are calling the CLI from a build server, you may want to exclude the elmah.io logo and copyright message using the --nologo parameter to reduce log output and to avoid cluttering the build output: elmahio deployment --nologo --apiKey API_KEY --version 1.0.0","title":"Create deployments from the elmah.io CLI"},{"location":"create-deployments-from-github-actions/","text":"Create deployments from GitHub Actions GitHub Actions is a great platform for building and releasing software. To notify elmah.io when you deploy a new version of your project, you will need an additional step in your build definition. Before you do that, start by creating new secrets: Go to your project on GitHub. Click the Settings tab. Click the Secrets navigation item. Click New repository secret . Name the secret ELMAH_IO_API_KEY . Insert your elmah.io API key in Value ( Where is my API key? ). Make sure to use an API key that includes the Deployments | Write permission ( How to configure API key permissions ). Click Add secret Do the same for your elmah.io log ID but name it ELMAH_IO_LOG_ID ( Where is my log ID? ). Insert the following step as the last one in your YAML build specification: - name: Create Deployment on elmah.io uses: elmahio/github-create-deployment-action@v1 with: apiKey: ${{ secrets.ELMAH_IO_API_KEY }} version: ${{ github.run_number }} logId: ${{ secrets.ELMAH_IO_LOG_ID }} The configuration will automatically notify elmah.io every time the build script is running. The build number ( github.run_number ) is used as the version for this sample, but you can modify this if you prefer another scheme. Here's a full overview of properties: Name Required Description apiKey \u2714\ufe0f An API key with permission to create deployments. version \u2714\ufe0f The version number of this deployment. The value of version can be a SemVer compliant string or any other syntax that you are using as your version numbering scheme. You can use ${{ github.run_number }} to use the build number as the version or you can pick another scheme or combine the two. description Optional description of this deployment. Can be markdown or cleartext. The latest commit message can be used as the description by using ${{ github.event.head_commit.message }} . userName The name of the person responsible for creating this deployment. This can be set manually or dynamically using the ${{ github.actor }} variable. userEmail The email of the person responsible for creating this deployment. There doesn't seem to be a way to pull the email responsible for triggering the build through variables, why this will need to be set manually. logId As default, deployments are attached to all logs of the organization. If you want a deployment to attach to a single log only, set this to the ID of that log.","title":"Create deployments from GitHub Actions"},{"location":"create-deployments-from-github-actions/#create-deployments-from-github-actions","text":"GitHub Actions is a great platform for building and releasing software. To notify elmah.io when you deploy a new version of your project, you will need an additional step in your build definition. Before you do that, start by creating new secrets: Go to your project on GitHub. Click the Settings tab. Click the Secrets navigation item. Click New repository secret . Name the secret ELMAH_IO_API_KEY . Insert your elmah.io API key in Value ( Where is my API key? ). Make sure to use an API key that includes the Deployments | Write permission ( How to configure API key permissions ). Click Add secret Do the same for your elmah.io log ID but name it ELMAH_IO_LOG_ID ( Where is my log ID? ). Insert the following step as the last one in your YAML build specification: - name: Create Deployment on elmah.io uses: elmahio/github-create-deployment-action@v1 with: apiKey: ${{ secrets.ELMAH_IO_API_KEY }} version: ${{ github.run_number }} logId: ${{ secrets.ELMAH_IO_LOG_ID }} The configuration will automatically notify elmah.io every time the build script is running. The build number ( github.run_number ) is used as the version for this sample, but you can modify this if you prefer another scheme. Here's a full overview of properties: Name Required Description apiKey \u2714\ufe0f An API key with permission to create deployments. version \u2714\ufe0f The version number of this deployment. The value of version can be a SemVer compliant string or any other syntax that you are using as your version numbering scheme. You can use ${{ github.run_number }} to use the build number as the version or you can pick another scheme or combine the two. description Optional description of this deployment. Can be markdown or cleartext. The latest commit message can be used as the description by using ${{ github.event.head_commit.message }} . userName The name of the person responsible for creating this deployment. This can be set manually or dynamically using the ${{ github.actor }} variable. userEmail The email of the person responsible for creating this deployment. There doesn't seem to be a way to pull the email responsible for triggering the build through variables, why this will need to be set manually. logId As default, deployments are attached to all logs of the organization. If you want a deployment to attach to a single log only, set this to the ID of that log.","title":"Create deployments from GitHub Actions"},{"location":"create-deployments-from-kudu/","text":"Create deployments from Kudu Kudu is the engine behind Git deployments on Microsoft Azure. To create a new elmah.io deployment every time you deploy a new app service to Azure, add a new post-deployment script by navigating your browser to https://yoursite.scm.azurewebsites.net where yoursite is the name of your Azure website. Click the Debug console and navigate to site\\deployments\\tools\\PostDeploymentActions (create it if it doesn't exist). To create the new PowerShell file, write the following in the prompt: touch CreateDeployment.ps1 With a post-deployment script running inside Kudu, we can to extract some more information about the current deployment. A full deployment PowerShell script for Kudu would look like this: $version = Get-Date -format u (Get-Content ..\\wwwroot\\web.config).replace('$version', $version) | Set-Content ..\\wwwroot\\web.config $ProgressPreference = \"SilentlyContinue\" $commit = [System.Environment]::GetEnvironmentVariable(\"SCM_COMMIT_MESSAGE\"); $commitId = [System.Environment]::GetEnvironmentVariable(\"SCM_COMMIT_ID\"); $httpHost = [System.Environment]::GetEnvironmentVariable(\"HTTP_HOST\"); $deployUrl = \"https://$httpHost/api/deployments/$commitId\" $username = \"MY_USERNAME\" $password = \"MY_PASSWORD\" $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $username,$password))) $deployInfo = Invoke-RestMethod -Method Get -Uri $deployUrl -Headers @{Authorization=(\"Basic {0}\" -f $base64AuthInfo)} $url = 'https://api.elmah.io/v3/deployments?api_key=API_KEY' $body = @{ version = $version description = $commit userName = $deployInfo.author userEmail = $deployInfo.author_email } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body (replace MY_USERNAME and MY_PASSWORD with your Azure deployment credentials and API_KEY with your elmah.io API key located on your organization settings page) The script generates a new version string from the current date and time. How you want your version string looking, is really up to you. To fetch additional information about the deployment, the Kudu deployments endpoint is requested with the current commit id. Finally, the script creates the deployment using the elmah.io REST API.","title":"Create deployments from Kudu"},{"location":"create-deployments-from-kudu/#create-deployments-from-kudu","text":"Kudu is the engine behind Git deployments on Microsoft Azure. To create a new elmah.io deployment every time you deploy a new app service to Azure, add a new post-deployment script by navigating your browser to https://yoursite.scm.azurewebsites.net where yoursite is the name of your Azure website. Click the Debug console and navigate to site\\deployments\\tools\\PostDeploymentActions (create it if it doesn't exist). To create the new PowerShell file, write the following in the prompt: touch CreateDeployment.ps1 With a post-deployment script running inside Kudu, we can to extract some more information about the current deployment. A full deployment PowerShell script for Kudu would look like this: $version = Get-Date -format u (Get-Content ..\\wwwroot\\web.config).replace('$version', $version) | Set-Content ..\\wwwroot\\web.config $ProgressPreference = \"SilentlyContinue\" $commit = [System.Environment]::GetEnvironmentVariable(\"SCM_COMMIT_MESSAGE\"); $commitId = [System.Environment]::GetEnvironmentVariable(\"SCM_COMMIT_ID\"); $httpHost = [System.Environment]::GetEnvironmentVariable(\"HTTP_HOST\"); $deployUrl = \"https://$httpHost/api/deployments/$commitId\" $username = \"MY_USERNAME\" $password = \"MY_PASSWORD\" $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $username,$password))) $deployInfo = Invoke-RestMethod -Method Get -Uri $deployUrl -Headers @{Authorization=(\"Basic {0}\" -f $base64AuthInfo)} $url = 'https://api.elmah.io/v3/deployments?api_key=API_KEY' $body = @{ version = $version description = $commit userName = $deployInfo.author userEmail = $deployInfo.author_email } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body (replace MY_USERNAME and MY_PASSWORD with your Azure deployment credentials and API_KEY with your elmah.io API key located on your organization settings page) The script generates a new version string from the current date and time. How you want your version string looking, is really up to you. To fetch additional information about the deployment, the Kudu deployments endpoint is requested with the current commit id. Finally, the script creates the deployment using the elmah.io REST API.","title":"Create deployments from Kudu"},{"location":"create-deployments-from-octopus-deploy/","text":"Create deployments from Octopus Deploy Notifying elmah.io of a new deployment from Octopus Deploy is supported through a custom step template. The step template can be installed in multiple ways as explained on Community step templates . In this document, the step template will be installed directly from the Process Editor : Go to the Process Editor and click the ADD STEP button. In the Choose Step Template section search for 'elmah.io': Hover over the 'elmah.io - Register Deployment' community template and click the INSTALL AND ADD button. In the Install and add modal click the SAVE button. The step template is now added to the process. Fill in your API key ( Where is my API key? ) and log ID ( Where is my log ID? ) in the step template fields and click the SAVE button: And we're done. On every new deployment, Octopus Deploy will notify elmah.io. In case you want an alternative version naming scheme, the Version field in the step template can be used to change the format.","title":"Create deployments from Octopus Deploy"},{"location":"create-deployments-from-octopus-deploy/#create-deployments-from-octopus-deploy","text":"Notifying elmah.io of a new deployment from Octopus Deploy is supported through a custom step template. The step template can be installed in multiple ways as explained on Community step templates . In this document, the step template will be installed directly from the Process Editor : Go to the Process Editor and click the ADD STEP button. In the Choose Step Template section search for 'elmah.io': Hover over the 'elmah.io - Register Deployment' community template and click the INSTALL AND ADD button. In the Install and add modal click the SAVE button. The step template is now added to the process. Fill in your API key ( Where is my API key? ) and log ID ( Where is my log ID? ) in the step template fields and click the SAVE button: And we're done. On every new deployment, Octopus Deploy will notify elmah.io. In case you want an alternative version naming scheme, the Version field in the step template can be used to change the format.","title":"Create deployments from Octopus Deploy"},{"location":"create-deployments-from-powershell/","text":"Create deployments from PowerShell If you release your software using a build or deployment server, creating the new release is easy using a bit of PowerShell. To request the deployments endpoint, write the following PowerShell script: $version = \"1.42.7\" $ProgressPreference = \"SilentlyContinue\" $url = 'https://api.elmah.io/v3/deployments?api_key=API_KEY' $body = @{ version = $version } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body (replace API_KEY with your API key found on your organization settings page) In the example, a simple version string is sent to the API and elmah.io will automatically put a timestamp on that. Overriding user information and description make the experience within the elmah.io UI better. Pulling release notes and the name and email of the deployer, is usually available through environment variables or similar, depending on the technology used for creating the deployment. Here's an example of a full payload for the create deployment endpoint: $body = @{ version = \"1.0.0\" created = [datetime]::UtcNow.ToString(\"o\") description = \"my deployment\" userName = \"Thomas\" userEmail = \"thomas@elmah.io\" logId = \"39e60b0b-21b4-4d12-8f09-81f3642c64be\" } In this example, the deployment belongs to a single log why the logId property is set. The description property can be used to include a changelog or similar. Markdown is supported.","title":"Create deployments from PowerShell"},{"location":"create-deployments-from-powershell/#create-deployments-from-powershell","text":"If you release your software using a build or deployment server, creating the new release is easy using a bit of PowerShell. To request the deployments endpoint, write the following PowerShell script: $version = \"1.42.7\" $ProgressPreference = \"SilentlyContinue\" $url = 'https://api.elmah.io/v3/deployments?api_key=API_KEY' $body = @{ version = $version } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body (replace API_KEY with your API key found on your organization settings page) In the example, a simple version string is sent to the API and elmah.io will automatically put a timestamp on that. Overriding user information and description make the experience within the elmah.io UI better. Pulling release notes and the name and email of the deployer, is usually available through environment variables or similar, depending on the technology used for creating the deployment. Here's an example of a full payload for the create deployment endpoint: $body = @{ version = \"1.0.0\" created = [datetime]::UtcNow.ToString(\"o\") description = \"my deployment\" userName = \"Thomas\" userEmail = \"thomas@elmah.io\" logId = \"39e60b0b-21b4-4d12-8f09-81f3642c64be\" } In this example, the deployment belongs to a single log why the logId property is set. The description property can be used to include a changelog or similar. Markdown is supported.","title":"Create deployments from PowerShell"},{"location":"create-deployments-from-umbraco-cloud/","text":"Create deployments from Umbraco Cloud Umbraco Cloud uses Azure to host Umbraco websites, so supporting deployment tracking pretty much corresponds to the steps specified in Using Kudu . Navigate to https://your-umbraco-site.scm.s1.umbraco.io where your-umbraco-site is the name of your Umbraco site. Click the Debug console link and navigate to site\\deployments\\tools\\PostDeploymentActions\\deploymenthooks (create it if it doesn't exist). Notice the folder deploymenthooks , which is required for your scripts to run on Umbraco Cloud. Unlike Kudu, Umbraco Cloud only executes cmd and bat files. Create a new cmd file: touch create-deployment.cmd with the following content: echo \"Creating elmah.io deployment\" cd %POST_DEPLOYMENT_ACTIONS_DIR% cd deploymenthooks powershell -command \". .\\create-deployment.ps1\" The script executes a PowerShell script, which we will create next: touch create-deployment.ps1 The content of the PowerShell script looks a lot like in Using Kudu , but with some minor tweaks to support Umbraco Cloud: $version = Get-Date -format u $ProgressPreference = \"SilentlyContinue\" $commitId = [System.Environment]::GetEnvironmentVariable(\"SCM_COMMIT_ID\"); $deployUrl = \"https://your-umbraco-site.scm.s1.umbraco.io/api/deployments/$commitId\" $username = \"MY_USERNAME\" $password = \"MY_PASSWORD\" $logId = \"LOG_ID\" $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $username,$password))) $deployInfo = Invoke-RestMethod -Method Get -Uri $deployUrl -Headers @{Authorization=(\"Basic {0}\" -f $base64AuthInfo)} $url = 'https://api.elmah.io/v3/deployments?api_key=API_KEY' $body = @{ version = $version description = $deployInfo.message userName = $deployInfo.author userEmail = $deployInfo.author_email logId = $logId } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body Replace your-umbraco-site with the name of your site, MY_USERNAME with your Umbraco Cloud username, MY_PASSWORD with your Umbraco Cloud password, LOG_ID with the id if the elmah.io log that should contain the deployments ( Where is my log ID? ), and finally API_KEY with your elmah.io API key, found and your organization settings page. There you go. When deploying changes to your Umbraco Cloud site, a new deployment is automatically created on elmah.io.","title":"Create deployments from Umbraco Cloud"},{"location":"create-deployments-from-umbraco-cloud/#create-deployments-from-umbraco-cloud","text":"Umbraco Cloud uses Azure to host Umbraco websites, so supporting deployment tracking pretty much corresponds to the steps specified in Using Kudu . Navigate to https://your-umbraco-site.scm.s1.umbraco.io where your-umbraco-site is the name of your Umbraco site. Click the Debug console link and navigate to site\\deployments\\tools\\PostDeploymentActions\\deploymenthooks (create it if it doesn't exist). Notice the folder deploymenthooks , which is required for your scripts to run on Umbraco Cloud. Unlike Kudu, Umbraco Cloud only executes cmd and bat files. Create a new cmd file: touch create-deployment.cmd with the following content: echo \"Creating elmah.io deployment\" cd %POST_DEPLOYMENT_ACTIONS_DIR% cd deploymenthooks powershell -command \". .\\create-deployment.ps1\" The script executes a PowerShell script, which we will create next: touch create-deployment.ps1 The content of the PowerShell script looks a lot like in Using Kudu , but with some minor tweaks to support Umbraco Cloud: $version = Get-Date -format u $ProgressPreference = \"SilentlyContinue\" $commitId = [System.Environment]::GetEnvironmentVariable(\"SCM_COMMIT_ID\"); $deployUrl = \"https://your-umbraco-site.scm.s1.umbraco.io/api/deployments/$commitId\" $username = \"MY_USERNAME\" $password = \"MY_PASSWORD\" $logId = \"LOG_ID\" $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((\"{0}:{1}\" -f $username,$password))) $deployInfo = Invoke-RestMethod -Method Get -Uri $deployUrl -Headers @{Authorization=(\"Basic {0}\" -f $base64AuthInfo)} $url = 'https://api.elmah.io/v3/deployments?api_key=API_KEY' $body = @{ version = $version description = $deployInfo.message userName = $deployInfo.author userEmail = $deployInfo.author_email logId = $logId } [Net.ServicePointManager]::SecurityProtocol = ` [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls Invoke-RestMethod -Method Post -Uri $url -Body $body Replace your-umbraco-site with the name of your site, MY_USERNAME with your Umbraco Cloud username, MY_PASSWORD with your Umbraco Cloud password, LOG_ID with the id if the elmah.io log that should contain the deployments ( Where is my log ID? ), and finally API_KEY with your elmah.io API key, found and your organization settings page. There you go. When deploying changes to your Umbraco Cloud site, a new deployment is automatically created on elmah.io.","title":"Create deployments from Umbraco Cloud"},{"location":"creating-rules-to-perform-actions-on-messages/","text":"Creating Rules to Perform Actions on Messages elmah.io comes with a great rule engine for performing various actions when messages are logged in your log. This guide is also available as a short video tutorial here: The rule engine is located beneath each log on the log settings page: A rule consists of three parts: a title, a query, and an action. The title should be a short text explaining what this rule does. We don't use the title for anything, so please write something that helps you identify rules and to keep them apart. The query should contain either a full-text search string or a Lucene query. When new messages are logged, the message is matched up against all queries registered on that log. If and only if a message matches a query, the action registered on the rule is performed. As mentioned above, the action part of a rule is executed when a message matches the query specified in the same rule. An action can be one of four types: Ignore, Hide, Mail, and HTTP Request. To illustrate how to use each action type, here are four examples of useful rules. Ignore errors with an HTTP status code of 400 Be aware that Ignore rules are only meant as a temporary way of ignoring messages. In case you want to permanently ignore one or more log messages, use client-side filtering as explained in the documentation for each client integration. In addition, there's a client-side filtering help dialog available on the log message details toolbar. Ignoring a large number of messages with Ignore rules will slow down your application logging, use unnecessary network bandwidth, and risk hitting the elmah.io API request limit. To ignore all messages with an HTTP status code of 400, you would need to set up the following: Title Query Then Ignore 400s statusCode:400 Ignore The rule would look like this in the UI: Hide warnings To hide all messages with a severity of Warning , you would need to set up the following: Title Query Then Hide Warnings severity:Warning Hide The rule would look like this in the UI: Send an email on all messages containing a word To send an email on all messages containing the word billing somewhere, you would need to set up the following: Title Query Then Mail on billing billing Email The rule would look like this in the UI: Make an HTTP request on all new and fatal messages To make an HTTP request on every new message with a severity of Fatal , you would need to set up the following: Title Query Then Request on new fatal isNew:true AND severity:Fatal HTTP The rule would look like this in the UI:","title":"Creating Rules to Perform Actions on Messages"},{"location":"creating-rules-to-perform-actions-on-messages/#creating-rules-to-perform-actions-on-messages","text":"elmah.io comes with a great rule engine for performing various actions when messages are logged in your log. This guide is also available as a short video tutorial here: The rule engine is located beneath each log on the log settings page: A rule consists of three parts: a title, a query, and an action. The title should be a short text explaining what this rule does. We don't use the title for anything, so please write something that helps you identify rules and to keep them apart. The query should contain either a full-text search string or a Lucene query. When new messages are logged, the message is matched up against all queries registered on that log. If and only if a message matches a query, the action registered on the rule is performed. As mentioned above, the action part of a rule is executed when a message matches the query specified in the same rule. An action can be one of four types: Ignore, Hide, Mail, and HTTP Request. To illustrate how to use each action type, here are four examples of useful rules.","title":"Creating Rules to Perform Actions on Messages"},{"location":"creating-rules-to-perform-actions-on-messages/#ignore-errors-with-an-http-status-code-of-400","text":"Be aware that Ignore rules are only meant as a temporary way of ignoring messages. In case you want to permanently ignore one or more log messages, use client-side filtering as explained in the documentation for each client integration. In addition, there's a client-side filtering help dialog available on the log message details toolbar. Ignoring a large number of messages with Ignore rules will slow down your application logging, use unnecessary network bandwidth, and risk hitting the elmah.io API request limit. To ignore all messages with an HTTP status code of 400, you would need to set up the following: Title Query Then Ignore 400s statusCode:400 Ignore The rule would look like this in the UI:","title":"Ignore errors with an HTTP status code of 400"},{"location":"creating-rules-to-perform-actions-on-messages/#hide-warnings","text":"To hide all messages with a severity of Warning , you would need to set up the following: Title Query Then Hide Warnings severity:Warning Hide The rule would look like this in the UI:","title":"Hide warnings"},{"location":"creating-rules-to-perform-actions-on-messages/#send-an-email-on-all-messages-containing-a-word","text":"To send an email on all messages containing the word billing somewhere, you would need to set up the following: Title Query Then Mail on billing billing Email The rule would look like this in the UI:","title":"Send an email on all messages containing a word"},{"location":"creating-rules-to-perform-actions-on-messages/#make-an-http-request-on-all-new-and-fatal-messages","text":"To make an HTTP request on every new message with a severity of Fatal , you would need to set up the following: Title Query Then Request on new fatal isNew:true AND severity:Fatal HTTP The rule would look like this in the UI:","title":"Make an HTTP request on all new and fatal messages"},{"location":"elmah-and-custom-errors/","text":"ELMAH and custom errors ELMAH and ASP.NET (MVC) custom errors aren't exactly known to be best friends. Question after question has been posted on forums like Stack Overflow, from people having problems with ELMAH, when custom errors are configured. These problems make perfect sense since both ELMAH and custom errors are designed to catch errors and do something about them. Before looking at some code, we recommend you to read Web.config customErrors element with ASP.NET explained and Demystifying ASP.NET MVC 5 Error Pages and Error Logging . Together, the posts are a great introduction to different ways of implementing custom error pages in ASP.NET MVC. Back to ELMAH. In most implementations of custom error pages, ASP.NET swallows any uncaught exceptions, putting ELMAH out of play. To overcome this issue, you can utilize MVC's IExceptionFilter to log all exceptions, whether or not it is handled by a custom error page: public class ElmahExceptionLogger : IExceptionFilter { public void OnException (ExceptionContext context) { if (context.ExceptionHandled) { ErrorSignal.FromCurrentContext().Raise(context.Exception); } } } The OnException method on ElmahExceptionLogger is executed every time an error is happening, by registering it in Application_Start : protected void Application_Start() { // ... GlobalConfiguration.Configuration.Filters.Add(new ElmahExceptionLogger()); // ... }","title":"ELMAH and custom errors"},{"location":"elmah-and-custom-errors/#elmah-and-custom-errors","text":"ELMAH and ASP.NET (MVC) custom errors aren't exactly known to be best friends. Question after question has been posted on forums like Stack Overflow, from people having problems with ELMAH, when custom errors are configured. These problems make perfect sense since both ELMAH and custom errors are designed to catch errors and do something about them. Before looking at some code, we recommend you to read Web.config customErrors element with ASP.NET explained and Demystifying ASP.NET MVC 5 Error Pages and Error Logging . Together, the posts are a great introduction to different ways of implementing custom error pages in ASP.NET MVC. Back to ELMAH. In most implementations of custom error pages, ASP.NET swallows any uncaught exceptions, putting ELMAH out of play. To overcome this issue, you can utilize MVC's IExceptionFilter to log all exceptions, whether or not it is handled by a custom error page: public class ElmahExceptionLogger : IExceptionFilter { public void OnException (ExceptionContext context) { if (context.ExceptionHandled) { ErrorSignal.FromCurrentContext().Raise(context.Exception); } } } The OnException method on ElmahExceptionLogger is executed every time an error is happening, by registering it in Application_Start : protected void Application_Start() { // ... GlobalConfiguration.Configuration.Filters.Add(new ElmahExceptionLogger()); // ... }","title":"ELMAH and custom errors"},{"location":"elmah-and-elmah-io-differences/","text":"ELMAH and elmah.io differences We receive a lot of questions like these: What is the difference between ELMAH and elmah.io? I thought ELMAH was free. Why do you suddenly charge? My ELMAH SQL Server configuration doesn't work. Why not? We understand the confusion. The purpose of this article is to give a bit of background of the differences between ELMAH and elmah.io and why they share similar names. What is ELMAH? ELMAH is an error logging framework originally developed by Atif Aziz able to log all unhandled exceptions from .NET web applications. Errors can be logged to a variety of destinations through ELMAH\u2019s plugin model called error logs. Plugins for XML, SQL Server, MySQL, Elasticsearch, and many more exists. ELMAH automatically collects a lot of information from the HTTP context when logging the error, giving you the possibility to inspect request parameters, cookies, and much more for the failed request. Custom errors can be logged to ELMAH, by manually calling the error log. What is elmah.io? elmah.io is a cloud-based error management system originally developed on top of ELMAH (see history for details). Besides supporting ELMAH, elmah.io also integrates with popular logging frameworks like log4net , NLog , Serilog , and web frameworks like ASP.NET Core . elmah.io offers a superior notification model to ELMAH, with integrations to mail, Slack, Microsoft Teams, and many others. elmah.io also built a lot of features outside the scope of ELMAH, like a complete uptime monitoring system. Comparison Feature ELMAH elmah.io Error Logging \u2705 \u2705 Self-hosted \u2705 \u274c Cloud-hosted \u274c \u2705 Search \u274c \u2705 New error detection \u274c \u2705 Error grouping \u274c \u2705 Issue tracking \u274c \u2705 log4net / NLog / Serilog \u274c \u2705 Clientside error logging \u274c \u2705 Slack/Teams/HipChat/etc. \u274c \u2705 Deployment tracking \u274c \u2705 Uptime monitoring \u274c \u2705 Heartbeats \u274c \u2705 Machine learning \u274c \u2705 Discount on popular software \u274c \u2705 History So, why name a service elmah.io, when only a minor part of a client integration uses ELMAH? When elmah.io was introduced back in 2013, the intention was to create a cloud-based error logger for ELMAH. We had some simple search and graphing possibilities, but the platform was meant as an alternative to host your own errors logs in SQL Server or similar. In time, elmah.io grew from being a hobby project to an actual company. During those years, we realized that the potential of the platform exceeded the possibilities with ELMAH in many ways. New features not available in ELMAH have been added constantly. A process that would have been nearly impossible with ELMAH's many storage integrations. Today, elmah.io is a full error management system for everything from console applications to web apps and serverless code hosted on Azure or AWS. We've built an entire uptime monitoring system, able to monitor not only if your website fails but also if it even responds to requests. Why not change the name to something else, you may be thinking? That is our wish as well. But changing your SaaS (software-as-a-service) company name isn't exactly easy. We have tried a couple of times, the first time back in 2016. We tried to name the different major features of elmah.io to sea creatures (like Stingray). We failed with the rename and people got confused. In 2017, we started looking at renaming the product again. This time to Unbug. We had learned from our previous mistake and this time silently started changing the name. We quickly realized that the domain change would cause a major risk in regards to SEO (search engine optimization) and confusion. For now, we are elmah.io. The name is not ideal, but it's a lesson learned for another time :)","title":"ELMAH and elmah.io differences"},{"location":"elmah-and-elmah-io-differences/#elmah-and-elmahio-differences","text":"We receive a lot of questions like these: What is the difference between ELMAH and elmah.io? I thought ELMAH was free. Why do you suddenly charge? My ELMAH SQL Server configuration doesn't work. Why not? We understand the confusion. The purpose of this article is to give a bit of background of the differences between ELMAH and elmah.io and why they share similar names.","title":"ELMAH and elmah.io differences"},{"location":"elmah-and-elmah-io-differences/#what-is-elmah","text":"ELMAH is an error logging framework originally developed by Atif Aziz able to log all unhandled exceptions from .NET web applications. Errors can be logged to a variety of destinations through ELMAH\u2019s plugin model called error logs. Plugins for XML, SQL Server, MySQL, Elasticsearch, and many more exists. ELMAH automatically collects a lot of information from the HTTP context when logging the error, giving you the possibility to inspect request parameters, cookies, and much more for the failed request. Custom errors can be logged to ELMAH, by manually calling the error log.","title":"What is ELMAH?"},{"location":"elmah-and-elmah-io-differences/#what-is-elmahio","text":"elmah.io is a cloud-based error management system originally developed on top of ELMAH (see history for details). Besides supporting ELMAH, elmah.io also integrates with popular logging frameworks like log4net , NLog , Serilog , and web frameworks like ASP.NET Core . elmah.io offers a superior notification model to ELMAH, with integrations to mail, Slack, Microsoft Teams, and many others. elmah.io also built a lot of features outside the scope of ELMAH, like a complete uptime monitoring system.","title":"What is elmah.io?"},{"location":"elmah-and-elmah-io-differences/#comparison","text":"Feature ELMAH elmah.io Error Logging \u2705 \u2705 Self-hosted \u2705 \u274c Cloud-hosted \u274c \u2705 Search \u274c \u2705 New error detection \u274c \u2705 Error grouping \u274c \u2705 Issue tracking \u274c \u2705 log4net / NLog / Serilog \u274c \u2705 Clientside error logging \u274c \u2705 Slack/Teams/HipChat/etc. \u274c \u2705 Deployment tracking \u274c \u2705 Uptime monitoring \u274c \u2705 Heartbeats \u274c \u2705 Machine learning \u274c \u2705 Discount on popular software \u274c \u2705","title":"Comparison"},{"location":"elmah-and-elmah-io-differences/#history","text":"So, why name a service elmah.io, when only a minor part of a client integration uses ELMAH? When elmah.io was introduced back in 2013, the intention was to create a cloud-based error logger for ELMAH. We had some simple search and graphing possibilities, but the platform was meant as an alternative to host your own errors logs in SQL Server or similar. In time, elmah.io grew from being a hobby project to an actual company. During those years, we realized that the potential of the platform exceeded the possibilities with ELMAH in many ways. New features not available in ELMAH have been added constantly. A process that would have been nearly impossible with ELMAH's many storage integrations. Today, elmah.io is a full error management system for everything from console applications to web apps and serverless code hosted on Azure or AWS. We've built an entire uptime monitoring system, able to monitor not only if your website fails but also if it even responds to requests. Why not change the name to something else, you may be thinking? That is our wish as well. But changing your SaaS (software-as-a-service) company name isn't exactly easy. We have tried a couple of times, the first time back in 2016. We tried to name the different major features of elmah.io to sea creatures (like Stingray). We failed with the rename and people got confused. In 2017, we started looking at renaming the product again. This time to Unbug. We had learned from our previous mistake and this time silently started changing the name. We quickly realized that the domain change would cause a major risk in regards to SEO (search engine optimization) and confusion. For now, we are elmah.io. The name is not ideal, but it's a lesson learned for another time :)","title":"History"},{"location":"elmah-io-apps-azure-boards/","text":"Install Azure Boards App for elmah.io Get your personal access token To create bugs on Azure Boards, you will need to generate a personal access token. Go to Azure DevOps and click the User settings icon in the top right corner. Select the Personal access tokens menu item in the dropdown. Finally, click the New Token button and fill in the details as shown below: For this example, we have picked 90 days expiration period, but you can decide on a shorter or longer period if you'd like. Remember to enable the Read & write scope under Work Items . Next, click the Create button and copy the generated token. Bugs created by elmah.io will have the CreatedBy set to the user generating the personal access token. If you want to identify bugs created by elmah.io, you should create the token from a new user (like elmahio@yourdomain.com). Install the Azure Boards App on elmah.io Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Azure Boards app and click the Install button: Paste the token copied in the previous step into the Token textbox. In the Organization textbox, input the name of your organization. For https://dev.azure.com/myorg/myproject, the organization name would be myorg . In the Project textbox, input the name of the project containing your board. For https://dev.azure.com/myorg/myproject, the project name would be myproject . If you want to embed all bugs created by the app beneath an overall work item, epic, or similar, fill in the optional ID in the Parent field. Click Save and the app is added to your log. When new errors are logged, bugs are automatically created in the configured Azure Board.","title":"Azure Boards"},{"location":"elmah-io-apps-azure-boards/#install-azure-boards-app-for-elmahio","text":"","title":"Install Azure Boards App for elmah.io"},{"location":"elmah-io-apps-azure-boards/#get-your-personal-access-token","text":"To create bugs on Azure Boards, you will need to generate a personal access token. Go to Azure DevOps and click the User settings icon in the top right corner. Select the Personal access tokens menu item in the dropdown. Finally, click the New Token button and fill in the details as shown below: For this example, we have picked 90 days expiration period, but you can decide on a shorter or longer period if you'd like. Remember to enable the Read & write scope under Work Items . Next, click the Create button and copy the generated token. Bugs created by elmah.io will have the CreatedBy set to the user generating the personal access token. If you want to identify bugs created by elmah.io, you should create the token from a new user (like elmahio@yourdomain.com).","title":"Get your personal access token"},{"location":"elmah-io-apps-azure-boards/#install-the-azure-boards-app-on-elmahio","text":"Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Azure Boards app and click the Install button: Paste the token copied in the previous step into the Token textbox. In the Organization textbox, input the name of your organization. For https://dev.azure.com/myorg/myproject, the organization name would be myorg . In the Project textbox, input the name of the project containing your board. For https://dev.azure.com/myorg/myproject, the project name would be myproject . If you want to embed all bugs created by the app beneath an overall work item, epic, or similar, fill in the optional ID in the Parent field. Click Save and the app is added to your log. When new errors are logged, bugs are automatically created in the configured Azure Board.","title":"Install the Azure Boards App on elmah.io"},{"location":"elmah-io-apps-bitbucket/","text":"Install Bitbucket App for elmah.io Get your App password To allow elmah.io to create issues on Bitbucket, you will need an App password. App passwords can be generated by clicking your user in the top right corner and selecting Personal settings . In the left menu, click the App passwords page ( https://bitbucket.org/account/settings/app-passwords/ ). To create a new password, click the Create app password button and input the following information: elmah.io only need the Issues - Write permission to create issues. To test the inputted values on elmah.io (later step) also check the Repositories - Read permission. After clicking the Create button, copy the generated app password. Install the Bitbucket App on elmah.io Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Bitbucket app and click the Install button: Paste the App password copied in the previous step into the APP PASSWORD textbox. In the TEAM textbox, input the name of the team/workspace owning the repository you want to create issues in. In the REPOSITORY textbox input the name of the repository. In the USERNAME textbox, input the name of the user generating the App password. In older installations, this can also contain the team/workspace name. Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured Bitbucket repository.","title":"Bitbucket"},{"location":"elmah-io-apps-bitbucket/#install-bitbucket-app-for-elmahio","text":"","title":"Install Bitbucket App for elmah.io"},{"location":"elmah-io-apps-bitbucket/#get-your-app-password","text":"To allow elmah.io to create issues on Bitbucket, you will need an App password. App passwords can be generated by clicking your user in the top right corner and selecting Personal settings . In the left menu, click the App passwords page ( https://bitbucket.org/account/settings/app-passwords/ ). To create a new password, click the Create app password button and input the following information: elmah.io only need the Issues - Write permission to create issues. To test the inputted values on elmah.io (later step) also check the Repositories - Read permission. After clicking the Create button, copy the generated app password.","title":"Get your App password"},{"location":"elmah-io-apps-bitbucket/#install-the-bitbucket-app-on-elmahio","text":"Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Bitbucket app and click the Install button: Paste the App password copied in the previous step into the APP PASSWORD textbox. In the TEAM textbox, input the name of the team/workspace owning the repository you want to create issues in. In the REPOSITORY textbox input the name of the repository. In the USERNAME textbox, input the name of the user generating the App password. In older installations, this can also contain the team/workspace name. Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured Bitbucket repository.","title":"Install the Bitbucket App on elmah.io"},{"location":"elmah-io-apps-botbuster/","text":"Install BotBuster App for elmah.io The BotBuster app is deprecated. Enable the Filter Crawlers toggle on the Filters tab to ignore errors generated by crawlers. The BotBuster app for elmah.io identifies and ignores messages generated by white hat bots like spiders, search engine bots, and similar. Under normal circumstances, you want to allow access for white hat bots, but you don't want to get a notification every time one of them tries to request a resource not found on the server. Installing BotBuster couldn't be simpler. Log into elmah.io and go to the log settings. Click the Apps tab. Locate the BotBuster app and click the Install button.","title":"BotBuster (deprecated)"},{"location":"elmah-io-apps-botbuster/#install-botbuster-app-for-elmahio","text":"The BotBuster app is deprecated. Enable the Filter Crawlers toggle on the Filters tab to ignore errors generated by crawlers. The BotBuster app for elmah.io identifies and ignores messages generated by white hat bots like spiders, search engine bots, and similar. Under normal circumstances, you want to allow access for white hat bots, but you don't want to get a notification every time one of them tries to request a resource not found on the server. Installing BotBuster couldn't be simpler. Log into elmah.io and go to the log settings. Click the Apps tab. Locate the BotBuster app and click the Install button.","title":"Install BotBuster App for elmah.io"},{"location":"elmah-io-apps-chatgpt/","text":"Install ChatGPT for elmah.io Log into elmah.io and go to the log settings. Click the Apps tab. Locate the ChatGPT app and click the Install button: Input your OpenAI API key ( Where do I find my OpenAI API Key? ). Next, select which language model to use. We currently support GPT-3.5-Turbo and GPT-4. As a default, elmah.io will only share the stack trace of an error with ChatGPT when you click the Get suggestion button in the elmah.io UI. If you want to include the source code and/or any SQL attached to the error, you can enable one or both toggles. Sharing the source will require you to bundle your source code alongside errors as documented here: How to include source code in log messages . Click Save and the app is added to your log. When you open errors valid for ChatGPT help, you will see a tab named AI next to Detail , Inspector , etc.","title":"ChatGPT"},{"location":"elmah-io-apps-chatgpt/#install-chatgpt-for-elmahio","text":"Log into elmah.io and go to the log settings. Click the Apps tab. Locate the ChatGPT app and click the Install button: Input your OpenAI API key ( Where do I find my OpenAI API Key? ). Next, select which language model to use. We currently support GPT-3.5-Turbo and GPT-4. As a default, elmah.io will only share the stack trace of an error with ChatGPT when you click the Get suggestion button in the elmah.io UI. If you want to include the source code and/or any SQL attached to the error, you can enable one or both toggles. Sharing the source will require you to bundle your source code alongside errors as documented here: How to include source code in log messages . Click Save and the app is added to your log. When you open errors valid for ChatGPT help, you will see a tab named AI next to Detail , Inspector , etc.","title":"Install ChatGPT for elmah.io"},{"location":"elmah-io-apps-clickup/","text":"Install ClickUp for elmah.io Log into elmah.io and go to the log settings page. Click the Apps tab. Locate the ClickUp app and click the Install button: You will need to input a ClickUp API token and the ID of the list to create tasks. The API token can be generated by navigating to ClickUp, clicking the profile photo in the bottom left corner, and clicking Apps . It is important to click the Apps link beneath your profile and not the ClickApps link beneath the team. On the Apps page, you can generate and copy a new token beneath the API Token section. The list ID can be found by going to the list on the ClickUp app and clicking the list name: When copying the link you will get a link similar to this: https://app.clickup.com/.../v/li/901200300647 The list ID is the last part of the URL ( 901200300647 in the example above). When both the API token and list ID are inputted on elmah.io, click the Test button to test the values. When the Test button turns green, click the Save button, and the app is added to your log. When new errors are logged, tasks are automatically created in the configured ClickUp list. Troubleshooting If errors aren't showing up in ClickUp, please check that the following are all true: When clicking the Test button on the ClickUp app settings screen, the button turns green. There's a message logged in the log where you set up the ClickUp integration. The message is marked as new (yellow star next to the title on the search result). The message is either of severity Error or Fatal . To trigger an error manually, go to https://api.elmah.io/swagger/index.html and input an elmah.io API key with the Messages - Write permission enabled. Expand Messages and the POST node with the URL /v3/messages/{logId} . Input your log ID and the following JSON: { \"title\": \"This is a test\", \"severity\": \"Error\" } Finally, click the Try it out! button and verify that the API returns a status code of 201 . The new error should show up in ClickUp. If testing this multiple times, you will need to make small adjustments to the title field inside the JSON, for additional errors to be marked as new.","title":"ClickUp"},{"location":"elmah-io-apps-clickup/#install-clickup-for-elmahio","text":"Log into elmah.io and go to the log settings page. Click the Apps tab. Locate the ClickUp app and click the Install button: You will need to input a ClickUp API token and the ID of the list to create tasks. The API token can be generated by navigating to ClickUp, clicking the profile photo in the bottom left corner, and clicking Apps . It is important to click the Apps link beneath your profile and not the ClickApps link beneath the team. On the Apps page, you can generate and copy a new token beneath the API Token section. The list ID can be found by going to the list on the ClickUp app and clicking the list name: When copying the link you will get a link similar to this: https://app.clickup.com/.../v/li/901200300647 The list ID is the last part of the URL ( 901200300647 in the example above). When both the API token and list ID are inputted on elmah.io, click the Test button to test the values. When the Test button turns green, click the Save button, and the app is added to your log. When new errors are logged, tasks are automatically created in the configured ClickUp list.","title":"Install ClickUp for elmah.io"},{"location":"elmah-io-apps-clickup/#troubleshooting","text":"If errors aren't showing up in ClickUp, please check that the following are all true: When clicking the Test button on the ClickUp app settings screen, the button turns green. There's a message logged in the log where you set up the ClickUp integration. The message is marked as new (yellow star next to the title on the search result). The message is either of severity Error or Fatal . To trigger an error manually, go to https://api.elmah.io/swagger/index.html and input an elmah.io API key with the Messages - Write permission enabled. Expand Messages and the POST node with the URL /v3/messages/{logId} . Input your log ID and the following JSON: { \"title\": \"This is a test\", \"severity\": \"Error\" } Finally, click the Try it out! button and verify that the API returns a status code of 201 . The new error should show up in ClickUp. If testing this multiple times, you will need to make small adjustments to the title field inside the JSON, for additional errors to be marked as new.","title":"Troubleshooting"},{"location":"elmah-io-apps-github/","text":"Install GitHub App for elmah.io Generate Personal Access Token To allow elmah.io to create issues on GitHub, you need a Personal Access Token. Sign in to GitHub, click your profile photo in the top right corner, and click Settings . On the Settings page click Developer settings followed by Personal access token . Here you can create a new token by clicking the Generate new token (classic) button: Input a token note and select an expiration date. If the repository you want issues created in is public, make sure to check the public_repo checkbox. If the repository is private, check the repo checkbox. Finally, click the Generate token button, and copy the generated token (colored with a green background) GitHub also supports fine-grained personal access tokens. This token can also be used on elmah.io. Make sure to select Read and write in the Issues permission. Install the GitHub App on elmah.io Log into elmah.io and go to the log settings. Click the Apps tab. Locate the GitHub app and click the Install button: Paste the token copied in the previous step into the Token textbox. In the Owner textbox, input the name of the user or organization owning the repository you want to create issues in. In the Repository textbox input the name of the repository. Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured GitHub repository.","title":"GitHub"},{"location":"elmah-io-apps-github/#install-github-app-for-elmahio","text":"","title":"Install GitHub App for elmah.io"},{"location":"elmah-io-apps-github/#generate-personal-access-token","text":"To allow elmah.io to create issues on GitHub, you need a Personal Access Token. Sign in to GitHub, click your profile photo in the top right corner, and click Settings . On the Settings page click Developer settings followed by Personal access token . Here you can create a new token by clicking the Generate new token (classic) button: Input a token note and select an expiration date. If the repository you want issues created in is public, make sure to check the public_repo checkbox. If the repository is private, check the repo checkbox. Finally, click the Generate token button, and copy the generated token (colored with a green background) GitHub also supports fine-grained personal access tokens. This token can also be used on elmah.io. Make sure to select Read and write in the Issues permission.","title":"Generate Personal Access Token"},{"location":"elmah-io-apps-github/#install-the-github-app-on-elmahio","text":"Log into elmah.io and go to the log settings. Click the Apps tab. Locate the GitHub app and click the Install button: Paste the token copied in the previous step into the Token textbox. In the Owner textbox, input the name of the user or organization owning the repository you want to create issues in. In the Repository textbox input the name of the repository. Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured GitHub repository.","title":"Install the GitHub App on elmah.io"},{"location":"elmah-io-apps-gitlab/","text":"Install GitLab App for elmah.io Generate Personal Access Token To allow elmah.io to create issues on GitLab, you will need to generate a Personal Access Token. To do so, log into GitLab, click your profile photo in the top right corner, and select Preferences . On the Preferences page click the Access Tokens menu item: Input a token name, expiration date, and check the api checkbox. Click the Create personal access token button and copy the generated token. Install the GitLab App on elmah.io Log into elmah.io and go to the log settings. Click the Apps tab. Locate the GitLab app and click the Install button: Paste the token copied in the previous step into the Token textbox. In the Project textbox, input the ID or name of the project you want issues created on. If you are self-hosting GitLab, input your custom URL in the URL textbox (for example https://gitlab.hooli.com). Click the Test button and observe it turn green. When clicking Save , the app is added to your log. When new errors are logged, issues are automatically created in the configured GitLab project.","title":"GitLab"},{"location":"elmah-io-apps-gitlab/#install-gitlab-app-for-elmahio","text":"","title":"Install GitLab App for elmah.io"},{"location":"elmah-io-apps-gitlab/#generate-personal-access-token","text":"To allow elmah.io to create issues on GitLab, you will need to generate a Personal Access Token. To do so, log into GitLab, click your profile photo in the top right corner, and select Preferences . On the Preferences page click the Access Tokens menu item: Input a token name, expiration date, and check the api checkbox. Click the Create personal access token button and copy the generated token.","title":"Generate Personal Access Token"},{"location":"elmah-io-apps-gitlab/#install-the-gitlab-app-on-elmahio","text":"Log into elmah.io and go to the log settings. Click the Apps tab. Locate the GitLab app and click the Install button: Paste the token copied in the previous step into the Token textbox. In the Project textbox, input the ID or name of the project you want issues created on. If you are self-hosting GitLab, input your custom URL in the URL textbox (for example https://gitlab.hooli.com). Click the Test button and observe it turn green. When clicking Save , the app is added to your log. When new errors are logged, issues are automatically created in the configured GitLab project.","title":"Install the GitLab App on elmah.io"},{"location":"elmah-io-apps-hipchat/","text":"Install HipChat App for elmah.io Generate OAuth 2 Token To allow elmah.io to log messages to HipChat, you will need to generate an OAuth 2 token. To do so, log into HipChat and go to the API Access page (replace elmahio with your subdomain). Input a label, click the Create button and copy the generated token. If you want to test your configuration using the Test button on the elmah.io UI, you will need to select both Send Notification and View Room in Scopes . Install the HipChat App on elmah.io Log into elmah.io and go to the log settings. Click the Apps tab. Locate the HipChat app and click the Install button: Paste the token copied in the previous step into the Token textbox. In the Room textbox, input the name of the HipChat chat room you want messages from elmah.io to show up in. Click Save and the app is added to your log. When new errors are logged, messages start appearing in the chat room that you configured. HipChat doesn't allow more than 500 requests per 5 minutes. If you generate more messages to elmah.io, not all of them will show up in HipChat because of this.","title":"HipChat"},{"location":"elmah-io-apps-hipchat/#install-hipchat-app-for-elmahio","text":"","title":"Install HipChat App for elmah.io"},{"location":"elmah-io-apps-hipchat/#generate-oauth-2-token","text":"To allow elmah.io to log messages to HipChat, you will need to generate an OAuth 2 token. To do so, log into HipChat and go to the API Access page (replace elmahio with your subdomain). Input a label, click the Create button and copy the generated token. If you want to test your configuration using the Test button on the elmah.io UI, you will need to select both Send Notification and View Room in Scopes .","title":"Generate OAuth 2 Token"},{"location":"elmah-io-apps-hipchat/#install-the-hipchat-app-on-elmahio","text":"Log into elmah.io and go to the log settings. Click the Apps tab. Locate the HipChat app and click the Install button: Paste the token copied in the previous step into the Token textbox. In the Room textbox, input the name of the HipChat chat room you want messages from elmah.io to show up in. Click Save and the app is added to your log. When new errors are logged, messages start appearing in the chat room that you configured. HipChat doesn't allow more than 500 requests per 5 minutes. If you generate more messages to elmah.io, not all of them will show up in HipChat because of this.","title":"Install the HipChat App on elmah.io"},{"location":"elmah-io-apps-ipfilter/","text":"Install IP Filter App for elmah.io The IP Filter app is deprecated. The IP Filter filter on the Filters tab offers more advanced IP filtering. The IP Filter app for elmah.io automatically ignores messages from one or more IP addresses. This is a great way to ignore errors generated by both crawlers and errors generated by you. To install IP Filter, click the Install button on the Apps tab. This will show the IP Filter settings page: To ignore messages from a single IP address, input the IP in both the From and To fields. To ignore messages from a range of IP addresses, input the start and end IP address in the From and To fields. Both IP addresses are included in the ignored range. The IP Filter app ignores every message matching the specified IP range. This means that if you are logging something like Information messages through Serilog or similar, these messages are also ignored. For a message to have an IP, you will need to specify a server variable named REMOTE_ADDR when creating the message. This variable is automatically added (if available) when using the integration for ELMAH.","title":"IP Filter (deprecated)"},{"location":"elmah-io-apps-ipfilter/#install-ip-filter-app-for-elmahio","text":"The IP Filter app is deprecated. The IP Filter filter on the Filters tab offers more advanced IP filtering. The IP Filter app for elmah.io automatically ignores messages from one or more IP addresses. This is a great way to ignore errors generated by both crawlers and errors generated by you. To install IP Filter, click the Install button on the Apps tab. This will show the IP Filter settings page: To ignore messages from a single IP address, input the IP in both the From and To fields. To ignore messages from a range of IP addresses, input the start and end IP address in the From and To fields. Both IP addresses are included in the ignored range. The IP Filter app ignores every message matching the specified IP range. This means that if you are logging something like Information messages through Serilog or similar, these messages are also ignored. For a message to have an IP, you will need to specify a server variable named REMOTE_ADDR when creating the message. This variable is automatically added (if available) when using the integration for ELMAH.","title":"Install IP Filter App for elmah.io"},{"location":"elmah-io-apps-jira/","text":"Install Jira App for elmah.io Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Jira app and click the Install button: Input your site name, which is the first part of the URL you use to log into Jira. For the URL https://elmahio.atlassian.net/ , the site parameter would be elmahio . In the Project field, input the key of the project. Note that a project has both a display name and a key. The property we are looking for here is the uppercase identifier of the project. To create issues on Jira, you will need to input the username and password of a user with permission to create issues in the project specified above. You can use your user credentials, but we recommend using a combination of your username and an API token. To generate a new token specific for elmah.io, go to the API Tokens page on your Jira account. Then click the Create API token button and input a label of your choice. Finally, click the Create button and an API token is generated for you. Make sure to copy this token, since you won't be able to access it once the dialog is closed. Go back to elmah.io and input your email in the Username field and the API token from the previous step in the Password field. If you don't like to use an existing user account for the integration, you can create a new Atlassian account for elmah.io and generate the API token from that account instead. Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured Jira project. Troubleshooting If errors aren't showing up in Jira, please check that the following are all true: When clicking the Test button on the Jira app settings screen, the button turns green. There's a message logged in the log where you set up the Jira integration. The message is marked as new (yellow star next to the title on the search result). The message is either of severity Error or Fatal . To trigger an error manually, go to https://api.elmah.io/swagger/index.html and input an elmah.io API key with the Messages - Write permission enabled. Expand Messages and the POST node with the URL /v3/messages/{logId} . Input your log ID and the following JSON: { \"title\": \"This is a test\", \"severity\": \"Error\" } Finally, click the Try it out! button and verify that the API returns a status code of 201 . The new error should show up in Jira. If testing this multiple times, you will need to make small adjustments to the title field inside the JSON, for additional errors to be marked as new.","title":"Jira"},{"location":"elmah-io-apps-jira/#install-jira-app-for-elmahio","text":"Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Jira app and click the Install button: Input your site name, which is the first part of the URL you use to log into Jira. For the URL https://elmahio.atlassian.net/ , the site parameter would be elmahio . In the Project field, input the key of the project. Note that a project has both a display name and a key. The property we are looking for here is the uppercase identifier of the project. To create issues on Jira, you will need to input the username and password of a user with permission to create issues in the project specified above. You can use your user credentials, but we recommend using a combination of your username and an API token. To generate a new token specific for elmah.io, go to the API Tokens page on your Jira account. Then click the Create API token button and input a label of your choice. Finally, click the Create button and an API token is generated for you. Make sure to copy this token, since you won't be able to access it once the dialog is closed. Go back to elmah.io and input your email in the Username field and the API token from the previous step in the Password field. If you don't like to use an existing user account for the integration, you can create a new Atlassian account for elmah.io and generate the API token from that account instead. Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured Jira project.","title":"Install Jira App for elmah.io"},{"location":"elmah-io-apps-jira/#troubleshooting","text":"If errors aren't showing up in Jira, please check that the following are all true: When clicking the Test button on the Jira app settings screen, the button turns green. There's a message logged in the log where you set up the Jira integration. The message is marked as new (yellow star next to the title on the search result). The message is either of severity Error or Fatal . To trigger an error manually, go to https://api.elmah.io/swagger/index.html and input an elmah.io API key with the Messages - Write permission enabled. Expand Messages and the POST node with the URL /v3/messages/{logId} . Input your log ID and the following JSON: { \"title\": \"This is a test\", \"severity\": \"Error\" } Finally, click the Try it out! button and verify that the API returns a status code of 201 . The new error should show up in Jira. If testing this multiple times, you will need to make small adjustments to the title field inside the JSON, for additional errors to be marked as new.","title":"Troubleshooting"},{"location":"elmah-io-apps-mailman/","text":"Install Mailman App for elmah.io The Mailman app is deprecated. Use an email rule available on the Rules tab for a more advanced email integration. The Mailman app for elmah.io sends out an email to an address of your choice, every time a new error is logged. To install Mailman, click the Install button on the Apps tab. This will show the Mailman settings page: Input a valid email address in the Email input box and click Save . The Mailman app will look at new errors only. Errors are defined by messages with a severity of Error or Fatal and with isNew == true . isNew is a field automatically added by elmah.io when indexing each message. isNew is calculated by looking for similarities between the new message and already logged messages.","title":"Mailman (deprecated)"},{"location":"elmah-io-apps-mailman/#install-mailman-app-for-elmahio","text":"The Mailman app is deprecated. Use an email rule available on the Rules tab for a more advanced email integration. The Mailman app for elmah.io sends out an email to an address of your choice, every time a new error is logged. To install Mailman, click the Install button on the Apps tab. This will show the Mailman settings page: Input a valid email address in the Email input box and click Save . The Mailman app will look at new errors only. Errors are defined by messages with a severity of Error or Fatal and with isNew == true . isNew is a field automatically added by elmah.io when indexing each message. isNew is calculated by looking for similarities between the new message and already logged messages.","title":"Install Mailman App for elmah.io"},{"location":"elmah-io-apps-pagerduty/","text":"Install PagerDuty for elmah.io Using the PagerDuty integration for elmah.io, you can set up advanced notification rules in PagerDuty when new errors are logged on elmah.io. Receive a phone call, text message, or one of the other options provided by PagerDuty, the second new errors are introduced on your websites or services. To integrate elmah.io with PagerDuty, you need to set up a new integration on PagerDuty and install the PagerDuty app on elmah.io. Setting up an integration on PagerDuty Sign in to PagerDuty. Navigate to the Services page. Select the service that you want to integrate to from elmah.io in the list of services. On the Integrations tab click the Add an integration button. On the Add Integrations page search for elmah.io and select it in the search result: Click the Add button. Expand the newly created integration: Copy the value in the Integration Key field. Install the PagerDuty app on elmah.io Next, the PagerDuty app needs to be installed on elmah.io. Sign in to elmah.io. Navigate to the Log Settings page of the log you want to integrate with PagerDuty. Go to the Apps tab. Locate the PagerDuty app and click the Install button. Input the Integration Key that you copied in a previous step in the INTEGRATION KEY field: Click the Save button. That's it. New errors stored in the selected log now trigger incidents in PagerDuty. To get help with this integration, make sure to reach out through the support widget on the elmah.io website.","title":"PagerDuty"},{"location":"elmah-io-apps-pagerduty/#install-pagerduty-for-elmahio","text":"Using the PagerDuty integration for elmah.io, you can set up advanced notification rules in PagerDuty when new errors are logged on elmah.io. Receive a phone call, text message, or one of the other options provided by PagerDuty, the second new errors are introduced on your websites or services. To integrate elmah.io with PagerDuty, you need to set up a new integration on PagerDuty and install the PagerDuty app on elmah.io.","title":"Install PagerDuty for elmah.io"},{"location":"elmah-io-apps-pagerduty/#setting-up-an-integration-on-pagerduty","text":"Sign in to PagerDuty. Navigate to the Services page. Select the service that you want to integrate to from elmah.io in the list of services. On the Integrations tab click the Add an integration button. On the Add Integrations page search for elmah.io and select it in the search result: Click the Add button. Expand the newly created integration: Copy the value in the Integration Key field.","title":"Setting up an integration on PagerDuty"},{"location":"elmah-io-apps-pagerduty/#install-the-pagerduty-app-on-elmahio","text":"Next, the PagerDuty app needs to be installed on elmah.io. Sign in to elmah.io. Navigate to the Log Settings page of the log you want to integrate with PagerDuty. Go to the Apps tab. Locate the PagerDuty app and click the Install button. Input the Integration Key that you copied in a previous step in the INTEGRATION KEY field: Click the Save button. That's it. New errors stored in the selected log now trigger incidents in PagerDuty. To get help with this integration, make sure to reach out through the support widget on the elmah.io website.","title":"Install the PagerDuty app on elmah.io"},{"location":"elmah-io-apps-request-a-new-integration/","text":"Request a new integration between elmah.io and another tool We are always on the lookout for creating new and useful integrations. We don't want to integrate with everything (that's what Zapier is for), but commonly used tools by .NET web developers are on our radar. To suggest a new integration feel free to reach out through the support widget in the lower right corner. Not all integration requests are implemented. Don't feel bad if we decide not implement your suggestion. When recommending a new integration we need the following information: Name of the tool. URL of the tool. Why do you need this integration? What do you expect from this integration? Does the tool provide an API? Anything else you think would help? Do you by any chance know anyone working on the tool?","title":"Request a new integration"},{"location":"elmah-io-apps-request-a-new-integration/#request-a-new-integration-between-elmahio-and-another-tool","text":"We are always on the lookout for creating new and useful integrations. We don't want to integrate with everything (that's what Zapier is for), but commonly used tools by .NET web developers are on our radar. To suggest a new integration feel free to reach out through the support widget in the lower right corner. Not all integration requests are implemented. Don't feel bad if we decide not implement your suggestion. When recommending a new integration we need the following information: Name of the tool. URL of the tool. Why do you need this integration? What do you expect from this integration? Does the tool provide an API? Anything else you think would help? Do you by any chance know anyone working on the tool?","title":"Request a new integration between elmah.io and another tool"},{"location":"elmah-io-apps-slack/","text":"Install Slack App for elmah.io Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Slack app and click the Install button. You will be redirected to Slack where you need to log into your workspace if not already. Once logged in, select the channel to send messages to: Click the Allow button and you will be redirected back to elmah.io. The integration to Slack is now installed. Slack doesn't allow more than a single request per second. If you generate more than one message to elmah.io per second, not all of them will show up in Slack because of this. Troubleshooting Errors don't show up in Slack. Here are a few things to try out. Make sure that the Slack app is installed on the log as described above. Only new errors are sent to Slack. A new error has a severity of Error or Fatal and is marked with a yellow star on the search tab. We only send new errors to help you stay out of Slack's API limits. If sending all errors, you could quickly end up in a scenario where the same error is sent multiple times and more important errors get ignored by Slack. Make sure that your token is still valid. The only way to resolve an issue where the token is no longer valid is to re-install the Slack app.","title":"Slack"},{"location":"elmah-io-apps-slack/#install-slack-app-for-elmahio","text":"Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Slack app and click the Install button. You will be redirected to Slack where you need to log into your workspace if not already. Once logged in, select the channel to send messages to: Click the Allow button and you will be redirected back to elmah.io. The integration to Slack is now installed. Slack doesn't allow more than a single request per second. If you generate more than one message to elmah.io per second, not all of them will show up in Slack because of this.","title":"Install Slack App for elmah.io"},{"location":"elmah-io-apps-slack/#troubleshooting","text":"Errors don't show up in Slack. Here are a few things to try out. Make sure that the Slack app is installed on the log as described above. Only new errors are sent to Slack. A new error has a severity of Error or Fatal and is marked with a yellow star on the search tab. We only send new errors to help you stay out of Slack's API limits. If sending all errors, you could quickly end up in a scenario where the same error is sent multiple times and more important errors get ignored by Slack. Make sure that your token is still valid. The only way to resolve an issue where the token is no longer valid is to re-install the Slack app.","title":"Troubleshooting"},{"location":"elmah-io-apps-teams/","text":"Install Microsoft Teams App for elmah.io To install the integration with Microsoft Teams, go to teams and click the Apps menu item. Search for \"elmah.io\" and click the app: Click the Add to a team button. In the dropdown, search for your team and/or channel: Click the Set up a connector button. A new webhook URL is generated. Click the Copy Text button followed by the Save button: The elmah.io integration is now configured on Microsoft Teams and you should see the following screen: The final step is to input the webhook URL that you just copied, into elmah.io. Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Microsoft Teams app and click the Install button. In the overlay, paste the URL from the previous step: Click Save and the app is added to your log. When new errors are logged, messages start appearing in the channel that you configured. The Office 365 API used behind the scenes for this app uses throttling rather than a maximum of allowed requests. This means that you may start experiencing messages not being sent, if you start logging a large amount of messages. We have experienced a lot of weird error codes when communicating with the API. An example of this is an exception while posting data to the API, but the data is successfully shown on Teams. The result of this error is, that elmah.io retries the failing request multiple times, which causes the same message to be shown multiple times on Teams. Troubleshooting Errors don't show up in Teams. Here are a few things to try out. Make sure that the Teams app is installed on the log as described above. Only new errors are sent to Teams. A new error has a severity of Error or Fatal and is marked with a yellow star on the search tab. We only send new errors to help you stay out of Teams' API limits. If sending all errors, you could quickly end up in a scenario where the same error is sent multiple times and more important errors get ignored by Teams. Re-install the app on elmah.io with the webhook URL provided by Teams. Remove the elmah.io configuration from Teams and re-install it. After re-installing the app, you will need to copy the new webhook URL provided by Teams and input it in the elmah.io Teams app as descrived above. Go to the Apps page on Teams and search for 'elmah.io'. Remove the app entirely, click F5 to refresh the page, and install the app again. You may be stuck on an older version of our app, which can be fixed by simply removing and installing the app again.","title":"Microsoft Teams"},{"location":"elmah-io-apps-teams/#install-microsoft-teams-app-for-elmahio","text":"To install the integration with Microsoft Teams, go to teams and click the Apps menu item. Search for \"elmah.io\" and click the app: Click the Add to a team button. In the dropdown, search for your team and/or channel: Click the Set up a connector button. A new webhook URL is generated. Click the Copy Text button followed by the Save button: The elmah.io integration is now configured on Microsoft Teams and you should see the following screen: The final step is to input the webhook URL that you just copied, into elmah.io. Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Microsoft Teams app and click the Install button. In the overlay, paste the URL from the previous step: Click Save and the app is added to your log. When new errors are logged, messages start appearing in the channel that you configured. The Office 365 API used behind the scenes for this app uses throttling rather than a maximum of allowed requests. This means that you may start experiencing messages not being sent, if you start logging a large amount of messages. We have experienced a lot of weird error codes when communicating with the API. An example of this is an exception while posting data to the API, but the data is successfully shown on Teams. The result of this error is, that elmah.io retries the failing request multiple times, which causes the same message to be shown multiple times on Teams.","title":"Install Microsoft Teams App for elmah.io"},{"location":"elmah-io-apps-teams/#troubleshooting","text":"Errors don't show up in Teams. Here are a few things to try out. Make sure that the Teams app is installed on the log as described above. Only new errors are sent to Teams. A new error has a severity of Error or Fatal and is marked with a yellow star on the search tab. We only send new errors to help you stay out of Teams' API limits. If sending all errors, you could quickly end up in a scenario where the same error is sent multiple times and more important errors get ignored by Teams. Re-install the app on elmah.io with the webhook URL provided by Teams. Remove the elmah.io configuration from Teams and re-install it. After re-installing the app, you will need to copy the new webhook URL provided by Teams and input it in the elmah.io Teams app as descrived above. Go to the Apps page on Teams and search for 'elmah.io'. Remove the app entirely, click F5 to refresh the page, and install the app again. You may be stuck on an older version of our app, which can be fixed by simply removing and installing the app again.","title":"Troubleshooting"},{"location":"elmah-io-apps-trello/","text":"Install Trello App for elmah.io For elmah.io to communicate with the Trello API, we will need an API key and token. The API key is available here: https://trello.com/app-key . If you don't have a personal token available on that site, create a new Power-Up as described on the page. When the Power-Up is created, you can create a new API key on that page. To get the token, visit the following URL in your browser: https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&name=Server%20Token&key=API_KEY . Remember to replace API_KEY with your Trello API key located in the previous step. When clicking the Allow button, Trello will generate a new token for you and show it in the browser window. elmah.io will create cards on a board list of your choice. Unfortunately, Trello didn't provide a way to obtain list IDs. The easiest way is to open Developer Tools in your browser and click an existing card inside the list you want elmah.io to create new cards in. Locate the request for the card details in the Network tab and click the Preview tab. The list id is in the card details: Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Trello app and click the Install button: Input the API key, token, and list ID, all located in the previous steps. Click the Test button to test that everything works and finally, click Save . New errors now trigger elmah.io to create a card with the details of the error in Trello. Troubleshooting If errors aren't showing up in Trello, please check that the following are all true: When clicking the Test button on the Trello app settings screen, the button turns green. There's a message logged in the log where you set up the Trello integration. The message is marked as new (yellow star next to the title on the search result). The message is either of severity Error or Fatal . To trigger an error manually, go to https://api.elmah.io/swagger/index.html and input an elmah.io API key with the Messages - Write permission enabled. Expand Messages and the POST node with the URL /v3/messages/{logId} . Input your log ID and the following JSON: { \"title\": \"This is a test\", \"severity\": \"Error\" } Finally, click the Try it out! button and verify that the API returns a status code of 201 . The new error should show up in Trello. If testing this multiple times, you will need to make small adjustments to the title field inside the JSON, for additional errors to be marked as new.","title":"Trello"},{"location":"elmah-io-apps-trello/#install-trello-app-for-elmahio","text":"For elmah.io to communicate with the Trello API, we will need an API key and token. The API key is available here: https://trello.com/app-key . If you don't have a personal token available on that site, create a new Power-Up as described on the page. When the Power-Up is created, you can create a new API key on that page. To get the token, visit the following URL in your browser: https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&name=Server%20Token&key=API_KEY . Remember to replace API_KEY with your Trello API key located in the previous step. When clicking the Allow button, Trello will generate a new token for you and show it in the browser window. elmah.io will create cards on a board list of your choice. Unfortunately, Trello didn't provide a way to obtain list IDs. The easiest way is to open Developer Tools in your browser and click an existing card inside the list you want elmah.io to create new cards in. Locate the request for the card details in the Network tab and click the Preview tab. The list id is in the card details: Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Trello app and click the Install button: Input the API key, token, and list ID, all located in the previous steps. Click the Test button to test that everything works and finally, click Save . New errors now trigger elmah.io to create a card with the details of the error in Trello.","title":"Install Trello App for elmah.io"},{"location":"elmah-io-apps-trello/#troubleshooting","text":"If errors aren't showing up in Trello, please check that the following are all true: When clicking the Test button on the Trello app settings screen, the button turns green. There's a message logged in the log where you set up the Trello integration. The message is marked as new (yellow star next to the title on the search result). The message is either of severity Error or Fatal . To trigger an error manually, go to https://api.elmah.io/swagger/index.html and input an elmah.io API key with the Messages - Write permission enabled. Expand Messages and the POST node with the URL /v3/messages/{logId} . Input your log ID and the following JSON: { \"title\": \"This is a test\", \"severity\": \"Error\" } Finally, click the Try it out! button and verify that the API returns a status code of 201 . The new error should show up in Trello. If testing this multiple times, you will need to make small adjustments to the title field inside the JSON, for additional errors to be marked as new.","title":"Troubleshooting"},{"location":"elmah-io-apps-twilio/","text":"Install Twilio for elmah.io To send SMS/Text messages with Twilio, you will need to sign up for Twilio first. Twilio provides a range of good tutorials when signing up, why we don't want to duplicate them here. When signed up, you will have access to a Twilio phone number to send messages from, an Account SID and a token needed to authenticate Twilio. These pieces of information will be used below when installing the Twilio app on elmah.io. Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Twilio app and click the Install button: Input your Twilio phone number (available on https://www.twilio.com/console/phone-numbers/incoming) in the From field. Input the phone number you want receiving error reports from elmah.io in the To field. Remember to fully qualify the number with a plus and the language code (US example: +12025550170 - UK example: +441632960775). Copy your Account SID and Auth Token from the Twilio Dashboard and input them in the fields on elmah.io. Click Save and the app is added to your log. When new errors are logged, an SMS/Text message is automatically sent to the configured phone number. Troubleshooting If errors aren't being sent to your phone, verify that the configured variables work. To do so, replace the four variables in the top of this PowerShell script and execute it: $sid = \"INSERT_SID\" $token = \"INSERT_TOKEN\" $from = \"INSERT_FROM\" $to = \"INSERT_TO\" $url = \"https://api.twilio.com/2010-04-01/Accounts/$sid/Messages.json\" $pair = \"$($sid):$($token)\" $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair)) $basicAuthValue = \"Basic $encodedCreds\" $Headers = @{ Authorization = $basicAuthValue ContentType = \"application/x-www-form-urlencoded\" } $from = $from.Replace(\"+\", \"%2B\") $to = $to.Replace(\"+\", \"%2B\") $response = Invoke-WebRequest -Uri $url -Method POST -Headers $Headers -Body \"Body=Affirmative&From=$from&To=$to\" You should see a text message on your phone. The script will output any errors from Twilio if something isn't working.","title":"Twilio"},{"location":"elmah-io-apps-twilio/#install-twilio-for-elmahio","text":"To send SMS/Text messages with Twilio, you will need to sign up for Twilio first. Twilio provides a range of good tutorials when signing up, why we don't want to duplicate them here. When signed up, you will have access to a Twilio phone number to send messages from, an Account SID and a token needed to authenticate Twilio. These pieces of information will be used below when installing the Twilio app on elmah.io. Log into elmah.io and go to the log settings. Click the Apps tab. Locate the Twilio app and click the Install button: Input your Twilio phone number (available on https://www.twilio.com/console/phone-numbers/incoming) in the From field. Input the phone number you want receiving error reports from elmah.io in the To field. Remember to fully qualify the number with a plus and the language code (US example: +12025550170 - UK example: +441632960775). Copy your Account SID and Auth Token from the Twilio Dashboard and input them in the fields on elmah.io. Click Save and the app is added to your log. When new errors are logged, an SMS/Text message is automatically sent to the configured phone number.","title":"Install Twilio for elmah.io"},{"location":"elmah-io-apps-twilio/#troubleshooting","text":"If errors aren't being sent to your phone, verify that the configured variables work. To do so, replace the four variables in the top of this PowerShell script and execute it: $sid = \"INSERT_SID\" $token = \"INSERT_TOKEN\" $from = \"INSERT_FROM\" $to = \"INSERT_TO\" $url = \"https://api.twilio.com/2010-04-01/Accounts/$sid/Messages.json\" $pair = \"$($sid):$($token)\" $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair)) $basicAuthValue = \"Basic $encodedCreds\" $Headers = @{ Authorization = $basicAuthValue ContentType = \"application/x-www-form-urlencoded\" } $from = $from.Replace(\"+\", \"%2B\") $to = $to.Replace(\"+\", \"%2B\") $response = Invoke-WebRequest -Uri $url -Method POST -Headers $Headers -Body \"Body=Affirmative&From=$from&To=$to\" You should see a text message on your phone. The script will output any errors from Twilio if something isn't working.","title":"Troubleshooting"},{"location":"elmah-io-apps-youtrack/","text":"Install YouTrack App for elmah.io Get your token To allow elmah.io to create issues on YouTrack, you will need a permanent token. Go to your YouTrack profile, click the Account Security . Here you can generate a new token: Copy the generated token. Install the YouTrack App on elmah.io Log into elmah.io and go to the log settings. Click the Apps tab. Locate the YouTrack app and click the Install button. Input your token and the base URL of your YouTrack Cloud installation. Next, click the Login button to fetch the list of projects from YouTrack: Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured YouTrack project.","title":"YouTrack"},{"location":"elmah-io-apps-youtrack/#install-youtrack-app-for-elmahio","text":"","title":"Install YouTrack App for elmah.io"},{"location":"elmah-io-apps-youtrack/#get-your-token","text":"To allow elmah.io to create issues on YouTrack, you will need a permanent token. Go to your YouTrack profile, click the Account Security . Here you can generate a new token: Copy the generated token.","title":"Get your token"},{"location":"elmah-io-apps-youtrack/#install-the-youtrack-app-on-elmahio","text":"Log into elmah.io and go to the log settings. Click the Apps tab. Locate the YouTrack app and click the Install button. Input your token and the base URL of your YouTrack Cloud installation. Next, click the Login button to fetch the list of projects from YouTrack: Click Save and the app is added to your log. When new errors are logged, issues are automatically created in the configured YouTrack project.","title":"Install the YouTrack App on elmah.io"},{"location":"email-troubleshooting/","text":"Email troubleshooting Email troubleshooting Emails on new errors only Email bounced Invalid email Check your promotional and/or spam folder So, you aren't receiving emails from elmah.io? Here is a collection of things to know/try out. Emails on new errors only The most common reason for not receiving emails when errors are logged is that elmah.io only sends the New Error email when an error that we haven't seen before is logged. New errors are marked with a yellow star next to the log message in the UI and can be searched through either search filters or full-text search: isNew:true The new detection algorithm is implemented by looking at a range of fields like the title, type, and severity. Only severities Error and Fatal marked as isNew trigger an email. Email bounced We use AWS to send out all transactional emails from elmah.io. We get a notification from AWS when an email bounces and we stop sending to that email address, even if any new emails wouldn't cause a bounce. Beneath your profile, you will be able to see if your email caused a bounce: As the error message says, get in contact for us to try and reach the email address again. Invalid email Ok, this may seem obvious. But this happens more often than you would think. Typos are a common cause of invalid emails. Specifying a mailing list or group address doesn't always play nice with elmah.io either. For instance, Office 365 distribution groups block external emails as the default. The easiest way to check your inputted email address is to send a new message to that address from an external email provider. Check your promotional and/or spam folder We do a lot to keep our email reputation high. But some email clients may treat similar-looking emails as promotional or spam. Remember to check those folders and mark messages as important if spotting them in the wrong folder.","title":"Email troubleshooting"},{"location":"email-troubleshooting/#email-troubleshooting","text":"Email troubleshooting Emails on new errors only Email bounced Invalid email Check your promotional and/or spam folder So, you aren't receiving emails from elmah.io? Here is a collection of things to know/try out.","title":"Email troubleshooting"},{"location":"email-troubleshooting/#emails-on-new-errors-only","text":"The most common reason for not receiving emails when errors are logged is that elmah.io only sends the New Error email when an error that we haven't seen before is logged. New errors are marked with a yellow star next to the log message in the UI and can be searched through either search filters or full-text search: isNew:true The new detection algorithm is implemented by looking at a range of fields like the title, type, and severity. Only severities Error and Fatal marked as isNew trigger an email.","title":"Emails on new errors only"},{"location":"email-troubleshooting/#email-bounced","text":"We use AWS to send out all transactional emails from elmah.io. We get a notification from AWS when an email bounces and we stop sending to that email address, even if any new emails wouldn't cause a bounce. Beneath your profile, you will be able to see if your email caused a bounce: As the error message says, get in contact for us to try and reach the email address again.","title":"Email bounced"},{"location":"email-troubleshooting/#invalid-email","text":"Ok, this may seem obvious. But this happens more often than you would think. Typos are a common cause of invalid emails. Specifying a mailing list or group address doesn't always play nice with elmah.io either. For instance, Office 365 distribution groups block external emails as the default. The easiest way to check your inputted email address is to send a new message to that address from an external email provider.","title":"Invalid email"},{"location":"email-troubleshooting/#check-your-promotional-andor-spam-folder","text":"We do a lot to keep our email reputation high. But some email clients may treat similar-looking emails as promotional or spam. Remember to check those folders and mark messages as important if spotting them in the wrong folder.","title":"Check your promotional and/or spam folder"},{"location":"handle-elmah-io-downtime/","text":"Handle elmah.io downtime Like every other SaaS product out there, we cannot promise you 100% uptime on elmah.io. We understand, that your logging data is extremely important for your business and we do everything in our power to secure that elmah.io is running smoothly. To monitor our APIs and websites, check out status.elmah.io . It is our general recommendation to implement code that listens for communication errors with the elmah.io API and log errors elsewhere. How you do this depends on which elmah.io NuGet package you have installed. The documentation for each package will show how to subscribe to errors. For Elmah.Io.Client it would look similar to this: var elmahIo = ElmahioAPI.Create(\"API_KEY\"); elmahIo.Messages.OnMessageFail += (sender, args) => { var message = args.Message; var exception = args.Error; // TODO: log it }; For a logging framework like Serilog, it would look similar to this: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnError = (msg, ex) => { // TODO: log it } }) .CreateLogger(); It is important not to log errors in OnMessageFail and OnError callbacks to elmah.io, since that could cause an infinite loop. Check out the documentation for the package you are using for additional details. Response explanation Here's an overview of the types of errors you can experience from the API: Response Meaning Timeout Something is very wrong with our API or Azure. You can be sure that we are working 24/7 to fix it. 500 The API is reachable, but we have a problem communicating with Azure Service bus. Azure has great uptime and all of our resources are dedicated and replicated. Still, we experience short periods of downtime from time to time. 429 We allow a maximum (per API key) of 500 requests per minute and 3600 per hour. 429 means that you have crossed that line. This status code doesn't indicate that the API is down. 4xx Something is wrong with the request. Check out the API documentation for details. This status code doesn't indicate that the API is down.","title":"Handle elmah.io downtime"},{"location":"handle-elmah-io-downtime/#handle-elmahio-downtime","text":"Like every other SaaS product out there, we cannot promise you 100% uptime on elmah.io. We understand, that your logging data is extremely important for your business and we do everything in our power to secure that elmah.io is running smoothly. To monitor our APIs and websites, check out status.elmah.io . It is our general recommendation to implement code that listens for communication errors with the elmah.io API and log errors elsewhere. How you do this depends on which elmah.io NuGet package you have installed. The documentation for each package will show how to subscribe to errors. For Elmah.Io.Client it would look similar to this: var elmahIo = ElmahioAPI.Create(\"API_KEY\"); elmahIo.Messages.OnMessageFail += (sender, args) => { var message = args.Message; var exception = args.Error; // TODO: log it }; For a logging framework like Serilog, it would look similar to this: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnError = (msg, ex) => { // TODO: log it } }) .CreateLogger(); It is important not to log errors in OnMessageFail and OnError callbacks to elmah.io, since that could cause an infinite loop. Check out the documentation for the package you are using for additional details.","title":"Handle elmah.io downtime"},{"location":"handle-elmah-io-downtime/#response-explanation","text":"Here's an overview of the types of errors you can experience from the API: Response Meaning Timeout Something is very wrong with our API or Azure. You can be sure that we are working 24/7 to fix it. 500 The API is reachable, but we have a problem communicating with Azure Service bus. Azure has great uptime and all of our resources are dedicated and replicated. Still, we experience short periods of downtime from time to time. 429 We allow a maximum (per API key) of 500 requests per minute and 3600 per hour. 429 means that you have crossed that line. This status code doesn't indicate that the API is down. 4xx Something is wrong with the request. Check out the API documentation for details. This status code doesn't indicate that the API is down.","title":"Response explanation"},{"location":"heartbeats-troubleshooting/","text":"Heartbeats Troubleshooting Common problems and how to fix them Here you will a list of common problems and how to solve them. Timeout when creating heartbeats through Elmah.Io.Client If you experience a timeout when calling the Healthy , Degraded , or Unhealthy method, you may want to adjust the default HTTP timeout. Elmah.Io.Client has a default timeout of 5 seconds to make sure that logging to elmah.io from a web application won't slow down the web app too much in case of slow response time from the elmah.io API. While 99.9% of the requests to the elmah.io API finish within this timeout, problems with Azure, the network connection, and a lot of other issues can happen. Since heartbeats typically run outside the scope of a web request, it's safe to increase the default HTTP timeout in this case: var api = ElmahioAPI.Create(\"API_KEY\", new ElmahIoOptions { Timeout = new TimeSpan(0, 0, 30) }); The example set a timeout of 30 seconds. SocketException when creating heartbeats through Elmah.Io.Client A System.Net.Sockets.SocketException when communicating with the elmah.io API can mean multiple things. The API can be down or there's network problems between your machine and the API. Increasing the timeout as shown in the previous section should be step one. If you still experience socket exceptions, it might help to implement retries. This can be done by setting up a custom HttpClient : builder.Services .AddHttpClient(\"elmahio\") .AddPolicyHandler(HttpPolicyExtensions .HandleTransientHttpError() .WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(i))); The AddPolicyHandler is available when installing the Microsoft.Extensions.Http.Polly NuGet package. Next, create the elmah.io client with the custom HttpClient : var httpClient = httpClientFactory.CreateClient(\"elmahio\"); var elmahIoClient = ElmahioAPI.Create(\"API_KEY\", options, httpClient);","title":"Heartbeats Troubleshooting"},{"location":"heartbeats-troubleshooting/#heartbeats-troubleshooting","text":"","title":"Heartbeats Troubleshooting"},{"location":"heartbeats-troubleshooting/#common-problems-and-how-to-fix-them","text":"Here you will a list of common problems and how to solve them.","title":"Common problems and how to fix them"},{"location":"heartbeats-troubleshooting/#timeout-when-creating-heartbeats-through-elmahioclient","text":"If you experience a timeout when calling the Healthy , Degraded , or Unhealthy method, you may want to adjust the default HTTP timeout. Elmah.Io.Client has a default timeout of 5 seconds to make sure that logging to elmah.io from a web application won't slow down the web app too much in case of slow response time from the elmah.io API. While 99.9% of the requests to the elmah.io API finish within this timeout, problems with Azure, the network connection, and a lot of other issues can happen. Since heartbeats typically run outside the scope of a web request, it's safe to increase the default HTTP timeout in this case: var api = ElmahioAPI.Create(\"API_KEY\", new ElmahIoOptions { Timeout = new TimeSpan(0, 0, 30) }); The example set a timeout of 30 seconds.","title":"Timeout when creating heartbeats through Elmah.Io.Client"},{"location":"heartbeats-troubleshooting/#socketexception-when-creating-heartbeats-through-elmahioclient","text":"A System.Net.Sockets.SocketException when communicating with the elmah.io API can mean multiple things. The API can be down or there's network problems between your machine and the API. Increasing the timeout as shown in the previous section should be step one. If you still experience socket exceptions, it might help to implement retries. This can be done by setting up a custom HttpClient : builder.Services .AddHttpClient(\"elmahio\") .AddPolicyHandler(HttpPolicyExtensions .HandleTransientHttpError() .WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(i))); The AddPolicyHandler is available when installing the Microsoft.Extensions.Http.Polly NuGet package. Next, create the elmah.io client with the custom HttpClient : var httpClient = httpClientFactory.CreateClient(\"elmahio\"); var elmahIoClient = ElmahioAPI.Create(\"API_KEY\", options, httpClient);","title":"SocketException when creating heartbeats through Elmah.Io.Client"},{"location":"how-does-the-new-detection-work/","text":"How does the new detection work Being able to identify when a logged error is new or a duplicate of an already logged error, is one of the most important features on elmah.io. A lot of other features are based on this mechanism to help you reduce the number of emails, Slack/Teams messages, and much more. We often get questions about how this works and how to tweak it, why this article should bring some clarity to questions about this feature. When logging messages to elmah.io using either one of the integrations or through the API, we automatically set a flag named isNew on each log message. Calculating the value of this field is based on a rather complex algorithm. The implementation is closed-source but not a huge secret. Each message is assigned a hash value based on a range of fields like the message template, the severity, the URL, and more. Some values are normalized or modified before being sent as input to the hash function. An example of this is removing numbers from the log message which will ensure that Error on product 1 and Error on product 2 will be considered the same log message. When receiving a new log message, we check if an existing message with the same hash is already stored in the log. If not, the new message will be marked as New by setting the isNew flag to true . If we already found one or more log messages with the same hash, the new message will have its isNew flag set to false . Messages and apps Most apps and features around sending messages from elmah.io are based on the isNew flag. This means that only new errors trigger the New Error Email , the Slack and Teams apps, etc. This is done to avoid flooding the recipient system with emails or messages. You typically don't want 1,000 emails if the same error occurs 1,000 times. Error occurrence is still important, why there are other features to help you deal with this like the Error Occurrence Email and spike-based machine learning features. Modifying the hash function We sometimes get requests to modify the hash function, but unfortunately, that's currently not possible. We change the implementation from time to time to improve the uniqueness detection over time. If you have an example of two log messages that should have been considered unique or not considered unique, feel free to reach out. This may or may not result in changes to the hash function. There are still some possibilities to force two log messages to not be unique. The obvious is to include different variables inside the log message. Remember that numbers are removed, why this must consist of letters. Another approach is to put individual values in the source field. This can be done in all integrations by implementing the OnMessage action. Some integrations also support setting the source field by including it as structured properties like Error from {source} . Setting re-occurring messages as New A common request is to get a notification if an error that you believed was fixed re-occur. This scenario is built into elmah.io's issue tracker. When marking a log message as fixed through the UI or API, elmah.io automatically marks all instances of the log message as fixed. If a new log message with the same hash is logged at some point, the isNew flag on this message will be set to true . This will trigger the New Error Email and most of the integrations again. Retention Depending on your current plan, each subscription provides x days of retention for log messages. This means that log messages are automatically deleted after x days in the database. Once all instances of a log message are deleted, a new log message generating the same hash as the deleted messages will be marked as new. To increase the chance of log messages being marked as new, you can lower the retention on each log on the Log Settings page.","title":"How does the new detection work"},{"location":"how-does-the-new-detection-work/#how-does-the-new-detection-work","text":"Being able to identify when a logged error is new or a duplicate of an already logged error, is one of the most important features on elmah.io. A lot of other features are based on this mechanism to help you reduce the number of emails, Slack/Teams messages, and much more. We often get questions about how this works and how to tweak it, why this article should bring some clarity to questions about this feature. When logging messages to elmah.io using either one of the integrations or through the API, we automatically set a flag named isNew on each log message. Calculating the value of this field is based on a rather complex algorithm. The implementation is closed-source but not a huge secret. Each message is assigned a hash value based on a range of fields like the message template, the severity, the URL, and more. Some values are normalized or modified before being sent as input to the hash function. An example of this is removing numbers from the log message which will ensure that Error on product 1 and Error on product 2 will be considered the same log message. When receiving a new log message, we check if an existing message with the same hash is already stored in the log. If not, the new message will be marked as New by setting the isNew flag to true . If we already found one or more log messages with the same hash, the new message will have its isNew flag set to false .","title":"How does the new detection work"},{"location":"how-does-the-new-detection-work/#messages-and-apps","text":"Most apps and features around sending messages from elmah.io are based on the isNew flag. This means that only new errors trigger the New Error Email , the Slack and Teams apps, etc. This is done to avoid flooding the recipient system with emails or messages. You typically don't want 1,000 emails if the same error occurs 1,000 times. Error occurrence is still important, why there are other features to help you deal with this like the Error Occurrence Email and spike-based machine learning features.","title":"Messages and apps"},{"location":"how-does-the-new-detection-work/#modifying-the-hash-function","text":"We sometimes get requests to modify the hash function, but unfortunately, that's currently not possible. We change the implementation from time to time to improve the uniqueness detection over time. If you have an example of two log messages that should have been considered unique or not considered unique, feel free to reach out. This may or may not result in changes to the hash function. There are still some possibilities to force two log messages to not be unique. The obvious is to include different variables inside the log message. Remember that numbers are removed, why this must consist of letters. Another approach is to put individual values in the source field. This can be done in all integrations by implementing the OnMessage action. Some integrations also support setting the source field by including it as structured properties like Error from {source} .","title":"Modifying the hash function"},{"location":"how-does-the-new-detection-work/#setting-re-occurring-messages-as-new","text":"A common request is to get a notification if an error that you believed was fixed re-occur. This scenario is built into elmah.io's issue tracker. When marking a log message as fixed through the UI or API, elmah.io automatically marks all instances of the log message as fixed. If a new log message with the same hash is logged at some point, the isNew flag on this message will be set to true . This will trigger the New Error Email and most of the integrations again.","title":"Setting re-occurring messages as New"},{"location":"how-does-the-new-detection-work/#retention","text":"Depending on your current plan, each subscription provides x days of retention for log messages. This means that log messages are automatically deleted after x days in the database. Once all instances of a log message are deleted, a new log message generating the same hash as the deleted messages will be marked as new. To increase the chance of log messages being marked as new, you can lower the retention on each log on the Log Settings page.","title":"Retention"},{"location":"how-prices-are-calculated/","text":"How prices are calculated When using elmah.io, you may experience the need to switch plans, update your credit card, or do other tasks which will require changes to your subscription. This document explains the different upgrade/downgrade and payment paths. Switch to a higher plan You can switch to a larger plan at all times. If you purchased a Small Business ($29) on June 1 and want to upgrade to Business ($49) on June 15, we charge you ~ $35. You already paid $15 for half of June on the Small Business plan, so the remaining amount is deducted from the $49. Your next payment will be on July 15. Switch to a lower plan You can switch to a lower plan when your current subscription is renewed. If you purchased a Business plan ($49) on June 15. and downgrade to a Small Business plan ($29) on July 1, you would be charged $49 on June 15, and $29 on July 15. You commit to either a month or a year in advance. Update your credit card A new credit card can be inputted at any time during your subscription. The payment provider will automatically charge the new credit card on the next payment. Purchase a top-up If you need additional messages but don't want to upgrade to a larger plan permanently, you can purchase a top-up. The purchase is made on the credit card already configured on the subscription. If you want to buy the top-up on a different credit card, you will need to use the Update credit card feature first. The price of the top-up is always $19, and you can purchase as many top-ups as you want. Switch payment interval Switching from monthly to annual or annual to monthly is possible. If you switch from monthly to annual, you are charged the difference between the two plans as in Switch to a higher plan . If you switch from annual to monthly, one of two scenarios can happen. If you switch to the same plan or lower, your plan will automatically switch when your current subscription renews (like in Switch to a lower plan ). If you switch to a higher plan, your plan will switch immediately as in Switch to a higher plan . If the price of the remaining time on your current plan covers the price of the new plan, the exceeding amount will be credited to your balance and used to pay for the new plan and any upcoming invoices. Paying for top-ups with the balance is currently not supported. Purchase a subscription from Denmark We are based in Denmark, why selling services to another Danish company requires us to include 25% VAT (moms). The price on the various dialogs will automatically show the price including VAT as well as the VAT amount. Your invoices will include the amount in VAT to inform SKAT.","title":"How prices are calculated"},{"location":"how-prices-are-calculated/#how-prices-are-calculated","text":"When using elmah.io, you may experience the need to switch plans, update your credit card, or do other tasks which will require changes to your subscription. This document explains the different upgrade/downgrade and payment paths.","title":"How prices are calculated"},{"location":"how-prices-are-calculated/#switch-to-a-higher-plan","text":"You can switch to a larger plan at all times. If you purchased a Small Business ($29) on June 1 and want to upgrade to Business ($49) on June 15, we charge you ~ $35. You already paid $15 for half of June on the Small Business plan, so the remaining amount is deducted from the $49. Your next payment will be on July 15.","title":"Switch to a higher plan"},{"location":"how-prices-are-calculated/#switch-to-a-lower-plan","text":"You can switch to a lower plan when your current subscription is renewed. If you purchased a Business plan ($49) on June 15. and downgrade to a Small Business plan ($29) on July 1, you would be charged $49 on June 15, and $29 on July 15. You commit to either a month or a year in advance.","title":"Switch to a lower plan"},{"location":"how-prices-are-calculated/#update-your-credit-card","text":"A new credit card can be inputted at any time during your subscription. The payment provider will automatically charge the new credit card on the next payment.","title":"Update your credit card"},{"location":"how-prices-are-calculated/#purchase-a-top-up","text":"If you need additional messages but don't want to upgrade to a larger plan permanently, you can purchase a top-up. The purchase is made on the credit card already configured on the subscription. If you want to buy the top-up on a different credit card, you will need to use the Update credit card feature first. The price of the top-up is always $19, and you can purchase as many top-ups as you want.","title":"Purchase a top-up"},{"location":"how-prices-are-calculated/#switch-payment-interval","text":"Switching from monthly to annual or annual to monthly is possible. If you switch from monthly to annual, you are charged the difference between the two plans as in Switch to a higher plan . If you switch from annual to monthly, one of two scenarios can happen. If you switch to the same plan or lower, your plan will automatically switch when your current subscription renews (like in Switch to a lower plan ). If you switch to a higher plan, your plan will switch immediately as in Switch to a higher plan . If the price of the remaining time on your current plan covers the price of the new plan, the exceeding amount will be credited to your balance and used to pay for the new plan and any upcoming invoices. Paying for top-ups with the balance is currently not supported.","title":"Switch payment interval"},{"location":"how-prices-are-calculated/#purchase-a-subscription-from-denmark","text":"We are based in Denmark, why selling services to another Danish company requires us to include 25% VAT (moms). The price on the various dialogs will automatically show the price including VAT as well as the VAT amount. Your invoices will include the amount in VAT to inform SKAT.","title":"Purchase a subscription from Denmark"},{"location":"how-to-avoid-emails-getting-classified-as-spam/","text":"How to avoid emails getting classified as spam We do everything in our power to maintain a good email domain reputation. Sometimes emails sent from elmah.io may be classified as spam in your email client. The easiest way to avoid this is to inspect the sender in an email received from @elmah.io and add it to your contact list (we primarily send emails from info@elmah.io and noreply@elmah.io ). How you do this depends on your email client but all major clients have this option. In the sections below, there are alternatives to the contact list approach for various email clients. Gmail If you don't want to add elmah.io addresses to your contact list you can use Gmail's Filters feature to always classify *@elmah.io as not spam. To do so, go to Settings | Filters and create a new filter: Click Create filter and check the Never send it to Spam option: Finally, click the Create filter button, and emails from elmah.io will no longer be classified as spam.","title":"How to avoid emails getting classified as spam"},{"location":"how-to-avoid-emails-getting-classified-as-spam/#how-to-avoid-emails-getting-classified-as-spam","text":"We do everything in our power to maintain a good email domain reputation. Sometimes emails sent from elmah.io may be classified as spam in your email client. The easiest way to avoid this is to inspect the sender in an email received from @elmah.io and add it to your contact list (we primarily send emails from info@elmah.io and noreply@elmah.io ). How you do this depends on your email client but all major clients have this option. In the sections below, there are alternatives to the contact list approach for various email clients.","title":"How to avoid emails getting classified as spam"},{"location":"how-to-avoid-emails-getting-classified-as-spam/#gmail","text":"If you don't want to add elmah.io addresses to your contact list you can use Gmail's Filters feature to always classify *@elmah.io as not spam. To do so, go to Settings | Filters and create a new filter: Click Create filter and check the Never send it to Spam option: Finally, click the Create filter button, and emails from elmah.io will no longer be classified as spam.","title":"Gmail"},{"location":"how-to-configure-api-key-permissions/","text":"How to configure API key permissions Security on the elmah.io API is handled through the use of API keys. All requests to the API must be accompanied by an API key. When creating your organization, a default API key was automatically created. API keys can be revoked and you can create multiple API keys for different purposes and projects. Much like a user can be awarded different levels of access on elmah.io, API keys also have a set of permissions. The default created API key for your organization, only have permission to write log messages to elmah.io. To configure permissions for a new or existing API key, either click the Edit or Add API Key button on the API Keys tab of your organization settings. This will show the API key editor view: As mentioned previously, new keys have the messages_write permission enabled only. This permission will cover logging from your application to elmah.io. If your application needs to browse messages from elmah.io, create new logs/applications, etc. you will need to enable the corresponding permission. Notice that read permissions don't need to be enabled, for you to browse logs and log messages on the elmah.io UI. API keys are used by the range of client integrations only. Your API key shouldn't be shared outside your organization. In some situations, you will need to share your API key (like when logging from JavaScript). In these cases, it's essential that your API key only has the messages_write permission enabled. With all permissions enabled, everyone will be able to browse your logs.","title":"How to configure API key permissions"},{"location":"how-to-configure-api-key-permissions/#how-to-configure-api-key-permissions","text":"Security on the elmah.io API is handled through the use of API keys. All requests to the API must be accompanied by an API key. When creating your organization, a default API key was automatically created. API keys can be revoked and you can create multiple API keys for different purposes and projects. Much like a user can be awarded different levels of access on elmah.io, API keys also have a set of permissions. The default created API key for your organization, only have permission to write log messages to elmah.io. To configure permissions for a new or existing API key, either click the Edit or Add API Key button on the API Keys tab of your organization settings. This will show the API key editor view: As mentioned previously, new keys have the messages_write permission enabled only. This permission will cover logging from your application to elmah.io. If your application needs to browse messages from elmah.io, create new logs/applications, etc. you will need to enable the corresponding permission. Notice that read permissions don't need to be enabled, for you to browse logs and log messages on the elmah.io UI. API keys are used by the range of client integrations only. Your API key shouldn't be shared outside your organization. In some situations, you will need to share your API key (like when logging from JavaScript). In these cases, it's essential that your API key only has the messages_write permission enabled. With all permissions enabled, everyone will be able to browse your logs.","title":"How to configure API key permissions"},{"location":"how-to-correlate-messages-across-services/","text":"How to correlate messages across services How to correlate messages across services CorrelationId explained Setting CorrelationId Elmah.Io.Client.Extensions.Correlation ASP.NET Core Microsoft.Extensions.Logging Serilog NLog log4net W3C Trace Context A common architecture is to spread out code across multiple services. Some developers like to split up their code into microservices. Others have requirements for running code asynchronous behind a queue. The common theme here is that code often runs in multiple processes spread across multiple servers. While logging can be easily set up in different projects and services, being able to correlate log messages across multiple services isn't normally available when logging to individual log outputs. Imagine a console application making an HTTP request to your public API. The API calls an internal API, which again puts a message on a queue. A service consumes messages from the queue and ends up logging an error in the error log. Seeing the entire log trace from receiving the request on the API straight down the chain resulting in the error, highly increases the chance of figuring out what went wrong. With the Correlation feature on elmah.io, we want to help you achieve just that. In this article, you will learn how to set up Correlations. Correlation currently requires all of your applications to log to the same log on elmah.io. For you to separate log messages from each service, we recommend setting the Application property on all log messages. Filtering on log messages is available through the elmah.io UI. We may want to explore ways to use Correlation across multiple error logs in the future. CorrelationId explained The way log messages are correlated on elmah.io is based on a property on all log messages named correlationId . The field is available both when creating log messages through our API as well as in the Elmah.Io.Client NuGet package. The property is a string which means that you can use whatever schema you want for the correlation ID. To set the correlation ID using the client, use the following code: var myCorrelationId = \"42\"; var client = ElmahioAPI.Create(\"API_KEY\"); await client.Messages.CreateAndNotifyAsync(new Guid(\"LOG_ID\"), new CreateMessage { Title = \"Hello World\", // ... CorrelationId = myCorrelationId }); In a real-world scenario, myCorrelationId wouldn't be hardcoded but pulled from a shared header, message ID, or similar. As long as all services set CorrelationId to the same ID, log messages within a correlation can be searched on the elmah.io UI: Setting CorrelationId How you set the correlation ID depends on which integration you are using. In some cases, a correlation ID is set automatically while others will require a few lines of code. Elmah.Io.Client.Extensions.Correlation We have developed a NuGet package dedicated to setting the correlation ID from the current activity in an easy way. The package can be used together with all of the various client integrations we offer (like Elmah.Io.AspNetCore and Elmah.Io.NLog ). Start by installing the package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client.Extensions.Correlation dotnet add package Elmah.Io.Client.Extensions.Correlation <PackageReference Include=\"Elmah.Io.Client.Extensions.Correlation\" Version=\"5.*\" /> paket add Elmah.Io.Client.Extensions.Correlation Next, call the WithCorrelationIdFromActivity method as part of the OnMessage action/event. How you do this depends on which of the client integrations you are using. For Elmah.Io.Client it can be done like this: var elmahIo = ElmahioAPI.Create(\"API_KEY\"); elmahIo.Messages.OnMessage += (sender, args) => { args.Message.WithCorrelationIdFromActivity(); }; For Elmah.Io.AspNetCore it can be done like this: builder.Services.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.WithCorrelationIdFromActivity(); }; }); ASP.NET Core When logging uncaught errors using the Elmah.Io.AspNetCore package, we automatically pick up any traceparent header and put the trace ID as part of the error logged to elmah.io. For an overview of wrapping calls to your ASP.NET Core API in an Activity check out the section about W3C Trace Context. If you want to set the correlation ID manually, you can use the OnMessage action: builder.Services.Configure<ElmahIoOptions>(o => { o.OnMessage = msg => { msg.CorrelationId = \"42\"; }; }); When requested through the browser, a traceparent is not automatically added, unless you manually do so by using an extension as shown in the W3C section. In this case, you can either install the Elmah.Io.Client.Extensions.Correlation package as already explained, or set the correlationId manually by installing the System.Diagnostics.DiagnosticSource NuGet package and adding the following code to the OnMessage action: o.OnMessage = msg => { msg.CorrelationId = System.Diagnostics.Activity.Current?.TraceId.ToString(); }; Microsoft.Extensions.Logging To store a correlation ID when logging through Microsoft.Extensions.Logging you can either set the CorrelationId property (manually or using the Elmah.Io.Client.Extensions.Correlation NuGet package) or rely on the automatic behavior built into Microsoft.Extensions.Logging. To manually set the correlation you can include correlationId as part of the log message: logger.LogInformation(\"A log message with {correlationId}\", \"42\"); Or you can put the correlation ID as part of a logging scope: using (logger.BeginScope(new { CorrelationId = \"42\" })) { logger.LogInformation(\"A log message\"); } In some cases, a correlation ID will be set automatically. If there is a current active Activity (see later), Microsoft.Extensions.Logging automatically decorates all log messages with a custom property named TraceId . The elmah.io backend will pick up any value in the TraceId and use that as the correlation ID. Serilog When logging through Serilog a correlation ID can be added to one or more log messages in multiple ways. The most obvious being on the log message itself: Log.Information(\"A log message with {correlationId}\", \"42\"); If you don't want correlation ID as part of the log message, you can push the property using LogContext : using (LogContext.PushProperty(\"correlationId\", \"42\")) { Log.Information(\"A log message\"); } You will need to install the LogContext enricher for this to work: Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() // ... .CreateLogger(); The elmah.io sink for Serilog is an async batching sync. This means that log messages are not logged in the same millisecond as one of the logging methods on the Log class is called and the current activity is no longer set. When logging from a web application or other project types where the activity is short-lived, you either need to include the correlation ID as part of the message (as shown in the previous examples) or you need to store the correlation ID as part of the request. For ASP.NET Core this can be done using middleware: app.Use(async (ctx, next) => { IDisposable disposeMe = null; var activity = Activity.Current; if (activity != null) { disposeMe = LogContext.PushProperty(\"correlationid\", activity.TraceId.ToString()); } try { await next(); } finally { disposeMe?.Dispose(); } }); All calls to Serilog within the web request will have the correlation ID set to the TraceId of the current activity. Other frameworks support similar features for enriching the current request or invocation with custom properties. NLog Correlation ID can be set on log messages logged through NLog in multiple ways. The first approach is to include the ID directly in the log message: logger.Info(\"A log message with {correlationId}\", \"42\"); If you don't want the ID as part of the log message you can use either NLog's two context objects: using (MappedDiagnosticsLogicalContext.SetScoped(\"correlationId\", \"42\")) { logger.Info(\"A log message\"); } GlobalDiagnosticsContext.Set(\"correlationId\", \"42\"); You can also add the property manually to the log message using the log event builder API available in NLog: var infoMessage = new LogEventInfo(LogLevel.Info, \"\", \"A log message\"); infoMessage.Properties.Add(\"correlationid\", \"42\"); logger.Info(infoMessage); log4net Correlation ID can be set on log messages logged through log4net in multiple ways. You can include it directly on the LoggingEvent : var properties = new PropertiesDictionary(); properties[\"correlationid\"] = \"42\"; log.Logger.Log(new LoggingEvent(new LoggingEventData { Level = Level.Info, TimeStampUtc = DateTime.UtcNow, Properties = properties, Message = \"A log message\", })); You most likely use the Info , Warn , and similar helper methods to store log messages. In this case, you can set the correlation ID on the ThreadContext : ThreadContext.Properties[\"correlationid\"] = \"42\"; log.Info(\"A log message\"); Please notice that correlationid in both examples must be in all lowercase. W3C Trace Context The class Activity has been mentioned a couple of times already. Let's take a look at what that is and how it relates to W3C Trace Context. Trace Context is a specification by W3C for implementing distributed tracing across multiple processes which are already widely adopted. If you generate a trace identifier in a client initiating a chain of events, different Microsoft technologies like ASP.NET Core already pick up the extended set of headers and include those as part of log messages logged through Microsoft.Extensions.Logging. Let's say we have a console application calling an API and we want to log messages in both the console app and in the API and correlate them in elmah.io. In both the console app and in the ASP.NET Core application, you would set up Elmah.Io.Extensions.Logging using the default configuration. Then in the console application, you will wrap the call to the API in an Activity : var httpClient = new HttpClient(); var activity = new Activity(\"ApiCall\").Start(); try { using (logger.BeginScope(new { TraceId = activity.TraceId, ParentId = activity.ParentSpanId, SpanId = activity.SpanId })) { logger.LogInformation(\"Fetching data from the API\"); var result = await httpClient.GetStreamAsync(\"https://localhost:44396/ping\"); // ... } } finally { activity.Stop(); } By creating and starting a new Activity before we call the API, log messages in the console app can be decorated with three additional properties: TraceId , ParentId , and SpanId . This will make sure that all log messages logged within this scope will get the correlation ID set on elmah.io. HttpClient automatically picks up the activity and decorates the request with additional headers. On the API we simply log as normal: [ApiController] [Route(\"[controller]\")] public class PingController : ControllerBase { private readonly ILogger<PingController> _logger; public PingController(ILogger<PingController> logger) { _logger = logger; } [HttpGet] public string Get() { _logger.LogInformation(\"Received a ping\"); return \"pong\"; } } Notice that we didn't have to decorate log messages with additional properties. ASP.NET Core automatically picks up the new headers and decorates all log messages with the correct trace ID. If you want to test this through a browser, you'll need to modify the request headers before the request is made to your web application. There is a range of extensions available to help with this. For the following example, we'll use ModHeader : The extension will enrich all requests with a header named traceparent in the format VERSION-TRACE_ID-SPAN_ID-TRACE_FLAGS . elmah.io will automatically pick up this header and set correlationId to the value of TRACE_ID . If you don't get a correlation ID set on your log messages, we recommend installing the Elmah.Io.Client.Extensions.Correlation NuGet package and calling the following method in the OnMessage event/action: msg.WithCorrelationIdFromActivity();","title":"How to correlate messages across services"},{"location":"how-to-correlate-messages-across-services/#how-to-correlate-messages-across-services","text":"How to correlate messages across services CorrelationId explained Setting CorrelationId Elmah.Io.Client.Extensions.Correlation ASP.NET Core Microsoft.Extensions.Logging Serilog NLog log4net W3C Trace Context A common architecture is to spread out code across multiple services. Some developers like to split up their code into microservices. Others have requirements for running code asynchronous behind a queue. The common theme here is that code often runs in multiple processes spread across multiple servers. While logging can be easily set up in different projects and services, being able to correlate log messages across multiple services isn't normally available when logging to individual log outputs. Imagine a console application making an HTTP request to your public API. The API calls an internal API, which again puts a message on a queue. A service consumes messages from the queue and ends up logging an error in the error log. Seeing the entire log trace from receiving the request on the API straight down the chain resulting in the error, highly increases the chance of figuring out what went wrong. With the Correlation feature on elmah.io, we want to help you achieve just that. In this article, you will learn how to set up Correlations. Correlation currently requires all of your applications to log to the same log on elmah.io. For you to separate log messages from each service, we recommend setting the Application property on all log messages. Filtering on log messages is available through the elmah.io UI. We may want to explore ways to use Correlation across multiple error logs in the future.","title":"How to correlate messages across services"},{"location":"how-to-correlate-messages-across-services/#correlationid-explained","text":"The way log messages are correlated on elmah.io is based on a property on all log messages named correlationId . The field is available both when creating log messages through our API as well as in the Elmah.Io.Client NuGet package. The property is a string which means that you can use whatever schema you want for the correlation ID. To set the correlation ID using the client, use the following code: var myCorrelationId = \"42\"; var client = ElmahioAPI.Create(\"API_KEY\"); await client.Messages.CreateAndNotifyAsync(new Guid(\"LOG_ID\"), new CreateMessage { Title = \"Hello World\", // ... CorrelationId = myCorrelationId }); In a real-world scenario, myCorrelationId wouldn't be hardcoded but pulled from a shared header, message ID, or similar. As long as all services set CorrelationId to the same ID, log messages within a correlation can be searched on the elmah.io UI:","title":"CorrelationId explained"},{"location":"how-to-correlate-messages-across-services/#setting-correlationid","text":"How you set the correlation ID depends on which integration you are using. In some cases, a correlation ID is set automatically while others will require a few lines of code.","title":"Setting CorrelationId"},{"location":"how-to-correlate-messages-across-services/#elmahioclientextensionscorrelation","text":"We have developed a NuGet package dedicated to setting the correlation ID from the current activity in an easy way. The package can be used together with all of the various client integrations we offer (like Elmah.Io.AspNetCore and Elmah.Io.NLog ). Start by installing the package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client.Extensions.Correlation dotnet add package Elmah.Io.Client.Extensions.Correlation <PackageReference Include=\"Elmah.Io.Client.Extensions.Correlation\" Version=\"5.*\" /> paket add Elmah.Io.Client.Extensions.Correlation Next, call the WithCorrelationIdFromActivity method as part of the OnMessage action/event. How you do this depends on which of the client integrations you are using. For Elmah.Io.Client it can be done like this: var elmahIo = ElmahioAPI.Create(\"API_KEY\"); elmahIo.Messages.OnMessage += (sender, args) => { args.Message.WithCorrelationIdFromActivity(); }; For Elmah.Io.AspNetCore it can be done like this: builder.Services.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.WithCorrelationIdFromActivity(); }; });","title":"Elmah.Io.Client.Extensions.Correlation"},{"location":"how-to-correlate-messages-across-services/#aspnet-core","text":"When logging uncaught errors using the Elmah.Io.AspNetCore package, we automatically pick up any traceparent header and put the trace ID as part of the error logged to elmah.io. For an overview of wrapping calls to your ASP.NET Core API in an Activity check out the section about W3C Trace Context. If you want to set the correlation ID manually, you can use the OnMessage action: builder.Services.Configure<ElmahIoOptions>(o => { o.OnMessage = msg => { msg.CorrelationId = \"42\"; }; }); When requested through the browser, a traceparent is not automatically added, unless you manually do so by using an extension as shown in the W3C section. In this case, you can either install the Elmah.Io.Client.Extensions.Correlation package as already explained, or set the correlationId manually by installing the System.Diagnostics.DiagnosticSource NuGet package and adding the following code to the OnMessage action: o.OnMessage = msg => { msg.CorrelationId = System.Diagnostics.Activity.Current?.TraceId.ToString(); };","title":"ASP.NET Core"},{"location":"how-to-correlate-messages-across-services/#microsoftextensionslogging","text":"To store a correlation ID when logging through Microsoft.Extensions.Logging you can either set the CorrelationId property (manually or using the Elmah.Io.Client.Extensions.Correlation NuGet package) or rely on the automatic behavior built into Microsoft.Extensions.Logging. To manually set the correlation you can include correlationId as part of the log message: logger.LogInformation(\"A log message with {correlationId}\", \"42\"); Or you can put the correlation ID as part of a logging scope: using (logger.BeginScope(new { CorrelationId = \"42\" })) { logger.LogInformation(\"A log message\"); } In some cases, a correlation ID will be set automatically. If there is a current active Activity (see later), Microsoft.Extensions.Logging automatically decorates all log messages with a custom property named TraceId . The elmah.io backend will pick up any value in the TraceId and use that as the correlation ID.","title":"Microsoft.Extensions.Logging"},{"location":"how-to-correlate-messages-across-services/#serilog","text":"When logging through Serilog a correlation ID can be added to one or more log messages in multiple ways. The most obvious being on the log message itself: Log.Information(\"A log message with {correlationId}\", \"42\"); If you don't want correlation ID as part of the log message, you can push the property using LogContext : using (LogContext.PushProperty(\"correlationId\", \"42\")) { Log.Information(\"A log message\"); } You will need to install the LogContext enricher for this to work: Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() // ... .CreateLogger(); The elmah.io sink for Serilog is an async batching sync. This means that log messages are not logged in the same millisecond as one of the logging methods on the Log class is called and the current activity is no longer set. When logging from a web application or other project types where the activity is short-lived, you either need to include the correlation ID as part of the message (as shown in the previous examples) or you need to store the correlation ID as part of the request. For ASP.NET Core this can be done using middleware: app.Use(async (ctx, next) => { IDisposable disposeMe = null; var activity = Activity.Current; if (activity != null) { disposeMe = LogContext.PushProperty(\"correlationid\", activity.TraceId.ToString()); } try { await next(); } finally { disposeMe?.Dispose(); } }); All calls to Serilog within the web request will have the correlation ID set to the TraceId of the current activity. Other frameworks support similar features for enriching the current request or invocation with custom properties.","title":"Serilog"},{"location":"how-to-correlate-messages-across-services/#nlog","text":"Correlation ID can be set on log messages logged through NLog in multiple ways. The first approach is to include the ID directly in the log message: logger.Info(\"A log message with {correlationId}\", \"42\"); If you don't want the ID as part of the log message you can use either NLog's two context objects: using (MappedDiagnosticsLogicalContext.SetScoped(\"correlationId\", \"42\")) { logger.Info(\"A log message\"); } GlobalDiagnosticsContext.Set(\"correlationId\", \"42\"); You can also add the property manually to the log message using the log event builder API available in NLog: var infoMessage = new LogEventInfo(LogLevel.Info, \"\", \"A log message\"); infoMessage.Properties.Add(\"correlationid\", \"42\"); logger.Info(infoMessage);","title":"NLog"},{"location":"how-to-correlate-messages-across-services/#log4net","text":"Correlation ID can be set on log messages logged through log4net in multiple ways. You can include it directly on the LoggingEvent : var properties = new PropertiesDictionary(); properties[\"correlationid\"] = \"42\"; log.Logger.Log(new LoggingEvent(new LoggingEventData { Level = Level.Info, TimeStampUtc = DateTime.UtcNow, Properties = properties, Message = \"A log message\", })); You most likely use the Info , Warn , and similar helper methods to store log messages. In this case, you can set the correlation ID on the ThreadContext : ThreadContext.Properties[\"correlationid\"] = \"42\"; log.Info(\"A log message\"); Please notice that correlationid in both examples must be in all lowercase.","title":"log4net"},{"location":"how-to-correlate-messages-across-services/#w3c-trace-context","text":"The class Activity has been mentioned a couple of times already. Let's take a look at what that is and how it relates to W3C Trace Context. Trace Context is a specification by W3C for implementing distributed tracing across multiple processes which are already widely adopted. If you generate a trace identifier in a client initiating a chain of events, different Microsoft technologies like ASP.NET Core already pick up the extended set of headers and include those as part of log messages logged through Microsoft.Extensions.Logging. Let's say we have a console application calling an API and we want to log messages in both the console app and in the API and correlate them in elmah.io. In both the console app and in the ASP.NET Core application, you would set up Elmah.Io.Extensions.Logging using the default configuration. Then in the console application, you will wrap the call to the API in an Activity : var httpClient = new HttpClient(); var activity = new Activity(\"ApiCall\").Start(); try { using (logger.BeginScope(new { TraceId = activity.TraceId, ParentId = activity.ParentSpanId, SpanId = activity.SpanId })) { logger.LogInformation(\"Fetching data from the API\"); var result = await httpClient.GetStreamAsync(\"https://localhost:44396/ping\"); // ... } } finally { activity.Stop(); } By creating and starting a new Activity before we call the API, log messages in the console app can be decorated with three additional properties: TraceId , ParentId , and SpanId . This will make sure that all log messages logged within this scope will get the correlation ID set on elmah.io. HttpClient automatically picks up the activity and decorates the request with additional headers. On the API we simply log as normal: [ApiController] [Route(\"[controller]\")] public class PingController : ControllerBase { private readonly ILogger<PingController> _logger; public PingController(ILogger<PingController> logger) { _logger = logger; } [HttpGet] public string Get() { _logger.LogInformation(\"Received a ping\"); return \"pong\"; } } Notice that we didn't have to decorate log messages with additional properties. ASP.NET Core automatically picks up the new headers and decorates all log messages with the correct trace ID. If you want to test this through a browser, you'll need to modify the request headers before the request is made to your web application. There is a range of extensions available to help with this. For the following example, we'll use ModHeader : The extension will enrich all requests with a header named traceparent in the format VERSION-TRACE_ID-SPAN_ID-TRACE_FLAGS . elmah.io will automatically pick up this header and set correlationId to the value of TRACE_ID . If you don't get a correlation ID set on your log messages, we recommend installing the Elmah.Io.Client.Extensions.Correlation NuGet package and calling the following method in the OnMessage event/action: msg.WithCorrelationIdFromActivity();","title":"W3C Trace Context"},{"location":"how-to-enable-two-factor-login/","text":"How to enable two-factor login How to enable two-factor login Two-factor with an elmah.io username and password Two-factor with a social provider elmah.io supports two-factor login through either one of the social providers or a two-factor app like Google Authenticator or Authy. Two-factor with an elmah.io username and password When signing into elmah.io with a username and password, two-factor authentication can be enabled on the Security tab on your profile: Follow the instructions on the page to install either Google Authenticator or Authy. Once you have the app installed, scan the on-screen QR code and input the generated token in the field in step 3. Once two-factor authentication has been successfully set up, the following screen is shown: Two-factor authentication can be disabled at any time by inputting a new code from the authenticator app in the text field and clicking the Deactivate two-factor login button. We recommend that you sign out after enabling two-factor authentication to invalidate the current session. Popular authenticator apps like Google Authenticator and Microsoft Authenticator support cloud backup. Make sure to enable this in case you lose your phone. When cloud backup is enabled, you can sign in with your main account when you get a new phone and all of your stored accounts will be automatically restored. Two-factor with a social provider When using one of the social providers to log in to elmah.io, two-factor authentication can be enabled through either Twitter, Facebook, Microsoft, or Google. Check out the documentation for each authentication mechanism for details on how to enable two-factor authentication.","title":"How to enable two-factor login"},{"location":"how-to-enable-two-factor-login/#how-to-enable-two-factor-login","text":"How to enable two-factor login Two-factor with an elmah.io username and password Two-factor with a social provider elmah.io supports two-factor login through either one of the social providers or a two-factor app like Google Authenticator or Authy.","title":"How to enable two-factor login"},{"location":"how-to-enable-two-factor-login/#two-factor-with-an-elmahio-username-and-password","text":"When signing into elmah.io with a username and password, two-factor authentication can be enabled on the Security tab on your profile: Follow the instructions on the page to install either Google Authenticator or Authy. Once you have the app installed, scan the on-screen QR code and input the generated token in the field in step 3. Once two-factor authentication has been successfully set up, the following screen is shown: Two-factor authentication can be disabled at any time by inputting a new code from the authenticator app in the text field and clicking the Deactivate two-factor login button. We recommend that you sign out after enabling two-factor authentication to invalidate the current session. Popular authenticator apps like Google Authenticator and Microsoft Authenticator support cloud backup. Make sure to enable this in case you lose your phone. When cloud backup is enabled, you can sign in with your main account when you get a new phone and all of your stored accounts will be automatically restored.","title":"Two-factor with an elmah.io username and password"},{"location":"how-to-enable-two-factor-login/#two-factor-with-a-social-provider","text":"When using one of the social providers to log in to elmah.io, two-factor authentication can be enabled through either Twitter, Facebook, Microsoft, or Google. Check out the documentation for each authentication mechanism for details on how to enable two-factor authentication.","title":"Two-factor with a social provider"},{"location":"how-to-get-elmah-io-to-resolve-the-correct-client-ip/","text":"How to get elmah.io to resolve the correct client IP elmah.io try to resolve the IP of the client causing a log message, no matter what severity (Error, Information, etc.) and platform (browser, web-server, etc.) a log message is sent from. This is done by looking at multiple pieces of information provided by the sender. In some cases, elmah.io may not be able to resolve the IP or resolve a wrong IP address. In this document, you will find help getting the right client IP into elmah.io. Missing IP when using a proxy If you are using a proxy layer in between the client and your web server, you may experience log messages without a client IP. This is probably caused by the proxy hiding the original IP from your web server. Most proxies offer an alternative server variable like the X-Forwarded-For header. You can inspect the server variables on the Server Variables tab on elmah.io and check if your proxy includes the original IP in any of the variables. We support custom headers from a range of proxies (like Cloudflare). Most proxies support some kind of settings area where the X-Forwarded-For header can be enabled. If you are using a proxy that uses custom headers, please make sure to reach out and we may want to include the custom header to elmah.io. Wrong IP when using a proxy If the client IP is wrong when behind a proxy, it is typically because the proxy replaces the client IP when calling your server with the IP of its server. This is a poor practice and makes it very hard for elmah.io to figure out which IP belongs to the user and which one to the proxy. Luckily, this is configurable in a lot of proxies through their settings area. Missing IP This can be caused by several issues. In most instances, the client doesn't include any server variables that reveal the IP address. In this case, the client IP will not be available within the elmah.io UI. In some cases, you may know the user's IP from session variables or similar. To include an IP on messages logged to elmah.io, you can implement the OnMessage event or action, depending on which integration you are using. In this example, we use the OnMessage event on the Elmah.Io.Client package to include the user's IP manually: var userIp = \"1.1.1.1\"; // <-- just for the demo var client = ElmahioAPI.Create(\"API_KEY\"); client.Messages.OnMessage += (sender, args) => { var message = args.Message; if (message.ServerVariables == null) message.ServerVariables = new List<Item>(); message.ServerVariables.Add(new Item(\"X-FORWARDED-FOR\", userIp)); };","title":"How to get elmah.io to resolve the correct client IP"},{"location":"how-to-get-elmah-io-to-resolve-the-correct-client-ip/#how-to-get-elmahio-to-resolve-the-correct-client-ip","text":"elmah.io try to resolve the IP of the client causing a log message, no matter what severity (Error, Information, etc.) and platform (browser, web-server, etc.) a log message is sent from. This is done by looking at multiple pieces of information provided by the sender. In some cases, elmah.io may not be able to resolve the IP or resolve a wrong IP address. In this document, you will find help getting the right client IP into elmah.io.","title":"How to get elmah.io to resolve the correct client IP"},{"location":"how-to-get-elmah-io-to-resolve-the-correct-client-ip/#missing-ip-when-using-a-proxy","text":"If you are using a proxy layer in between the client and your web server, you may experience log messages without a client IP. This is probably caused by the proxy hiding the original IP from your web server. Most proxies offer an alternative server variable like the X-Forwarded-For header. You can inspect the server variables on the Server Variables tab on elmah.io and check if your proxy includes the original IP in any of the variables. We support custom headers from a range of proxies (like Cloudflare). Most proxies support some kind of settings area where the X-Forwarded-For header can be enabled. If you are using a proxy that uses custom headers, please make sure to reach out and we may want to include the custom header to elmah.io.","title":"Missing IP when using a proxy"},{"location":"how-to-get-elmah-io-to-resolve-the-correct-client-ip/#wrong-ip-when-using-a-proxy","text":"If the client IP is wrong when behind a proxy, it is typically because the proxy replaces the client IP when calling your server with the IP of its server. This is a poor practice and makes it very hard for elmah.io to figure out which IP belongs to the user and which one to the proxy. Luckily, this is configurable in a lot of proxies through their settings area.","title":"Wrong IP when using a proxy"},{"location":"how-to-get-elmah-io-to-resolve-the-correct-client-ip/#missing-ip","text":"This can be caused by several issues. In most instances, the client doesn't include any server variables that reveal the IP address. In this case, the client IP will not be available within the elmah.io UI. In some cases, you may know the user's IP from session variables or similar. To include an IP on messages logged to elmah.io, you can implement the OnMessage event or action, depending on which integration you are using. In this example, we use the OnMessage event on the Elmah.Io.Client package to include the user's IP manually: var userIp = \"1.1.1.1\"; // <-- just for the demo var client = ElmahioAPI.Create(\"API_KEY\"); client.Messages.OnMessage += (sender, args) => { var message = args.Message; if (message.ServerVariables == null) message.ServerVariables = new List<Item>(); message.ServerVariables.Add(new Item(\"X-FORWARDED-FOR\", userIp)); };","title":"Missing IP"},{"location":"how-to-get-the-sql-tab-to-show-up/","text":"How to get the SQL tab to show up The elmah.io UI can offer to show any SQL code part of the current context of logging a message. The code will show up in the log message details as a tab named SQL : The tab shows a formatted view of the SQL code including any parameters and/or syntax errors. This can help debug exceptions thrown while executing SQL code against a relational database. To make the SQL tab show up, custom information needs to be included in the Data dictionary of a log message. The following sections will go into detail on how to include the custom information in various ways. Entity Framework Core Entity Framework Core is easy since it already includes any SQL code as part of the messages logged through Microsoft.Extensions.Logging's ILogger . The SQL code and parameters are logged as two properties named commandText and parameters . elmah.io will automatically pick up these properties and show the SQL tab with the formatted results. As a default, all values in parameters are not logged as part of the message. You will see this from values being set to ? in the UI. To have the real values show up, you will need to enable sensitive data logging when setting up EF Core: services.AddDbContext<Context>(options => { // Other code like: options.UseSqlServer(connectionString); options.EnableSensitiveDataLogging(true); // \u2b05\ufe0f Set this to true }); This should not be set if you include sensitive details like social security numbers, passwords, and similar as SQL query parameters. Manually If you want to attach SQL to a log message made manually, you can go one of two ways. The first way is to fill in the commandText and parameters Data entries shown above. When creating a message on Elmah.Io.Client it could look like this: client.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Log message with SQL attached\", Severity = Severity.Error.ToString(), Data = new List<Item> { new Item { Key = \"commandText\", Value = \"SELECT * FROM USERS WHERE Name = @columnValue OR Name = @columnTwoValue\" }, new Item { Key = \"parameters\", Value = \"columnValue='Eduard' (Nullable = false) (Size = 6), columnTwoValue='Thomas' (Nullable = false) (Size = 6)\" }, }, }); The value of the parameters item needs to correspond to the format of that Entity Framework and the System.Data namespace uses. The second approach is to provide elmah.io with a single Data item named X-ELMAHIO-SQL . The value of this item should be a JSON format as seen in the following example: var sql = new { Raw = \"SELECT * FROM USERS WHERE Name = @columnValue OR Name = @columnTwoValue\", Parameters = new[] { new { IsNullable = false, Size = 6, Name = \"columnValue\", Value = \"Eduard\" }, new { IsNullable = false, Size = 6, Name = \"columnTwoValue\", Value = \"Thomas\" }, }, }; client.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Log message with SQL attached\", Severity = Severity.Error.ToString(), Data = new List<Item> { new Item { Key = \"X-ELMAHIO-SQL\", Value = JsonConvert.SerializeObject(sql) }, }, }); The JSON generated by serializing the anonymous object will look like this: { \"Raw\": \"SELECT * FROM USERS WHERE Name = @columnValue OR Name = @columnTwoValue\", \"Parameters\": [ { \"IsNullable\": false, \"Size\": 6, \"Name\": \"columnValue\", \"Value\": \"Eduard\" }, { \"IsNullable\": false, \"Size\": 6, \"Name\": \"columnTwoValue\", \"Value\": \"Thomas\" } ] }","title":"How to get the SQL tab to show up"},{"location":"how-to-get-the-sql-tab-to-show-up/#how-to-get-the-sql-tab-to-show-up","text":"The elmah.io UI can offer to show any SQL code part of the current context of logging a message. The code will show up in the log message details as a tab named SQL : The tab shows a formatted view of the SQL code including any parameters and/or syntax errors. This can help debug exceptions thrown while executing SQL code against a relational database. To make the SQL tab show up, custom information needs to be included in the Data dictionary of a log message. The following sections will go into detail on how to include the custom information in various ways.","title":"How to get the SQL tab to show up"},{"location":"how-to-get-the-sql-tab-to-show-up/#entity-framework-core","text":"Entity Framework Core is easy since it already includes any SQL code as part of the messages logged through Microsoft.Extensions.Logging's ILogger . The SQL code and parameters are logged as two properties named commandText and parameters . elmah.io will automatically pick up these properties and show the SQL tab with the formatted results. As a default, all values in parameters are not logged as part of the message. You will see this from values being set to ? in the UI. To have the real values show up, you will need to enable sensitive data logging when setting up EF Core: services.AddDbContext<Context>(options => { // Other code like: options.UseSqlServer(connectionString); options.EnableSensitiveDataLogging(true); // \u2b05\ufe0f Set this to true }); This should not be set if you include sensitive details like social security numbers, passwords, and similar as SQL query parameters.","title":"Entity Framework Core"},{"location":"how-to-get-the-sql-tab-to-show-up/#manually","text":"If you want to attach SQL to a log message made manually, you can go one of two ways. The first way is to fill in the commandText and parameters Data entries shown above. When creating a message on Elmah.Io.Client it could look like this: client.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Log message with SQL attached\", Severity = Severity.Error.ToString(), Data = new List<Item> { new Item { Key = \"commandText\", Value = \"SELECT * FROM USERS WHERE Name = @columnValue OR Name = @columnTwoValue\" }, new Item { Key = \"parameters\", Value = \"columnValue='Eduard' (Nullable = false) (Size = 6), columnTwoValue='Thomas' (Nullable = false) (Size = 6)\" }, }, }); The value of the parameters item needs to correspond to the format of that Entity Framework and the System.Data namespace uses. The second approach is to provide elmah.io with a single Data item named X-ELMAHIO-SQL . The value of this item should be a JSON format as seen in the following example: var sql = new { Raw = \"SELECT * FROM USERS WHERE Name = @columnValue OR Name = @columnTwoValue\", Parameters = new[] { new { IsNullable = false, Size = 6, Name = \"columnValue\", Value = \"Eduard\" }, new { IsNullable = false, Size = 6, Name = \"columnTwoValue\", Value = \"Thomas\" }, }, }; client.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Log message with SQL attached\", Severity = Severity.Error.ToString(), Data = new List<Item> { new Item { Key = \"X-ELMAHIO-SQL\", Value = JsonConvert.SerializeObject(sql) }, }, }); The JSON generated by serializing the anonymous object will look like this: { \"Raw\": \"SELECT * FROM USERS WHERE Name = @columnValue OR Name = @columnTwoValue\", \"Parameters\": [ { \"IsNullable\": false, \"Size\": 6, \"Name\": \"columnValue\", \"Value\": \"Eduard\" }, { \"IsNullable\": false, \"Size\": 6, \"Name\": \"columnTwoValue\", \"Value\": \"Thomas\" } ] }","title":"Manually"},{"location":"how-to-include-source-code-in-log-messages/","text":"How to include source code in log messages How to include source code in log messages From the file system From the PDB file Manually Troubleshooting Sometimes, being able to see the exact code causing an error, is much more helpful than looking at other details around the current HTTP context and similar. If you often find yourself opening Visual Studio or Code to inspect the failing line, embedding source code in errors and log messages will speed up the process. In this article, you will learn how to configure elmah.io to include source code when logging messages using the Elmah.Io.Client.Extensions.SourceCode NuGet package. The Elmah.Io.Client.Extensions.SourceCode package requires Elmah.Io.Client version 4.0 or newer. No matter what integration you are using (with a few exceptions) you are using the Elmah.Io.Client NuGet package to communicate with the elmah.io API. We have built a range of extensions for this package, to avoid including too many features not related to communicating with the API into the client package. One of them is for including source code when logging messages. Start by installing the Elmah.Io.Client.Extensions.SourceCode NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client.Extensions.SourceCode dotnet add package Elmah.Io.Client.Extensions.SourceCode <PackageReference Include=\"Elmah.Io.Client.Extensions.SourceCode\" Version=\"5.*\" /> paket add Elmah.Io.Client.Extensions.SourceCode There are currently three ways of including source code with log messages. The first two ways require the Elmah.Io.Client.Extensions.SourceCode package, while the third one can be done manually. From the file system This is the most simple approach meant for local development. When logging a stack trace from your local machine, the trace includes the absolute path to the file on your file system, as well as the line causing a log message (typically an error). To set this up, you will need to implement the OnMessage event through the Elmah.Io.Client package. Depending on which integration you are using, the name of that event or action can vary. What you are looking to do is to call the WithSourceCodeFromFileSystem method on log messages you want to include source code. This is an example when using the Elmah.Io.Client directly: var elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); elmahIoClient.Messages.OnMessage += (sender, e) => e.Message.WithSourceCodeFromFileSystem(); Using an integration like Elmah.Io.AspNetCore uses the same method: services.AddElmahIo(options => { options.OnMessage = msg => msg.WithSourceCodeFromFileSystem(); }); This will automatically instruct Elmah.Io.Client.Extensions.Source to try and parse any stack trace in the details property and embed the source code. For an example of how to use the WithSourceCodeFromFileSystem method, check out the following sample: Elmah.Io.Client.Extensions.SourceCode.FileSystem . From the PDB file When deploying your code on another environment, you typically don't have the original code available. If you copy your source code to the same absolute path as when building, you can use the file-system approach shown above. If not, embedding the source code in the PDB file can be the option. Before doing so, make sure you include filename and line numbers in stack traces on all environments as shown here: Include filename and line number in stack traces . For the old project template the Debugging information field needs a value of Portable . For the new project template the Debug symbols field needs a value of PDB file, portable across platforms . To embed source code in the PDB file built alongside your DLL files, include the following property in your csproj file: <PropertyGroup> <EmbedAllSources>true</EmbedAllSources> </PropertyGroup> Be aware that this will include your original source code in your deployment which may not be a good approach if other people have access to the environment or binary files. Next, call the WithSourceCodeFromPdb method: var elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); elmahIoClient.Messages.OnMessage += (sender, e) => e.Message.WithSourceCodeFromPdb(); For an example of how to do this from ASP.NET Core, you can use the same approach as specified in the previous section: services.AddElmahIo(options => { options.OnMessage = msg => msg.WithSourceCodeFromPdb(); }); All of our integrations support a message callback somehow. For an example of how to use the WithSourceCodeFromPdb method, check out the following sample: Elmah.Io.Client.Extensions.SourceCode.PdbSample for .NET and Elmah.Io.Client.Extensions.SourceCode.NetFrameworkPdb for .NET Framework. Manually In case you want to include source code manually, you can use the OnMessage event and the Code property on the CreateMessage class: var elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); elmahIoClient.Messages.OnMessage += (sender, e) => { e.Message.Code = FetchCode(); } You will need to implement the FetchCode method to return the source code to include. Only 21 lines of code are supported for now. In case you want elmah.io to show the correct line numbers, you will need to tell us how the first line number in the provided code matches your original source file as well as the line number causing the error. This is done by adding two Item s to the Data dictionary on CreateMessage : var elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); elmahIoClient.Messages.OnMessage += (sender, e) => { e.Message.Code = FetchCode(); if (e.Message.Data == null) e.Message.Data = new List<Item>(); e.Message.Data.Add(new Item(\"X-ELMAHIO-CODESTARTLINE\", \"42\")); e.Message.Data.Add(new Item(\"X-ELMAHIO-CODELINE\", \"51\")); } This will show line number 42 next to the first code line and highlight line number 51 in the elmah.io UI. Troubleshooting If no source code shows up on elmah.io log messages, you can start by running through the following checks: Make sure that the log message contains a stack trace in the details field. Make sure that the stack trace contains absolute path filenames and line numbers for the code causing the stack trace. Make sure that you are calling the WithSourceCodeFromPdb or WithSourceCodeFromFileSystem method. Make sure that the Elmah.Io.Client.Extensions.SourceCode.dll file is in your deployed application. Make sure that your project has Portable set in Debugging information or PDB File, portable across platforms set in Debug symbols . For PDB files, make sure that you have included the EmbedAllSources element in your csproj file. Look inside the Data tab on the logged message. It may contain a key named X-ELMAHIO-CODEERROR with a value explaining what went wrong.","title":"How to include source code in log messages"},{"location":"how-to-include-source-code-in-log-messages/#how-to-include-source-code-in-log-messages","text":"How to include source code in log messages From the file system From the PDB file Manually Troubleshooting Sometimes, being able to see the exact code causing an error, is much more helpful than looking at other details around the current HTTP context and similar. If you often find yourself opening Visual Studio or Code to inspect the failing line, embedding source code in errors and log messages will speed up the process. In this article, you will learn how to configure elmah.io to include source code when logging messages using the Elmah.Io.Client.Extensions.SourceCode NuGet package. The Elmah.Io.Client.Extensions.SourceCode package requires Elmah.Io.Client version 4.0 or newer. No matter what integration you are using (with a few exceptions) you are using the Elmah.Io.Client NuGet package to communicate with the elmah.io API. We have built a range of extensions for this package, to avoid including too many features not related to communicating with the API into the client package. One of them is for including source code when logging messages. Start by installing the Elmah.Io.Client.Extensions.SourceCode NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client.Extensions.SourceCode dotnet add package Elmah.Io.Client.Extensions.SourceCode <PackageReference Include=\"Elmah.Io.Client.Extensions.SourceCode\" Version=\"5.*\" /> paket add Elmah.Io.Client.Extensions.SourceCode There are currently three ways of including source code with log messages. The first two ways require the Elmah.Io.Client.Extensions.SourceCode package, while the third one can be done manually.","title":"How to include source code in log messages"},{"location":"how-to-include-source-code-in-log-messages/#from-the-file-system","text":"This is the most simple approach meant for local development. When logging a stack trace from your local machine, the trace includes the absolute path to the file on your file system, as well as the line causing a log message (typically an error). To set this up, you will need to implement the OnMessage event through the Elmah.Io.Client package. Depending on which integration you are using, the name of that event or action can vary. What you are looking to do is to call the WithSourceCodeFromFileSystem method on log messages you want to include source code. This is an example when using the Elmah.Io.Client directly: var elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); elmahIoClient.Messages.OnMessage += (sender, e) => e.Message.WithSourceCodeFromFileSystem(); Using an integration like Elmah.Io.AspNetCore uses the same method: services.AddElmahIo(options => { options.OnMessage = msg => msg.WithSourceCodeFromFileSystem(); }); This will automatically instruct Elmah.Io.Client.Extensions.Source to try and parse any stack trace in the details property and embed the source code. For an example of how to use the WithSourceCodeFromFileSystem method, check out the following sample: Elmah.Io.Client.Extensions.SourceCode.FileSystem .","title":"From the file system"},{"location":"how-to-include-source-code-in-log-messages/#from-the-pdb-file","text":"When deploying your code on another environment, you typically don't have the original code available. If you copy your source code to the same absolute path as when building, you can use the file-system approach shown above. If not, embedding the source code in the PDB file can be the option. Before doing so, make sure you include filename and line numbers in stack traces on all environments as shown here: Include filename and line number in stack traces . For the old project template the Debugging information field needs a value of Portable . For the new project template the Debug symbols field needs a value of PDB file, portable across platforms . To embed source code in the PDB file built alongside your DLL files, include the following property in your csproj file: <PropertyGroup> <EmbedAllSources>true</EmbedAllSources> </PropertyGroup> Be aware that this will include your original source code in your deployment which may not be a good approach if other people have access to the environment or binary files. Next, call the WithSourceCodeFromPdb method: var elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); elmahIoClient.Messages.OnMessage += (sender, e) => e.Message.WithSourceCodeFromPdb(); For an example of how to do this from ASP.NET Core, you can use the same approach as specified in the previous section: services.AddElmahIo(options => { options.OnMessage = msg => msg.WithSourceCodeFromPdb(); }); All of our integrations support a message callback somehow. For an example of how to use the WithSourceCodeFromPdb method, check out the following sample: Elmah.Io.Client.Extensions.SourceCode.PdbSample for .NET and Elmah.Io.Client.Extensions.SourceCode.NetFrameworkPdb for .NET Framework.","title":"From the PDB file"},{"location":"how-to-include-source-code-in-log-messages/#manually","text":"In case you want to include source code manually, you can use the OnMessage event and the Code property on the CreateMessage class: var elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); elmahIoClient.Messages.OnMessage += (sender, e) => { e.Message.Code = FetchCode(); } You will need to implement the FetchCode method to return the source code to include. Only 21 lines of code are supported for now. In case you want elmah.io to show the correct line numbers, you will need to tell us how the first line number in the provided code matches your original source file as well as the line number causing the error. This is done by adding two Item s to the Data dictionary on CreateMessage : var elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); elmahIoClient.Messages.OnMessage += (sender, e) => { e.Message.Code = FetchCode(); if (e.Message.Data == null) e.Message.Data = new List<Item>(); e.Message.Data.Add(new Item(\"X-ELMAHIO-CODESTARTLINE\", \"42\")); e.Message.Data.Add(new Item(\"X-ELMAHIO-CODELINE\", \"51\")); } This will show line number 42 next to the first code line and highlight line number 51 in the elmah.io UI.","title":"Manually"},{"location":"how-to-include-source-code-in-log-messages/#troubleshooting","text":"If no source code shows up on elmah.io log messages, you can start by running through the following checks: Make sure that the log message contains a stack trace in the details field. Make sure that the stack trace contains absolute path filenames and line numbers for the code causing the stack trace. Make sure that you are calling the WithSourceCodeFromPdb or WithSourceCodeFromFileSystem method. Make sure that the Elmah.Io.Client.Extensions.SourceCode.dll file is in your deployed application. Make sure that your project has Portable set in Debugging information or PDB File, portable across platforms set in Debug symbols . For PDB files, make sure that you have included the EmbedAllSources element in your csproj file. Look inside the Data tab on the logged message. It may contain a key named X-ELMAHIO-CODEERROR with a value explaining what went wrong.","title":"Troubleshooting"},{"location":"how-to-manage-subscriptions-update-credit-cards-etc/","text":"How to manage subscriptions, update credit cards, etc. Your subscription is managed from the organization settings page. To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard: When on the organization settings page, click the Subscription tab. Your subscription can be managed by scrolling to the bottom of the page and looking for the currently active subscription: In the following sections, we will go through each button. Purchase top-up If getting near your monthly log message limit you can purchase a top-up to avoid having to permanently upgrade to a larger plan. Top-ups are priced at $19 and will add 25,000 messages and 1,000 emails to your subscription for the rest of the calendar month. Update Credit Card If your recent payment failed or you received a new credit card from your bank, you can use this button to input the new credit card. Cancel Subscription To cancel your current subscription you can click the Cancel Subscription button. This will open the chat where we will guide you through the process. We don't use a manual offboarding process to try and convince your to stay or to make it hard for you to leave. When leaving a system like elmah.io, we need additional details from you like when you want to cancel (mostly geared towards annual subscriptions) and to make sure that you have backed up all data from elmah.io before it is cleaned up.","title":"How to manage subscriptions, update credit cards, etc."},{"location":"how-to-manage-subscriptions-update-credit-cards-etc/#how-to-manage-subscriptions-update-credit-cards-etc","text":"Your subscription is managed from the organization settings page. To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard: When on the organization settings page, click the Subscription tab. Your subscription can be managed by scrolling to the bottom of the page and looking for the currently active subscription: In the following sections, we will go through each button.","title":"How to manage subscriptions, update credit cards, etc."},{"location":"how-to-manage-subscriptions-update-credit-cards-etc/#purchase-top-up","text":"If getting near your monthly log message limit you can purchase a top-up to avoid having to permanently upgrade to a larger plan. Top-ups are priced at $19 and will add 25,000 messages and 1,000 emails to your subscription for the rest of the calendar month.","title":"Purchase top-up"},{"location":"how-to-manage-subscriptions-update-credit-cards-etc/#update-credit-card","text":"If your recent payment failed or you received a new credit card from your bank, you can use this button to input the new credit card.","title":"Update Credit Card"},{"location":"how-to-manage-subscriptions-update-credit-cards-etc/#cancel-subscription","text":"To cancel your current subscription you can click the Cancel Subscription button. This will open the chat where we will guide you through the process. We don't use a manual offboarding process to try and convince your to stay or to make it hard for you to leave. When leaving a system like elmah.io, we need additional details from you like when you want to cancel (mostly geared towards annual subscriptions) and to make sure that you have backed up all data from elmah.io before it is cleaned up.","title":"Cancel Subscription"},{"location":"how-to-rename-a-log/","text":"How to rename a log Logs can be renamed from the Dashboard by anyone with Administrator access to the log. To rename a log, click the small icon in the lower right corner: When clicking the icon the log box will flip and you will be able to input a new name. The edit log box also lets you assign the log to an environment, subscribe/unsubscribe from emails, as well as change the color of the log. Be aware that changing the name, environment, or color will be visible for all users with access to the log.","title":"How to rename a log"},{"location":"how-to-rename-a-log/#how-to-rename-a-log","text":"Logs can be renamed from the Dashboard by anyone with Administrator access to the log. To rename a log, click the small icon in the lower right corner: When clicking the icon the log box will flip and you will be able to input a new name. The edit log box also lets you assign the log to an environment, subscribe/unsubscribe from emails, as well as change the color of the log. Be aware that changing the name, environment, or color will be visible for all users with access to the log.","title":"How to rename a log"},{"location":"how-to-run-elmah-io-in-dark-mode/","text":"How to run elmah.io in dark mode Some developers prefer applications with a dark mode either full-time or when working those late hours. The elmah.io application (app.elmah.io) has a light theme but can be run in dark mode using a browser extension. We recommend the following extensions for running elmah.io in dark mode: Dark Mode - Night Eye - Chrome - Edge - Firefox Dark Reader - Chrome - Edge - Firefox","title":"How to run elmah.io in dark mode"},{"location":"how-to-run-elmah-io-in-dark-mode/#how-to-run-elmahio-in-dark-mode","text":"Some developers prefer applications with a dark mode either full-time or when working those late hours. The elmah.io application (app.elmah.io) has a light theme but can be run in dark mode using a browser extension. We recommend the following extensions for running elmah.io in dark mode: Dark Mode - Night Eye - Chrome - Edge - Firefox Dark Reader - Chrome - Edge - Firefox","title":"How to run elmah.io in dark mode"},{"location":"how-to-search-custom-data/","text":"How to search custom data Custom data is not searchable by default. Sometimes it makes sense that errors can be searched from values logged as part of custom data. For now, this feature is supported through the use of variable naming, but we may extend this to a configuration option through the API or UI as well. To make a custom variable and its value searchable through the UI (as well as through the API), name the variable with the prefix X-ELMAHIO-SEARCH- . The variable will become searchable through the name added after the prefix. Examples: ASP.NET ASP.NET Core Serilog NLog log4net Microsoft.Extensions.Logging Elmah.ErrorLog.GetDefault(null); var logger = Elmah.Io.ErrorLog.Client; logger.OnMessage += (sender, args) => { if (args.Message.Data == null) args.Message.Data = new List<Item>(); args.Message.Data.Add(new Item { Key = \"X-ELMAHIO-SEARCH-author\", Value = \"Walter Sobchak\" }); }; builder.Services.AddElmahIo(o => { o.OnMessage = message => { if (message.Data == null) message.Data = new List<Item>(); message.Data.Add(new Item { Key = \"X-ELMAHIO-SEARCH-author\", Value = \"Walter Sobchak\" }); }; }); using (LogContext.PushProperty(\"X-ELMAHIO-SEARCH-author\", \"Walter Sobchak\")) { logger.Error(\"You see what happens, Larry?\"); } var errorMessage = new LogEventInfo(LogLevel.Error, \"\", \"You see what happens, Larry?\"); errorMessage.Properties.Add(\"X-ELMAHIO-SEARCH-author\", \"Walter Sobchak\"); log.Error(errorMessage); var properties = new PropertiesDictionary(); properties[\"X-ELMAHIO-SEARCH-author\"] = \"Walter Sobchak\"; log.Logger.Log(new LoggingEvent(new LoggingEventData { Level = Level.Error, TimeStampUtc = DateTime.UtcNow, Properties = properties, Message = \"You see what happens, Larry?\", })); var scope = new Dictionary<string, object> { { \"X-ELMAHIO-SEARCH-author\", \"Walter Sobchak\" } }; using (logger.BeginScope(scope})) { logger.LogError(\"You see what happens, Larry?\"); } The examples will make author searchable using this query: data.author:\"Walter Sobchak\" Observe how X-ELMAHIO-SEARCH- is replaced with the data. prefix when indexed in elmah.io. Adding searchable properties is available when logging exceptions too: try { // ... } catch (NullReferenceException e) { e.Data.Add(\"X-ELMAHIO-SEARCH-author\", \"Walter Sobchak\"); // Log the exception or throw e to use this catch for decorating the exception } To avoid someone filling up our cluster with custom data, only the first three variables containing X-ELMAHIO-SEARCH- are made searchable. Also, variables with a value containing more than 256 characters are not indexed.","title":"How to search custom data"},{"location":"how-to-search-custom-data/#how-to-search-custom-data","text":"Custom data is not searchable by default. Sometimes it makes sense that errors can be searched from values logged as part of custom data. For now, this feature is supported through the use of variable naming, but we may extend this to a configuration option through the API or UI as well. To make a custom variable and its value searchable through the UI (as well as through the API), name the variable with the prefix X-ELMAHIO-SEARCH- . The variable will become searchable through the name added after the prefix. Examples: ASP.NET ASP.NET Core Serilog NLog log4net Microsoft.Extensions.Logging Elmah.ErrorLog.GetDefault(null); var logger = Elmah.Io.ErrorLog.Client; logger.OnMessage += (sender, args) => { if (args.Message.Data == null) args.Message.Data = new List<Item>(); args.Message.Data.Add(new Item { Key = \"X-ELMAHIO-SEARCH-author\", Value = \"Walter Sobchak\" }); }; builder.Services.AddElmahIo(o => { o.OnMessage = message => { if (message.Data == null) message.Data = new List<Item>(); message.Data.Add(new Item { Key = \"X-ELMAHIO-SEARCH-author\", Value = \"Walter Sobchak\" }); }; }); using (LogContext.PushProperty(\"X-ELMAHIO-SEARCH-author\", \"Walter Sobchak\")) { logger.Error(\"You see what happens, Larry?\"); } var errorMessage = new LogEventInfo(LogLevel.Error, \"\", \"You see what happens, Larry?\"); errorMessage.Properties.Add(\"X-ELMAHIO-SEARCH-author\", \"Walter Sobchak\"); log.Error(errorMessage); var properties = new PropertiesDictionary(); properties[\"X-ELMAHIO-SEARCH-author\"] = \"Walter Sobchak\"; log.Logger.Log(new LoggingEvent(new LoggingEventData { Level = Level.Error, TimeStampUtc = DateTime.UtcNow, Properties = properties, Message = \"You see what happens, Larry?\", })); var scope = new Dictionary<string, object> { { \"X-ELMAHIO-SEARCH-author\", \"Walter Sobchak\" } }; using (logger.BeginScope(scope})) { logger.LogError(\"You see what happens, Larry?\"); } The examples will make author searchable using this query: data.author:\"Walter Sobchak\" Observe how X-ELMAHIO-SEARCH- is replaced with the data. prefix when indexed in elmah.io. Adding searchable properties is available when logging exceptions too: try { // ... } catch (NullReferenceException e) { e.Data.Add(\"X-ELMAHIO-SEARCH-author\", \"Walter Sobchak\"); // Log the exception or throw e to use this catch for decorating the exception } To avoid someone filling up our cluster with custom data, only the first three variables containing X-ELMAHIO-SEARCH- are made searchable. Also, variables with a value containing more than 256 characters are not indexed.","title":"How to search custom data"},{"location":"include-filename-and-line-number-in-stacktraces/","text":"Include filename and line number in stack traces If you are running .NET Core/.NET 5 you no longer need to enable filenames and line numbers manually. When deploying your application to the test and production environment, you normally want to use the Release configuration. When doing so, your code is optimized, web.config transformation is running, and a few additional things. But, part of running on a Release build is, that you lose the ability to see filenames and line numbers in the stack traces produced by your system. .NET offers the concept of PDB files, which are automatically generated when building your code. The PDB file contains information for the debugger to work, like which file to look up when a breakpoint is reached in your code. Unless you have changed the default settings inside Visual Studio, both the Debug and Release configuration generates a PDB file. So, if both Debug and Release produce a PDB file, why do Debug builds include file name and line number in stack traces, while the Release build doesn't? The reason is most often caused by the fact that PDB files aren't published as part of the deployment. To do so, right-click your project in Visual Studio and select Properties . Click the Package/Publish Web tab and make sure that the Release configuration is selected in the dropdown. Next, remove the checkmark in Exclude generated debug symbols : Also, make sure that the PDB file is generated as part of Release builds. Select the Build tab and click Advanced... . In Debug Info you want to make sure that either Pdb-only or Portable is selected ( Pdb-only being the default): On your next deployment, PDB files are published as part of the build. Depending on who you talk to, deploying PDB files as part of your build may be considered a hack. Since PDB files can contain sensitive information about your implementation, publishing these files should only be done, if you have full control of the environment you are deploying to. When releasing software to external users/customers, you don't want to include your PDB files. In this case, you should store the PDB files internally, in a symbol server or similar.","title":"Include filename and line number in stack traces"},{"location":"include-filename-and-line-number-in-stacktraces/#include-filename-and-line-number-in-stack-traces","text":"If you are running .NET Core/.NET 5 you no longer need to enable filenames and line numbers manually. When deploying your application to the test and production environment, you normally want to use the Release configuration. When doing so, your code is optimized, web.config transformation is running, and a few additional things. But, part of running on a Release build is, that you lose the ability to see filenames and line numbers in the stack traces produced by your system. .NET offers the concept of PDB files, which are automatically generated when building your code. The PDB file contains information for the debugger to work, like which file to look up when a breakpoint is reached in your code. Unless you have changed the default settings inside Visual Studio, both the Debug and Release configuration generates a PDB file. So, if both Debug and Release produce a PDB file, why do Debug builds include file name and line number in stack traces, while the Release build doesn't? The reason is most often caused by the fact that PDB files aren't published as part of the deployment. To do so, right-click your project in Visual Studio and select Properties . Click the Package/Publish Web tab and make sure that the Release configuration is selected in the dropdown. Next, remove the checkmark in Exclude generated debug symbols : Also, make sure that the PDB file is generated as part of Release builds. Select the Build tab and click Advanced... . In Debug Info you want to make sure that either Pdb-only or Portable is selected ( Pdb-only being the default): On your next deployment, PDB files are published as part of the build. Depending on who you talk to, deploying PDB files as part of your build may be considered a hack. Since PDB files can contain sensitive information about your implementation, publishing these files should only be done, if you have full control of the environment you are deploying to. When releasing software to external users/customers, you don't want to include your PDB files. In this case, you should store the PDB files internally, in a symbol server or similar.","title":"Include filename and line number in stack traces"},{"location":"integrate-elmah-io-with-pipedream/","text":"Integrate with Pipedream Pipedream is a service similar to Zapier and IFTTT to help integrate systems without having to write code. In this article, we use an integration point provided by elmah.io and Pipedream called a trigger. A trigger is something that triggers an action in Pipedream. In the case of elmah.io, the trigger available is when new errors are logged to your log. Actions exist on the other side of the integration and tell Pipedream what to do every time a trigger is fired. This guide will show you how to set up the trigger. What action you want to execute when new errors are logged will depend on the tools and workflows used in your organization. Create a new account on pipedream.com. Then click the New button on the Workflows page. The create new workflow page is shown: Search for elmah.io in the search field and select the app and the New Error trigger: Click the Connect new account button and input an API key with permission to both get logs and messages in the api_key field: Click the Save button and select the log to integrate with in the Log ID dropdown: Click the Create source button and wait for: If no events are shown, force an error from the application integrated with the chosen log or create a test error through the API. Remember that only errors marked with the new flag are shown as events in Pipedream. Select an event and click the Continue button. The elmah.io trigger is now configured. Select an app and event of your choice to create actions on the newly created trigger.","title":"Pipedream"},{"location":"integrate-elmah-io-with-pipedream/#integrate-with-pipedream","text":"Pipedream is a service similar to Zapier and IFTTT to help integrate systems without having to write code. In this article, we use an integration point provided by elmah.io and Pipedream called a trigger. A trigger is something that triggers an action in Pipedream. In the case of elmah.io, the trigger available is when new errors are logged to your log. Actions exist on the other side of the integration and tell Pipedream what to do every time a trigger is fired. This guide will show you how to set up the trigger. What action you want to execute when new errors are logged will depend on the tools and workflows used in your organization. Create a new account on pipedream.com. Then click the New button on the Workflows page. The create new workflow page is shown: Search for elmah.io in the search field and select the app and the New Error trigger: Click the Connect new account button and input an API key with permission to both get logs and messages in the api_key field: Click the Save button and select the log to integrate with in the Log ID dropdown: Click the Create source button and wait for: If no events are shown, force an error from the application integrated with the chosen log or create a test error through the API. Remember that only errors marked with the new flag are shown as events in Pipedream. Select an event and click the Continue button. The elmah.io trigger is now configured. Select an app and event of your choice to create actions on the newly created trigger.","title":"Integrate with Pipedream"},{"location":"integrate-elmah-io-with-zapier/","text":"Integrate with Zapier Zapier is the place to go if you need to integrate two or more online systems. In this article, we use an integration point provided by elmah.io and Zapier called a trigger. A trigger is (as the name suggest) something that triggers an action in Zapier. In the case of elmah.io, the trigger available is when new errors are logged to your log. Actions exist on the other side of the integration and tell Zapier what to do every time a trigger is fired. This guide will show you how to set up the trigger. What action you want to execute when new errors are logged will depend on the tools and workflows used in your organization. Create a new account on zapier.com. Then click the Make a Zap button. The create new Zap page is shown: Search for elmah.io in the search field and select the app and the New Error trigger: Click Continue and you will be presented with the following screen: Click the Sign in to elmah.io button or select an existing account if you have already set up other zaps using elmah.io. Adding a new account will show a popup asking you to sign in to elmah.io: Sign in with your elmah.io username/password or social provider. On the following screen you will be asked to authorize elmah.io to notify Zapier every time a new error is logged in a log selected on a later stage: Click the Authorize button and your account will be added to the account list on Zapier: Click Continue . In the following step you will select the elmah.io log that you want to integrate with Zapier: The dropdown contains all of the logs you have access to within your organization. Select a log and click Continue . In the following step you will test the trigger: Click Test trigger and Zapier will pull recent errors from the chosen log. Select the error the represents how a typical error in the chosen log looks like. The values from the chosen error will be used when filling in the action, why selecting a good example in this step can make it easier to configure later on. When you are done, click the Continue button. The elmah.io trigger is now configured. Select an app and event of your choice to create actions on the newly created trigger.","title":"Zapier"},{"location":"integrate-elmah-io-with-zapier/#integrate-with-zapier","text":"Zapier is the place to go if you need to integrate two or more online systems. In this article, we use an integration point provided by elmah.io and Zapier called a trigger. A trigger is (as the name suggest) something that triggers an action in Zapier. In the case of elmah.io, the trigger available is when new errors are logged to your log. Actions exist on the other side of the integration and tell Zapier what to do every time a trigger is fired. This guide will show you how to set up the trigger. What action you want to execute when new errors are logged will depend on the tools and workflows used in your organization. Create a new account on zapier.com. Then click the Make a Zap button. The create new Zap page is shown: Search for elmah.io in the search field and select the app and the New Error trigger: Click Continue and you will be presented with the following screen: Click the Sign in to elmah.io button or select an existing account if you have already set up other zaps using elmah.io. Adding a new account will show a popup asking you to sign in to elmah.io: Sign in with your elmah.io username/password or social provider. On the following screen you will be asked to authorize elmah.io to notify Zapier every time a new error is logged in a log selected on a later stage: Click the Authorize button and your account will be added to the account list on Zapier: Click Continue . In the following step you will select the elmah.io log that you want to integrate with Zapier: The dropdown contains all of the logs you have access to within your organization. Select a log and click Continue . In the following step you will test the trigger: Click Test trigger and Zapier will pull recent errors from the chosen log. Select the error the represents how a typical error in the chosen log looks like. The values from the chosen error will be used when filling in the action, why selecting a good example in this step can make it easier to configure later on. When you are done, click the Continue button. The elmah.io trigger is now configured. Select an app and event of your choice to create actions on the newly created trigger.","title":"Integrate with Zapier"},{"location":"integrations-high-level-overview/","text":"Integrations high-level overview elmah.io offers integration with an extensive list of logging- and web-frameworks. Get the full overview here. The table below shows all of the frameworks with official support. The Async column contains a checkmark if the integration is logging asynchronously. The Bulk column contains a checkmark if the integration is logging to elmah.io in bulk. In addition, elmah.io can be installed in a range of different products and services not mentioned (using the integrations below). Look through the menu to the left to see all of the possible integrations. Framework Stable Build Source Samples Async Bulk ASP.NET \u2714\ufe0f ASP.NET MVC \ufe0f\u2714\ufe0f ASP.NET Web API \u2714\ufe0f ASP.NET Core \u2714\ufe0f Blazor \u2714\ufe0f Microsoft.Extensions.Logging \u2714\ufe0f \u2714\ufe0f Serilog \u2714\ufe0f \u2714\ufe0f NLog \u2714\ufe0f \u2714\ufe0f log4net \u2714\ufe0f Umbraco \u2714\ufe0f Azure Websites \u2714\ufe0f Azure Functions \u2714\ufe0f Isolated Azure Functions \u2714\ufe0f JavaScript \u2714\ufe0f Xamarin \u2714\ufe0f Uno \u2714\ufe0f WPF \u2714\ufe0f","title":"Integrations high level overview"},{"location":"integrations-high-level-overview/#integrations-high-level-overview","text":"elmah.io offers integration with an extensive list of logging- and web-frameworks. Get the full overview here. The table below shows all of the frameworks with official support. The Async column contains a checkmark if the integration is logging asynchronously. The Bulk column contains a checkmark if the integration is logging to elmah.io in bulk. In addition, elmah.io can be installed in a range of different products and services not mentioned (using the integrations below). Look through the menu to the left to see all of the possible integrations. Framework Stable Build Source Samples Async Bulk ASP.NET \u2714\ufe0f ASP.NET MVC \ufe0f\u2714\ufe0f ASP.NET Web API \u2714\ufe0f ASP.NET Core \u2714\ufe0f Blazor \u2714\ufe0f Microsoft.Extensions.Logging \u2714\ufe0f \u2714\ufe0f Serilog \u2714\ufe0f \u2714\ufe0f NLog \u2714\ufe0f \u2714\ufe0f log4net \u2714\ufe0f Umbraco \u2714\ufe0f Azure Websites \u2714\ufe0f Azure Functions \u2714\ufe0f Isolated Azure Functions \u2714\ufe0f JavaScript \u2714\ufe0f Xamarin \u2714\ufe0f Uno \u2714\ufe0f WPF \u2714\ufe0f","title":"Integrations high-level overview"},{"location":"javascript-troubleshooting/","text":"JavaScript Troubleshooting Errors aren't logged If errors aren't logged from JavaScript, here's a list of things to try out: Make sure that the log with the specified ID exists. Make sure that the log isn't disabled and/or contains any ignore filters that could ignore client-side errors. Make sure that the API key is valid and contains the Messages | Write permission. Enable debugging when initializing elmah.io.javascript to get additional debug and error messages from within the script printed to the browser console: new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID', debug: true }); If your webserver includes the Content-Security-Policy header make sure to include api.elmah.io as an allowed domain. Missing information on log messages When logging uncaught errors with elmah.io.javascript you get a lot of additional information stored as part of the log messages. Like the client IP and browser details. If you don't see this information on the messages logged from your application, it's probably because you are using the log function: logger.log({ title: 'This is a custom log message', severity: 'Error' }); The log function only logs what you tell it to log. To include the additional information, switch to use the message builder: var msg = logger.message(); // Get a prefilled message msg.title = 'This is a custom log message'; msg.severity = 'Error'; logger.log(msg); Missing stack trace on errors If errors logged through elmah.io.javascript have a stack trace, it is logged as part of the error on elmah.io. If errors don't include a stack trace, the following actions may fix it: Not all errors include a stack trace. Make sure that the thrown error does include a stack trace by inspecting: e.stack Move the elmahio.js script import to the top of the list of all referenced JavaScript files. Remove any defer or async attributes from the elmahio.js script import. The elmahio.js script import can include those attributes, but errors during initialization may not include stack trace or even be omitted if elmah.io.javascript hasn't been loaded yet. CORS problems when running on localhost When running with elmah.io.javascript on localhost you may see errors in the console like this: Access to XMLHttpRequest at 'https://api.elmah.io/v3/messages/...' from origin 'http://localhost' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Browsers like Chrome don't allow CORS when running locally. There are three ways to fix this: Run Chrome with the --disable-web-security switch. Run your website on a hostname like https://mymachine . Allow CORS on localhost with extensions like CORS Unblock for Chrome or Allow CORS: Access-Control-Allow-Origin for Firefox.","title":"JavaScript troubleshooting"},{"location":"javascript-troubleshooting/#javascript-troubleshooting","text":"","title":"JavaScript Troubleshooting"},{"location":"javascript-troubleshooting/#errors-arent-logged","text":"If errors aren't logged from JavaScript, here's a list of things to try out: Make sure that the log with the specified ID exists. Make sure that the log isn't disabled and/or contains any ignore filters that could ignore client-side errors. Make sure that the API key is valid and contains the Messages | Write permission. Enable debugging when initializing elmah.io.javascript to get additional debug and error messages from within the script printed to the browser console: new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID', debug: true }); If your webserver includes the Content-Security-Policy header make sure to include api.elmah.io as an allowed domain.","title":"Errors aren't logged"},{"location":"javascript-troubleshooting/#missing-information-on-log-messages","text":"When logging uncaught errors with elmah.io.javascript you get a lot of additional information stored as part of the log messages. Like the client IP and browser details. If you don't see this information on the messages logged from your application, it's probably because you are using the log function: logger.log({ title: 'This is a custom log message', severity: 'Error' }); The log function only logs what you tell it to log. To include the additional information, switch to use the message builder: var msg = logger.message(); // Get a prefilled message msg.title = 'This is a custom log message'; msg.severity = 'Error'; logger.log(msg);","title":"Missing information on log messages"},{"location":"javascript-troubleshooting/#missing-stack-trace-on-errors","text":"If errors logged through elmah.io.javascript have a stack trace, it is logged as part of the error on elmah.io. If errors don't include a stack trace, the following actions may fix it: Not all errors include a stack trace. Make sure that the thrown error does include a stack trace by inspecting: e.stack Move the elmahio.js script import to the top of the list of all referenced JavaScript files. Remove any defer or async attributes from the elmahio.js script import. The elmahio.js script import can include those attributes, but errors during initialization may not include stack trace or even be omitted if elmah.io.javascript hasn't been loaded yet.","title":"Missing stack trace on errors"},{"location":"javascript-troubleshooting/#cors-problems-when-running-on-localhost","text":"When running with elmah.io.javascript on localhost you may see errors in the console like this: Access to XMLHttpRequest at 'https://api.elmah.io/v3/messages/...' from origin 'http://localhost' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Browsers like Chrome don't allow CORS when running locally. There are three ways to fix this: Run Chrome with the --disable-web-security switch. Run your website on a hostname like https://mymachine . Allow CORS on localhost with extensions like CORS Unblock for Chrome or Allow CORS: Access-Control-Allow-Origin for Firefox.","title":"CORS problems when running on localhost"},{"location":"logging-breadcrumbs-from-asp-net-core/","text":"Logging breadcrumbs from ASP.NET Core Logging breadcrumbs from ASP.NET Core Manually logging breadcrumbs Logging breadcrumbs from Microsoft.Extensions.Logging Filtering breadcrumbs You can log one or more breadcrumbs as part of both automatic and manually logged errors. Breadcrumbs indicate steps happening just before a message logged by Elmah.Io.AspNetCore . Breadcrumbs with elmah.io and ASP.NET Core are supported in two ways: manual and through Microsoft.Extensions.Logging. Manually logging breadcrumbs If you want to log a breadcrumb manually as part of an MVC controller action or similar, you can use the ElmahIoApi class: ElmahIoApi.AddBreadcrumb( new Breadcrumb(DateTime.UtcNow, message: \"Requesting the front page\"), HttpContext); Notice that the Breadcrumb class is located in the Elmah.Io.Client package that will be automatically installed when installing Elmah.Io.AspNetCore . The Breadcrumb class is either in the Elmah.Io.Client or Elmah.Io.Client.Models namespace, depending on which version of Elmah.Io.Client you have installed. The best example of a helpful breadcrumb is logging the input model to all endpoints as a breadcrumb. This will show you exactly which parameters the user sends to your website. The following example is created for ASP.NET Core MVC, but similar solutions can be built for other MVC features as well. Create a new class named BreadcrumbFilterAttribute and place it somewhere inside your MVC project: public class BreadcrumbFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { var arguments = context.ActionArguments; if (arguments.Count == 0) return; ElmahIoApi.AddBreadcrumb( new Breadcrumb( DateTime.UtcNow, \"Information\", \"Request\", string.Join(\", \", arguments.Select(a => $\"{a.Key} = {JsonSerializer.Serialize(a.Value)}\"))), context.HttpContext); } } The action filter converts the action arguments to a comma-separated string and logs it as a breadcrumb. You can either decorate each controller with the BreadcrumbFilterAttribute or add it globally: builder.Services.AddControllersWithViews(options => { options.Filters.Add(new BreadcrumbFilterAttribute()); }); Logging breadcrumbs from Microsoft.Extensions.Logging We also provide an automatic generation of breadcrumbs using Microsoft.Extensions.Logging. This will pick up all log messages logged through the ILogger and include those as part of an error logged. This behavior is currently in opt-in mode, meaning that you will need to enable it in options: builder.Services.AddElmahIo(options => { // ... options.TreatLoggingAsBreadcrumbs = true; }); The boolean can also be configured through appsettings.json : { // ... \"ElmahIo\": { // ... \"TreatLoggingAsBreadcrumbs\": true } } When enabling this automatic behavior, you may need to adjust the log level included as breadcrumbs. This is done in the appsettings.json file by including the following JSON: { \"Logging\": { // ... \"ElmahIoBreadcrumbs\": { \"LogLevel\": { \"Default\": \"Information\" } } } } Filtering breadcrumbs Breadcrumbs can be filtered using one or more rules as well: builder.Services.AddElmahIo(options => { // ... options.OnFilterBreadcrumb = breadcrumb => breadcrumb.Message == \"A message we don't want as a breadcrumb\"; });","title":"Logging breadcrumbs from ASP.NET Core"},{"location":"logging-breadcrumbs-from-asp-net-core/#logging-breadcrumbs-from-aspnet-core","text":"Logging breadcrumbs from ASP.NET Core Manually logging breadcrumbs Logging breadcrumbs from Microsoft.Extensions.Logging Filtering breadcrumbs You can log one or more breadcrumbs as part of both automatic and manually logged errors. Breadcrumbs indicate steps happening just before a message logged by Elmah.Io.AspNetCore . Breadcrumbs with elmah.io and ASP.NET Core are supported in two ways: manual and through Microsoft.Extensions.Logging.","title":"Logging breadcrumbs from ASP.NET Core"},{"location":"logging-breadcrumbs-from-asp-net-core/#manually-logging-breadcrumbs","text":"If you want to log a breadcrumb manually as part of an MVC controller action or similar, you can use the ElmahIoApi class: ElmahIoApi.AddBreadcrumb( new Breadcrumb(DateTime.UtcNow, message: \"Requesting the front page\"), HttpContext); Notice that the Breadcrumb class is located in the Elmah.Io.Client package that will be automatically installed when installing Elmah.Io.AspNetCore . The Breadcrumb class is either in the Elmah.Io.Client or Elmah.Io.Client.Models namespace, depending on which version of Elmah.Io.Client you have installed. The best example of a helpful breadcrumb is logging the input model to all endpoints as a breadcrumb. This will show you exactly which parameters the user sends to your website. The following example is created for ASP.NET Core MVC, but similar solutions can be built for other MVC features as well. Create a new class named BreadcrumbFilterAttribute and place it somewhere inside your MVC project: public class BreadcrumbFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { var arguments = context.ActionArguments; if (arguments.Count == 0) return; ElmahIoApi.AddBreadcrumb( new Breadcrumb( DateTime.UtcNow, \"Information\", \"Request\", string.Join(\", \", arguments.Select(a => $\"{a.Key} = {JsonSerializer.Serialize(a.Value)}\"))), context.HttpContext); } } The action filter converts the action arguments to a comma-separated string and logs it as a breadcrumb. You can either decorate each controller with the BreadcrumbFilterAttribute or add it globally: builder.Services.AddControllersWithViews(options => { options.Filters.Add(new BreadcrumbFilterAttribute()); });","title":"Manually logging breadcrumbs"},{"location":"logging-breadcrumbs-from-asp-net-core/#logging-breadcrumbs-from-microsoftextensionslogging","text":"We also provide an automatic generation of breadcrumbs using Microsoft.Extensions.Logging. This will pick up all log messages logged through the ILogger and include those as part of an error logged. This behavior is currently in opt-in mode, meaning that you will need to enable it in options: builder.Services.AddElmahIo(options => { // ... options.TreatLoggingAsBreadcrumbs = true; }); The boolean can also be configured through appsettings.json : { // ... \"ElmahIo\": { // ... \"TreatLoggingAsBreadcrumbs\": true } } When enabling this automatic behavior, you may need to adjust the log level included as breadcrumbs. This is done in the appsettings.json file by including the following JSON: { \"Logging\": { // ... \"ElmahIoBreadcrumbs\": { \"LogLevel\": { \"Default\": \"Information\" } } } }","title":"Logging breadcrumbs from Microsoft.Extensions.Logging"},{"location":"logging-breadcrumbs-from-asp-net-core/#filtering-breadcrumbs","text":"Breadcrumbs can be filtered using one or more rules as well: builder.Services.AddElmahIo(options => { // ... options.OnFilterBreadcrumb = breadcrumb => breadcrumb.Message == \"A message we don't want as a breadcrumb\"; });","title":"Filtering breadcrumbs"},{"location":"logging-custom-data/","text":"Logging custom data ELMAH stores a lot of contextual information when an error occurs. Things like cookies, stack trace, server variables, and much more are stored to ease debugging the error at a later point in time. Most error log implementations for ELMAH doesn't support custom variables. Luckily, this is not the case for the elmah.io client. Let's look at some code. You have two options for decorating your errors with custom variables. Use the Data dictionary on .NET's Exception type All exceptions in .NET contain a property named Data and of type IDictionary . The Data dictionary is intended for user-defined information about the exception. The elmah.io client iterates through key/values in this dictionary and ships it off to elmah.io's API . To log custom data using Data , just add a new key/value pair to the Data dictionary: try { CallSomeBusinessLogic(inputValue); } catch (Exception e) { e.Data.Add(\"InputValueWas\", inputValue); ErrorSignal.FromCurrentContext().Raise(e); } In the example, a custom variable named InputValueWas with the value of the inputValue variable is added. This way you will be able to see which input value caused the exception. Use the OnMessage hook in the elmah.io client You may not use ELMAH's ErrorSignal feature but rely on ELMAH to log uncaught exceptions only. In this scenario, you probably don't have access to the thrown exception. The elmah.io client offers a hook for you to be able to execute code every time something is logged: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = Elmah.Io.ErrorLog.Client; logger.OnMessage += (sender, args) => { if (args.Message.Data == null) args.Message.Data = new List<Item>(); args.Message.Data.Add(new Item { Key = \"SomeOtherVariable\", Value = someVariable }); }; You may not have seen the Logger type of elmah.io before, but what's important to know right now is, that Logger is responsible for logging messages to the elmah.io API. Another new term here is Message. A message is the type encapsulating all of the information about the thrown exception. In the code example, a new event handler is subscribed to the OnMessage event. This tells the elmah.io client to execute your event handler, before actually logging an exception to elmah.io. The event is used to add a custom variable to the Data dictionary of the message logged to elmah.io. Looking at your custom data Custom data are shown on the Data tab on the extended messages details page. To open inspect custom data go to the log search page, extend a log message, click the three bars (hamburger) icon in the upper right corner. The custom data is beneath the Data tab. As the content in the other tabs of the message details, you will be able to filter results by the variable key.","title":"Logging custom data"},{"location":"logging-custom-data/#logging-custom-data","text":"ELMAH stores a lot of contextual information when an error occurs. Things like cookies, stack trace, server variables, and much more are stored to ease debugging the error at a later point in time. Most error log implementations for ELMAH doesn't support custom variables. Luckily, this is not the case for the elmah.io client. Let's look at some code. You have two options for decorating your errors with custom variables.","title":"Logging custom data"},{"location":"logging-custom-data/#use-the-data-dictionary-on-nets-exception-type","text":"All exceptions in .NET contain a property named Data and of type IDictionary . The Data dictionary is intended for user-defined information about the exception. The elmah.io client iterates through key/values in this dictionary and ships it off to elmah.io's API . To log custom data using Data , just add a new key/value pair to the Data dictionary: try { CallSomeBusinessLogic(inputValue); } catch (Exception e) { e.Data.Add(\"InputValueWas\", inputValue); ErrorSignal.FromCurrentContext().Raise(e); } In the example, a custom variable named InputValueWas with the value of the inputValue variable is added. This way you will be able to see which input value caused the exception.","title":"Use the Data dictionary on .NET's Exception type"},{"location":"logging-custom-data/#use-the-onmessage-hook-in-the-elmahio-client","text":"You may not use ELMAH's ErrorSignal feature but rely on ELMAH to log uncaught exceptions only. In this scenario, you probably don't have access to the thrown exception. The elmah.io client offers a hook for you to be able to execute code every time something is logged: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = Elmah.Io.ErrorLog.Client; logger.OnMessage += (sender, args) => { if (args.Message.Data == null) args.Message.Data = new List<Item>(); args.Message.Data.Add(new Item { Key = \"SomeOtherVariable\", Value = someVariable }); }; You may not have seen the Logger type of elmah.io before, but what's important to know right now is, that Logger is responsible for logging messages to the elmah.io API. Another new term here is Message. A message is the type encapsulating all of the information about the thrown exception. In the code example, a new event handler is subscribed to the OnMessage event. This tells the elmah.io client to execute your event handler, before actually logging an exception to elmah.io. The event is used to add a custom variable to the Data dictionary of the message logged to elmah.io.","title":"Use the OnMessage hook in the elmah.io client"},{"location":"logging-custom-data/#looking-at-your-custom-data","text":"Custom data are shown on the Data tab on the extended messages details page. To open inspect custom data go to the log search page, extend a log message, click the three bars (hamburger) icon in the upper right corner. The custom data is beneath the Data tab. As the content in the other tabs of the message details, you will be able to filter results by the variable key.","title":"Looking at your custom data"},{"location":"logging-errors-programmatically/","text":"Logging errors programmatically So you've set up a shiny new ELMAH log and all of your unhandled errors are logged to ELMAH. Now you're wondering: \"How do I log my handled errors to ELMAH programmatically?\" You are in luck! ELMAH provides a nice API to do just that through error signaling. Consider the following code: try { int i = 0; int result = 42 / i; } catch (DivideByZeroException e) { // What to do? } In this example, a System.DivideByZeroException is thrown when trying to divide by zero, but what if we want to catch (and log) that exception instead of throwing it back through the call stack? With ELMAH's ErrorSignal class we can log the error: try { int i = 0; int result = 42 / i; } catch (DivideByZeroException e) { ErrorSignal.FromCurrentContext().Raise(e); } We call the static method FromCurrentContext on the ErrorSignal class, which returns a new object for doing the actual logging. Logging happens through the Raise method, which logs the exception to the configured ELMAH storage endpoint. In the example above, I use the FromCurrentContext helper to create a new instance of ErrorSignal . ELMAH also works outside the context of a webserver and in this case, you would simply use the default logger with null as the HTTP context: ErrorLog.GetDefault(null).Log(new Error(e)); If you simply want to log text messages and don't need all of the HTTP context information, consider using one of our integrations for popular logging frameworks like log4net , NLog , or Serilog . Also, the Elmah.Io.Client package contains a logging API documented here .","title":"Logging errors programmatically"},{"location":"logging-errors-programmatically/#logging-errors-programmatically","text":"So you've set up a shiny new ELMAH log and all of your unhandled errors are logged to ELMAH. Now you're wondering: \"How do I log my handled errors to ELMAH programmatically?\" You are in luck! ELMAH provides a nice API to do just that through error signaling. Consider the following code: try { int i = 0; int result = 42 / i; } catch (DivideByZeroException e) { // What to do? } In this example, a System.DivideByZeroException is thrown when trying to divide by zero, but what if we want to catch (and log) that exception instead of throwing it back through the call stack? With ELMAH's ErrorSignal class we can log the error: try { int i = 0; int result = 42 / i; } catch (DivideByZeroException e) { ErrorSignal.FromCurrentContext().Raise(e); } We call the static method FromCurrentContext on the ErrorSignal class, which returns a new object for doing the actual logging. Logging happens through the Raise method, which logs the exception to the configured ELMAH storage endpoint. In the example above, I use the FromCurrentContext helper to create a new instance of ErrorSignal . ELMAH also works outside the context of a webserver and in this case, you would simply use the default logger with null as the HTTP context: ErrorLog.GetDefault(null).Log(new Error(e)); If you simply want to log text messages and don't need all of the HTTP context information, consider using one of our integrations for popular logging frameworks like log4net , NLog , or Serilog . Also, the Elmah.Io.Client package contains a logging API documented here .","title":"Logging errors programmatically"},{"location":"logging-from-a-custom-http-module/","text":"Logging from a custom HTTP module Some developers like to gather all logging into a single module. An example of this would be to log to multiple log destinations and maybe even enrich log messages to multiple loggers with the same info. We always recommend using the modules and handlers that come with ELMAH. But in case you want to log from a module manually, here's the recipe: public class CustomLoggingModule : IHttpModule { public void Init(HttpApplication context) { context.Error += Application_Error; } public void Application_Error(object sender, EventArgs messageData) { HttpApplication application = (HttpApplication)sender; var context = application.Context; var error = new Error(application.Server.GetLastError(), context); var log = ErrorLog.GetDefault(context); log.Log(error); } public void Dispose() { } } In the example, I've created a new module named CustomLoggingModule . The module needs to be configured in web.config as explained here . When starting up the application, ASP.NET calls the Init -method. In this method, an Error event handler is set. Every time a new error is happening in your web application, ASP.NET now calls the Application_Error -method. In this method, I wrap the last thrown error in ELMAH's Error object and log it through the ErrorLog class. Be aware that logging errors this way, disables ELMAH's built-in events like filtering.","title":"Logging from a custom HTTP module"},{"location":"logging-from-a-custom-http-module/#logging-from-a-custom-http-module","text":"Some developers like to gather all logging into a single module. An example of this would be to log to multiple log destinations and maybe even enrich log messages to multiple loggers with the same info. We always recommend using the modules and handlers that come with ELMAH. But in case you want to log from a module manually, here's the recipe: public class CustomLoggingModule : IHttpModule { public void Init(HttpApplication context) { context.Error += Application_Error; } public void Application_Error(object sender, EventArgs messageData) { HttpApplication application = (HttpApplication)sender; var context = application.Context; var error = new Error(application.Server.GetLastError(), context); var log = ErrorLog.GetDefault(context); log.Log(error); } public void Dispose() { } } In the example, I've created a new module named CustomLoggingModule . The module needs to be configured in web.config as explained here . When starting up the application, ASP.NET calls the Init -method. In this method, an Error event handler is set. Every time a new error is happening in your web application, ASP.NET now calls the Application_Error -method. In this method, I wrap the last thrown error in ELMAH's Error object and log it through the ErrorLog class. Be aware that logging errors this way, disables ELMAH's built-in events like filtering.","title":"Logging from a custom HTTP module"},{"location":"logging-heartbeats-from-asp-net-core/","text":"Logging heartbeats from ASP.NET Core Logging heartbeats from ASP.NET Core Additional options Application name Callbacks Period Ignoring heartbeats on localhost, staging, etc. Troubleshooting ASP.NET Core offers a feature called Health Checks from version 2.2 and forward. For more information about health checks, check out our blog post: ASP.NET Core 2.2 Health Checks Explained . The Heartbeats feature on elmah.io supports health checks too, by publishing health check results as heartbeats. To publish health checks as elmah.io heartbeats, install the Elmah.Io.AspNetCore.HealthChecks NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.HealthChecks dotnet add package Elmah.Io.AspNetCore.HealthChecks <PackageReference Include=\"Elmah.Io.AspNetCore.HealthChecks\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.HealthChecks Then configure the elmah.io health check publisher: builder .Services .AddHealthChecks() .AddElmahIoPublisher(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); options.HeartbeatId = \"HEARTBEAT_ID\"; }); Replace the variables with the correct values as explained in Set up Heartbeats . Remember to use an API key that includes the Heartbeats - Write permission. Additional options Application name Much like the error logging integration with ASP.NET Core, you can set an application name on log messages produced by Heartbeats. To do so, set the Application property when adding the publisher: .AddElmahIoPublisher(options => { ... options.Application = \"My app\"; }); If Application is not set, log messages will receive a default value of Heartbeats to make the messages distinguishable from other messages. Callbacks The elmah.io publisher offer callbacks already known from Elmah.Io.AspNetCore . OnHeartbeat The OnHeartbeat callback can be used to set a version number on all log messages produced by a heartbeat and/or trigger custom code every time a heartbeat is logged to elmah.io: .AddElmahIoPublisher(options => { ... options.OnHeartbeat = hb => { hb.Version = \"1.0.0\"; }; }); OnFilter The OnFilter callback can used to ignore one or more heartbeats: .AddElmahIoPublisher(options => { ... options.OnFilter = hb => { return hb.Result == \"Degraded\"; }; }); The example ignores any Degraded heartbeats. OnError The OnError callback can be used to listen for errors communicating with the elmah.io API: .AddElmahIoPublisher(options => { ... options.OnError = (hb, ex) => { // Do something }; }); The elmah.io publisher already logs any internal errors through Microsoft.Extensions.Logging, why you don't need to do that in the OnError handler. If you are using another logging framework and don't have that hooked up on Microsoft.Extensions.Logging, the OnError is a good place to add some additional logging. Period As default, ASP.NET Core runs health checks every 30 seconds when setting up a publisher. To change this interval, add the following code: builder.Services.Configure<HealthCheckPublisherOptions>(options => { options.Period = TimeSpan.FromMinutes(5); }); There's a bug in ASP.NET Core 2.2 that requires you to use reflection when setting Period : builder.Services.Configure<HealthCheckPublisherOptions>(options => { var prop = options.GetType().GetField(\"_period\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); prop.SetValue(options, TimeSpan.FromMinutes(5)); }); If setting Period to 5 minutes, you should set the heartbeat interval on elmah.io to 5 minutes and grace to 1 minute. Ignoring heartbeats on localhost, staging, etc. Monitoring heartbeats is important in your production environment. When running locally or even on staging, you probably don't want to monitor heartbeats. ASP.NET Core health checks doesn't seem to support a great deal of configuration through appsettings.json , Azure app settings, etc. The easiest way to tell ASP.NET Core to log heartbeats to elmah.io is to avoid setting up health checks unless a heartbeat id is configured: if (!string.IsNullOrWhiteSpace(Configuration[\"ElmahIo:HeartbeatId\"])) { builder.Services.AddHealthChecks().AddElmahIoPublisher(); } In this example, we only configure health checks and the elmah.io publisher if the ElmahIo:HeartbeatId setting is defined in config. Troubleshooting Here's a list of things to check for if no heartbeats are registered: Did you include both API_KEY , LOG_ID , and HEARTBEAT_ID ? The publisher needs to be called before the AddElmahIo call from Elmah.Io.AspNetCore : builder .Services .AddHealthChecks() .AddElmahIoPublisher(); builder.Services.AddElmahIo(); If you are using Health Checks UI, it needs to be configured after the AddElmahIoPublisher -method: builder .Services .AddHealthChecks() .AddElmahIoPublisher(); builder .Services .AddHealthChecksUI();","title":"Logging heartbeats from ASP.NET Core"},{"location":"logging-heartbeats-from-asp-net-core/#logging-heartbeats-from-aspnet-core","text":"Logging heartbeats from ASP.NET Core Additional options Application name Callbacks Period Ignoring heartbeats on localhost, staging, etc. Troubleshooting ASP.NET Core offers a feature called Health Checks from version 2.2 and forward. For more information about health checks, check out our blog post: ASP.NET Core 2.2 Health Checks Explained . The Heartbeats feature on elmah.io supports health checks too, by publishing health check results as heartbeats. To publish health checks as elmah.io heartbeats, install the Elmah.Io.AspNetCore.HealthChecks NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.HealthChecks dotnet add package Elmah.Io.AspNetCore.HealthChecks <PackageReference Include=\"Elmah.Io.AspNetCore.HealthChecks\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.HealthChecks Then configure the elmah.io health check publisher: builder .Services .AddHealthChecks() .AddElmahIoPublisher(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); options.HeartbeatId = \"HEARTBEAT_ID\"; }); Replace the variables with the correct values as explained in Set up Heartbeats . Remember to use an API key that includes the Heartbeats - Write permission.","title":"Logging heartbeats from ASP.NET Core"},{"location":"logging-heartbeats-from-asp-net-core/#additional-options","text":"","title":"Additional options"},{"location":"logging-heartbeats-from-asp-net-core/#application-name","text":"Much like the error logging integration with ASP.NET Core, you can set an application name on log messages produced by Heartbeats. To do so, set the Application property when adding the publisher: .AddElmahIoPublisher(options => { ... options.Application = \"My app\"; }); If Application is not set, log messages will receive a default value of Heartbeats to make the messages distinguishable from other messages.","title":"Application name"},{"location":"logging-heartbeats-from-asp-net-core/#callbacks","text":"The elmah.io publisher offer callbacks already known from Elmah.Io.AspNetCore . OnHeartbeat The OnHeartbeat callback can be used to set a version number on all log messages produced by a heartbeat and/or trigger custom code every time a heartbeat is logged to elmah.io: .AddElmahIoPublisher(options => { ... options.OnHeartbeat = hb => { hb.Version = \"1.0.0\"; }; }); OnFilter The OnFilter callback can used to ignore one or more heartbeats: .AddElmahIoPublisher(options => { ... options.OnFilter = hb => { return hb.Result == \"Degraded\"; }; }); The example ignores any Degraded heartbeats. OnError The OnError callback can be used to listen for errors communicating with the elmah.io API: .AddElmahIoPublisher(options => { ... options.OnError = (hb, ex) => { // Do something }; }); The elmah.io publisher already logs any internal errors through Microsoft.Extensions.Logging, why you don't need to do that in the OnError handler. If you are using another logging framework and don't have that hooked up on Microsoft.Extensions.Logging, the OnError is a good place to add some additional logging.","title":"Callbacks"},{"location":"logging-heartbeats-from-asp-net-core/#period","text":"As default, ASP.NET Core runs health checks every 30 seconds when setting up a publisher. To change this interval, add the following code: builder.Services.Configure<HealthCheckPublisherOptions>(options => { options.Period = TimeSpan.FromMinutes(5); }); There's a bug in ASP.NET Core 2.2 that requires you to use reflection when setting Period : builder.Services.Configure<HealthCheckPublisherOptions>(options => { var prop = options.GetType().GetField(\"_period\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); prop.SetValue(options, TimeSpan.FromMinutes(5)); }); If setting Period to 5 minutes, you should set the heartbeat interval on elmah.io to 5 minutes and grace to 1 minute.","title":"Period"},{"location":"logging-heartbeats-from-asp-net-core/#ignoring-heartbeats-on-localhost-staging-etc","text":"Monitoring heartbeats is important in your production environment. When running locally or even on staging, you probably don't want to monitor heartbeats. ASP.NET Core health checks doesn't seem to support a great deal of configuration through appsettings.json , Azure app settings, etc. The easiest way to tell ASP.NET Core to log heartbeats to elmah.io is to avoid setting up health checks unless a heartbeat id is configured: if (!string.IsNullOrWhiteSpace(Configuration[\"ElmahIo:HeartbeatId\"])) { builder.Services.AddHealthChecks().AddElmahIoPublisher(); } In this example, we only configure health checks and the elmah.io publisher if the ElmahIo:HeartbeatId setting is defined in config.","title":"Ignoring heartbeats on localhost, staging, etc."},{"location":"logging-heartbeats-from-asp-net-core/#troubleshooting","text":"Here's a list of things to check for if no heartbeats are registered: Did you include both API_KEY , LOG_ID , and HEARTBEAT_ID ? The publisher needs to be called before the AddElmahIo call from Elmah.Io.AspNetCore : builder .Services .AddHealthChecks() .AddElmahIoPublisher(); builder.Services.AddElmahIo(); If you are using Health Checks UI, it needs to be configured after the AddElmahIoPublisher -method: builder .Services .AddHealthChecks() .AddElmahIoPublisher(); builder .Services .AddHealthChecksUI();","title":"Troubleshooting"},{"location":"logging-heartbeats-from-aws-lambdas/","text":"Logging heartbeats from AWS Lambdas AWS Lambdas running on a schedule are great candidates for logging heartbeats to elmah.io. To send a healthy heartbeat when the Lambda runs successfully and an unhealthy heartbeat when an error happens, start by installing the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Include elmah.io API key, log ID, and heartbeat ID in your code. In this example, they are added as static fields: private const string ApiKey = \"API_KEY\"; private const string HeartbeatId = \"HEARTBEAT_ID\"; private static Guid LogId = new Guid(\"LOG_ID\"); Replace API_KEY with an API key with the Heartbeats | Write permission ( Where is my API key? ), HEARTBEAT_ID with the ID of the heartbeat available on the elmah.io UI, and LOG_ID with the ID of the log containing the heartbeat ( Where is my log ID? ). Create the elmah.io client and store the IHeartbeat object somewhere. In the following example, it is initialized in the Main method and stored in a static field: private static IHeartbeats heartbeats; private static async Task Main(string[] args) { heartbeats = ElmahioAPI.Create(ApiKey).Heartbeats; // ... } In the function handler, wrap your code in try/catch and call either the Healthy or Unhealthy method: public static string FunctionHandler(string input, ILambdaContext context) { try { // Lambda code goes here heartbeats.Healthy(LogId, HeartbeatId); return input?.ToUpper(); } catch (Exception e) { heartbeats.Unhealthy(LogId, HeartbeatId, e.ToString()); throw; } } When the lambda code runs (replace the Lambda code goes here comment with your code) without exceptions, a healthy heartbeat is logged to elmah.io. In case of an exception, an unhealthy heartbeat is logged to elmah.io. In case your Lambda doesn't run at all, elmah.io automatically logs a missing heartbeat.","title":"Logging heartbeats from AWS Lambdas"},{"location":"logging-heartbeats-from-aws-lambdas/#logging-heartbeats-from-aws-lambdas","text":"AWS Lambdas running on a schedule are great candidates for logging heartbeats to elmah.io. To send a healthy heartbeat when the Lambda runs successfully and an unhealthy heartbeat when an error happens, start by installing the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Include elmah.io API key, log ID, and heartbeat ID in your code. In this example, they are added as static fields: private const string ApiKey = \"API_KEY\"; private const string HeartbeatId = \"HEARTBEAT_ID\"; private static Guid LogId = new Guid(\"LOG_ID\"); Replace API_KEY with an API key with the Heartbeats | Write permission ( Where is my API key? ), HEARTBEAT_ID with the ID of the heartbeat available on the elmah.io UI, and LOG_ID with the ID of the log containing the heartbeat ( Where is my log ID? ). Create the elmah.io client and store the IHeartbeat object somewhere. In the following example, it is initialized in the Main method and stored in a static field: private static IHeartbeats heartbeats; private static async Task Main(string[] args) { heartbeats = ElmahioAPI.Create(ApiKey).Heartbeats; // ... } In the function handler, wrap your code in try/catch and call either the Healthy or Unhealthy method: public static string FunctionHandler(string input, ILambdaContext context) { try { // Lambda code goes here heartbeats.Healthy(LogId, HeartbeatId); return input?.ToUpper(); } catch (Exception e) { heartbeats.Unhealthy(LogId, HeartbeatId, e.ToString()); throw; } } When the lambda code runs (replace the Lambda code goes here comment with your code) without exceptions, a healthy heartbeat is logged to elmah.io. In case of an exception, an unhealthy heartbeat is logged to elmah.io. In case your Lambda doesn't run at all, elmah.io automatically logs a missing heartbeat.","title":"Logging heartbeats from AWS Lambdas"},{"location":"logging-heartbeats-from-azure-functions/","text":"Logging heartbeats from Azure Functions Logging heartbeats from Azure Functions Using a filter in Elmah.Io.Functions Manually using Elmah.Io.Client Using a separate heartbeat function Azure Functions are great candidates for adding heartbeats. For web APIs implemented with Azure Functions, you should create a /health endpoint and ping that using Uptime Monitoring . But for timer triggered, queue triggers, and similar function apps, heartbeats are a great way to verify that your function is successfully running. The rest of this document is split into different ways of adding heartbeats to one or more functions. Using a filter in Elmah.Io.Functions The easiest way of including a heartbeat is to include the ElmahIoHeartbeatFilter available in the Elmah.Io.Functions package. This will automatically publish a Healthy or Unhealthy heartbeat, depending on if your functions execute successfully. This option is great for timer-triggered functions like nightly batch jobs. Start by installing the Elmah.Io.Functions package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions dotnet add package Elmah.Io.Functions <PackageReference Include=\"Elmah.Io.Functions\" Version=\"5.*\" /> paket add Elmah.Io.Functions Elmah.Io.Functions requires dependency injection part of the Microsoft.Azure.Functions.Extensions package, why you will need this package if not already added. Extend the Startup.cs (or whatever you named your function startup class) file with the following code: using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Elmah.Io.Functions; [assembly: FunctionsStartup(typeof(My.FunctionApp.Startup))] namespace My.FunctionApp { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { var config = new ConfigurationBuilder() .AddJsonFile(\"local.settings.json\", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.HeartbeatId = config[\"heartbeatId\"]; }); builder.Services.AddSingleton<IFunctionFilter, ElmahIoHeartbeatFilter>(); } } } The code installs the ElmahIoHeartbeatFilter class, which will handle all of the communication with the elmah.io API. Finally, add the config variables ( apiKey , logId , and heartbeatId ) to the local.settings.json file, environment variables, Azure configuration settings, or in whatever way you specify settings for your function app. Manually using Elmah.Io.Client The example above installs the heartbeat filter for all functions. If you have multiple functions inside your function app, or you want greater control of when and how to send heartbeats, you can use Elmah.Io.Client to create heartbeats. Start by installing the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Extend the Startup.cs file with the following code: using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Elmah.Io.Client; [assembly: FunctionsStartup(typeof(My.FunctionApp.Startup))] namespace My.FunctionApp { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { var config = new ConfigurationBuilder() .AddJsonFile(\"local.settings.json\", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.AddSingleton(config); var elmahIo = ElmahioAPI.Create(config[\"apiKey\"]); builder.Services.AddSingleton(elmahIo.Heartbeats); } } } Inside your function, wrap all of the code in try/catch and add code to create either a Healthy or Unhealthy heartbeat: using System; using System.Threading.Tasks; using Elmah.Io.Client; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; namespace My.FunctionApp { public class TimedFunction { private readonly IHeartbeats heartbeats; private readonly IConfiguration configuration; public TimedFunction(IHeartbeats heartbeats, IConfiguration configuration) { this.heartbeats = heartbeats; this.configuration = configuration; } [FunctionName(\"TimedFunction\")] public async Task Run([TimerTrigger(\"0 0 * * * *\")]TimerInfo myTimer) { var heartbeatId = configuration[\"heartbeatId\"]; var logId = configuration[\"logId\"]; try { // Your function code goes here await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat { Result = \"Healthy\" }); } catch (Exception e) { await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat { Result = \"Unhealthy\", Reason = e.ToString(), }); } } } } If your function code executes successfully, a Healthy heartbeat is created. If an exception is thrown, an Unhealthy heartbeat with the thrown exception in Reason is created. Be aware that configuring a function to run in an internal (like every hour for the example above) does not ensure that the function is executed exactly on the hour. We recommend to set the grace period for these types of heartbeats to 15-30 minutes to avoid heartbeat errors when the timed function is past due. Using a separate heartbeat function You may want a single heartbeat representing your entire function app consisting of multiple functions. This is a good option if you want to create heartbeats from queue-triggered functions or similar. In these cases, you don't want to create a heartbeat every time a message from the queue is handled, but you will want to notify elmah.io if dependencies like database connection suddenly aren't available. We recommend creating a new heartbeat function for this kind of Function. Like in the previous example, make sure to extend your Startup.cs file like this: using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Elmah.Io.Client; [assembly: FunctionsStartup(typeof(My.FunctionApp.Startup))] namespace My.FunctionApp { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { var config = new ConfigurationBuilder() .AddJsonFile(\"local.settings.json\", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.AddSingleton(config); var elmahIo = ElmahioAPI.Create(config[\"apiKey\"]); builder.Services.AddSingleton(elmahIo.Heartbeats); } } } Then create a new timed function with the following code: using System; using System.Threading.Tasks; using Elmah.Io.Client; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; namespace My.FunctionApp { public class Heartbeat { private readonly IConfiguration config; private readonly IHeartbeats heartbeats; public Heartbeat(IHeartbeats heartbeats, IConfiguration config) { this.heartbeats = heartbeats; this.config = config; } [FunctionName(\"Heartbeat\")] public async Task Run([TimerTrigger(\"0 */5 * * * *\")]TimerInfo myTimer) { var result = \"Healthy\"; var reason = (string)null; try { // Check your dependencies here } catch (Exception e) { result = \"Unhealthy\"; reason = e.ToString(); } await heartbeats.CreateAsync(config[\"heartbeatId\"], config[\"logId\"], new CreateHeartbeat { Result = result, Reason = reason, }); } } } In the example above, the new function named Heartbeat (the name is entirely up to you) executes every 5 minutes. Replace the comment with your checks like opening a connection to the database. If everything works as it should, a Healthy heartbeat is logged to elmah.io. If an exception is thrown while checking your dependencies, an Unhealthy heartbeat is created. When running locally, you may want to disable heartbeats. You can use the Disable attribute for that by including the following code: #if DEBUG [Disable] #endif public class Heartbeat { // ... } or add the following to local.settings.json : { // ... \"Values\": { \"AzureWebJobs.Heartbeat.Disabled\": true, // ... } }","title":"Logging heartbeats from Azure Functions"},{"location":"logging-heartbeats-from-azure-functions/#logging-heartbeats-from-azure-functions","text":"Logging heartbeats from Azure Functions Using a filter in Elmah.Io.Functions Manually using Elmah.Io.Client Using a separate heartbeat function Azure Functions are great candidates for adding heartbeats. For web APIs implemented with Azure Functions, you should create a /health endpoint and ping that using Uptime Monitoring . But for timer triggered, queue triggers, and similar function apps, heartbeats are a great way to verify that your function is successfully running. The rest of this document is split into different ways of adding heartbeats to one or more functions.","title":"Logging heartbeats from Azure Functions"},{"location":"logging-heartbeats-from-azure-functions/#using-a-filter-in-elmahiofunctions","text":"The easiest way of including a heartbeat is to include the ElmahIoHeartbeatFilter available in the Elmah.Io.Functions package. This will automatically publish a Healthy or Unhealthy heartbeat, depending on if your functions execute successfully. This option is great for timer-triggered functions like nightly batch jobs. Start by installing the Elmah.Io.Functions package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions dotnet add package Elmah.Io.Functions <PackageReference Include=\"Elmah.Io.Functions\" Version=\"5.*\" /> paket add Elmah.Io.Functions Elmah.Io.Functions requires dependency injection part of the Microsoft.Azure.Functions.Extensions package, why you will need this package if not already added. Extend the Startup.cs (or whatever you named your function startup class) file with the following code: using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Elmah.Io.Functions; [assembly: FunctionsStartup(typeof(My.FunctionApp.Startup))] namespace My.FunctionApp { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { var config = new ConfigurationBuilder() .AddJsonFile(\"local.settings.json\", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.HeartbeatId = config[\"heartbeatId\"]; }); builder.Services.AddSingleton<IFunctionFilter, ElmahIoHeartbeatFilter>(); } } } The code installs the ElmahIoHeartbeatFilter class, which will handle all of the communication with the elmah.io API. Finally, add the config variables ( apiKey , logId , and heartbeatId ) to the local.settings.json file, environment variables, Azure configuration settings, or in whatever way you specify settings for your function app.","title":"Using a filter in Elmah.Io.Functions"},{"location":"logging-heartbeats-from-azure-functions/#manually-using-elmahioclient","text":"The example above installs the heartbeat filter for all functions. If you have multiple functions inside your function app, or you want greater control of when and how to send heartbeats, you can use Elmah.Io.Client to create heartbeats. Start by installing the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Extend the Startup.cs file with the following code: using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Elmah.Io.Client; [assembly: FunctionsStartup(typeof(My.FunctionApp.Startup))] namespace My.FunctionApp { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { var config = new ConfigurationBuilder() .AddJsonFile(\"local.settings.json\", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.AddSingleton(config); var elmahIo = ElmahioAPI.Create(config[\"apiKey\"]); builder.Services.AddSingleton(elmahIo.Heartbeats); } } } Inside your function, wrap all of the code in try/catch and add code to create either a Healthy or Unhealthy heartbeat: using System; using System.Threading.Tasks; using Elmah.Io.Client; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; namespace My.FunctionApp { public class TimedFunction { private readonly IHeartbeats heartbeats; private readonly IConfiguration configuration; public TimedFunction(IHeartbeats heartbeats, IConfiguration configuration) { this.heartbeats = heartbeats; this.configuration = configuration; } [FunctionName(\"TimedFunction\")] public async Task Run([TimerTrigger(\"0 0 * * * *\")]TimerInfo myTimer) { var heartbeatId = configuration[\"heartbeatId\"]; var logId = configuration[\"logId\"]; try { // Your function code goes here await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat { Result = \"Healthy\" }); } catch (Exception e) { await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat { Result = \"Unhealthy\", Reason = e.ToString(), }); } } } } If your function code executes successfully, a Healthy heartbeat is created. If an exception is thrown, an Unhealthy heartbeat with the thrown exception in Reason is created. Be aware that configuring a function to run in an internal (like every hour for the example above) does not ensure that the function is executed exactly on the hour. We recommend to set the grace period for these types of heartbeats to 15-30 minutes to avoid heartbeat errors when the timed function is past due.","title":"Manually using Elmah.Io.Client"},{"location":"logging-heartbeats-from-azure-functions/#using-a-separate-heartbeat-function","text":"You may want a single heartbeat representing your entire function app consisting of multiple functions. This is a good option if you want to create heartbeats from queue-triggered functions or similar. In these cases, you don't want to create a heartbeat every time a message from the queue is handled, but you will want to notify elmah.io if dependencies like database connection suddenly aren't available. We recommend creating a new heartbeat function for this kind of Function. Like in the previous example, make sure to extend your Startup.cs file like this: using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Elmah.Io.Client; [assembly: FunctionsStartup(typeof(My.FunctionApp.Startup))] namespace My.FunctionApp { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { var config = new ConfigurationBuilder() .AddJsonFile(\"local.settings.json\", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.AddSingleton(config); var elmahIo = ElmahioAPI.Create(config[\"apiKey\"]); builder.Services.AddSingleton(elmahIo.Heartbeats); } } } Then create a new timed function with the following code: using System; using System.Threading.Tasks; using Elmah.Io.Client; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; namespace My.FunctionApp { public class Heartbeat { private readonly IConfiguration config; private readonly IHeartbeats heartbeats; public Heartbeat(IHeartbeats heartbeats, IConfiguration config) { this.heartbeats = heartbeats; this.config = config; } [FunctionName(\"Heartbeat\")] public async Task Run([TimerTrigger(\"0 */5 * * * *\")]TimerInfo myTimer) { var result = \"Healthy\"; var reason = (string)null; try { // Check your dependencies here } catch (Exception e) { result = \"Unhealthy\"; reason = e.ToString(); } await heartbeats.CreateAsync(config[\"heartbeatId\"], config[\"logId\"], new CreateHeartbeat { Result = result, Reason = reason, }); } } } In the example above, the new function named Heartbeat (the name is entirely up to you) executes every 5 minutes. Replace the comment with your checks like opening a connection to the database. If everything works as it should, a Healthy heartbeat is logged to elmah.io. If an exception is thrown while checking your dependencies, an Unhealthy heartbeat is created. When running locally, you may want to disable heartbeats. You can use the Disable attribute for that by including the following code: #if DEBUG [Disable] #endif public class Heartbeat { // ... } or add the following to local.settings.json : { // ... \"Values\": { \"AzureWebJobs.Heartbeat.Disabled\": true, // ... } }","title":"Using a separate heartbeat function"},{"location":"logging-heartbeats-from-curl/","text":"Logging heartbeats from cURL Sometimes is just easier to use cURL when needing to call a REST API. Creating elmah.io heartbeats is easy using cURL and fits well into scripts, scheduled tasks, and similar. To create a new heartbeat, include the following cURL command in your script: curl -X POST \"https://api.elmah.io/v3/heartbeats/LOG_ID/HEARTBEAT_ID?api_key=API_KEY\" -H \"accept: application/json\" -H \"Content-Type: application/json-patch+json\" -d \"{ \\\"result\\\": \\\"Healthy\\\"}\" Remember to place LOG_ID , HEARTBEAT_ID , and API_KEY with the values found on the Heartbeats tab in elmah.io. To create an Unhealthy heartbeat, change the result in the body and include a reason : curl -X POST \"https://api.elmah.io/v3/heartbeats/LOG_ID/HEARTBEAT_ID?api_key=API_KEY\" -H \"accept: application/json\" -H \"Content-Type: application/json-patch+json\" -d \"{ \\\"result\\\": \\\"Unhealthy\\\", \\\"reason\\\": \\\"Something isn't working\\\" }\"","title":"Logging heartbeats from cURL"},{"location":"logging-heartbeats-from-curl/#logging-heartbeats-from-curl","text":"Sometimes is just easier to use cURL when needing to call a REST API. Creating elmah.io heartbeats is easy using cURL and fits well into scripts, scheduled tasks, and similar. To create a new heartbeat, include the following cURL command in your script: curl -X POST \"https://api.elmah.io/v3/heartbeats/LOG_ID/HEARTBEAT_ID?api_key=API_KEY\" -H \"accept: application/json\" -H \"Content-Type: application/json-patch+json\" -d \"{ \\\"result\\\": \\\"Healthy\\\"}\" Remember to place LOG_ID , HEARTBEAT_ID , and API_KEY with the values found on the Heartbeats tab in elmah.io. To create an Unhealthy heartbeat, change the result in the body and include a reason : curl -X POST \"https://api.elmah.io/v3/heartbeats/LOG_ID/HEARTBEAT_ID?api_key=API_KEY\" -H \"accept: application/json\" -H \"Content-Type: application/json-patch+json\" -d \"{ \\\"result\\\": \\\"Unhealthy\\\", \\\"reason\\\": \\\"Something isn't working\\\" }\"","title":"Logging heartbeats from cURL"},{"location":"logging-heartbeats-from-hangfire/","text":"Logging heartbeats from Hangfire Scheduling recurring tasks with Hangfire is easy. Monitoring if tasks are successfully executed or even run can be a challenge. With elmah.io Heartbeats, we provide native monitoring of Hangfire recurring tasks. To publish heartbeats from Hangifre, install the Elmah.Io.Heartbeats.Hangfire NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Heartbeats.Hangfire dotnet add package Elmah.Io.Heartbeats.Hangfire <PackageReference Include=\"Elmah.Io.Heartbeats.Hangfire\" Version=\"5.*\" /> paket add Elmah.Io.Heartbeats.Hangfire For this example, we'll schedule a method named Test to execute every minute: RecurringJob.AddOrUpdate(() => Test(), Cron.Minutely); To automatically publish a heartbeat when the job is executed, add the following using : using Elmah.Io.Heartbeats.Hangfire; And decorate the Test -method with the ElmahIoHeartbeat attribute: [ElmahIoHeartbeat(\"API_KEY\", \"LOG_ID\", \"HEARTBEAT_ID\")] public void Test() { // ... } Replace API_KEY ( Where is my API key? ), LOG_ID ( Where is my log ID? ), and HEARTBEAT_ID with the correct variables from elmah.io. When the job successfully runs, a Healthy heartbeat is logged to elmah.io. If an exception is thrown an Unhealthy heartbeat is logged. elmah.io will automatically create an error if a heartbeat is missing, as long as the heartbeat is correctly configured as explained in Set up Heartbeats . Move configuration to config files You normally don't include your API key, log ID, and heartbeat ID in C# code as shown in the example above. Unfortunately, Hangfire attributes doesn't support dependency injection or configuration from config files. There's a small \"hack\" that you can use to move the configuration to a configuration file by creating a custom attribute: using Elmah.Io.Heartbeats.Hangfire; using Hangfire.Common; using Hangfire.Server; using System.Configuration; public class AppSettingsElmahIoHeartbeatAttribute : JobFilterAttribute, IServerFilter { private readonly ElmahIoHeartbeatAttribute _inner; public AppSettingsElmahIoHeartbeatAttribute() { var apiKey = ConfigurationManager.AppSettings[\"apiKey\"]; var logId = ConfigurationManager.AppSettings[\"logId\"]; var heartbeatId = ConfigurationManager.AppSettings[\"heartbeatId\"]; _inner = new ElmahIoHeartbeatAttribute(apiKey, logId, heartbeatId); } public void OnPerformed(PerformedContext filterContext) { _inner.OnPerformed(filterContext); } public void OnPerforming(PerformingContext filterContext) { _inner.OnPerforming(filterContext); } } In the example the AppSettingsElmahIoHeartbeatAttribute class wrap ElmahIoHeartbeatAttribute . By doing so, it is possible to fetch configuration from application settings as part of the constructor. The approach would be similar when using IConfiguration (like in ASP.NET Core), but you will need to share a reference to the configuration object somehow. To use AppSettingsElmahIoHeartbeatAttribute simply add it to the method: [AppSettingsElmahIoHeartbeat] public void Test() { // ... } As an alternative, you can register the ElmahIoHeartbeatAttribute as a global attribute. In this example we use IConfiguration in ASP.NET Core to fetch configuration from the appsettings.json file: public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddHangfire(config => config // ... .UseFilter(new ElmahIoHeartbeatAttribute( Configuration[\"ElmahIo:ApiKey\"], Configuration[\"ElmahIo:LogId\"], Configuration[\"ElmahIo:HeartbeatId\"]))); } // ... } This will execute the ElmahIoHeartbeat filter for every Hangfire job which isn't ideal if running multiple jobs within the same project.","title":"Logging heartbeats from Hangfire"},{"location":"logging-heartbeats-from-hangfire/#logging-heartbeats-from-hangfire","text":"Scheduling recurring tasks with Hangfire is easy. Monitoring if tasks are successfully executed or even run can be a challenge. With elmah.io Heartbeats, we provide native monitoring of Hangfire recurring tasks. To publish heartbeats from Hangifre, install the Elmah.Io.Heartbeats.Hangfire NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Heartbeats.Hangfire dotnet add package Elmah.Io.Heartbeats.Hangfire <PackageReference Include=\"Elmah.Io.Heartbeats.Hangfire\" Version=\"5.*\" /> paket add Elmah.Io.Heartbeats.Hangfire For this example, we'll schedule a method named Test to execute every minute: RecurringJob.AddOrUpdate(() => Test(), Cron.Minutely); To automatically publish a heartbeat when the job is executed, add the following using : using Elmah.Io.Heartbeats.Hangfire; And decorate the Test -method with the ElmahIoHeartbeat attribute: [ElmahIoHeartbeat(\"API_KEY\", \"LOG_ID\", \"HEARTBEAT_ID\")] public void Test() { // ... } Replace API_KEY ( Where is my API key? ), LOG_ID ( Where is my log ID? ), and HEARTBEAT_ID with the correct variables from elmah.io. When the job successfully runs, a Healthy heartbeat is logged to elmah.io. If an exception is thrown an Unhealthy heartbeat is logged. elmah.io will automatically create an error if a heartbeat is missing, as long as the heartbeat is correctly configured as explained in Set up Heartbeats .","title":"Logging heartbeats from Hangfire"},{"location":"logging-heartbeats-from-hangfire/#move-configuration-to-config-files","text":"You normally don't include your API key, log ID, and heartbeat ID in C# code as shown in the example above. Unfortunately, Hangfire attributes doesn't support dependency injection or configuration from config files. There's a small \"hack\" that you can use to move the configuration to a configuration file by creating a custom attribute: using Elmah.Io.Heartbeats.Hangfire; using Hangfire.Common; using Hangfire.Server; using System.Configuration; public class AppSettingsElmahIoHeartbeatAttribute : JobFilterAttribute, IServerFilter { private readonly ElmahIoHeartbeatAttribute _inner; public AppSettingsElmahIoHeartbeatAttribute() { var apiKey = ConfigurationManager.AppSettings[\"apiKey\"]; var logId = ConfigurationManager.AppSettings[\"logId\"]; var heartbeatId = ConfigurationManager.AppSettings[\"heartbeatId\"]; _inner = new ElmahIoHeartbeatAttribute(apiKey, logId, heartbeatId); } public void OnPerformed(PerformedContext filterContext) { _inner.OnPerformed(filterContext); } public void OnPerforming(PerformingContext filterContext) { _inner.OnPerforming(filterContext); } } In the example the AppSettingsElmahIoHeartbeatAttribute class wrap ElmahIoHeartbeatAttribute . By doing so, it is possible to fetch configuration from application settings as part of the constructor. The approach would be similar when using IConfiguration (like in ASP.NET Core), but you will need to share a reference to the configuration object somehow. To use AppSettingsElmahIoHeartbeatAttribute simply add it to the method: [AppSettingsElmahIoHeartbeat] public void Test() { // ... } As an alternative, you can register the ElmahIoHeartbeatAttribute as a global attribute. In this example we use IConfiguration in ASP.NET Core to fetch configuration from the appsettings.json file: public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddHangfire(config => config // ... .UseFilter(new ElmahIoHeartbeatAttribute( Configuration[\"ElmahIo:ApiKey\"], Configuration[\"ElmahIo:LogId\"], Configuration[\"ElmahIo:HeartbeatId\"]))); } // ... } This will execute the ElmahIoHeartbeat filter for every Hangfire job which isn't ideal if running multiple jobs within the same project.","title":"Move configuration to config files"},{"location":"logging-heartbeats-from-isolated-azure-functions/","text":"Logging heartbeats from Isolated Azure Functions Logging heartbeats from Isolated Azure Functions Using middleware in Elmah.Io.Functions.Isolated Manually using Elmah.Io.Client Using a separate heartbeat function Isolated Azure Functions are great candidates for adding heartbeats. For web APIs implemented with Isolated Azure Functions, you should create a /health endpoint and ping that using Uptime Monitoring . But for timer triggered, queue triggers, and similar function apps, heartbeats are a great way to verify that your function is successfully running. The rest of this document is split into different ways of adding heartbeats to one or more functions. Using middleware in Elmah.Io.Functions.Isolated Scheduled functions or functions not running often can use the heartbeat middleware part of the Elmah.Io.Functions.Isolated package. This will log a Healthy or Unhealthy endpoint every time a function is running. All functions within the same function app uses the same middleware, why this is primarily inteded for function apps with one scheduled function. Start by installing the Elmah.Io.Functions.Isolated package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions.Isolated dotnet add package Elmah.Io.Functions.Isolated <PackageReference Include=\"Elmah.Io.Functions.Isolated\" Version=\"5.*\" /> paket add Elmah.Io.Functions.Isolated Extend the Program.cs file with the following code: .ConfigureFunctionsWorkerDefaults((context, app) => { app.AddHeartbeat(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); options.HeartbeatId = \"HEARTBEAT_ID\"; }); }) The code installs the heartbeat middleware, which will handle all of the communication with the elmah.io API. Manually using Elmah.Io.Client The example above installs the heartbeat filter for all functions. If you have multiple functions inside your function app, or you want greater control of when and how to send heartbeats, you can use Elmah.Io.Client to create heartbeats. Start by installing the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Extend the Program.cs file with the following code: .ConfigureServices((ctx, services) => { var elmahIo = ElmahioAPI.Create(\"API_KEY\"); services.AddSingleton(elmahIo.Heartbeats); }); Inside your function, wrap all of the code in try/catch and add code to create either a Healthy or Unhealthy heartbeat: public class TimedFunction { private readonly IHeartbeats heartbeats; private readonly IConfiguration configuration; public TimedFunction(IHeartbeats heartbeats, IConfiguration configuration) { this.heartbeats = heartbeats; this.configuration = configuration; } [FunctionName(\"TimedFunction\")] public async Task Run([TimerTrigger(\"0 0 * * * *\")]TimerInfo myTimer) { var heartbeatId = configuration[\"heartbeatId\"]; var logId = configuration[\"logId\"]; try { // Your function code goes here await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat { Result = \"Healthy\" }); } catch (Exception e) { await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat { Result = \"Unhealthy\", Reason = e.ToString(), }); } } } If your function code executes successfully, a Healthy heartbeat is created. If an exception is thrown, an Unhealthy heartbeat with the thrown exception in Reason is created. Be aware that configuring a function to run in an internal (like every hour for the example above) does not ensure that the function is executed exactly on the hour. We recommend to set the grace period for these types of heartbeats to 15-30 minutes to avoid heartbeat errors when the timed function is past due. Using a separate heartbeat function You may want a single heartbeat representing your entire function app consisting of multiple functions. This is a good option if you want to create heartbeats from queue-triggered functions or similar. In these cases, you don't want to create a heartbeat every time a message from the queue is handled, but you will want to notify elmah.io if dependencies like database connection suddenly aren't available. We recommend creating a new heartbeat function for this kind of Function. Like in the previous example, make sure to extend your Program.cs file like this: .ConfigureServices((ctx, services) => { var elmahIo = ElmahioAPI.Create(\"API_KEY\"); services.AddSingleton(elmahIo.Heartbeats); }); Then create a new timed function with the following code: using System; using System.Threading.Tasks; using Elmah.Io.Client; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; namespace My.FunctionApp { public class Heartbeat { private readonly IConfiguration config; private readonly IHeartbeats heartbeats; public Heartbeat(IHeartbeats heartbeats, IConfiguration config) { this.heartbeats = heartbeats; this.config = config; } [FunctionName(\"Heartbeat\")] public async Task Run([TimerTrigger(\"0 */5 * * * *\")]TimerInfo myTimer) { var result = \"Healthy\"; var reason = (string)null; try { // Check your dependencies here } catch (Exception e) { result = \"Unhealthy\"; reason = e.ToString(); } await heartbeats.CreateAsync(config[\"heartbeatId\"], config[\"logId\"], new CreateHeartbeat { Result = result, Reason = reason, }); } } } In the example above, the new function named Heartbeat (the name is entirely up to you) executes every 5 minutes. Replace the comment with your checks like opening a connection to the database. If everything works as it should, a Healthy heartbeat is logged to elmah.io. If an exception is thrown while checking your dependencies, an Unhealthy heartbeat is created. When running locally, you may want to disable heartbeats: #if DEBUG [FunctionName(\"Heartbeat\")] #endif public async Task Run([TimerTrigger(\"0 */5 * * * *\")]TimerInfo myTimer) { // ... }","title":"Logging heartbeats from Isolated Azure Functions"},{"location":"logging-heartbeats-from-isolated-azure-functions/#logging-heartbeats-from-isolated-azure-functions","text":"Logging heartbeats from Isolated Azure Functions Using middleware in Elmah.Io.Functions.Isolated Manually using Elmah.Io.Client Using a separate heartbeat function Isolated Azure Functions are great candidates for adding heartbeats. For web APIs implemented with Isolated Azure Functions, you should create a /health endpoint and ping that using Uptime Monitoring . But for timer triggered, queue triggers, and similar function apps, heartbeats are a great way to verify that your function is successfully running. The rest of this document is split into different ways of adding heartbeats to one or more functions.","title":"Logging heartbeats from Isolated Azure Functions"},{"location":"logging-heartbeats-from-isolated-azure-functions/#using-middleware-in-elmahiofunctionsisolated","text":"Scheduled functions or functions not running often can use the heartbeat middleware part of the Elmah.Io.Functions.Isolated package. This will log a Healthy or Unhealthy endpoint every time a function is running. All functions within the same function app uses the same middleware, why this is primarily inteded for function apps with one scheduled function. Start by installing the Elmah.Io.Functions.Isolated package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions.Isolated dotnet add package Elmah.Io.Functions.Isolated <PackageReference Include=\"Elmah.Io.Functions.Isolated\" Version=\"5.*\" /> paket add Elmah.Io.Functions.Isolated Extend the Program.cs file with the following code: .ConfigureFunctionsWorkerDefaults((context, app) => { app.AddHeartbeat(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); options.HeartbeatId = \"HEARTBEAT_ID\"; }); }) The code installs the heartbeat middleware, which will handle all of the communication with the elmah.io API.","title":"Using middleware in Elmah.Io.Functions.Isolated"},{"location":"logging-heartbeats-from-isolated-azure-functions/#manually-using-elmahioclient","text":"The example above installs the heartbeat filter for all functions. If you have multiple functions inside your function app, or you want greater control of when and how to send heartbeats, you can use Elmah.Io.Client to create heartbeats. Start by installing the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Extend the Program.cs file with the following code: .ConfigureServices((ctx, services) => { var elmahIo = ElmahioAPI.Create(\"API_KEY\"); services.AddSingleton(elmahIo.Heartbeats); }); Inside your function, wrap all of the code in try/catch and add code to create either a Healthy or Unhealthy heartbeat: public class TimedFunction { private readonly IHeartbeats heartbeats; private readonly IConfiguration configuration; public TimedFunction(IHeartbeats heartbeats, IConfiguration configuration) { this.heartbeats = heartbeats; this.configuration = configuration; } [FunctionName(\"TimedFunction\")] public async Task Run([TimerTrigger(\"0 0 * * * *\")]TimerInfo myTimer) { var heartbeatId = configuration[\"heartbeatId\"]; var logId = configuration[\"logId\"]; try { // Your function code goes here await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat { Result = \"Healthy\" }); } catch (Exception e) { await heartbeats.CreateAsync(heartbeatId, logId, new CreateHeartbeat { Result = \"Unhealthy\", Reason = e.ToString(), }); } } } If your function code executes successfully, a Healthy heartbeat is created. If an exception is thrown, an Unhealthy heartbeat with the thrown exception in Reason is created. Be aware that configuring a function to run in an internal (like every hour for the example above) does not ensure that the function is executed exactly on the hour. We recommend to set the grace period for these types of heartbeats to 15-30 minutes to avoid heartbeat errors when the timed function is past due.","title":"Manually using Elmah.Io.Client"},{"location":"logging-heartbeats-from-isolated-azure-functions/#using-a-separate-heartbeat-function","text":"You may want a single heartbeat representing your entire function app consisting of multiple functions. This is a good option if you want to create heartbeats from queue-triggered functions or similar. In these cases, you don't want to create a heartbeat every time a message from the queue is handled, but you will want to notify elmah.io if dependencies like database connection suddenly aren't available. We recommend creating a new heartbeat function for this kind of Function. Like in the previous example, make sure to extend your Program.cs file like this: .ConfigureServices((ctx, services) => { var elmahIo = ElmahioAPI.Create(\"API_KEY\"); services.AddSingleton(elmahIo.Heartbeats); }); Then create a new timed function with the following code: using System; using System.Threading.Tasks; using Elmah.Io.Client; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; namespace My.FunctionApp { public class Heartbeat { private readonly IConfiguration config; private readonly IHeartbeats heartbeats; public Heartbeat(IHeartbeats heartbeats, IConfiguration config) { this.heartbeats = heartbeats; this.config = config; } [FunctionName(\"Heartbeat\")] public async Task Run([TimerTrigger(\"0 */5 * * * *\")]TimerInfo myTimer) { var result = \"Healthy\"; var reason = (string)null; try { // Check your dependencies here } catch (Exception e) { result = \"Unhealthy\"; reason = e.ToString(); } await heartbeats.CreateAsync(config[\"heartbeatId\"], config[\"logId\"], new CreateHeartbeat { Result = result, Reason = reason, }); } } } In the example above, the new function named Heartbeat (the name is entirely up to you) executes every 5 minutes. Replace the comment with your checks like opening a connection to the database. If everything works as it should, a Healthy heartbeat is logged to elmah.io. If an exception is thrown while checking your dependencies, an Unhealthy heartbeat is created. When running locally, you may want to disable heartbeats: #if DEBUG [FunctionName(\"Heartbeat\")] #endif public async Task Run([TimerTrigger(\"0 */5 * * * *\")]TimerInfo myTimer) { // ... }","title":"Using a separate heartbeat function"},{"location":"logging-heartbeats-from-net-core-worker-services/","text":"Logging heartbeats from .NET (Core) Worker Services .NET (Core) offers Worker Services as a way to schedule recurring tasks either hosted inside an ASP.NET Core website or as a Windows Service. Monitoring that Worker Services run successfully, can be easily set up with elmah.io Heartbeats. To register heartbeats from a worker service, start by creating a new heartbeat on the elmah.io UI. For this example, we want to monitor that a Service Worker is running every 5 minutes, why we set Interval to 5 minutes and Grace to 1 minute. Next, install the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client In the Program.cs or Startup.cs file (depending on where you register dependencies), register IHeartbeats from the elmah.io client: .ConfigureServices((hostContext, services) => { var elmahIoApi = ElmahioAPI.Create(hostContext.Configuration[\"ElmahIo:ApiKey\"]); services.AddSingleton(elmahIoApi.Heartbeats); // ... services.AddHostedService<Worker>(); }); In the example, the configuration should be made available in the appsettings.json file as shown later in this article. In the service class ( Worker ) you can inject the IHeartbeats object, as well as additional configuration needed to create heartbeats: public class Worker : BackgroundService { private readonly IHeartbeats heartbeats; private readonly Guid logId; private readonly string heartbeatId; public Worker(IHeartbeats heartbeats, IConfiguration configuration) { this.heartbeats = heartbeats; this.logId = new Guid(configuration[\"ElmahIo:LogId\"]); this.heartbeatId = configuration[\"ElmahIo:HeartbeatId\"]; } } Inside the ExecuteAsync method, wrap the worker code in try-catch and call the HealthyAsync method when the worker successfully run and the UnhealthyAsync method when an exception occurs: protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { // Do work await heartbeats.HealthyAsync(logId, heartbeatId); } catch (Exception e) { await heartbeats.UnhealthyAsync(logId, heartbeatId, e.ToString()); } await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } } In the appsettings.json file, add the elmah.io configuration: { \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\", \"HeartbeatId\": \"HEARTBEAT_ID\" } } Replace the values with values found in the elmah.io UI. Remember to enable the Heartbeats | Write permission on the used API key.","title":"Logging heartbeats from .NET Core Worker Services"},{"location":"logging-heartbeats-from-net-core-worker-services/#logging-heartbeats-from-net-core-worker-services","text":".NET (Core) offers Worker Services as a way to schedule recurring tasks either hosted inside an ASP.NET Core website or as a Windows Service. Monitoring that Worker Services run successfully, can be easily set up with elmah.io Heartbeats. To register heartbeats from a worker service, start by creating a new heartbeat on the elmah.io UI. For this example, we want to monitor that a Service Worker is running every 5 minutes, why we set Interval to 5 minutes and Grace to 1 minute. Next, install the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client In the Program.cs or Startup.cs file (depending on where you register dependencies), register IHeartbeats from the elmah.io client: .ConfigureServices((hostContext, services) => { var elmahIoApi = ElmahioAPI.Create(hostContext.Configuration[\"ElmahIo:ApiKey\"]); services.AddSingleton(elmahIoApi.Heartbeats); // ... services.AddHostedService<Worker>(); }); In the example, the configuration should be made available in the appsettings.json file as shown later in this article. In the service class ( Worker ) you can inject the IHeartbeats object, as well as additional configuration needed to create heartbeats: public class Worker : BackgroundService { private readonly IHeartbeats heartbeats; private readonly Guid logId; private readonly string heartbeatId; public Worker(IHeartbeats heartbeats, IConfiguration configuration) { this.heartbeats = heartbeats; this.logId = new Guid(configuration[\"ElmahIo:LogId\"]); this.heartbeatId = configuration[\"ElmahIo:HeartbeatId\"]; } } Inside the ExecuteAsync method, wrap the worker code in try-catch and call the HealthyAsync method when the worker successfully run and the UnhealthyAsync method when an exception occurs: protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { // Do work await heartbeats.HealthyAsync(logId, heartbeatId); } catch (Exception e) { await heartbeats.UnhealthyAsync(logId, heartbeatId, e.ToString()); } await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } } In the appsettings.json file, add the elmah.io configuration: { \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\", \"HeartbeatId\": \"HEARTBEAT_ID\" } } Replace the values with values found in the elmah.io UI. Remember to enable the Heartbeats | Write permission on the used API key.","title":"Logging heartbeats from .NET (Core) Worker Services"},{"location":"logging-heartbeats-from-powershell/","text":"Logging heartbeats from PowerShell The Heartbeats feature is a great way to verify that scripts run successfully too. A lot of people have PowerShell scripts running on a schedule to clean up folders on the file system, make batch changes in a database, and more. To include heartbeats in your PowerShell script, wrap the code in try/catch and add either Healthy or Unhealthy result: $apiKey = \"API_KEY\" $logId = \"LOG_ID\" $heartbeatId = \"HEARTBEAT_ID\" $url = \"https://api.elmah.io/v3/heartbeats/$logId/$heartbeatId/?api_key=$apiKey\" try { # Your script goes here $body = @{ result = \"Healthy\" } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" } catch { $body = @{ result = \"Unhealthy\" reason = $_.Exception.Message } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" } If everything goes well, a Healthy heartbeat is logged using the Invoke-RestMethod cmdlet. If an exception is thrown in your script, an Unhealthy heartbeat is logged.","title":"Logging heartbeats from PowerShell"},{"location":"logging-heartbeats-from-powershell/#logging-heartbeats-from-powershell","text":"The Heartbeats feature is a great way to verify that scripts run successfully too. A lot of people have PowerShell scripts running on a schedule to clean up folders on the file system, make batch changes in a database, and more. To include heartbeats in your PowerShell script, wrap the code in try/catch and add either Healthy or Unhealthy result: $apiKey = \"API_KEY\" $logId = \"LOG_ID\" $heartbeatId = \"HEARTBEAT_ID\" $url = \"https://api.elmah.io/v3/heartbeats/$logId/$heartbeatId/?api_key=$apiKey\" try { # Your script goes here $body = @{ result = \"Healthy\" } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" } catch { $body = @{ result = \"Unhealthy\" reason = $_.Exception.Message } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" } If everything goes well, a Healthy heartbeat is logged using the Invoke-RestMethod cmdlet. If an exception is thrown in your script, an Unhealthy heartbeat is logged.","title":"Logging heartbeats from PowerShell"},{"location":"logging-heartbeats-from-umbraco/","text":"Logging heartbeats from Umbraco Logging heartbeats from Umbraco Umbraco >= 9 Umbraco 8 Umbraco comes with a nice health check feature that can carry out a range of built-in health checks as well as custom checks you may want to add. Umbraco Health Checks fits perfectly with elmah.io Heartbeats . To start publishing Umbraco Health Checks to elmah.io, create a new health check. Select 1 day in Interval and 5 minutes in Grace . The next step depends on the major version of Umbraco. For both examples, replace API_KEY , LOG_ID , and HEARTBEAT_ID with the values found on the elmah.io UI. When launching the website Umbraco automatically executes the health checks once every 24 hours and sends the results to elmah.io. Umbraco >= 9 Umbraco 9 is targeting .NET 5.0 which is no longer supported by Microsoft. This is why we have chosen to support Umbraco 10 and up only. Install the Elmah.Io.Umbraco NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco dotnet add package Elmah.Io.Umbraco <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"5.*\" /> paket add Elmah.Io.Umbraco To publish health check results to your newly created heartbeat, extend the appsettings.json file: { ... \"Umbraco\": { \"CMS\": { ... \"HealthChecks\": { \"Notification\": { \"Enabled\": true, \"NotificationMethods\": { \"elmah.io\": { \"Enabled\": true, \"Verbosity\": \"Summary\", \"Settings\": { \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\", \"heartbeatId\": \"HEARTBEAT_ID\" } } } } } } } } Umbraco 8 install the Elmah.Io.Umbraco v4 NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco -Version 4.2.21 dotnet add package Elmah.Io.Umbraco --version 4.2.21 <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"4.2.21\" /> paket add Elmah.Io.Umbraco --version 4.2.21 For Umbraco to automatically execute health checks, you will need to set your back office URL in the umbracoSettings.config file: <?xml version=\"1.0\" encoding=\"utf-8\" ?> <settings> <!-- ... --> <web.routing umbracoApplicationUrl=\"https://localhost:44381/umbraco/\"> </web.routing> </settings> (localhost is used as an example and should be replaced with a real URL) Umbraco comes with an email publisher already configured. To publish health check results to your newly created heartbeat, extend the HealthChecks.config file: <?xml version =\"1.0\" encoding=\"utf-8\" ?> <HealthChecks> <disabledChecks> </disabledChecks> <notificationSettings enabled=\"true\" firstRunTime=\"\" periodInHours=\"24\"> <notificationMethods> <notificationMethod alias=\"elmah.io\" enabled=\"true\" verbosity=\"Summary\"> <settings> <add key=\"apiKey\" value=\"API_KEY\" /> <add key=\"logId\" value=\"LOG_ID\" /> <add key=\"heartbeatId\" value=\"HEARTBEAT_ID\" /> </settings> </notificationMethod> <notificationMethod alias=\"email\" enabled=\"false\" verbosity=\"Summary\"> <settings> <add key=\"recipientEmail\" value=\"\" /> </settings> </notificationMethod> </notificationMethods> <disabledChecks> </disabledChecks> </notificationSettings> </HealthChecks> For this example, I have disabled the email notification publisher but you can run with both if you'd like.","title":"Logging heartbeats from Umbraco"},{"location":"logging-heartbeats-from-umbraco/#logging-heartbeats-from-umbraco","text":"Logging heartbeats from Umbraco Umbraco >= 9 Umbraco 8 Umbraco comes with a nice health check feature that can carry out a range of built-in health checks as well as custom checks you may want to add. Umbraco Health Checks fits perfectly with elmah.io Heartbeats . To start publishing Umbraco Health Checks to elmah.io, create a new health check. Select 1 day in Interval and 5 minutes in Grace . The next step depends on the major version of Umbraco. For both examples, replace API_KEY , LOG_ID , and HEARTBEAT_ID with the values found on the elmah.io UI. When launching the website Umbraco automatically executes the health checks once every 24 hours and sends the results to elmah.io.","title":"Logging heartbeats from Umbraco"},{"location":"logging-heartbeats-from-umbraco/#umbraco-9","text":"Umbraco 9 is targeting .NET 5.0 which is no longer supported by Microsoft. This is why we have chosen to support Umbraco 10 and up only. Install the Elmah.Io.Umbraco NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco dotnet add package Elmah.Io.Umbraco <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"5.*\" /> paket add Elmah.Io.Umbraco To publish health check results to your newly created heartbeat, extend the appsettings.json file: { ... \"Umbraco\": { \"CMS\": { ... \"HealthChecks\": { \"Notification\": { \"Enabled\": true, \"NotificationMethods\": { \"elmah.io\": { \"Enabled\": true, \"Verbosity\": \"Summary\", \"Settings\": { \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\", \"heartbeatId\": \"HEARTBEAT_ID\" } } } } } } } }","title":"Umbraco >= 9"},{"location":"logging-heartbeats-from-umbraco/#umbraco-8","text":"install the Elmah.Io.Umbraco v4 NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco -Version 4.2.21 dotnet add package Elmah.Io.Umbraco --version 4.2.21 <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"4.2.21\" /> paket add Elmah.Io.Umbraco --version 4.2.21 For Umbraco to automatically execute health checks, you will need to set your back office URL in the umbracoSettings.config file: <?xml version=\"1.0\" encoding=\"utf-8\" ?> <settings> <!-- ... --> <web.routing umbracoApplicationUrl=\"https://localhost:44381/umbraco/\"> </web.routing> </settings> (localhost is used as an example and should be replaced with a real URL) Umbraco comes with an email publisher already configured. To publish health check results to your newly created heartbeat, extend the HealthChecks.config file: <?xml version =\"1.0\" encoding=\"utf-8\" ?> <HealthChecks> <disabledChecks> </disabledChecks> <notificationSettings enabled=\"true\" firstRunTime=\"\" periodInHours=\"24\"> <notificationMethods> <notificationMethod alias=\"elmah.io\" enabled=\"true\" verbosity=\"Summary\"> <settings> <add key=\"apiKey\" value=\"API_KEY\" /> <add key=\"logId\" value=\"LOG_ID\" /> <add key=\"heartbeatId\" value=\"HEARTBEAT_ID\" /> </settings> </notificationMethod> <notificationMethod alias=\"email\" enabled=\"false\" verbosity=\"Summary\"> <settings> <add key=\"recipientEmail\" value=\"\" /> </settings> </notificationMethod> </notificationMethods> <disabledChecks> </disabledChecks> </notificationSettings> </HealthChecks> For this example, I have disabled the email notification publisher but you can run with both if you'd like.","title":"Umbraco 8"},{"location":"logging-heartbeats-from-windows-scheduled-tasks/","text":"Logging heartbeats from Windows Scheduled Tasks How you want to implement heartbeats from Windows Scheduled Tasks depends on how your task is implemented. For tasks written in C#, you typically want to persist heartbeats using the Elmah.Io.Client package as shown in Set up Heartbeats . For scheduled PowerShell or other scripts, you can trigger the elmah.io API as shown in Logging heartbeats from PowerShell and Logging heartbeats from cURL . Invoking the API either through the elmah.io client or using a REST client is the optimal way since you have greater control over what to log. This manual approach is not available when you are not in control of the scheduled code. Examples of this could be custom tools you install as Scheduled Tasks using schtasks.exe or tasks automatically registered when installing third-party software on your server. When configuring a heartbeat through the elmah.io UI you set an expected interval and grace period. If a heartbeat is not received in time, we will automatically log a missing heartbeat error. This will indicate that the scheduled task didn't run or fail and is something that should be looked at. In case you want to log a failing heartbeat as soon as the scheduled task is failing, you can do that using events logged to the Windows Event Log. To set it up, go through the following steps: Open Task Scheduler . Go to Task Scheduler Library and click the Create Task... button. Give the new task a proper name. In the Triggers tab click the New... button. In the New Trigger window select On an event in the Begin the task dropdown. In Settings select the Custom radio button. Click the New Event Filter... button. Select the XML tab and check the Edit query manually checkbox. Now input the XML from the following screenshot (source code later in this article). The custom query will trigger on all Application Error messages from an app named ConsoleApp14.exe . You will need to change the app name to the filename of the app running in the scheduled task. Click the OK button. In the New Trigger window click the OK button to save the trigger. Select the Actions tab. Click the New... button. Select the Start a program option in the Action dropdown. Input values like shown here: Click the OK button to save the action. Click the OK button to save the task. The final step is to add the c:\\scripts\\heartbeat.ps1 PowerShell script invoked by the task. For a better understanding of storing heartbeats through PowerShell check out Logging heartbeats from PowerShell . To log a failing heartbeat to elmah.io you can use the following PowerShell code: $apiKey = \"API_KEY\" $logId = \"LOG_ID\" $heartbeatId = \"HEARTBEAT_ID\" $url = \"https://api.elmah.io/v3/heartbeats/$logId/$heartbeatId/?api_key=$apiKey\" $body = @{ result = \"Unhealthy\" reason = \"Error in scheduled task\" } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" That's it. The new task just created will trigger every time there's an application error from your application. Notice that in case your application is manually logging errors to the event log, this will also trigger the task. Source code for reference: <QueryList> <Query Id=\"0\" Path=\"Application\"> <Select Path=\"Application\"> *[System[Provider[@Name='Application Error']]] and *[EventData[(Data[@Name=\"AppName\"]=\"ConsoleApp14.exe\")]] </Select> </Query> </QueryList>","title":"Logging heartbeats from Windows Scheduled Tasks"},{"location":"logging-heartbeats-from-windows-scheduled-tasks/#logging-heartbeats-from-windows-scheduled-tasks","text":"How you want to implement heartbeats from Windows Scheduled Tasks depends on how your task is implemented. For tasks written in C#, you typically want to persist heartbeats using the Elmah.Io.Client package as shown in Set up Heartbeats . For scheduled PowerShell or other scripts, you can trigger the elmah.io API as shown in Logging heartbeats from PowerShell and Logging heartbeats from cURL . Invoking the API either through the elmah.io client or using a REST client is the optimal way since you have greater control over what to log. This manual approach is not available when you are not in control of the scheduled code. Examples of this could be custom tools you install as Scheduled Tasks using schtasks.exe or tasks automatically registered when installing third-party software on your server. When configuring a heartbeat through the elmah.io UI you set an expected interval and grace period. If a heartbeat is not received in time, we will automatically log a missing heartbeat error. This will indicate that the scheduled task didn't run or fail and is something that should be looked at. In case you want to log a failing heartbeat as soon as the scheduled task is failing, you can do that using events logged to the Windows Event Log. To set it up, go through the following steps: Open Task Scheduler . Go to Task Scheduler Library and click the Create Task... button. Give the new task a proper name. In the Triggers tab click the New... button. In the New Trigger window select On an event in the Begin the task dropdown. In Settings select the Custom radio button. Click the New Event Filter... button. Select the XML tab and check the Edit query manually checkbox. Now input the XML from the following screenshot (source code later in this article). The custom query will trigger on all Application Error messages from an app named ConsoleApp14.exe . You will need to change the app name to the filename of the app running in the scheduled task. Click the OK button. In the New Trigger window click the OK button to save the trigger. Select the Actions tab. Click the New... button. Select the Start a program option in the Action dropdown. Input values like shown here: Click the OK button to save the action. Click the OK button to save the task. The final step is to add the c:\\scripts\\heartbeat.ps1 PowerShell script invoked by the task. For a better understanding of storing heartbeats through PowerShell check out Logging heartbeats from PowerShell . To log a failing heartbeat to elmah.io you can use the following PowerShell code: $apiKey = \"API_KEY\" $logId = \"LOG_ID\" $heartbeatId = \"HEARTBEAT_ID\" $url = \"https://api.elmah.io/v3/heartbeats/$logId/$heartbeatId/?api_key=$apiKey\" $body = @{ result = \"Unhealthy\" reason = \"Error in scheduled task\" } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" That's it. The new task just created will trigger every time there's an application error from your application. Notice that in case your application is manually logging errors to the event log, this will also trigger the task. Source code for reference: <QueryList> <Query Id=\"0\" Path=\"Application\"> <Select Path=\"Application\"> *[System[Provider[@Name='Application Error']]] and *[EventData[(Data[@Name=\"AppName\"]=\"ConsoleApp14.exe\")]] </Select> </Query> </QueryList>","title":"Logging heartbeats from Windows Scheduled Tasks"},{"location":"logging-through-a-http-proxy/","text":"Logging through a HTTP proxy You may find yourself in a situation, where your production web servers aren't allowing HTTP requests towards the public Internet. This also impacts the elmah.io client, which requires access to the URL https://api.elmah.io. A popular choice of implementing this kind of restriction nowadays is through a HTTP proxy like Squid or Nginx. Luckily the elmah.io client supports proxy configuration out of the box. Let's look at how to configure a HTTP proxy through web.config : <?xml version=\"1.0\" encoding=\"utf-8\"?> <configuration> <configSections> <sectionGroup name=\"elmah\"> <section name=\"security\" requirePermission=\"false\" type=\"Elmah.SecuritySectionHandler, Elmah\" /> <section name=\"errorLog\" requirePermission=\"false\" type=\"Elmah.ErrorLogSectionHandler, Elmah\" /> <section name=\"errorMail\" requirePermission=\"false\" type=\"Elmah.ErrorMailSectionHandler, Elmah\" /> <section name=\"errorFilter\" requirePermission=\"false\" type=\"Elmah.ErrorFilterSectionHandler, Elmah\" /> </sectionGroup> </configSections> <elmah> <security allowRemoteAccess=\"false\" /> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"...\" logId=\"...\" /> </elmah> <system.net> <defaultProxy> <proxy usesystemdefault=\"True\" proxyaddress=\"http://192.168.0.1:3128\" bypassonlocal=\"False\"/> </defaultProxy> </system.net> </configuration> The above example is of course greatly simplified. The elmah.io client automatically picks up the defaultProxy configuration through the system.net element. defaultProxy tunnels every request from your server, including requests to elmah.io, through the proxy located on 192.18.0.1 port 3128 (or whatever IP/hostname and port you are using). Proxies with username/password Some proxies require a username/password. Unfortunately, the defaultProxy element doesn't support authentication. You have two ways to set this up: Use default credentials Make sure to set the useDefaultCredentials attribute to true : <system.net> <defaultProxy useDefaultCredentials=\"true\"> <!-- ... --> </defaultProxy> </system.net> Run your web app (application pool) as a user with access to the proxy. Implement your own proxy Add the following class: public class AuthenticatingProxy : IWebProxy { public ICredentials Credentials { get { return new NetworkCredential(\"username\", \"password\"); } set {} } public Uri GetProxy(Uri destination) { return new Uri(\"http://localhost:8888\"); } public bool IsBypassed(Uri host) { return false; } } Configure the new proxy in web.config : <defaultProxy useDefaultCredentials=\"false\"> <module type=\"YourNamespace.AuthenticatingProxy, YourAssembly\" /> </defaultProxy>","title":"Logging through a HTTP proxy"},{"location":"logging-through-a-http-proxy/#logging-through-a-http-proxy","text":"You may find yourself in a situation, where your production web servers aren't allowing HTTP requests towards the public Internet. This also impacts the elmah.io client, which requires access to the URL https://api.elmah.io. A popular choice of implementing this kind of restriction nowadays is through a HTTP proxy like Squid or Nginx. Luckily the elmah.io client supports proxy configuration out of the box. Let's look at how to configure a HTTP proxy through web.config : <?xml version=\"1.0\" encoding=\"utf-8\"?> <configuration> <configSections> <sectionGroup name=\"elmah\"> <section name=\"security\" requirePermission=\"false\" type=\"Elmah.SecuritySectionHandler, Elmah\" /> <section name=\"errorLog\" requirePermission=\"false\" type=\"Elmah.ErrorLogSectionHandler, Elmah\" /> <section name=\"errorMail\" requirePermission=\"false\" type=\"Elmah.ErrorMailSectionHandler, Elmah\" /> <section name=\"errorFilter\" requirePermission=\"false\" type=\"Elmah.ErrorFilterSectionHandler, Elmah\" /> </sectionGroup> </configSections> <elmah> <security allowRemoteAccess=\"false\" /> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"...\" logId=\"...\" /> </elmah> <system.net> <defaultProxy> <proxy usesystemdefault=\"True\" proxyaddress=\"http://192.168.0.1:3128\" bypassonlocal=\"False\"/> </defaultProxy> </system.net> </configuration> The above example is of course greatly simplified. The elmah.io client automatically picks up the defaultProxy configuration through the system.net element. defaultProxy tunnels every request from your server, including requests to elmah.io, through the proxy located on 192.18.0.1 port 3128 (or whatever IP/hostname and port you are using).","title":"Logging through a HTTP proxy"},{"location":"logging-through-a-http-proxy/#proxies-with-usernamepassword","text":"Some proxies require a username/password. Unfortunately, the defaultProxy element doesn't support authentication. You have two ways to set this up:","title":"Proxies with username/password"},{"location":"logging-through-a-http-proxy/#use-default-credentials","text":"Make sure to set the useDefaultCredentials attribute to true : <system.net> <defaultProxy useDefaultCredentials=\"true\"> <!-- ... --> </defaultProxy> </system.net> Run your web app (application pool) as a user with access to the proxy.","title":"Use default credentials"},{"location":"logging-through-a-http-proxy/#implement-your-own-proxy","text":"Add the following class: public class AuthenticatingProxy : IWebProxy { public ICredentials Credentials { get { return new NetworkCredential(\"username\", \"password\"); } set {} } public Uri GetProxy(Uri destination) { return new Uri(\"http://localhost:8888\"); } public bool IsBypassed(Uri host) { return false; } } Configure the new proxy in web.config : <defaultProxy useDefaultCredentials=\"false\"> <module type=\"YourNamespace.AuthenticatingProxy, YourAssembly\" /> </defaultProxy>","title":"Implement your own proxy"},{"location":"logging-to-elmah-io-from-a-running-website-on-azure/","text":"Logging to elmah.io from a running website on Azure Adding elmah.io on a running website isn't the recommended way to install. It should be used if you are unable to deploy a new version only. To enable error logging to elmah.io, you usually install one of our client integrations through PowerShell or Visual Studio and deploy a new version of your website to a web server. Sometimes you need to monitor an already running website or don't want logging logic as part of your repository. Using the elmah.io Site Extension for Azure App Services, error logging can be added to an already running website. Check out this video tutorial or keep reading for the text version: To start logging errors from your Azure web application, go to the Azure Portal and select the website you want to monitor. Click the Extensions tool: Click the Add button and select .NET elmah.io for Azure : Accept the terms and click the Add button. The elmah.io Site Extension is now added. Once added, restart the website for the new extension to load. Finally, you need to add your API key ( Where is my API key? ) and log ID ( Where is my log ID? ) to Application settings : Make sure to use the app setting names ELMAHIO_APIKEY and ELMAHIO_LOGID . Your Azure web application now logs all uncaught exceptions to elmah.io. The elmah.io Site Extension comes with a couple of limitations: It only works for ASP.NET, MVC, Web API, and similar. ASP.NET Core websites should be installed locally and re-deployed. .NET Full Framework 4.6 and newer is required. Custom code or configuration may swallow exceptions. Like custom errors or when using the HandleErrorAttribute attribute in ASP.NET MVC. In this case, the correct NuGet package needs to be installed in your code and deployed to Azure (like the Elmah.Io.Mvc package for ASP.NET MVC). Troubleshooting ConfigurationErrorsException: Could not load file or assembly 'Elmah' or one of its dependencies. The system cannot find the file specified. After uninstalling the elmah.io site extension, you may see the configuration error above. This means that elmah.io's uninstall script for some reason wasn't allowed to run or resulted in an error. To make sure that elmah.io is completely removed, follow these steps: Stop your website. Browse your website files through Kudu. Remove all files starting with Elmah . Start your website. Error while uninstalling the site extension While uninstalling the site extension you may see errors like this: Failed to delete Site Extension: .NET elmah.io for Azure.{\"Message\":\"An error has occurred.\",\"ExceptionMessage\":\"The system cannot find the file specified. C:\\home\\SiteExtensions\\Elmah.Io.Azure.SiteExtension\\uninstall.cmd In this case, the elmah.io for Azure site extension needs to be uninstalled manually. To do that, go to Kudu Services beneath the Advanced Tools section in the website on Azure. In the Debug console navigate to site/wwwroot/bin and delete all files prefixed with Elmah (up to four files).","title":"Logging from a running website on Azure"},{"location":"logging-to-elmah-io-from-a-running-website-on-azure/#logging-to-elmahio-from-a-running-website-on-azure","text":"Adding elmah.io on a running website isn't the recommended way to install. It should be used if you are unable to deploy a new version only. To enable error logging to elmah.io, you usually install one of our client integrations through PowerShell or Visual Studio and deploy a new version of your website to a web server. Sometimes you need to monitor an already running website or don't want logging logic as part of your repository. Using the elmah.io Site Extension for Azure App Services, error logging can be added to an already running website. Check out this video tutorial or keep reading for the text version: To start logging errors from your Azure web application, go to the Azure Portal and select the website you want to monitor. Click the Extensions tool: Click the Add button and select .NET elmah.io for Azure : Accept the terms and click the Add button. The elmah.io Site Extension is now added. Once added, restart the website for the new extension to load. Finally, you need to add your API key ( Where is my API key? ) and log ID ( Where is my log ID? ) to Application settings : Make sure to use the app setting names ELMAHIO_APIKEY and ELMAHIO_LOGID . Your Azure web application now logs all uncaught exceptions to elmah.io. The elmah.io Site Extension comes with a couple of limitations: It only works for ASP.NET, MVC, Web API, and similar. ASP.NET Core websites should be installed locally and re-deployed. .NET Full Framework 4.6 and newer is required. Custom code or configuration may swallow exceptions. Like custom errors or when using the HandleErrorAttribute attribute in ASP.NET MVC. In this case, the correct NuGet package needs to be installed in your code and deployed to Azure (like the Elmah.Io.Mvc package for ASP.NET MVC).","title":"Logging to elmah.io from a running website on Azure"},{"location":"logging-to-elmah-io-from-a-running-website-on-azure/#troubleshooting","text":"ConfigurationErrorsException: Could not load file or assembly 'Elmah' or one of its dependencies. The system cannot find the file specified. After uninstalling the elmah.io site extension, you may see the configuration error above. This means that elmah.io's uninstall script for some reason wasn't allowed to run or resulted in an error. To make sure that elmah.io is completely removed, follow these steps: Stop your website. Browse your website files through Kudu. Remove all files starting with Elmah . Start your website. Error while uninstalling the site extension While uninstalling the site extension you may see errors like this: Failed to delete Site Extension: .NET elmah.io for Azure.{\"Message\":\"An error has occurred.\",\"ExceptionMessage\":\"The system cannot find the file specified. C:\\home\\SiteExtensions\\Elmah.Io.Azure.SiteExtension\\uninstall.cmd In this case, the elmah.io for Azure site extension needs to be uninstalled manually. To do that, go to Kudu Services beneath the Advanced Tools section in the website on Azure. In the Debug console navigate to site/wwwroot/bin and delete all files prefixed with Elmah (up to four files).","title":"Troubleshooting"},{"location":"logging-to-elmah-io-from-a-running-website-on-iis/","text":"Logging to elmah.io from a running website on IIS Adding elmah.io on a running website isn't the recommended way to install. It should be used if you are unable to deploy a new version only. To enable error logging to elmah.io, you usually install one of our client integrations through PowerShell or Visual Studio and deploy a new version of your website to a web server. Sometimes you need to monitor an already running website or don't want logging logic as part of your repository. elmah.io can be added to a running website by following this guide. Run the following command somewhere on your computer: nuget install elmah.io It is recommended to run this locally to avoid having to install nuget.exe on the machine running IIS (typically a production environment). If you don't have NuGet installed, there are a range of download options available here . From the folder where you ran the command, copy the following files to the bin folder of your running website: elmah.corelibrary.x.y.z\\lib\\Elmah.dll elmah.io.x.y.z\\lib\\net45\\Elmah.Io.dll Elmah.Io.Client.x.y.z\\lib\\<.net version your website is using>\\Elmah.Io.Client.dll Newtonsoft.Json.x.y.z\\lib\\<.net version your website is using>\\Newtonsoft.Json.dll Configure elmah.io in Web.config as described here: Configure elmah.io manually (you don't need to call the Install-Package command). Notice that the AppDomain will restart when saving changes to the Web.config file. If the website doesn't start logging errors to elmah.io, you may need to restart it.","title":"Logging from a running website on IIS"},{"location":"logging-to-elmah-io-from-a-running-website-on-iis/#logging-to-elmahio-from-a-running-website-on-iis","text":"Adding elmah.io on a running website isn't the recommended way to install. It should be used if you are unable to deploy a new version only. To enable error logging to elmah.io, you usually install one of our client integrations through PowerShell or Visual Studio and deploy a new version of your website to a web server. Sometimes you need to monitor an already running website or don't want logging logic as part of your repository. elmah.io can be added to a running website by following this guide. Run the following command somewhere on your computer: nuget install elmah.io It is recommended to run this locally to avoid having to install nuget.exe on the machine running IIS (typically a production environment). If you don't have NuGet installed, there are a range of download options available here . From the folder where you ran the command, copy the following files to the bin folder of your running website: elmah.corelibrary.x.y.z\\lib\\Elmah.dll elmah.io.x.y.z\\lib\\net45\\Elmah.Io.dll Elmah.Io.Client.x.y.z\\lib\\<.net version your website is using>\\Elmah.Io.Client.dll Newtonsoft.Json.x.y.z\\lib\\<.net version your website is using>\\Newtonsoft.Json.dll Configure elmah.io in Web.config as described here: Configure elmah.io manually (you don't need to call the Install-Package command). Notice that the AppDomain will restart when saving changes to the Web.config file. If the website doesn't start logging errors to elmah.io, you may need to restart it.","title":"Logging to elmah.io from a running website on IIS"},{"location":"logging-to-elmah-io-from-angular/","text":"Logging to elmah.io from Angular elmah.io.javascript works great with Angular applications too. To log all errors happening in your Angular app, install elmah.io.javascript through npm as described in Logging from JavaScript . In the same folder as the app.module.ts file add a new file named elmah-io-error-handler.ts and include the following content: import {ErrorHandler} from '@angular/core'; import * as Elmahio from 'elmah.io.javascript'; export class ElmahIoErrorHandler implements ErrorHandler { logger: Elmahio; constructor() { this.logger = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); } handleError(error) { if (error && error.message) { this.logger.error(error.message, error); } else { this.logger.error('Error in application', error); } } } Reference both ErrorHandler and ElmahIoErrorHandler in the app.module.ts file: import { BrowserModule } from '@angular/platform-browser'; import {ErrorHandler, NgModule} from '@angular/core'; // \u2b05\ufe0f Add ErrorHandler import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import {ElmahIoErrorHandler} from './elmah-io-error-handler'; // \u2b05\ufe0f Reference ElmahIoErrorHandler @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule ], // \u2b07\ufe0f Reference both handlers providers: [{ provide: ErrorHandler, useClass: ElmahIoErrorHandler }], bootstrap: [AppComponent] }) export class AppModule { } All errors are shipped to the handleError -function by Angular and logged to elmah.io. Check out the Elmah.Io.JavaScript.AngularAspNetCore and Elmah.Io.JavaScript.AngularWebpack samples for some real working code. AngularJS/Angular 1 For AngularJS you need to implement the $exceptionHandler instead: (function () { 'use strict'; angular.module('app').factory('$exceptionHandler', ['$log', function controller($log) { var logger = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); return function elmahExceptionHandler(exception, cause) { $log.error(exception, cause); logger.error(exception.message, exception); }; }]); })();","title":"Logging from Angular"},{"location":"logging-to-elmah-io-from-angular/#logging-to-elmahio-from-angular","text":"elmah.io.javascript works great with Angular applications too. To log all errors happening in your Angular app, install elmah.io.javascript through npm as described in Logging from JavaScript . In the same folder as the app.module.ts file add a new file named elmah-io-error-handler.ts and include the following content: import {ErrorHandler} from '@angular/core'; import * as Elmahio from 'elmah.io.javascript'; export class ElmahIoErrorHandler implements ErrorHandler { logger: Elmahio; constructor() { this.logger = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); } handleError(error) { if (error && error.message) { this.logger.error(error.message, error); } else { this.logger.error('Error in application', error); } } } Reference both ErrorHandler and ElmahIoErrorHandler in the app.module.ts file: import { BrowserModule } from '@angular/platform-browser'; import {ErrorHandler, NgModule} from '@angular/core'; // \u2b05\ufe0f Add ErrorHandler import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import {ElmahIoErrorHandler} from './elmah-io-error-handler'; // \u2b05\ufe0f Reference ElmahIoErrorHandler @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule ], // \u2b07\ufe0f Reference both handlers providers: [{ provide: ErrorHandler, useClass: ElmahIoErrorHandler }], bootstrap: [AppComponent] }) export class AppModule { } All errors are shipped to the handleError -function by Angular and logged to elmah.io. Check out the Elmah.Io.JavaScript.AngularAspNetCore and Elmah.Io.JavaScript.AngularWebpack samples for some real working code.","title":"Logging to elmah.io from Angular"},{"location":"logging-to-elmah-io-from-angular/#angularjsangular-1","text":"For AngularJS you need to implement the $exceptionHandler instead: (function () { 'use strict'; angular.module('app').factory('$exceptionHandler', ['$log', function controller($log) { var logger = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); return function elmahExceptionHandler(exception, cause) { $log.error(exception, cause); logger.error(exception.message, exception); }; }]); })();","title":"AngularJS/Angular 1"},{"location":"logging-to-elmah-io-from-aspnet-core/","text":"Logging to elmah.io from ASP.NET Core Logging to elmah.io from ASP.NET Core Configuring API key and log ID in options Logging exceptions manually Breadcrumbs Additional options Setting application name Hooks Decorate from HTTP context Include source code Remove sensitive form data Formatting exceptions Logging responses not throwing an exception Logging through a proxy Logging health check results If you are looking to log all uncaught errors from ASP.NET Core, you've come to the right place. For help setting up general .NET Core logging similar to log4net, check out Logging from Microsoft.Extensions.Logging . To log all warnings and errors from ASP.NET Core, install the following NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore dotnet add package Elmah.Io.AspNetCore <PackageReference Include=\"Elmah.Io.AspNetCore\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore In the Startup.cs file, add a new using statement: using Elmah.Io.AspNetCore; Standard Top-level statements Call AddElmahIo in the ConfigureServices -method: public void ConfigureServices(IServiceCollection services) { services.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); // ... } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Call UseElmahIo in the Configure -method: public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory fac) { // ... app.UseElmahIo(); // ... } Call AddElmahIo in the Program.cs file: builder.Services.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Call UseElmahIo in the Program.cs file: app.UseElmahIo(); Make sure to call the UseElmahIo -method after installation of other pieces of middleware handling exceptions and auth (like UseDeveloperExceptionPage , UseExceptionHandler , UseAuthentication , and UseAuthorization ), but before any calls to UseEndpoints , UseMvc , MapRazorPages , and similar. That's it. Every uncaught exception will be logged to elmah.io. For an example of configuring elmah.io with ASP.NET Core minimal APIs, check out this sample . Configuring API key and log ID in options If you have different environments (everyone has a least localhost and production), you should consider adding the API key and log ID in your appsettings.json file: { // ... \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } } Configuring elmah.io is done by calling the Configure -method before AddElmahIo : Standard Top-level statements public void ConfigureServices(IServiceCollection services) { services.Configure<ElmahIoOptions>(Configuration.GetSection(\"ElmahIo\")); services.AddElmahIo(); } Notice that you still need to call AddElmahIo to correctly register middleware dependencies. Finally, call the UseElmahIo -method (as you would do with config in C# too): public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseElmahIo(); // ... } You can still configure additional options on the ElmahIoOptions object: public void ConfigureServices(IServiceCollection services) { services.Configure<ElmahIoOptions>(Configuration.GetSection(\"ElmahIo\")); services.Configure<ElmahIoOptions>(options => { options.OnMessage = msg => { msg.Version = \"1.0.0\"; }; }); services.AddElmahIo(); } builder.Services.Configure<ElmahIoOptions>(builder.Configuration.GetSection(\"ElmahIo\")); builder.Services.AddElmahIo(); Notice that you still need to call AddElmahIo to correctly register middleware dependencies. Finally, call the UseElmahIo -method (as you would do with config in C# too): app.UseElmahIo(); You can still configure additional options on the ElmahIoOptions object: builder.Services.Configure<ElmahIoOptions>(builder.Configuration.GetSection(\"ElmahIo\")); builder.Services.Configure<ElmahIoOptions>(o => { o.OnMessage = msg => { msg.Version = \"1.0.0\"; }; }); builder.Services.AddElmahIo(); Logging exceptions manually While automatically logging all uncaught exceptions is definitely a nice feature, sometimes you may want to catch exceptions and log them manually. If you just want to log the exception details, without all of the contextual information about the HTTP context (cookies, server variables, etc.), we recommend you to look at our integration for Microsoft.Extensions.Logging . If the context is important for the error, you can utilize the Ship -methods available in Elmah.Io.AspNetCore : try { var i = 0; var result = 42/i; } catch (DivideByZeroException e) { e.Ship(HttpContext); } When catching an exception (in this example an DivideByZeroException ), you call the Ship extension method with the current HTTP context as parameter. From Elmah.Io.AspNetCore version 3.12.* or newer, you can log manually using the ElmahIoApi class as well: ElmahIoApi.Log(e, HttpContext); The Ship -method uses ElmahIoApi underneath why both methods will give the same end result. Breadcrumbs See Logging breadcrumbs from ASP.NET Core . Additional options Setting application name If logging to the same log from multiple web apps it is a good idea to set unique application names from each app. This will let you search and filter errors on the elmah.io UI. To set an application name, add the following code to the options: builder.Services.AddElmahIo(o => { // ... o.Application = \"MyApp\"; }); The application name can also be configured through appsettings.json : { // ... \"ElmahIo\": { // ... \"Application\": \"MyApp\" } } Hooks elmah.io for ASP.NET Core supports a range of actions for hooking into the process of logging messages. Hooks are registered as actions when installing the elmah.io middleware: builder.Services.AddElmahIo(options => { // ... options.OnMessage = message => { message.Version = \"42\"; }; options.OnError = (message, exception) => { logger.LogError(1, exception, \"Error during log to elmah.io\"); }; }); The actions provide a mechanism for hooking into the log process. The action registered in the OnMessage property is called by elmah.io just before logging a new message to the API. Use this action to decorate/enrich your log messages with additional data, like a version number. The OnError action is called if communication with the elmah.io API failed. If this happens, you should log the message to a local log (through Microsoft.Extensions.Logging, Serilog or similar). Do not log to elmah.io in your OnError action, since that could cause an infinite loop in your code. While elmah.io supports ignore rules serverside, you may want to filter out errors without even hitting the elmah.io API. Using the OnFilter function on the options object, filtering is easy: builder.Services.AddElmahIo(options => { // ... options.OnFilter = message => { return message.Type == \"System.NullReferenceException\"; }; }); The example above, ignores all messages of type System.NullReferenceException . Decorate from HTTP context When implementing the OnMessage action as shown above you don't have access to the current HTTP context. Elmah.Io.AspNetCore already tries to fill in as many fields as possible from the current context, but you may want to tweak something from time to time. In this case, you can implement a custom decorator like this: public class DecorateElmahIoMessages : IConfigureOptions<ElmahIoOptions> { private readonly IHttpContextAccessor httpContextAccessor; public DecorateElmahIoMessages(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public void Configure(ElmahIoOptions options) { options.OnMessage = msg => { var context = httpContextAccessor.HttpContext; msg.User = context?.User?.Identity?.Name; }; } } Standard Top-level statements Then register IHttpContextAccessor and the new class in the ConfigureServices method in the Startup.cs file: public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddSingleton<IConfigureOptions<ElmahIoOptions>, DecorateElmahIoMessages>(); // ... } Then register IHttpContextAccessor and the new class in the in the Program.cs file: builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton<IConfigureOptions<ElmahIoOptions>, DecorateElmahIoMessages>(); Decorating messages using IConfigureOptions requires Elmah.Io.AspNetCore version 4.1.37 or newer. Include source code You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: builder.Services.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; }); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward. Remove sensitive form data The OnMessage event can be used to filter sensitive form data as well. In the following example, we remove the server variable named Secret-Key from all messages, before sending them to elmah.io. builder.Services.AddElmahIo(options => { // ... options.OnMessage = msg => { var item = msg.ServerVariables.FirstOrDefault(x => x.Key == \"Secret-Key\"); if (item != null) { msg.ServerVariables.Remove(item); } }; }); Formatting exceptions A default exception formatter is used to format any exceptions, before sending them off to the elmah.io API. To override the format of the details field in elmah.io, set a new IExceptionFormatter in the ExceptionFormatter property on the ElmahIoOptions object: builder.Services.AddElmahIo(options => { // ... options.ExceptionFormatter = new DefaultExceptionFormatter(); } Besides the default exception formatted ( DefaultExceptionFormatter ), Elmah.Io.AspNetCore comes with a formatter called YellowScreenOfDeathExceptionFormatter . This formatter, outputs an exception and its inner exceptions as a list of exceptions, much like on the ASP.NET yellow screen of death. If you want, implementing your own exception formatter, requires you to implement a single method. Logging responses not throwing an exception As default, uncaught exceptions (500's) and 404's are logged automatically. Let's say you have a controller returning a Bad Request and want to log that as well. Since returning a 400 from a controller doesn't trigger an exception, you will need to configure this status code: builder.Services.AddElmahIo(options => { // ... options.HandledStatusCodesToLog = new List<int> { 400 }; } The list can also be configured through appsettings.json : { // ... \"ElmahIo\": { // ... \"HandledStatusCodesToLog\": [ 400 ], } } When configuring status codes through the appsettings.json file, 404 s will always be logged. To avoid this, configure the list in C# as shown above. Logging through a proxy Since ASP.NET Core no longer support proxy configuration through web.config , you can log to elmah.io by configuring a proxy manually: builder.Services.AddElmahIo(options => { // ... options.WebProxy = new System.Net.WebProxy(\"localhost\", 8888); } In this example, the elmah.io client routes all traffic through http://localhost:8000 . Logging health check results Check out Logging heartbeats from ASP.NET Core for details.","title":"Logging from ASP.NET Core"},{"location":"logging-to-elmah-io-from-aspnet-core/#logging-to-elmahio-from-aspnet-core","text":"Logging to elmah.io from ASP.NET Core Configuring API key and log ID in options Logging exceptions manually Breadcrumbs Additional options Setting application name Hooks Decorate from HTTP context Include source code Remove sensitive form data Formatting exceptions Logging responses not throwing an exception Logging through a proxy Logging health check results If you are looking to log all uncaught errors from ASP.NET Core, you've come to the right place. For help setting up general .NET Core logging similar to log4net, check out Logging from Microsoft.Extensions.Logging . To log all warnings and errors from ASP.NET Core, install the following NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore dotnet add package Elmah.Io.AspNetCore <PackageReference Include=\"Elmah.Io.AspNetCore\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore In the Startup.cs file, add a new using statement: using Elmah.Io.AspNetCore; Standard Top-level statements Call AddElmahIo in the ConfigureServices -method: public void ConfigureServices(IServiceCollection services) { services.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); // ... } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Call UseElmahIo in the Configure -method: public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory fac) { // ... app.UseElmahIo(); // ... } Call AddElmahIo in the Program.cs file: builder.Services.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Call UseElmahIo in the Program.cs file: app.UseElmahIo(); Make sure to call the UseElmahIo -method after installation of other pieces of middleware handling exceptions and auth (like UseDeveloperExceptionPage , UseExceptionHandler , UseAuthentication , and UseAuthorization ), but before any calls to UseEndpoints , UseMvc , MapRazorPages , and similar. That's it. Every uncaught exception will be logged to elmah.io. For an example of configuring elmah.io with ASP.NET Core minimal APIs, check out this sample .","title":"Logging to elmah.io from ASP.NET Core"},{"location":"logging-to-elmah-io-from-aspnet-core/#configuring-api-key-and-log-id-in-options","text":"If you have different environments (everyone has a least localhost and production), you should consider adding the API key and log ID in your appsettings.json file: { // ... \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } } Configuring elmah.io is done by calling the Configure -method before AddElmahIo : Standard Top-level statements public void ConfigureServices(IServiceCollection services) { services.Configure<ElmahIoOptions>(Configuration.GetSection(\"ElmahIo\")); services.AddElmahIo(); } Notice that you still need to call AddElmahIo to correctly register middleware dependencies. Finally, call the UseElmahIo -method (as you would do with config in C# too): public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseElmahIo(); // ... } You can still configure additional options on the ElmahIoOptions object: public void ConfigureServices(IServiceCollection services) { services.Configure<ElmahIoOptions>(Configuration.GetSection(\"ElmahIo\")); services.Configure<ElmahIoOptions>(options => { options.OnMessage = msg => { msg.Version = \"1.0.0\"; }; }); services.AddElmahIo(); } builder.Services.Configure<ElmahIoOptions>(builder.Configuration.GetSection(\"ElmahIo\")); builder.Services.AddElmahIo(); Notice that you still need to call AddElmahIo to correctly register middleware dependencies. Finally, call the UseElmahIo -method (as you would do with config in C# too): app.UseElmahIo(); You can still configure additional options on the ElmahIoOptions object: builder.Services.Configure<ElmahIoOptions>(builder.Configuration.GetSection(\"ElmahIo\")); builder.Services.Configure<ElmahIoOptions>(o => { o.OnMessage = msg => { msg.Version = \"1.0.0\"; }; }); builder.Services.AddElmahIo();","title":"Configuring API key and log ID in options"},{"location":"logging-to-elmah-io-from-aspnet-core/#logging-exceptions-manually","text":"While automatically logging all uncaught exceptions is definitely a nice feature, sometimes you may want to catch exceptions and log them manually. If you just want to log the exception details, without all of the contextual information about the HTTP context (cookies, server variables, etc.), we recommend you to look at our integration for Microsoft.Extensions.Logging . If the context is important for the error, you can utilize the Ship -methods available in Elmah.Io.AspNetCore : try { var i = 0; var result = 42/i; } catch (DivideByZeroException e) { e.Ship(HttpContext); } When catching an exception (in this example an DivideByZeroException ), you call the Ship extension method with the current HTTP context as parameter. From Elmah.Io.AspNetCore version 3.12.* or newer, you can log manually using the ElmahIoApi class as well: ElmahIoApi.Log(e, HttpContext); The Ship -method uses ElmahIoApi underneath why both methods will give the same end result.","title":"Logging exceptions manually"},{"location":"logging-to-elmah-io-from-aspnet-core/#breadcrumbs","text":"See Logging breadcrumbs from ASP.NET Core .","title":"Breadcrumbs"},{"location":"logging-to-elmah-io-from-aspnet-core/#additional-options","text":"","title":"Additional options"},{"location":"logging-to-elmah-io-from-aspnet-core/#setting-application-name","text":"If logging to the same log from multiple web apps it is a good idea to set unique application names from each app. This will let you search and filter errors on the elmah.io UI. To set an application name, add the following code to the options: builder.Services.AddElmahIo(o => { // ... o.Application = \"MyApp\"; }); The application name can also be configured through appsettings.json : { // ... \"ElmahIo\": { // ... \"Application\": \"MyApp\" } }","title":"Setting application name"},{"location":"logging-to-elmah-io-from-aspnet-core/#hooks","text":"elmah.io for ASP.NET Core supports a range of actions for hooking into the process of logging messages. Hooks are registered as actions when installing the elmah.io middleware: builder.Services.AddElmahIo(options => { // ... options.OnMessage = message => { message.Version = \"42\"; }; options.OnError = (message, exception) => { logger.LogError(1, exception, \"Error during log to elmah.io\"); }; }); The actions provide a mechanism for hooking into the log process. The action registered in the OnMessage property is called by elmah.io just before logging a new message to the API. Use this action to decorate/enrich your log messages with additional data, like a version number. The OnError action is called if communication with the elmah.io API failed. If this happens, you should log the message to a local log (through Microsoft.Extensions.Logging, Serilog or similar). Do not log to elmah.io in your OnError action, since that could cause an infinite loop in your code. While elmah.io supports ignore rules serverside, you may want to filter out errors without even hitting the elmah.io API. Using the OnFilter function on the options object, filtering is easy: builder.Services.AddElmahIo(options => { // ... options.OnFilter = message => { return message.Type == \"System.NullReferenceException\"; }; }); The example above, ignores all messages of type System.NullReferenceException .","title":"Hooks"},{"location":"logging-to-elmah-io-from-aspnet-core/#decorate-from-http-context","text":"When implementing the OnMessage action as shown above you don't have access to the current HTTP context. Elmah.Io.AspNetCore already tries to fill in as many fields as possible from the current context, but you may want to tweak something from time to time. In this case, you can implement a custom decorator like this: public class DecorateElmahIoMessages : IConfigureOptions<ElmahIoOptions> { private readonly IHttpContextAccessor httpContextAccessor; public DecorateElmahIoMessages(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public void Configure(ElmahIoOptions options) { options.OnMessage = msg => { var context = httpContextAccessor.HttpContext; msg.User = context?.User?.Identity?.Name; }; } } Standard Top-level statements Then register IHttpContextAccessor and the new class in the ConfigureServices method in the Startup.cs file: public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddSingleton<IConfigureOptions<ElmahIoOptions>, DecorateElmahIoMessages>(); // ... } Then register IHttpContextAccessor and the new class in the in the Program.cs file: builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton<IConfigureOptions<ElmahIoOptions>, DecorateElmahIoMessages>(); Decorating messages using IConfigureOptions requires Elmah.Io.AspNetCore version 4.1.37 or newer.","title":"Decorate from HTTP context"},{"location":"logging-to-elmah-io-from-aspnet-core/#include-source-code","text":"You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: builder.Services.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; }); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.","title":"Include source code"},{"location":"logging-to-elmah-io-from-aspnet-core/#remove-sensitive-form-data","text":"The OnMessage event can be used to filter sensitive form data as well. In the following example, we remove the server variable named Secret-Key from all messages, before sending them to elmah.io. builder.Services.AddElmahIo(options => { // ... options.OnMessage = msg => { var item = msg.ServerVariables.FirstOrDefault(x => x.Key == \"Secret-Key\"); if (item != null) { msg.ServerVariables.Remove(item); } }; });","title":"Remove sensitive form data"},{"location":"logging-to-elmah-io-from-aspnet-core/#formatting-exceptions","text":"A default exception formatter is used to format any exceptions, before sending them off to the elmah.io API. To override the format of the details field in elmah.io, set a new IExceptionFormatter in the ExceptionFormatter property on the ElmahIoOptions object: builder.Services.AddElmahIo(options => { // ... options.ExceptionFormatter = new DefaultExceptionFormatter(); } Besides the default exception formatted ( DefaultExceptionFormatter ), Elmah.Io.AspNetCore comes with a formatter called YellowScreenOfDeathExceptionFormatter . This formatter, outputs an exception and its inner exceptions as a list of exceptions, much like on the ASP.NET yellow screen of death. If you want, implementing your own exception formatter, requires you to implement a single method.","title":"Formatting exceptions"},{"location":"logging-to-elmah-io-from-aspnet-core/#logging-responses-not-throwing-an-exception","text":"As default, uncaught exceptions (500's) and 404's are logged automatically. Let's say you have a controller returning a Bad Request and want to log that as well. Since returning a 400 from a controller doesn't trigger an exception, you will need to configure this status code: builder.Services.AddElmahIo(options => { // ... options.HandledStatusCodesToLog = new List<int> { 400 }; } The list can also be configured through appsettings.json : { // ... \"ElmahIo\": { // ... \"HandledStatusCodesToLog\": [ 400 ], } } When configuring status codes through the appsettings.json file, 404 s will always be logged. To avoid this, configure the list in C# as shown above.","title":"Logging responses not throwing an exception"},{"location":"logging-to-elmah-io-from-aspnet-core/#logging-through-a-proxy","text":"Since ASP.NET Core no longer support proxy configuration through web.config , you can log to elmah.io by configuring a proxy manually: builder.Services.AddElmahIo(options => { // ... options.WebProxy = new System.Net.WebProxy(\"localhost\", 8888); } In this example, the elmah.io client routes all traffic through http://localhost:8000 .","title":"Logging through a proxy"},{"location":"logging-to-elmah-io-from-aspnet-core/#logging-health-check-results","text":"Check out Logging heartbeats from ASP.NET Core for details.","title":"Logging health check results"},{"location":"logging-to-elmah-io-from-aspnet-mvc/","text":"Logging to elmah.io from ASP.NET MVC Even though ELMAH works out of the box with ASP.NET MVC, ELMAH and MVC provide some features which interfere with one another. As usual, the great community around ELMAH has done something to fix this, by using the Elmah.Mvc NuGet package. We've built a package for ASP.NET MVC exclusively, which installs all the necessary packages. To start logging exceptions from ASP.NET MVC, install the Elmah.Io.Mvc NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Mvc dotnet add package Elmah.Io.Mvc <PackageReference Include=\"Elmah.Io.Mvc\" Version=\"5.*\" /> paket add Elmah.Io.Mvc During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). That's it. Every unhandled exception in ASP.NET MVC is logged to elmah.io. As part of the installation, we also installed Elmah.MVC , which adds some interesting logic around routing and authentication. Take a look in the web.config for application settings with the elmah.mvc. prefix. For documentation about these settings, check out the Elmah.MVC project on GitHub. Since Elmah.MVC configures an URL for accessing the ELMAH UI (just /elmah and not /elmah.axd ), you can remove the location element in web.config , added by the Elmah.Io.Mvc NuGet package installer.","title":"Logging from ASP.NET MVC"},{"location":"logging-to-elmah-io-from-aspnet-mvc/#logging-to-elmahio-from-aspnet-mvc","text":"Even though ELMAH works out of the box with ASP.NET MVC, ELMAH and MVC provide some features which interfere with one another. As usual, the great community around ELMAH has done something to fix this, by using the Elmah.Mvc NuGet package. We've built a package for ASP.NET MVC exclusively, which installs all the necessary packages. To start logging exceptions from ASP.NET MVC, install the Elmah.Io.Mvc NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Mvc dotnet add package Elmah.Io.Mvc <PackageReference Include=\"Elmah.Io.Mvc\" Version=\"5.*\" /> paket add Elmah.Io.Mvc During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). That's it. Every unhandled exception in ASP.NET MVC is logged to elmah.io. As part of the installation, we also installed Elmah.MVC , which adds some interesting logic around routing and authentication. Take a look in the web.config for application settings with the elmah.mvc. prefix. For documentation about these settings, check out the Elmah.MVC project on GitHub. Since Elmah.MVC configures an URL for accessing the ELMAH UI (just /elmah and not /elmah.axd ), you can remove the location element in web.config , added by the Elmah.Io.Mvc NuGet package installer.","title":"Logging to elmah.io from ASP.NET MVC"},{"location":"logging-to-elmah-io-from-aws-beanstalk/","text":"Logging to elmah.io from AWS Beanstalk Logging to elmah.io from AWS Beanstalk ASP.NET / MVC / Web API ASP.NET Core Logging to elmah.io from .NET applications deployed on AWS Beanstalk is as easy as with other cloud hosting services. Since Beanstalk runs normal ASP.NET, MVC, Web API, and Core applications, setting up elmah.io almost follows the guides already available in the elmah.io documentation. There are a few things to notice when needing to configure elmah.io, which will be explained in this document. ASP.NET / MVC / Web API To install elmah.io in ASP.NET , MVC , and/or Web API , please follow the guidelines for each framework. You can specify one set of API key and log ID in the Web.config file and another set in the Web.release.config file as explained here: Use multiple logs for different environments . If you want to include your production API key and log ID on AWS only (to avoid having sensitive information in source control), you can do this using Environment properties on AWS. Go to your environment on the AWS console and click the Configuration tab. Click the Edit button beneath the Software category and scroll to the bottom. There you will see a section named Environment properties . Input your API key and log ID: AWS inserts the properties as application settings in the Web.config file. To make sure that elmah.io uses API key and log ID from appSettings , change the <elmah> element to reference the keys specified on AWS: <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKeyKey=\"elmahio-apikey\" logIdKey=\"elmahio-logid\" /> The apiKeyKey and logIdKey attributes reference the app settings keys. Finally, if you have an API key and/or log ID specified as part of the appSettings> element in Web.config , you will need to remove those when running in production. The reason for this is that AWS only insert missing keys. To do so, modify your Web.release.config file: <appSettings> <add key=\"elmahio-logid\" xdt:Transform=\"Remove\" xdt:Locator=\"Match(key)\" /> <add key=\"elmahio-apikey\" xdt:Transform=\"Remove\" xdt:Locator=\"Match(key)\" /> </appSettings> ASP.NET Core To install elmah.io in ASP.NET Core, follow this guide: Logging to elmah.io from ASP.NET Core . If you want to include your production API key and log ID on AWS only (to avoid having sensitive information in source control), you can do this using Environment properties on AWS. Go to your environment on the AWS console and click the Configuration tab. Click the Edit button beneath the Software category and scroll to the bottom. There you will see a section named Environment properties . Input your API key and log ID: This example uses the double underscore syntax to set the ApiKey and LogId properties in the appsettings.json file: { \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } }","title":"Logging from AWS Beanstalk"},{"location":"logging-to-elmah-io-from-aws-beanstalk/#logging-to-elmahio-from-aws-beanstalk","text":"Logging to elmah.io from AWS Beanstalk ASP.NET / MVC / Web API ASP.NET Core Logging to elmah.io from .NET applications deployed on AWS Beanstalk is as easy as with other cloud hosting services. Since Beanstalk runs normal ASP.NET, MVC, Web API, and Core applications, setting up elmah.io almost follows the guides already available in the elmah.io documentation. There are a few things to notice when needing to configure elmah.io, which will be explained in this document.","title":"Logging to elmah.io from AWS Beanstalk"},{"location":"logging-to-elmah-io-from-aws-beanstalk/#aspnet-mvc-web-api","text":"To install elmah.io in ASP.NET , MVC , and/or Web API , please follow the guidelines for each framework. You can specify one set of API key and log ID in the Web.config file and another set in the Web.release.config file as explained here: Use multiple logs for different environments . If you want to include your production API key and log ID on AWS only (to avoid having sensitive information in source control), you can do this using Environment properties on AWS. Go to your environment on the AWS console and click the Configuration tab. Click the Edit button beneath the Software category and scroll to the bottom. There you will see a section named Environment properties . Input your API key and log ID: AWS inserts the properties as application settings in the Web.config file. To make sure that elmah.io uses API key and log ID from appSettings , change the <elmah> element to reference the keys specified on AWS: <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKeyKey=\"elmahio-apikey\" logIdKey=\"elmahio-logid\" /> The apiKeyKey and logIdKey attributes reference the app settings keys. Finally, if you have an API key and/or log ID specified as part of the appSettings> element in Web.config , you will need to remove those when running in production. The reason for this is that AWS only insert missing keys. To do so, modify your Web.release.config file: <appSettings> <add key=\"elmahio-logid\" xdt:Transform=\"Remove\" xdt:Locator=\"Match(key)\" /> <add key=\"elmahio-apikey\" xdt:Transform=\"Remove\" xdt:Locator=\"Match(key)\" /> </appSettings>","title":"ASP.NET / MVC / Web API"},{"location":"logging-to-elmah-io-from-aws-beanstalk/#aspnet-core","text":"To install elmah.io in ASP.NET Core, follow this guide: Logging to elmah.io from ASP.NET Core . If you want to include your production API key and log ID on AWS only (to avoid having sensitive information in source control), you can do this using Environment properties on AWS. Go to your environment on the AWS console and click the Configuration tab. Click the Edit button beneath the Software category and scroll to the bottom. There you will see a section named Environment properties . Input your API key and log ID: This example uses the double underscore syntax to set the ApiKey and LogId properties in the appsettings.json file: { \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } }","title":"ASP.NET Core"},{"location":"logging-to-elmah-io-from-aws-lambdas/","text":"Logging to elmah.io from AWS Lambdas Logging to elmah.io from AWS Lambdas Logging to elmah.io from AWS Serverless Application Logging when using Amazon.Lambda.AspNetCoreServer.Hosting Logging from AWS Lambda Project Since AWS now supports .NET Core, logging to elmah.io from a lambda is easy. Logging to elmah.io from AWS Serverless Application AWS Serverless Applications are running on ASP.NET Core. The configuration matches our documentation for ASP.NET Core. Check out Logging from ASP.NET Core for details on how to log all uncaught exceptions from an AWS Serverless Application. The .NET SDK for AWS comes with native support for logging to CloudWatch. We recommend using Microsoft.Extensions.Logging for logging everything to CloudWatch and warnings and errors to elmah.io. The configuration follows that of Logging from Microsoft.Extensions.Logging . AWS Serverless Applications doesn't have a Program.cs file. To configure logging, you will need to modify either LambdaEntryPoint.cs , LocalEntryPoint.cs or both: public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction { protected override void Init(IWebHostBuilder builder) { builder .UseStartup<Startup>() .ConfigureLogging((ctx, logging) => { logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); }); } } The same configuration would go into LocalEntryPoint.cs , if you want to log from localhost as well. Logging when using Amazon.Lambda.AspNetCoreServer.Hosting AWS supports running ASP.NET Core applications as Lambdas using the Amazon.Lambda.AspNetCoreServer.Hosting package. This can serve as an easy way for .NET developers like us to create minimal API-based endpoints and deploy them as Lambda functions on AWS. There's a downside to deploying ASP.NET Core this way since AWS will kill the process when it decides that it is no longer needed. The Elmah.Io.Extensions.Logging package runs an internal message queue and stores log messages asynchronously to better handle a large workload. When AWS kills the process without disposing of configured loggers, log messages queued for processing are left unhandled. To solve this, Elmah.Io.Extensions.Logging supports a property named Synchronous that disables the internal message queue and stores log messages in a synchronous way. You may still experience log messages not being stored, but that's a consequence of AWS's choice of killing the process rather than shutting it down nicely (like ASP.NET Core). To log messages synchronously, include the following code in your logging setup: builder.Services.AddLogging(logging => { logging.AddElmahIo(options => { // ... options.Synchronous = true; }); }); Be aware that logging a large number of log messages synchronously, may slow down your application and/or cause thread exhaustion. We recommend only logging errors this way and not debug, information, and similar. Logging from AWS Lambda Project AWS Lambda Project comes with native support for CloudWatch too. In our experience, it's not possible to configure multiple destinations on LambdaLogger , why you would want to use another framework when logging to elmah.io from an AWS Lambda Project. We recommend using a logging framework like Serilog , Microsoft.Extensions.Logging , NLog , or log4net .","title":"Logging from AWS Lambdas"},{"location":"logging-to-elmah-io-from-aws-lambdas/#logging-to-elmahio-from-aws-lambdas","text":"Logging to elmah.io from AWS Lambdas Logging to elmah.io from AWS Serverless Application Logging when using Amazon.Lambda.AspNetCoreServer.Hosting Logging from AWS Lambda Project Since AWS now supports .NET Core, logging to elmah.io from a lambda is easy.","title":"Logging to elmah.io from AWS Lambdas"},{"location":"logging-to-elmah-io-from-aws-lambdas/#logging-to-elmahio-from-aws-serverless-application","text":"AWS Serverless Applications are running on ASP.NET Core. The configuration matches our documentation for ASP.NET Core. Check out Logging from ASP.NET Core for details on how to log all uncaught exceptions from an AWS Serverless Application. The .NET SDK for AWS comes with native support for logging to CloudWatch. We recommend using Microsoft.Extensions.Logging for logging everything to CloudWatch and warnings and errors to elmah.io. The configuration follows that of Logging from Microsoft.Extensions.Logging . AWS Serverless Applications doesn't have a Program.cs file. To configure logging, you will need to modify either LambdaEntryPoint.cs , LocalEntryPoint.cs or both: public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction { protected override void Init(IWebHostBuilder builder) { builder .UseStartup<Startup>() .ConfigureLogging((ctx, logging) => { logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); }); } } The same configuration would go into LocalEntryPoint.cs , if you want to log from localhost as well.","title":"Logging to elmah.io from AWS Serverless Application"},{"location":"logging-to-elmah-io-from-aws-lambdas/#logging-when-using-amazonlambdaaspnetcoreserverhosting","text":"AWS supports running ASP.NET Core applications as Lambdas using the Amazon.Lambda.AspNetCoreServer.Hosting package. This can serve as an easy way for .NET developers like us to create minimal API-based endpoints and deploy them as Lambda functions on AWS. There's a downside to deploying ASP.NET Core this way since AWS will kill the process when it decides that it is no longer needed. The Elmah.Io.Extensions.Logging package runs an internal message queue and stores log messages asynchronously to better handle a large workload. When AWS kills the process without disposing of configured loggers, log messages queued for processing are left unhandled. To solve this, Elmah.Io.Extensions.Logging supports a property named Synchronous that disables the internal message queue and stores log messages in a synchronous way. You may still experience log messages not being stored, but that's a consequence of AWS's choice of killing the process rather than shutting it down nicely (like ASP.NET Core). To log messages synchronously, include the following code in your logging setup: builder.Services.AddLogging(logging => { logging.AddElmahIo(options => { // ... options.Synchronous = true; }); }); Be aware that logging a large number of log messages synchronously, may slow down your application and/or cause thread exhaustion. We recommend only logging errors this way and not debug, information, and similar.","title":"Logging when using Amazon.Lambda.AspNetCoreServer.Hosting"},{"location":"logging-to-elmah-io-from-aws-lambdas/#logging-from-aws-lambda-project","text":"AWS Lambda Project comes with native support for CloudWatch too. In our experience, it's not possible to configure multiple destinations on LambdaLogger , why you would want to use another framework when logging to elmah.io from an AWS Lambda Project. We recommend using a logging framework like Serilog , Microsoft.Extensions.Logging , NLog , or log4net .","title":"Logging from AWS Lambda Project"},{"location":"logging-to-elmah-io-from-azure-functions/","text":"Logging to elmah.io from Azure Functions Logging to elmah.io from Azure Functions Application name Message hooks Decorating log messages Include source code Handle errors Error filtering Logging through ILogger Log filtering Azure Functions v1 Logging errors from Azure Functions requires only a few lines of code. We've created a client specifically for Azure Functions. If your are looking for logging from Isolated Azure Functions (out of process) check out Logging to elmah.io from Isolated Azure Functions . Install the newest Elmah.Io.Functions package in your Azure Functions project: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions dotnet add package Elmah.Io.Functions <PackageReference Include=\"Elmah.Io.Functions\" Version=\"5.*\" /> paket add Elmah.Io.Functions The elmah.io integration for Azure Functions uses function filters and dependency injection part of the Microsoft.Azure.Functions.Extensions package. To configure elmah.io, open the Startup.cs file or create a new one if not already there. In the Configure -method, add the elmah.io options and exception filter: using Elmah.Io.Functions; using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; [assembly: FunctionsStartup(typeof(MyFunction.Startup))] namespace MyFunction { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { var config = new ConfigurationBuilder() .AddJsonFile(\"local.settings.json\", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); }); builder.Services.AddSingleton<IFunctionFilter, ElmahIoExceptionFilter>(); } } } Notice how API key and log ID are configured through the ElmahIoFunctionOptions object. In the last line of the Configure -method, the ElmahIoExceptionFilter -filter is configured. This filter will automatically catch any exception caused by your filter and log it to elmah.io. A quick comment about the obsolete warning showed when using the package. Microsoft marked IFunctionFilter as obsolete. Not because it will be removed, but because they may change the way attributes work in functions in the future. For now, you can suppress this warning with the following code: #pragma warning disable CS0618 // Type or member is obsolete builder.Services.AddSingleton<IFunctionFilter, ElmahIoExceptionFilter>(); #pragma warning restore CS0618 // Type or member is obsolete In your settings, add the apiKey and logId variables: { // ... \"Values\": { // ... \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with your log ID. When running on Azure or similar, you can overwrite apiKey and logId with application settings or environment variables as already thoroughly documented on Microsoft's documentation. Application name To set the application name on all errors, set the Application property during initialization: builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.Application = \"MyFunction\"; }); Message hooks Elmah.Io.Functions provide message hooks similar to the integrations with ASP.NET and ASP.NET Core. Decorating log messages To include additional information on log messages, you can use the OnMessage event when initializing ElmahIoFunctionOptions : builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.OnMessage = msg => { msg.Version = \"1.0.0\"; }; }); The example above includes a version number on all errors. Include source code You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; }); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Handle errors To handle any errors happening while processing a log message, you can use the OnError event when initializing ElmahIoFunctionOptions : builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.OnError = (msg, ex) => { logger.LogError(ex, ex.Message); }; }); The example above logs any errors during communication with elmah.io to a local log. Error filtering To ignore specific errors based on their content, you can use the OnFilter event when initializing ElmahIoFunctionOptions : builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.OnFilter = msg => { return msg.Method == \"GET\"; }; }); The example above ignores any errors generated during an HTTP GET request. Logging through ILogger Azure Functions can log through Microsoft.Extensions.Logging (MEL) too. By adding the filter, as shown above, all uncaught exceptions are automatically logged. But when configuring your Function app to log through MEL, custom messages can be logged through the ILogger interface. Furthermore, you will get detailed log messages from within the Function host. To set this up, install the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Then extend your Startup.cs file like this: builder.Services.AddLogging(logging => { logging.AddElmahIo(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); }); In the example, only warning messages and above are logged to elmah.io. You can remove the filter or set another log level if you want to log more. Jump to Log filtering to learn how to configure filters from config. Either pass an ILogger to your function method: public class MyFunction { public static void Run([TimerTrigger(\"...\")]TimerInfo myTimer, ILogger log) { log.LogWarning(\"This is a warning\"); } } Or inject an ILoggerFactory and create a logger as part of the constructor: public class MyFunction { private readonly ILogger log; public Function1(ILoggerFactory loggerFactory) { this.log = loggerFactory.CreateLogger(\"MyFunction\"); } public void Run([TimerTrigger(\"...\")]TimerInfo myTimer) { log.LogWarning(\"This is a warning\"); } } Log filtering The code above filters out all log messages with a severity lower than Warning . You can use all of the log filtering capabilities of Microsoft.Extensions.Logging to enable and disable various log levels from multiple categories. A common requirement is to only log Warning and more severe originating from the Azure Functions runtime, but log Information messages from your function code. This can be enabled through a custom category: public class MyFunction { private readonly ILogger log; public Function1(ILoggerFactory loggerFactory) { this.log = loggerFactory.CreateLogger(\"MyFunction\"); } public void Run([TimerTrigger(\"...\")]TimerInfo myTimer) { log.LogInformation(\"This is an information message\"); } } The MyFunction category will need configuration in either C# or in the host.json file: { // ... \"logging\": { \"logLevel\": { \"default\": \"Warning\", \"MyFunction\": \"Information\" } } } Azure Functions v1 The recent Elmah.Io.Functions package no longer supports Azure Functions v1. You can still log from Functions v1 using an older version of the package. Check out Logging to elmah.io from Azure WebJobs for details. The guide is for Azure WebJobs but installation for Functions v1 is identical.","title":"Logging from Azure Functions"},{"location":"logging-to-elmah-io-from-azure-functions/#logging-to-elmahio-from-azure-functions","text":"Logging to elmah.io from Azure Functions Application name Message hooks Decorating log messages Include source code Handle errors Error filtering Logging through ILogger Log filtering Azure Functions v1 Logging errors from Azure Functions requires only a few lines of code. We've created a client specifically for Azure Functions. If your are looking for logging from Isolated Azure Functions (out of process) check out Logging to elmah.io from Isolated Azure Functions . Install the newest Elmah.Io.Functions package in your Azure Functions project: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions dotnet add package Elmah.Io.Functions <PackageReference Include=\"Elmah.Io.Functions\" Version=\"5.*\" /> paket add Elmah.Io.Functions The elmah.io integration for Azure Functions uses function filters and dependency injection part of the Microsoft.Azure.Functions.Extensions package. To configure elmah.io, open the Startup.cs file or create a new one if not already there. In the Configure -method, add the elmah.io options and exception filter: using Elmah.Io.Functions; using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; [assembly: FunctionsStartup(typeof(MyFunction.Startup))] namespace MyFunction { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { var config = new ConfigurationBuilder() .AddJsonFile(\"local.settings.json\", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); }); builder.Services.AddSingleton<IFunctionFilter, ElmahIoExceptionFilter>(); } } } Notice how API key and log ID are configured through the ElmahIoFunctionOptions object. In the last line of the Configure -method, the ElmahIoExceptionFilter -filter is configured. This filter will automatically catch any exception caused by your filter and log it to elmah.io. A quick comment about the obsolete warning showed when using the package. Microsoft marked IFunctionFilter as obsolete. Not because it will be removed, but because they may change the way attributes work in functions in the future. For now, you can suppress this warning with the following code: #pragma warning disable CS0618 // Type or member is obsolete builder.Services.AddSingleton<IFunctionFilter, ElmahIoExceptionFilter>(); #pragma warning restore CS0618 // Type or member is obsolete In your settings, add the apiKey and logId variables: { // ... \"Values\": { // ... \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with your log ID. When running on Azure or similar, you can overwrite apiKey and logId with application settings or environment variables as already thoroughly documented on Microsoft's documentation.","title":"Logging to elmah.io from Azure Functions"},{"location":"logging-to-elmah-io-from-azure-functions/#application-name","text":"To set the application name on all errors, set the Application property during initialization: builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.Application = \"MyFunction\"; });","title":"Application name"},{"location":"logging-to-elmah-io-from-azure-functions/#message-hooks","text":"Elmah.Io.Functions provide message hooks similar to the integrations with ASP.NET and ASP.NET Core.","title":"Message hooks"},{"location":"logging-to-elmah-io-from-azure-functions/#decorating-log-messages","text":"To include additional information on log messages, you can use the OnMessage event when initializing ElmahIoFunctionOptions : builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.OnMessage = msg => { msg.Version = \"1.0.0\"; }; }); The example above includes a version number on all errors.","title":"Decorating log messages"},{"location":"logging-to-elmah-io-from-azure-functions/#include-source-code","text":"You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; }); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.","title":"Include source code"},{"location":"logging-to-elmah-io-from-azure-functions/#handle-errors","text":"To handle any errors happening while processing a log message, you can use the OnError event when initializing ElmahIoFunctionOptions : builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.OnError = (msg, ex) => { logger.LogError(ex, ex.Message); }; }); The example above logs any errors during communication with elmah.io to a local log.","title":"Handle errors"},{"location":"logging-to-elmah-io-from-azure-functions/#error-filtering","text":"To ignore specific errors based on their content, you can use the OnFilter event when initializing ElmahIoFunctionOptions : builder.Services.Configure<ElmahIoFunctionOptions>(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); o.OnFilter = msg => { return msg.Method == \"GET\"; }; }); The example above ignores any errors generated during an HTTP GET request.","title":"Error filtering"},{"location":"logging-to-elmah-io-from-azure-functions/#logging-through-ilogger","text":"Azure Functions can log through Microsoft.Extensions.Logging (MEL) too. By adding the filter, as shown above, all uncaught exceptions are automatically logged. But when configuring your Function app to log through MEL, custom messages can be logged through the ILogger interface. Furthermore, you will get detailed log messages from within the Function host. To set this up, install the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Then extend your Startup.cs file like this: builder.Services.AddLogging(logging => { logging.AddElmahIo(o => { o.ApiKey = config[\"apiKey\"]; o.LogId = new Guid(config[\"logId\"]); }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); }); In the example, only warning messages and above are logged to elmah.io. You can remove the filter or set another log level if you want to log more. Jump to Log filtering to learn how to configure filters from config. Either pass an ILogger to your function method: public class MyFunction { public static void Run([TimerTrigger(\"...\")]TimerInfo myTimer, ILogger log) { log.LogWarning(\"This is a warning\"); } } Or inject an ILoggerFactory and create a logger as part of the constructor: public class MyFunction { private readonly ILogger log; public Function1(ILoggerFactory loggerFactory) { this.log = loggerFactory.CreateLogger(\"MyFunction\"); } public void Run([TimerTrigger(\"...\")]TimerInfo myTimer) { log.LogWarning(\"This is a warning\"); } }","title":"Logging through ILogger"},{"location":"logging-to-elmah-io-from-azure-functions/#log-filtering","text":"The code above filters out all log messages with a severity lower than Warning . You can use all of the log filtering capabilities of Microsoft.Extensions.Logging to enable and disable various log levels from multiple categories. A common requirement is to only log Warning and more severe originating from the Azure Functions runtime, but log Information messages from your function code. This can be enabled through a custom category: public class MyFunction { private readonly ILogger log; public Function1(ILoggerFactory loggerFactory) { this.log = loggerFactory.CreateLogger(\"MyFunction\"); } public void Run([TimerTrigger(\"...\")]TimerInfo myTimer) { log.LogInformation(\"This is an information message\"); } } The MyFunction category will need configuration in either C# or in the host.json file: { // ... \"logging\": { \"logLevel\": { \"default\": \"Warning\", \"MyFunction\": \"Information\" } } }","title":"Log filtering"},{"location":"logging-to-elmah-io-from-azure-functions/#azure-functions-v1","text":"The recent Elmah.Io.Functions package no longer supports Azure Functions v1. You can still log from Functions v1 using an older version of the package. Check out Logging to elmah.io from Azure WebJobs for details. The guide is for Azure WebJobs but installation for Functions v1 is identical.","title":"Azure Functions v1"},{"location":"logging-to-elmah-io-from-azure-webjobs/","text":"Logging to elmah.io from Azure WebJobs Logging errors from Azure WebJobs requires only a few lines of code. We've created a client specifically for Azure WebJobs. Support for Azure WebJobs has been stopped on version 3.1.23 of the Elmah.Io.Functions package. The newer versions only work with Azure Functions. Install the Elmah.Io.Functions package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions -Version 3.1.23 dotnet add package Elmah.Io.Functions --version 3.1.23 <PackageReference Include=\"Elmah.Io.Functions\" Version=\"3.1.23\" /> paket add Elmah.Io.Functions --version 3.1.23 Log all uncaught exceptions using the ElmahIoExceptionFilter attribute: [ElmahIoExceptionFilter(\"API_KEY\", \"LOG_ID\")] public class Functions { public static void ProcessQueueMessage([QueueTrigger(\"queue\")] string msg, TextWriter log) { throw new Exception(\"Some exception\"); } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with your log ID. If your WebJob method is declared as async, remember to change the return type to Task . Without it, ElmahIoExceptionFilter is never invoked. The filter also supports config variables: [ElmahIoExceptionFilter(\"%apiKey%\", \"%logId%\")] The variables above, would require you to add your API key and log ID to your App.config : <configuration> <appSettings> <add key=\"apiKey\" value=\"API_KEY\"/> <add key=\"logId\" value=\"LOG_ID\"/> </appSettings> </configuration>","title":"Logging from Azure WebJobs"},{"location":"logging-to-elmah-io-from-azure-webjobs/#logging-to-elmahio-from-azure-webjobs","text":"Logging errors from Azure WebJobs requires only a few lines of code. We've created a client specifically for Azure WebJobs. Support for Azure WebJobs has been stopped on version 3.1.23 of the Elmah.Io.Functions package. The newer versions only work with Azure Functions. Install the Elmah.Io.Functions package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions -Version 3.1.23 dotnet add package Elmah.Io.Functions --version 3.1.23 <PackageReference Include=\"Elmah.Io.Functions\" Version=\"3.1.23\" /> paket add Elmah.Io.Functions --version 3.1.23 Log all uncaught exceptions using the ElmahIoExceptionFilter attribute: [ElmahIoExceptionFilter(\"API_KEY\", \"LOG_ID\")] public class Functions { public static void ProcessQueueMessage([QueueTrigger(\"queue\")] string msg, TextWriter log) { throw new Exception(\"Some exception\"); } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with your log ID. If your WebJob method is declared as async, remember to change the return type to Task . Without it, ElmahIoExceptionFilter is never invoked. The filter also supports config variables: [ElmahIoExceptionFilter(\"%apiKey%\", \"%logId%\")] The variables above, would require you to add your API key and log ID to your App.config : <configuration> <appSettings> <add key=\"apiKey\" value=\"API_KEY\"/> <add key=\"logId\" value=\"LOG_ID\"/> </appSettings> </configuration>","title":"Logging to elmah.io from Azure WebJobs"},{"location":"logging-to-elmah-io-from-blazor/","text":"Logging to elmah.io from Blazor Logging to elmah.io from Blazor Blazor Server App Include details from the HTTP context Blazor WebAssembly App (wasm) Blazor (United) App Blazor Server App To start logging to elmah.io from a Blazor Server App, install the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging In the Program.cs file, add elmah.io logging configuration: builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). All uncaught exceptions are automatically logged to elmah.io. Exceptions can be logged manually, by injecting an ILogger into your view and adding try/catch : @using Microsoft.Extensions.Logging @inject ILogger<FetchData> logger <!-- ... --> @functions { WeatherForecast[] forecasts; protected override async Task OnInitAsync() { try { forecasts = await Http .GetJsonAsync<WeatherForecast[]>(\"api/SampleData/WeatherForecasts-nonexisting\"); } catch (Exception e) { logger.LogError(e, e.Message); } } } Information and other severities can be logged as well: @using Microsoft.Extensions.Logging @inject ILogger<Counter> logger <!-- ... --> @functions { int currentCount = 0; void IncrementCount() { currentCount++; logger.LogInformation(\"Incremented count to {currentCount}\", currentCount); } } Include details from the HTTP context Microsoft.Extensions.Logging doesn't know that it is running inside a web server. That is why Elmah.Io.Extensions.Logging doesn't include HTTP contextual information like URL and status code as default. To do so, install the Elmah.Io.AspNetCore.ExtensionsLogging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.ExtensionsLogging dotnet add package Elmah.Io.AspNetCore.ExtensionsLogging <PackageReference Include=\"Elmah.Io.AspNetCore.ExtensionsLogging\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.ExtensionsLogging And add the following code to the Program.cs file: app.UseElmahIoExtensionsLogging(); Make sure to call this method just before the call to UseRouting and UseEndpoints . This will include some of the information you are looking for. There's a problem when running Blazor Server where you will see some of the URLs logged as part of errors on elmah.io having the value /_blazor . This is because Blazor doesn't work like traditional websites where the client requests the server and returns an HTML or JSON response. When navigating the UI, parts of the UI are loaded through SignalR, which causes the URL to be /_blazor . Unfortunately, we haven't found a good way to fix this globally. You can include the current URL on manual log statements by injecting a NavigationManager in the top of your .razor file: @inject NavigationManager navigationManager Then wrap your logging code in a new scope: Uri.TryCreate(navigationManager.Uri, UriKind.Absolute, out Uri url); using (Logger.BeginScope(new Dictionary<string, object> { { \"url\", url.AbsolutePath } })) { logger.LogError(exception, \"An error happened\"); } The code uses the current URL from the injected NavigationManager object. Blazor WebAssembly App (wasm) To start logging to elmah.io from a Blazor Wasm App, install the Elmah.Io.Blazor.Wasm NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Blazor.Wasm dotnet add package Elmah.Io.Blazor.Wasm <PackageReference Include=\"Elmah.Io.Blazor.Wasm\" Version=\"4.*\" /> paket add Elmah.Io.Blazor.Wasm In the Program.cs file, add elmah.io logging configuration: builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). All uncaught exceptions are automatically logged to elmah.io after calling AddElmahIo . Errors and other severities can be logged manually, by injecting an ILogger into your view and adding try/catch or by implementing error boundaries: @page \"/\" @inject ILogger<Index> logger @code { protected override void OnInitialized() { logger.LogInformation(\"Initializing index view\"); try { object text = \"Text\"; var cast = (int)text; } catch (InvalidCastException e) { logger.LogError(e, \"An error happened\"); } } } The following may be implemented by the package later: Additional information about the HTTP context (like cookies, URL, and user). Internal message queue and/or batch processing like Microsoft.Extensions.Logging . Support for logging scopes. Blazor (United) App .NET 8 introduces a new approach to developing Blazor applications, formerly known as Blazor United. We have started experimenting a bit with Blazor Apps which have the option of rendering both server-side and client-side from within the same Blazor application. As shown in the sections above, using server-side rendering needs Elmah.Io.Extensions.Logging while client-side rendering needs Elmah.Io.Blazor.Wasm . You cannot have both packages installed and configured in the same project so you need to stick to one of them for Blazor (United) Apps. Since the Elmah.Io.Extensions.Logging package doesn't work with Blazor WebAssembly, we recommend installing the Elmah.Io.Blazor.Wasm package if you want to log from both server-side and client-side. Once the new Blazor App framework matures, we will probably consolidate features from both packages into an Elmah.Io.Blazor package or similar.","title":"Logging from Blazor"},{"location":"logging-to-elmah-io-from-blazor/#logging-to-elmahio-from-blazor","text":"Logging to elmah.io from Blazor Blazor Server App Include details from the HTTP context Blazor WebAssembly App (wasm) Blazor (United) App","title":"Logging to elmah.io from Blazor"},{"location":"logging-to-elmah-io-from-blazor/#blazor-server-app","text":"To start logging to elmah.io from a Blazor Server App, install the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging In the Program.cs file, add elmah.io logging configuration: builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). All uncaught exceptions are automatically logged to elmah.io. Exceptions can be logged manually, by injecting an ILogger into your view and adding try/catch : @using Microsoft.Extensions.Logging @inject ILogger<FetchData> logger <!-- ... --> @functions { WeatherForecast[] forecasts; protected override async Task OnInitAsync() { try { forecasts = await Http .GetJsonAsync<WeatherForecast[]>(\"api/SampleData/WeatherForecasts-nonexisting\"); } catch (Exception e) { logger.LogError(e, e.Message); } } } Information and other severities can be logged as well: @using Microsoft.Extensions.Logging @inject ILogger<Counter> logger <!-- ... --> @functions { int currentCount = 0; void IncrementCount() { currentCount++; logger.LogInformation(\"Incremented count to {currentCount}\", currentCount); } }","title":"Blazor Server App"},{"location":"logging-to-elmah-io-from-blazor/#include-details-from-the-http-context","text":"Microsoft.Extensions.Logging doesn't know that it is running inside a web server. That is why Elmah.Io.Extensions.Logging doesn't include HTTP contextual information like URL and status code as default. To do so, install the Elmah.Io.AspNetCore.ExtensionsLogging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.ExtensionsLogging dotnet add package Elmah.Io.AspNetCore.ExtensionsLogging <PackageReference Include=\"Elmah.Io.AspNetCore.ExtensionsLogging\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.ExtensionsLogging And add the following code to the Program.cs file: app.UseElmahIoExtensionsLogging(); Make sure to call this method just before the call to UseRouting and UseEndpoints . This will include some of the information you are looking for. There's a problem when running Blazor Server where you will see some of the URLs logged as part of errors on elmah.io having the value /_blazor . This is because Blazor doesn't work like traditional websites where the client requests the server and returns an HTML or JSON response. When navigating the UI, parts of the UI are loaded through SignalR, which causes the URL to be /_blazor . Unfortunately, we haven't found a good way to fix this globally. You can include the current URL on manual log statements by injecting a NavigationManager in the top of your .razor file: @inject NavigationManager navigationManager Then wrap your logging code in a new scope: Uri.TryCreate(navigationManager.Uri, UriKind.Absolute, out Uri url); using (Logger.BeginScope(new Dictionary<string, object> { { \"url\", url.AbsolutePath } })) { logger.LogError(exception, \"An error happened\"); } The code uses the current URL from the injected NavigationManager object.","title":"Include details from the HTTP context"},{"location":"logging-to-elmah-io-from-blazor/#blazor-webassembly-app-wasm","text":"To start logging to elmah.io from a Blazor Wasm App, install the Elmah.Io.Blazor.Wasm NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Blazor.Wasm dotnet add package Elmah.Io.Blazor.Wasm <PackageReference Include=\"Elmah.Io.Blazor.Wasm\" Version=\"4.*\" /> paket add Elmah.Io.Blazor.Wasm In the Program.cs file, add elmah.io logging configuration: builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). All uncaught exceptions are automatically logged to elmah.io after calling AddElmahIo . Errors and other severities can be logged manually, by injecting an ILogger into your view and adding try/catch or by implementing error boundaries: @page \"/\" @inject ILogger<Index> logger @code { protected override void OnInitialized() { logger.LogInformation(\"Initializing index view\"); try { object text = \"Text\"; var cast = (int)text; } catch (InvalidCastException e) { logger.LogError(e, \"An error happened\"); } } } The following may be implemented by the package later: Additional information about the HTTP context (like cookies, URL, and user). Internal message queue and/or batch processing like Microsoft.Extensions.Logging . Support for logging scopes.","title":"Blazor WebAssembly App (wasm)"},{"location":"logging-to-elmah-io-from-blazor/#blazor-united-app","text":".NET 8 introduces a new approach to developing Blazor applications, formerly known as Blazor United. We have started experimenting a bit with Blazor Apps which have the option of rendering both server-side and client-side from within the same Blazor application. As shown in the sections above, using server-side rendering needs Elmah.Io.Extensions.Logging while client-side rendering needs Elmah.Io.Blazor.Wasm . You cannot have both packages installed and configured in the same project so you need to stick to one of them for Blazor (United) Apps. Since the Elmah.Io.Extensions.Logging package doesn't work with Blazor WebAssembly, we recommend installing the Elmah.Io.Blazor.Wasm package if you want to log from both server-side and client-side. Once the new Blazor App framework matures, we will probably consolidate features from both packages into an Elmah.Io.Blazor package or similar.","title":"Blazor (United) App"},{"location":"logging-to-elmah-io-from-blogengine-net/","text":"Logging to elmah.io from BlogEngine.NET Because BlogEngine.NET is written in ASP.NET, it doesn't need any custom code to use ELMAH and elmah.io. ELMAH works out of the box for most web frameworks by Microsoft. If you are building and deploying the code yourself, installing elmah.io is achieved using our NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). When installed, BlogEngine.NET starts reporting errors to elmah.io. To check it out, force an internal server error or similar, and visit /elmah.axd or the search area of your log at elmah.io. Some of you may use the BlogEngine.NET binaries or even installed it using a one-click installer. In this case you will need to add elmah.io manually. To do that, use a tool like NuGet Package Explorer to download the most recent versions of ELMAH and elmah.io. Copy Elmah.dll and Elmah.Io.dll to the bin directory of your BlogEngine.NET installation. Also modify your web.config to include the ELMAH config as shown in the config example. Last but not least, remember to add the elmah.io error logger configuration as a child node to the <elmah> element: <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> Where API_KEY is your API key and LOG_ID is your log ID. To wrap this up, you may have noticed that there's a NuGet package to bring ELMAH support into BlogEngine.NET. This package adds the ELMAH assembly and config as well as adds a nice BlogEngine.NET compliant URL for browsing errors. Feel free to use this package, but remember to add it after the elmah.io package. Also, make sure to clean up the dual error log configuration: <elmah> <security allowRemoteAccess=\"false\" /> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"APIKEY\" logId=\"LOGID\" /> <security allowRemoteAccess=\"true\" /> <errorLog type=\"Elmah.SqlServerCompactErrorLog, Elmah\" connectionStringName=\"elmah-sqlservercompact\" /> </elmah>","title":"Logging from BlogEngine.NET"},{"location":"logging-to-elmah-io-from-blogengine-net/#logging-to-elmahio-from-blogenginenet","text":"Because BlogEngine.NET is written in ASP.NET, it doesn't need any custom code to use ELMAH and elmah.io. ELMAH works out of the box for most web frameworks by Microsoft. If you are building and deploying the code yourself, installing elmah.io is achieved using our NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). When installed, BlogEngine.NET starts reporting errors to elmah.io. To check it out, force an internal server error or similar, and visit /elmah.axd or the search area of your log at elmah.io. Some of you may use the BlogEngine.NET binaries or even installed it using a one-click installer. In this case you will need to add elmah.io manually. To do that, use a tool like NuGet Package Explorer to download the most recent versions of ELMAH and elmah.io. Copy Elmah.dll and Elmah.Io.dll to the bin directory of your BlogEngine.NET installation. Also modify your web.config to include the ELMAH config as shown in the config example. Last but not least, remember to add the elmah.io error logger configuration as a child node to the <elmah> element: <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> Where API_KEY is your API key and LOG_ID is your log ID. To wrap this up, you may have noticed that there's a NuGet package to bring ELMAH support into BlogEngine.NET. This package adds the ELMAH assembly and config as well as adds a nice BlogEngine.NET compliant URL for browsing errors. Feel free to use this package, but remember to add it after the elmah.io package. Also, make sure to clean up the dual error log configuration: <elmah> <security allowRemoteAccess=\"false\" /> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"APIKEY\" logId=\"LOGID\" /> <security allowRemoteAccess=\"true\" /> <errorLog type=\"Elmah.SqlServerCompactErrorLog, Elmah\" connectionStringName=\"elmah-sqlservercompact\" /> </elmah>","title":"Logging to elmah.io from BlogEngine.NET"},{"location":"logging-to-elmah-io-from-console-application/","text":"Logging to elmah.io from C# and console applications Logging to elmah.io from C# and console applications Structured logging Breadcrumbs Events OnMessage Include source code OnMessageFail Bulk upload Options Proxy Obfuscate form values Full example If you need to log to elmah.io and you cannot use one of the integrations we provide, logging through the Elmah.Io.Client NuGet package is dead simple. To start logging, install the Elmah.Io.Client package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Create a new ElmahioAPI : var logger = ElmahioAPI.Create(\"API_KEY\"); Replace API_KEY with your API key ( Where is my API key? ). The elmah.io client supports logging in different log levels much like other logging frameworks for .NET: var logId = new Guid(\"LOG_ID\"); logger.Messages.Fatal(logId, new ApplicationException(\"A fatal exception\"), \"Fatal message\"); logger.Messages.Error(logId, new ApplicationException(\"An exception\"), \"Error message\"); logger.Messages.Warning(logId, \"A warning\"); logger.Messages.Information(logId, \"An info message\"); logger.Messages.Debug(logId, \"A debug message\"); logger.Messages.Verbose(logId, \"A verbose message\"); Replace LOG_ID with your log ID from elmah.io ( Where is my log ID? ). To have 100% control of how the message is logged to elmah.io, you can use the CreateAndNotify -method: logger.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Hello World\", Application = \"Elmah.Io.Client sample\", Detail = \"This is a long description of the error. Maybe even a stack trace\", Severity = Severity.Error.ToString(), Data = new List<Item> { new Item {Key = \"Username\", Value = \"Man in black\"} }, Form = new List<Item> { new Item {Key = \"Password\", Value = \"SecretPassword\"}, new Item {Key = \"pwd\", Value = \"Other secret value\"}, new Item {Key = \"visible form item\", Value = \"With a value\"} } }); Structured logging Like the integrations for Serilog, NLog and, Microsoft.Extensions.Logging, the elmah.io client supports structured logging: logger.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Thomas says Hello\", TitleTemplate = \"{User} says Hello\", }); Breadcrumbs You can log one or more breadcrumbs as part of a log message. Breadcrumbs indicate steps happening just before a log message (typically an error). Breadcrumbs are supported through the Breadcrumbs property on the CreateMessage class: logger.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Oh no, an error happened\", Severity = \"Error\", Breadcrumbs = new List<Breadcrumb> { new Breadcrumb { DateTime = DateTime.UtcNow.AddSeconds(-10), Action = \"Navigation\", Message = \"Navigate from / to /signin\", Severity = \"Information\" }, new Breadcrumb { DateTime = DateTime.UtcNow.AddSeconds(-3), Action = \"Click\", Message = \"#submit\", Severity = \"Information\" }, new Breadcrumb { DateTime = DateTime.UtcNow.AddSeconds(-2), Action = \"Submit\", Message = \"#loginform\", Severity = \"Information\" }, new Breadcrumb { DateTime = DateTime.UtcNow.AddSeconds(-1), Action = \"Request\", Message = \"/save\", Severity = \"Information\" } } }); Breadcrumbs will be ordered by the DateTime field on the elmah.io API why the order you add them to the Breadcrumbs property isn't that important. Be aware that only the 10 most recent breadcrumbs and breadcrumbs with a date less than or equal to the logged message are stored. In the example above, only Information breadcrumbs are added. The Severity property accepts the same severities as on the log message itself. Events The elmah.io client supports two different events: OnMessage and OnMessageFail . OnMessage To get a callback every time a new message is being logged to elmah.io, you can implement the OnMessage event. This is a great chance to decorate all log messages with a specific property or similar. logger.Messages.OnMessage += (sender, eventArgs) => { eventArgs.Message.Version = \"1.0.0\"; }; Include source code You can use the OnMessage event to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage event handler: logger.Messages.OnMessage += (sender, eventArgs) => { eventArgs.Message.WithSourceCodeFromPdb(); }; Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward. OnMessageFail Logging to elmah.io can fail if the network connection is down, if elmah.io experiences downtime, or something third. To make sure you log an error elsewhere if this happens, you can implement the OnMessageFail event: logger.Messages.OnMessageFail += (sender, eventArgs) => { System.Console.Error.WriteLine(\"Error when logging to elmah.io\"); }; Bulk upload If logging many messages to elmah.io, bulk upload can be a way to optimize performance. The elmah.io client supports bulk upload using the CreateBulkAndNotify -method: logger.Messages.CreateBulkAndNotify(logId, new[] { new CreateMessage { Title = \"This is a bulk message\" }, new CreateMessage { Title = \"This is another bulk message\" }, }.ToList()); Options The elmah.io client contains a set of default options that you can override. Proxy To log through a HTTP proxy, set the WebProxy property: var logger = ElmahioAPI.Create(\"API_KEY\", new ElmahIoOptions { WebProxy = new WebProxy(\"localhost\", 8888) }); A proxy needs to be specified as part of the options sent to the ElmahioAPI.Create method to make sure that the underlying HttpClient is properly initialized. Obfuscate form values When logging POSTs with form values, you don't want users' passwords and similar logged to elmah.io. The elmah.io client automatically filters form keys named password and pwd . Using the FormKeysToObfuscate you can tell the client to obfuscate additional form entries: var logger = ElmahioAPI.Create(\"API_KEY\"); logger.Options.FormKeysToObfuscate.Add(\"secret_key\"); Full example Here's a full example of how to catch all exceptions in a console application and log as many information as possible to elmah.io: class Program { private static IElmahioAPI elmahIo; static void Main(string[] args) { try { AppDomain.CurrentDomain.UnhandledException += (sender, e) => LogException(e.ExceptionObject as Exception); // Run some code } catch (Exception e) { LogException(e); } } private static void LogException(Exception e) { if (elmahIo == null) elmahIo = ElmahioAPI.Create(\"API_KEY\"); var baseException = e?.GetBaseException(); elmahIo.Messages.CreateAndNotify(new Guid(\"LOG_ID\"), new CreateMessage { Data = e?.ToDataList(), Detail = e?.ToString(), Hostname = Environment.MachineName, Severity = Severity.Error.ToString(), Source = baseException?.Source, Title = baseException?.Message ?? \"An error happened\", Type = baseException?.GetType().FullName, User = Environment.UserName, }); } } The code will catch all exceptions both from the catch block and exceptions reported through the UnhandledException event.","title":"Logging from C# and console applications"},{"location":"logging-to-elmah-io-from-console-application/#logging-to-elmahio-from-c-and-console-applications","text":"Logging to elmah.io from C# and console applications Structured logging Breadcrumbs Events OnMessage Include source code OnMessageFail Bulk upload Options Proxy Obfuscate form values Full example If you need to log to elmah.io and you cannot use one of the integrations we provide, logging through the Elmah.Io.Client NuGet package is dead simple. To start logging, install the Elmah.Io.Client package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Create a new ElmahioAPI : var logger = ElmahioAPI.Create(\"API_KEY\"); Replace API_KEY with your API key ( Where is my API key? ). The elmah.io client supports logging in different log levels much like other logging frameworks for .NET: var logId = new Guid(\"LOG_ID\"); logger.Messages.Fatal(logId, new ApplicationException(\"A fatal exception\"), \"Fatal message\"); logger.Messages.Error(logId, new ApplicationException(\"An exception\"), \"Error message\"); logger.Messages.Warning(logId, \"A warning\"); logger.Messages.Information(logId, \"An info message\"); logger.Messages.Debug(logId, \"A debug message\"); logger.Messages.Verbose(logId, \"A verbose message\"); Replace LOG_ID with your log ID from elmah.io ( Where is my log ID? ). To have 100% control of how the message is logged to elmah.io, you can use the CreateAndNotify -method: logger.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Hello World\", Application = \"Elmah.Io.Client sample\", Detail = \"This is a long description of the error. Maybe even a stack trace\", Severity = Severity.Error.ToString(), Data = new List<Item> { new Item {Key = \"Username\", Value = \"Man in black\"} }, Form = new List<Item> { new Item {Key = \"Password\", Value = \"SecretPassword\"}, new Item {Key = \"pwd\", Value = \"Other secret value\"}, new Item {Key = \"visible form item\", Value = \"With a value\"} } });","title":"Logging to elmah.io from C# and console applications"},{"location":"logging-to-elmah-io-from-console-application/#structured-logging","text":"Like the integrations for Serilog, NLog and, Microsoft.Extensions.Logging, the elmah.io client supports structured logging: logger.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Thomas says Hello\", TitleTemplate = \"{User} says Hello\", });","title":"Structured logging"},{"location":"logging-to-elmah-io-from-console-application/#breadcrumbs","text":"You can log one or more breadcrumbs as part of a log message. Breadcrumbs indicate steps happening just before a log message (typically an error). Breadcrumbs are supported through the Breadcrumbs property on the CreateMessage class: logger.Messages.CreateAndNotify(logId, new CreateMessage { Title = \"Oh no, an error happened\", Severity = \"Error\", Breadcrumbs = new List<Breadcrumb> { new Breadcrumb { DateTime = DateTime.UtcNow.AddSeconds(-10), Action = \"Navigation\", Message = \"Navigate from / to /signin\", Severity = \"Information\" }, new Breadcrumb { DateTime = DateTime.UtcNow.AddSeconds(-3), Action = \"Click\", Message = \"#submit\", Severity = \"Information\" }, new Breadcrumb { DateTime = DateTime.UtcNow.AddSeconds(-2), Action = \"Submit\", Message = \"#loginform\", Severity = \"Information\" }, new Breadcrumb { DateTime = DateTime.UtcNow.AddSeconds(-1), Action = \"Request\", Message = \"/save\", Severity = \"Information\" } } }); Breadcrumbs will be ordered by the DateTime field on the elmah.io API why the order you add them to the Breadcrumbs property isn't that important. Be aware that only the 10 most recent breadcrumbs and breadcrumbs with a date less than or equal to the logged message are stored. In the example above, only Information breadcrumbs are added. The Severity property accepts the same severities as on the log message itself.","title":"Breadcrumbs"},{"location":"logging-to-elmah-io-from-console-application/#events","text":"The elmah.io client supports two different events: OnMessage and OnMessageFail .","title":"Events"},{"location":"logging-to-elmah-io-from-console-application/#onmessage","text":"To get a callback every time a new message is being logged to elmah.io, you can implement the OnMessage event. This is a great chance to decorate all log messages with a specific property or similar. logger.Messages.OnMessage += (sender, eventArgs) => { eventArgs.Message.Version = \"1.0.0\"; };","title":"OnMessage"},{"location":"logging-to-elmah-io-from-console-application/#include-source-code","text":"You can use the OnMessage event to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage event handler: logger.Messages.OnMessage += (sender, eventArgs) => { eventArgs.Message.WithSourceCodeFromPdb(); }; Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.","title":"Include source code"},{"location":"logging-to-elmah-io-from-console-application/#onmessagefail","text":"Logging to elmah.io can fail if the network connection is down, if elmah.io experiences downtime, or something third. To make sure you log an error elsewhere if this happens, you can implement the OnMessageFail event: logger.Messages.OnMessageFail += (sender, eventArgs) => { System.Console.Error.WriteLine(\"Error when logging to elmah.io\"); };","title":"OnMessageFail"},{"location":"logging-to-elmah-io-from-console-application/#bulk-upload","text":"If logging many messages to elmah.io, bulk upload can be a way to optimize performance. The elmah.io client supports bulk upload using the CreateBulkAndNotify -method: logger.Messages.CreateBulkAndNotify(logId, new[] { new CreateMessage { Title = \"This is a bulk message\" }, new CreateMessage { Title = \"This is another bulk message\" }, }.ToList());","title":"Bulk upload"},{"location":"logging-to-elmah-io-from-console-application/#options","text":"The elmah.io client contains a set of default options that you can override.","title":"Options"},{"location":"logging-to-elmah-io-from-console-application/#proxy","text":"To log through a HTTP proxy, set the WebProxy property: var logger = ElmahioAPI.Create(\"API_KEY\", new ElmahIoOptions { WebProxy = new WebProxy(\"localhost\", 8888) }); A proxy needs to be specified as part of the options sent to the ElmahioAPI.Create method to make sure that the underlying HttpClient is properly initialized.","title":"Proxy"},{"location":"logging-to-elmah-io-from-console-application/#obfuscate-form-values","text":"When logging POSTs with form values, you don't want users' passwords and similar logged to elmah.io. The elmah.io client automatically filters form keys named password and pwd . Using the FormKeysToObfuscate you can tell the client to obfuscate additional form entries: var logger = ElmahioAPI.Create(\"API_KEY\"); logger.Options.FormKeysToObfuscate.Add(\"secret_key\");","title":"Obfuscate form values"},{"location":"logging-to-elmah-io-from-console-application/#full-example","text":"Here's a full example of how to catch all exceptions in a console application and log as many information as possible to elmah.io: class Program { private static IElmahioAPI elmahIo; static void Main(string[] args) { try { AppDomain.CurrentDomain.UnhandledException += (sender, e) => LogException(e.ExceptionObject as Exception); // Run some code } catch (Exception e) { LogException(e); } } private static void LogException(Exception e) { if (elmahIo == null) elmahIo = ElmahioAPI.Create(\"API_KEY\"); var baseException = e?.GetBaseException(); elmahIo.Messages.CreateAndNotify(new Guid(\"LOG_ID\"), new CreateMessage { Data = e?.ToDataList(), Detail = e?.ToString(), Hostname = Environment.MachineName, Severity = Severity.Error.ToString(), Source = baseException?.Source, Title = baseException?.Message ?? \"An error happened\", Type = baseException?.GetType().FullName, User = Environment.UserName, }); } } The code will catch all exceptions both from the catch block and exceptions reported through the UnhandledException event.","title":"Full example"},{"location":"logging-to-elmah-io-from-corewcf/","text":"Logging to elmah.io from CoreWCF elmah.io supports CoreWCF using our integration with Microsoft.Extensions.Logging. Start by installing the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Configure logging as part of the configuration (typically in the Program.cs file): builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want messages logged. CoreWCF will now send all messages logged from your application to elmah.io. CoreWCF doesn't log uncaught exceptions happening in WCF services to Microsoft.Extensions.Logging as you'd expect if coming from ASP.NET Core. To do this, you will need to add a custom error logger by including the following class: public class ElmahIoErrorHandler : IErrorHandler { private readonly ILogger<ElmahIoErrorHandler> logger; public ElmahIoErrorHandler(ILogger<ElmahIoErrorHandler> logger) { this.logger = logger; } public bool HandleError(Exception error) => false; public void ProvideFault(Exception error, MessageVersion version, ref Message fault) { if (error == null) return; logger.LogError(error, error.Message); } } The ElmahIoErrorHandler class will be called by CoreWCF when exceptions are thrown and log the to the configure ILogger . To invoke the error handler, add the following service behavior: public class ElmahIoErrorBehavior : IServiceBehavior { private readonly ILogger<ElmahIoErrorHandler> logger; public ElmahIoErrorBehavior(ILogger<ElmahIoErrorHandler> logger) { this.logger = logger; } public void Validate(ServiceDescription description, ServiceHostBase serviceHostBase) { } public void AddBindingParameters(ServiceDescription description, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters) { } public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase) { var errorHandler = (IErrorHandler)Activator.CreateInstance(typeof(ElmahIoErrorHandler), logger); foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers) { ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher; channelDispatcher.ErrorHandlers.Add(errorHandler); } } } The service behavior will look up the ElmahIoErrorHandler and register it with CoreWCF. The code above is hardcoded to work with the elmah.io error handler only. If you have multiple error handlers, you will need to register all of them. Finally, register the service behavior in the Program.cs file: builder.Services.AddSingleton<IServiceBehavior, ElmahIoErrorBehavior>(); Uncaught errors will now be logged to elmah.io. All of the settings from Elmah.Io.Extensions.Logging not mentioned on this page work with CoreWCF. Check out Logging to elmah.io from Microsoft.Extensions.Logging for details.","title":"Logging from CoreWCF"},{"location":"logging-to-elmah-io-from-corewcf/#logging-to-elmahio-from-corewcf","text":"elmah.io supports CoreWCF using our integration with Microsoft.Extensions.Logging. Start by installing the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Configure logging as part of the configuration (typically in the Program.cs file): builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want messages logged. CoreWCF will now send all messages logged from your application to elmah.io. CoreWCF doesn't log uncaught exceptions happening in WCF services to Microsoft.Extensions.Logging as you'd expect if coming from ASP.NET Core. To do this, you will need to add a custom error logger by including the following class: public class ElmahIoErrorHandler : IErrorHandler { private readonly ILogger<ElmahIoErrorHandler> logger; public ElmahIoErrorHandler(ILogger<ElmahIoErrorHandler> logger) { this.logger = logger; } public bool HandleError(Exception error) => false; public void ProvideFault(Exception error, MessageVersion version, ref Message fault) { if (error == null) return; logger.LogError(error, error.Message); } } The ElmahIoErrorHandler class will be called by CoreWCF when exceptions are thrown and log the to the configure ILogger . To invoke the error handler, add the following service behavior: public class ElmahIoErrorBehavior : IServiceBehavior { private readonly ILogger<ElmahIoErrorHandler> logger; public ElmahIoErrorBehavior(ILogger<ElmahIoErrorHandler> logger) { this.logger = logger; } public void Validate(ServiceDescription description, ServiceHostBase serviceHostBase) { } public void AddBindingParameters(ServiceDescription description, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters) { } public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase) { var errorHandler = (IErrorHandler)Activator.CreateInstance(typeof(ElmahIoErrorHandler), logger); foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers) { ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher; channelDispatcher.ErrorHandlers.Add(errorHandler); } } } The service behavior will look up the ElmahIoErrorHandler and register it with CoreWCF. The code above is hardcoded to work with the elmah.io error handler only. If you have multiple error handlers, you will need to register all of them. Finally, register the service behavior in the Program.cs file: builder.Services.AddSingleton<IServiceBehavior, ElmahIoErrorBehavior>(); Uncaught errors will now be logged to elmah.io. All of the settings from Elmah.Io.Extensions.Logging not mentioned on this page work with CoreWCF. Check out Logging to elmah.io from Microsoft.Extensions.Logging for details.","title":"Logging to elmah.io from CoreWCF"},{"location":"logging-to-elmah-io-from-devexpress/","text":"Logging to elmah.io from DevExpress (eXpressApp Framework) eXpressApp Framework (XAF) is built on top of ASP.NET. Installing elmah.io corresponds to any other ASP.NET site: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). To verify the integration, throw a new exception in Default.aspx or similar: <body class=\"VerticalTemplate\"> <% throw new Exception(\"Test exception\"); %> <form id=\"form2\" runat=\"server\"> <!-- ... --> </form> </body> Launch the project and see the test exception flow into elmah.io.","title":"Logging from DevExpress (eXpressApp Framework)"},{"location":"logging-to-elmah-io-from-devexpress/#logging-to-elmahio-from-devexpress-expressapp-framework","text":"eXpressApp Framework (XAF) is built on top of ASP.NET. Installing elmah.io corresponds to any other ASP.NET site: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). To verify the integration, throw a new exception in Default.aspx or similar: <body class=\"VerticalTemplate\"> <% throw new Exception(\"Test exception\"); %> <form id=\"form2\" runat=\"server\"> <!-- ... --> </form> </body> Launch the project and see the test exception flow into elmah.io.","title":"Logging to elmah.io from DevExpress (eXpressApp Framework)"},{"location":"logging-to-elmah-io-from-elmah/","text":"Logging to elmah.io from ASP.NET / WebForms This article will explain the steps necessary to log errors from your web application into elmah.io. We also offer more specific guides on ASP.NET MVC , Web API , and a lot of other web- and logging-frameworks. Read through this tutorial and head over to a tutorial specific for your choice of framework afterwards. This guide is also available as a short video tutorial here: Create a new ASP.NET Web Application in Visual Studio : Select a project template of your choice: Navigate to elmah.io and login using username/password or your favorite social provider. When logged in, elmah.io redirects you to the dashboard. If you just signed up, you will be guided through the process of creating an organization and a log. When the log has been created, elmah.io shows you the install instructions. If you are currently on the dashboard, click the gears icon on the lower right corner of the log box. Don't pay too much attention to the install steps, because the rest of this tutorial will guide you through the installation. Keep the page open in order to copy your API key and log ID at a later step: Navigate back to your web project, right click References and select Manage NuGet Packages : In the NuGet dialog, search for elmah.io: Select the elmah.io package and click Install . Input your API key and log ID in the dialog appearing during installation of the NuGet package: You're ready to rock and roll. Either add throw new Exception(\"Test\"); somewhere or hit F5 and input a URL you know doesn't exist (like http://localhost:64987/notfound). To verify that the installation of elmah.io is successful, navigate back to the elmah.io dashboard and select the Search tab of your newly created log: Congrats! Every error on your application is now logged to elmah.io.","title":"Logging from WebForms"},{"location":"logging-to-elmah-io-from-elmah/#logging-to-elmahio-from-aspnet-webforms","text":"This article will explain the steps necessary to log errors from your web application into elmah.io. We also offer more specific guides on ASP.NET MVC , Web API , and a lot of other web- and logging-frameworks. Read through this tutorial and head over to a tutorial specific for your choice of framework afterwards. This guide is also available as a short video tutorial here: Create a new ASP.NET Web Application in Visual Studio : Select a project template of your choice: Navigate to elmah.io and login using username/password or your favorite social provider. When logged in, elmah.io redirects you to the dashboard. If you just signed up, you will be guided through the process of creating an organization and a log. When the log has been created, elmah.io shows you the install instructions. If you are currently on the dashboard, click the gears icon on the lower right corner of the log box. Don't pay too much attention to the install steps, because the rest of this tutorial will guide you through the installation. Keep the page open in order to copy your API key and log ID at a later step: Navigate back to your web project, right click References and select Manage NuGet Packages : In the NuGet dialog, search for elmah.io: Select the elmah.io package and click Install . Input your API key and log ID in the dialog appearing during installation of the NuGet package: You're ready to rock and roll. Either add throw new Exception(\"Test\"); somewhere or hit F5 and input a URL you know doesn't exist (like http://localhost:64987/notfound). To verify that the installation of elmah.io is successful, navigate back to the elmah.io dashboard and select the Search tab of your newly created log: Congrats! Every error on your application is now logged to elmah.io.","title":"Logging to elmah.io from ASP.NET / WebForms"},{"location":"logging-to-elmah-io-from-entity-framework-core/","text":"Logging to elmah.io from Entity Framework Core Both elmah.io and Entity Framework Core supports logging through Microsoft.Extensions.Logging. To log all errors happening inside Entity Framework Core, install the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Then add elmah.io to a new or existing LoggerFactory : var loggerFactory = new LoggerFactory() .AddElmahIo(\"API_KEY\", new Guid(\"LOG_ID\")); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the log ID ( Where is my log ID? ) that should receive errors from Entity Framework. When using Entity Framework Core from ASP.NET Core, you never create a LoggerFactory . Factories are provided through DI by ASP.NET Core. Check out this sample for details. Finally, enable logging in Entity Framework Core: optionsBuilder .UseLoggerFactory(loggerFactory) .UseSqlServer(/*...*/); ( UseSqlServer included for illustration purposes only - elmah.io works with any provider) That's it! All errors happening in Entity Framework Core, are now logged in elmah.io.","title":"Logging from Entity Framework Core"},{"location":"logging-to-elmah-io-from-entity-framework-core/#logging-to-elmahio-from-entity-framework-core","text":"Both elmah.io and Entity Framework Core supports logging through Microsoft.Extensions.Logging. To log all errors happening inside Entity Framework Core, install the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Then add elmah.io to a new or existing LoggerFactory : var loggerFactory = new LoggerFactory() .AddElmahIo(\"API_KEY\", new Guid(\"LOG_ID\")); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the log ID ( Where is my log ID? ) that should receive errors from Entity Framework. When using Entity Framework Core from ASP.NET Core, you never create a LoggerFactory . Factories are provided through DI by ASP.NET Core. Check out this sample for details. Finally, enable logging in Entity Framework Core: optionsBuilder .UseLoggerFactory(loggerFactory) .UseSqlServer(/*...*/); ( UseSqlServer included for illustration purposes only - elmah.io works with any provider) That's it! All errors happening in Entity Framework Core, are now logged in elmah.io.","title":"Logging to elmah.io from Entity Framework Core"},{"location":"logging-to-elmah-io-from-google-cloud-functions/","text":"Logging to elmah.io from Google Cloud Functions Logging to elmah.io from Google Cloud Functions uses our integration with Microsoft.Extensions.Logging . To start logging, install the Elmah.Io.Extensions.Logging NuGet package through the Cloud Shell or locally: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging In the root of your Function app create a new file named Startup.cs : public class Startup : FunctionsStartup { public override void ConfigureServices(WebHostBuilderContext ctx, IServiceCollection services) { services.AddLogging(logging => { logging.AddElmahIo(o => { o.ApiKey = \"API_KEY\"; o.LogId = new Guid(\"LOG_ID\"); }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); }); } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log to store messages in ( Where is my log ID? ). The filter tells Google Cloud Functions to log warnings and above to elmah.io only. If you want to log detailed information about what goes on inside Google Cloud Functions, you can lower the log level. Decorate your function with a FunctionsStartup attribute: [FunctionsStartup(typeof(Startup))] public class Function : IHttpFunction { // ... } All uncaught exceptions happening in your function as well as log messages sent from Google Cloud Functions are now stored in elmah.io. To log messages manually, you can inject an ILogger in your function: public class Function : IHttpFunction { private ILogger<Function> _logger; public Function(ILogger<Function> logger) { _logger = logger; } // ... } Then log messages using the injected logger: _logger.LogWarning(\"Your log message goes here\");","title":"Logging from Google Cloud Functions"},{"location":"logging-to-elmah-io-from-google-cloud-functions/#logging-to-elmahio-from-google-cloud-functions","text":"Logging to elmah.io from Google Cloud Functions uses our integration with Microsoft.Extensions.Logging . To start logging, install the Elmah.Io.Extensions.Logging NuGet package through the Cloud Shell or locally: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging In the root of your Function app create a new file named Startup.cs : public class Startup : FunctionsStartup { public override void ConfigureServices(WebHostBuilderContext ctx, IServiceCollection services) { services.AddLogging(logging => { logging.AddElmahIo(o => { o.ApiKey = \"API_KEY\"; o.LogId = new Guid(\"LOG_ID\"); }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); }); } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log to store messages in ( Where is my log ID? ). The filter tells Google Cloud Functions to log warnings and above to elmah.io only. If you want to log detailed information about what goes on inside Google Cloud Functions, you can lower the log level. Decorate your function with a FunctionsStartup attribute: [FunctionsStartup(typeof(Startup))] public class Function : IHttpFunction { // ... } All uncaught exceptions happening in your function as well as log messages sent from Google Cloud Functions are now stored in elmah.io. To log messages manually, you can inject an ILogger in your function: public class Function : IHttpFunction { private ILogger<Function> _logger; public Function(ILogger<Function> logger) { _logger = logger; } // ... } Then log messages using the injected logger: _logger.LogWarning(\"Your log message goes here\");","title":"Logging to elmah.io from Google Cloud Functions"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/","text":"Logging to elmah.io from Isolated Azure Functions Logging to elmah.io from Isolated Azure Functions Application name Message hooks Decorating log messages Include source code Handle errors Error filtering Logging through ILogger Log filtering Logging errors from Isolated Azure Functions requires only a few lines of code. We've created clients specifically for Isolated Azure Functions. If your are looking for logging from Azure Functions (in process) check out Logging to elmah.io from Azure Functions . Install the Elmah.Io.Functions.Isolated package in your project to get started: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions.Isolated dotnet add package Elmah.Io.Functions.Isolated <PackageReference Include=\"Elmah.Io.Functions.Isolated\" Version=\"5.*\" /> paket add Elmah.Io.Functions.Isolated Next, call the AddElmahIo method inside ConfigureFunctionsWorkerDefaults : .ConfigureFunctionsWorkerDefaults((context, app) => { app.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); }) Also, include a using of the Elmah.Io.Functions.Isolated namespace. elmah.io now automatically identifies any uncaught exceptions and logs them to the specified log. Check out the samples for more ways to configure elmah.io. Application name To set the application name on all errors, set the Application property: app.AddElmahIo(options => { // ... options.Application = \"MyFunction\"; }); Message hooks Elmah.Io.Functions.Isolated provide message hooks similar to the integrations with ASP.NET and ASP.NET Core. Decorating log messages To include additional information on log messages, you can use the OnMessage action: app.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.Version = \"1.0.0\"; }; }); The example above includes a version number on all errors. Include source code You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: app.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; }); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Handle errors To handle any errors happening while processing a log message, you can use the OnError action: app.AddElmahIo(options => { // ... options.OnError = (msg, ex) => { logger.LogError(ex, ex.Message); }; }); The example above logs any errors during communication with elmah.io to a local log. Error filtering To ignore specific errors based on their content, you can use the OnFilter action: app.AddElmahIo(options => { // ... options.OnFilter = msg => { return msg.Method == \"GET\"; }; }); The example above ignores any errors generated during an HTTP GET request. Logging through ILogger Isolated Azure Functions can log through Microsoft.Extensions.Logging (MEL) too. When configuring your Function app to log through MEL, custom messages can be logged through the ILogger interface. Furthermore, you will get detailed log messages from within the Function host. To set this up, install the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Then extend your Program.cs file like this: var host = new HostBuilder() // ... .ConfigureLogging(logging => { logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); }) // ... .Build(); In the example, only warning messages and above are logged to elmah.io. You can remove the filter or set another log level if you want to log more. Jump to Log filtering to learn how to configure filters from config. Either pass an ILogger to your function method: public class MyFunction { public static void Run([TimerTrigger(\"...\")]TimerInfo myTimer, ILogger<MyFunction> logger) { logger.LogWarning(\"This is a warning\"); } } Or inject an ILoggerFactory and create a logger as part of the constructor: public class MyFunction { private readonly ILogger<MyFunction> logger; public Function1(ILoggerFactory loggerFactory) { this.logger = loggerFactory.CreateLogger<MyFunction>(); } public void Run([TimerTrigger(\"...\")]TimerInfo myTimer) { logger.LogWarning(\"This is a warning\"); } } Log filtering The code above filters out all log messages with a severity lower than Warning . You can use all of the log filtering capabilities of Microsoft.Extensions.Logging to enable and disable various log levels from multiple categories. A common requirement is to only log Warning and more severe originating from the Azure Functions runtime, but log Information messages from your function code. This can be enabled through a custom category: public class MyFunction { public void Run([TimerTrigger(\"...\")]TimerInfo myTimer, ILogger<MyFunction> logger) { logger.LogInformation(\"This is an information message\"); } } The MyFunction category will need configuration in either C# or in the host.json file: { // ... \"logging\": { \"logLevel\": { \"default\": \"Warning\", \"MyFunction\": \"Information\" } } }","title":"Logging from Isolated Azure Functions"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/#logging-to-elmahio-from-isolated-azure-functions","text":"Logging to elmah.io from Isolated Azure Functions Application name Message hooks Decorating log messages Include source code Handle errors Error filtering Logging through ILogger Log filtering Logging errors from Isolated Azure Functions requires only a few lines of code. We've created clients specifically for Isolated Azure Functions. If your are looking for logging from Azure Functions (in process) check out Logging to elmah.io from Azure Functions . Install the Elmah.Io.Functions.Isolated package in your project to get started: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Functions.Isolated dotnet add package Elmah.Io.Functions.Isolated <PackageReference Include=\"Elmah.Io.Functions.Isolated\" Version=\"5.*\" /> paket add Elmah.Io.Functions.Isolated Next, call the AddElmahIo method inside ConfigureFunctionsWorkerDefaults : .ConfigureFunctionsWorkerDefaults((context, app) => { app.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); }) Also, include a using of the Elmah.Io.Functions.Isolated namespace. elmah.io now automatically identifies any uncaught exceptions and logs them to the specified log. Check out the samples for more ways to configure elmah.io.","title":"Logging to elmah.io from Isolated Azure Functions"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/#application-name","text":"To set the application name on all errors, set the Application property: app.AddElmahIo(options => { // ... options.Application = \"MyFunction\"; });","title":"Application name"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/#message-hooks","text":"Elmah.Io.Functions.Isolated provide message hooks similar to the integrations with ASP.NET and ASP.NET Core.","title":"Message hooks"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/#decorating-log-messages","text":"To include additional information on log messages, you can use the OnMessage action: app.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.Version = \"1.0.0\"; }; }); The example above includes a version number on all errors.","title":"Decorating log messages"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/#include-source-code","text":"You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: app.AddElmahIo(options => { // ... options.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; }); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io.","title":"Include source code"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/#handle-errors","text":"To handle any errors happening while processing a log message, you can use the OnError action: app.AddElmahIo(options => { // ... options.OnError = (msg, ex) => { logger.LogError(ex, ex.Message); }; }); The example above logs any errors during communication with elmah.io to a local log.","title":"Handle errors"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/#error-filtering","text":"To ignore specific errors based on their content, you can use the OnFilter action: app.AddElmahIo(options => { // ... options.OnFilter = msg => { return msg.Method == \"GET\"; }; }); The example above ignores any errors generated during an HTTP GET request.","title":"Error filtering"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/#logging-through-ilogger","text":"Isolated Azure Functions can log through Microsoft.Extensions.Logging (MEL) too. When configuring your Function app to log through MEL, custom messages can be logged through the ILogger interface. Furthermore, you will get detailed log messages from within the Function host. To set this up, install the Elmah.Io.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Then extend your Program.cs file like this: var host = new HostBuilder() // ... .ConfigureLogging(logging => { logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); }) // ... .Build(); In the example, only warning messages and above are logged to elmah.io. You can remove the filter or set another log level if you want to log more. Jump to Log filtering to learn how to configure filters from config. Either pass an ILogger to your function method: public class MyFunction { public static void Run([TimerTrigger(\"...\")]TimerInfo myTimer, ILogger<MyFunction> logger) { logger.LogWarning(\"This is a warning\"); } } Or inject an ILoggerFactory and create a logger as part of the constructor: public class MyFunction { private readonly ILogger<MyFunction> logger; public Function1(ILoggerFactory loggerFactory) { this.logger = loggerFactory.CreateLogger<MyFunction>(); } public void Run([TimerTrigger(\"...\")]TimerInfo myTimer) { logger.LogWarning(\"This is a warning\"); } }","title":"Logging through ILogger"},{"location":"logging-to-elmah-io-from-isolated-azure-functions/#log-filtering","text":"The code above filters out all log messages with a severity lower than Warning . You can use all of the log filtering capabilities of Microsoft.Extensions.Logging to enable and disable various log levels from multiple categories. A common requirement is to only log Warning and more severe originating from the Azure Functions runtime, but log Information messages from your function code. This can be enabled through a custom category: public class MyFunction { public void Run([TimerTrigger(\"...\")]TimerInfo myTimer, ILogger<MyFunction> logger) { logger.LogInformation(\"This is an information message\"); } } The MyFunction category will need configuration in either C# or in the host.json file: { // ... \"logging\": { \"logLevel\": { \"default\": \"Warning\", \"MyFunction\": \"Information\" } } }","title":"Log filtering"},{"location":"logging-to-elmah-io-from-javascript/","text":"Logging to elmah.io from JavaScript Logging to elmah.io from JavaScript Installation Options Application name Debug output Message filtering Events Enriching log messages Handling errors Breadcrumbs Logging manually Logging from console IntelliSense Message reference elmah.io doesn't only support server-side .NET logging. We also log JavaScript errors happening on your website. Logging client-side errors require nothing more than installing the elmahio.js script on your website. Organizations created end 2023 and forward will have an API key named JavaScript automatically generated. Remember to either use this or generate a new API key with messages_write permission only. This makes it easy to revoke the API key if someone starts sending messages to your log with your key. elmahio.js supports all modern browsers like Chrome, Edge, Firefox, and Safari. Internet Explorer 10 and 11 are supported too, but because of internal dependencies on the stacktrace-gps library, nothing older than IE10 is supported. If you want to see elmahio.js in action before installing it on your site, feel free to play on the Playground . Installation Pick an installation method of your choice: Manually CDN npm NuGet Library Manager ASP.NET Core Bower Download the latest release as a zip: https://github.com/elmahio/elmah.io.javascript/releases Unpack and copy elmahio.min.js to the Scripts folder or whatever folder you use to store JavaScript files. Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/Scripts/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"https://cdn.jsdelivr.net/gh/elmahio/elmah.io.javascript@latest/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Install the elmah.io.javascript npm package: npm install elmah.io.javascript Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/node_modules/elmah.io.javascript/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Since Bower is no longer maintained , installing elmah.io.javascript through Bower, is supported using bower-npm-resolver . Install the resolver: npm install bower-npm-resolver --save Add the resolver in your .bowerrc file: { \"resolvers\": [ \"bower-npm-resolver\" ] } Install the elmah.io.javascript npm package: bower install npm:elmah.io.javascript --save Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/bower_components/elmah.io.javascript/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Add the elmah.io.javascript library in your libman.json file: { // ... \"libraries\": [ // ... { \"provider\": \"filesystem\", \"library\": \"https://raw.githubusercontent.com/elmahio/elmah.io.javascript/3.7.1/dist/elmahio.min.js\", \"destination\": \"wwwroot/lib/elmahio\" } ] } or using the LibMan CLI: libman install https://raw.githubusercontent.com/elmahio/elmah.io.javascript/3.7.1/dist/elmahio.min.js --provider filesystem --destination wwwroot\\lib\\elmahio Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/lib/elmahio/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Install the elmah.io.javascript NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package elmah.io.javascript dotnet add package elmah.io.javascript <PackageReference Include=\"elmah.io.javascript\" Version=\"4.*\" /> paket add elmah.io.javascript Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/Scripts/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> If not already configured, follow the guide installing elmah.io in ASP.NET Core . Install the Elmah.Io.AspNetCore.TagHelpers NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.TagHelpers dotnet add package Elmah.Io.AspNetCore.TagHelpers <PackageReference Include=\"Elmah.Io.AspNetCore.TagHelpers\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.TagHelpers Copy and paste the following line to the top of the _Layout.cshtml file: @addTagHelper *, Elmah.Io.AspNetCore.TagHelpers In the bottom of the file (but before referencing other JavaScript files), add the following tag helper: <elmah-io/> If you want to log JavaScript errors from production only, make sure to move the elmah-io element inside the tag <environment exclude=\"Development\"> . elmah.io automatically pulls your API key and log ID from the options specified as part of the installation for logging serverside errors from ASP.NET Core. That's it. All uncaught errors on your website, are now logged to elmah.io. Options If you prefer configuring in code (or need to access the options for something else), API key and log ID can be configured by referencing the elmahio.min.js script without parameters: <script src=\"~/scripts/elmahio.min.js\" type=\"text/javascript\"></script> Then initialize the logger in JavaScript: new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); Application name The application property on elmah.io can be set on all log messages by setting the application option: new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID', application: 'My application name' }); Debug output For debug purposes, debug output from the logger to the console can be enabled using the debug option: new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID', debug: true }); Message filtering Log messages can be filtered, by adding a filter handler in options: new Elmahio({ // ... filter: function(msg) { return msg.severity === 'Verbose'; } }); In the example, all log messages with a severity of Verbose , are not logged to elmah.io. Events Enriching log messages Log messages can be enriched by subscribing to the message event: new Elmahio({ // ... }).on('message', function(msg) { if (!msg.data) msg.data = []; msg.data.push({key: 'MyCustomKey', value: 'MyCustomValue'}); }); In the example, all log messages are enriched with a data variable with the key MyCustomKey and value MyCustomValue . Handling errors To react to errors happening in elmah.io.javascript, subscribe to the error event: new Elmahio({ // ... }).on('error', function(status, text) { console.log('An error happened in elmah.io.javascript', status, text); }); In the example, all errors are written to the console. Breadcrumbs Breadcrumbs can be used to decorate errors with events or actions happening just before logging the error. Breadcrumbs can be added manually: logger.addBreadcrumb('User clicked button x', 'Information', 'click'); You would want to enrich your code with a range of different breadcrumbs depending on important user actions in your application. As default, a maximum of 10 breadcrumbs are stored in memory at all times. The list acts as first in first out, where adding a new breadcrumb to a full list will automatically remove the oldest breadcrumb in the list. The allowed number of breadcrumbs in the list can be changed using the breadcrumbsNumber option: var logger = new Elmahio({ // ... breadcrumbsNumber: 15 }); This will store a maximum of 15 breadcrumbs. Currently, we allow 25 as the highest possible value. elmah.io.javascript can also be configured to automatically generate breadcrumbs from important actions like click events and xhr: var logger = new Elmahio({ // ... breadcrumbs: true }); We are planning to enable automatic breadcrumbs in the future but for now, it's an opt-in feature. Automatic breadcrumbs will be included in the same list as manually added breadcrumbs why the breadcrumbsNumber option is still valid. Logging manually You may want to log errors manually or even log information messages from JavaScript. To do so, Elmahio is a logging framework too: var logger = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); logger.verbose('This is verbose'); logger.verbose('This is verbose', new Error('A JavaScript error object')); logger.debug('This is debug'); logger.debug('This is debug', new Error('A JavaScript error object')); logger.information('This is information'); logger.information('This is information', new Error('A JavaScript error object')); logger.warning('This is warning'); logger.warning('This is warning', new Error('A JavaScript error object')); logger.error('This is error'); logger.error('This is error', new Error('A JavaScript error object')); logger.fatal('This is fatal'); logger.fatal('This is fatal', new Error('A JavaScript error object')); logger.log({ title: 'This is a custom log message', type: 'Of some type', severity: 'Error' }); var msg = logger.message(); // Get a prefilled message msg.title = \"This is a custom log message\"; logger.log(msg); var msg = logger.message(new Error('A JavaScript error object')); // Get a prefilled message including error details // Set custom variables. If not needed, simply use logger.error(...) instead. logger.log(msg); The Error object used, should be a JavaScript Error object . As for the log -function, check out message reference . Manual logging only works when initializing the elmah.io logger from code. Logging from console If you don't like to share the Elmahio logger or you want to hook elmah.io logging up to existing code, you can capture log messages from console . To do so, set the captureConsoleMinimumLevel option: var log = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID', captureConsoleMinimumLevel: 'error' }); console.error('This is an %s message.', 'error'); captureConsoleMinimumLevel can be set to one of the following values: none : will disable logging from console (default). debug : will capture logging from console.debug , console.info , console.warn , console.error . info : will capture logging from console.info , console.warn , console.error . warn : will capture logging from console.warn , console.error . error : will capture logging from console.error . Capturing the console only works when initializing the elmah.io logger from code. Also, console.log is not captured. IntelliSense If installing through npm or similar, Visual Studio should pick up the TypeScript mappings from the elmah.io.javascript package. If not, add the following line at the top of the JavaScript file where you want elmah.io.javascript IntelliSense: /// <reference path=\"/path/to/elmahio.d.ts\" /> Message reference This is an example of the elmah.io.javascript Message object that is used in various callbacks, etc.: { title: 'this.remove is not a function', detail: 'TypeError: this.remove is not a function\\n at (undefined:12:14)', source: 'index.js', severity: 'Error', type: 'TypeError', url: '/current/page', method: 'GET', application: 'MyApp', hostname: 'WOPR', statusCode: 400, user: 'DavidLightman', version: '1.0.0', cookies: [ { key: '_ga', value: 'GA1.3.1580453215.1783132008' } ], queryString: [ { key: 'id', value: '42' } ], data: [ { key: 'User-Language', value: 'en-US' }, { key: 'Color-Depth', value: '24' } ], serverVariables: [ { key: 'User-Agent', value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' } ], breadcrumbs: [ { severity: 'Information', event: 'click', message: 'A user clicked' } ] } For a complete definition, check out the Message interface in the elmah.io.javascript TypeScript mappings .","title":"Logging from JavaScript"},{"location":"logging-to-elmah-io-from-javascript/#logging-to-elmahio-from-javascript","text":"Logging to elmah.io from JavaScript Installation Options Application name Debug output Message filtering Events Enriching log messages Handling errors Breadcrumbs Logging manually Logging from console IntelliSense Message reference elmah.io doesn't only support server-side .NET logging. We also log JavaScript errors happening on your website. Logging client-side errors require nothing more than installing the elmahio.js script on your website. Organizations created end 2023 and forward will have an API key named JavaScript automatically generated. Remember to either use this or generate a new API key with messages_write permission only. This makes it easy to revoke the API key if someone starts sending messages to your log with your key. elmahio.js supports all modern browsers like Chrome, Edge, Firefox, and Safari. Internet Explorer 10 and 11 are supported too, but because of internal dependencies on the stacktrace-gps library, nothing older than IE10 is supported. If you want to see elmahio.js in action before installing it on your site, feel free to play on the Playground .","title":"Logging to elmah.io from JavaScript"},{"location":"logging-to-elmah-io-from-javascript/#installation","text":"Pick an installation method of your choice: Manually CDN npm NuGet Library Manager ASP.NET Core Bower Download the latest release as a zip: https://github.com/elmahio/elmah.io.javascript/releases Unpack and copy elmahio.min.js to the Scripts folder or whatever folder you use to store JavaScript files. Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/Scripts/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"https://cdn.jsdelivr.net/gh/elmahio/elmah.io.javascript@latest/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Install the elmah.io.javascript npm package: npm install elmah.io.javascript Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/node_modules/elmah.io.javascript/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Since Bower is no longer maintained , installing elmah.io.javascript through Bower, is supported using bower-npm-resolver . Install the resolver: npm install bower-npm-resolver --save Add the resolver in your .bowerrc file: { \"resolvers\": [ \"bower-npm-resolver\" ] } Install the elmah.io.javascript npm package: bower install npm:elmah.io.javascript --save Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/bower_components/elmah.io.javascript/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Add the elmah.io.javascript library in your libman.json file: { // ... \"libraries\": [ // ... { \"provider\": \"filesystem\", \"library\": \"https://raw.githubusercontent.com/elmahio/elmah.io.javascript/3.7.1/dist/elmahio.min.js\", \"destination\": \"wwwroot/lib/elmahio\" } ] } or using the LibMan CLI: libman install https://raw.githubusercontent.com/elmahio/elmah.io.javascript/3.7.1/dist/elmahio.min.js --provider filesystem --destination wwwroot\\lib\\elmahio Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/lib/elmahio/dist/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> Install the elmah.io.javascript NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package elmah.io.javascript dotnet add package elmah.io.javascript <PackageReference Include=\"elmah.io.javascript\" Version=\"4.*\" /> paket add elmah.io.javascript Reference elmahio.min.js just before the </body> tag (but before all other JavaScripts) in your shared _Layout.cshtml or all HTML files, depending on how you've structured your site: <script src=\"~/Scripts/elmahio.min.js?apiKey=API_KEY&logId=LOG_ID\" type=\"text/javascript\"></script> If not already configured, follow the guide installing elmah.io in ASP.NET Core . Install the Elmah.Io.AspNetCore.TagHelpers NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.TagHelpers dotnet add package Elmah.Io.AspNetCore.TagHelpers <PackageReference Include=\"Elmah.Io.AspNetCore.TagHelpers\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.TagHelpers Copy and paste the following line to the top of the _Layout.cshtml file: @addTagHelper *, Elmah.Io.AspNetCore.TagHelpers In the bottom of the file (but before referencing other JavaScript files), add the following tag helper: <elmah-io/> If you want to log JavaScript errors from production only, make sure to move the elmah-io element inside the tag <environment exclude=\"Development\"> . elmah.io automatically pulls your API key and log ID from the options specified as part of the installation for logging serverside errors from ASP.NET Core. That's it. All uncaught errors on your website, are now logged to elmah.io.","title":"Installation"},{"location":"logging-to-elmah-io-from-javascript/#options","text":"If you prefer configuring in code (or need to access the options for something else), API key and log ID can be configured by referencing the elmahio.min.js script without parameters: <script src=\"~/scripts/elmahio.min.js\" type=\"text/javascript\"></script> Then initialize the logger in JavaScript: new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' });","title":"Options"},{"location":"logging-to-elmah-io-from-javascript/#application-name","text":"The application property on elmah.io can be set on all log messages by setting the application option: new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID', application: 'My application name' });","title":"Application name"},{"location":"logging-to-elmah-io-from-javascript/#debug-output","text":"For debug purposes, debug output from the logger to the console can be enabled using the debug option: new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID', debug: true });","title":"Debug output"},{"location":"logging-to-elmah-io-from-javascript/#message-filtering","text":"Log messages can be filtered, by adding a filter handler in options: new Elmahio({ // ... filter: function(msg) { return msg.severity === 'Verbose'; } }); In the example, all log messages with a severity of Verbose , are not logged to elmah.io.","title":"Message filtering"},{"location":"logging-to-elmah-io-from-javascript/#events","text":"","title":"Events"},{"location":"logging-to-elmah-io-from-javascript/#enriching-log-messages","text":"Log messages can be enriched by subscribing to the message event: new Elmahio({ // ... }).on('message', function(msg) { if (!msg.data) msg.data = []; msg.data.push({key: 'MyCustomKey', value: 'MyCustomValue'}); }); In the example, all log messages are enriched with a data variable with the key MyCustomKey and value MyCustomValue .","title":"Enriching log messages"},{"location":"logging-to-elmah-io-from-javascript/#handling-errors","text":"To react to errors happening in elmah.io.javascript, subscribe to the error event: new Elmahio({ // ... }).on('error', function(status, text) { console.log('An error happened in elmah.io.javascript', status, text); }); In the example, all errors are written to the console.","title":"Handling errors"},{"location":"logging-to-elmah-io-from-javascript/#breadcrumbs","text":"Breadcrumbs can be used to decorate errors with events or actions happening just before logging the error. Breadcrumbs can be added manually: logger.addBreadcrumb('User clicked button x', 'Information', 'click'); You would want to enrich your code with a range of different breadcrumbs depending on important user actions in your application. As default, a maximum of 10 breadcrumbs are stored in memory at all times. The list acts as first in first out, where adding a new breadcrumb to a full list will automatically remove the oldest breadcrumb in the list. The allowed number of breadcrumbs in the list can be changed using the breadcrumbsNumber option: var logger = new Elmahio({ // ... breadcrumbsNumber: 15 }); This will store a maximum of 15 breadcrumbs. Currently, we allow 25 as the highest possible value. elmah.io.javascript can also be configured to automatically generate breadcrumbs from important actions like click events and xhr: var logger = new Elmahio({ // ... breadcrumbs: true }); We are planning to enable automatic breadcrumbs in the future but for now, it's an opt-in feature. Automatic breadcrumbs will be included in the same list as manually added breadcrumbs why the breadcrumbsNumber option is still valid.","title":"Breadcrumbs"},{"location":"logging-to-elmah-io-from-javascript/#logging-manually","text":"You may want to log errors manually or even log information messages from JavaScript. To do so, Elmahio is a logging framework too: var logger = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); logger.verbose('This is verbose'); logger.verbose('This is verbose', new Error('A JavaScript error object')); logger.debug('This is debug'); logger.debug('This is debug', new Error('A JavaScript error object')); logger.information('This is information'); logger.information('This is information', new Error('A JavaScript error object')); logger.warning('This is warning'); logger.warning('This is warning', new Error('A JavaScript error object')); logger.error('This is error'); logger.error('This is error', new Error('A JavaScript error object')); logger.fatal('This is fatal'); logger.fatal('This is fatal', new Error('A JavaScript error object')); logger.log({ title: 'This is a custom log message', type: 'Of some type', severity: 'Error' }); var msg = logger.message(); // Get a prefilled message msg.title = \"This is a custom log message\"; logger.log(msg); var msg = logger.message(new Error('A JavaScript error object')); // Get a prefilled message including error details // Set custom variables. If not needed, simply use logger.error(...) instead. logger.log(msg); The Error object used, should be a JavaScript Error object . As for the log -function, check out message reference . Manual logging only works when initializing the elmah.io logger from code.","title":"Logging manually"},{"location":"logging-to-elmah-io-from-javascript/#logging-from-console","text":"If you don't like to share the Elmahio logger or you want to hook elmah.io logging up to existing code, you can capture log messages from console . To do so, set the captureConsoleMinimumLevel option: var log = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID', captureConsoleMinimumLevel: 'error' }); console.error('This is an %s message.', 'error'); captureConsoleMinimumLevel can be set to one of the following values: none : will disable logging from console (default). debug : will capture logging from console.debug , console.info , console.warn , console.error . info : will capture logging from console.info , console.warn , console.error . warn : will capture logging from console.warn , console.error . error : will capture logging from console.error . Capturing the console only works when initializing the elmah.io logger from code. Also, console.log is not captured.","title":"Logging from console"},{"location":"logging-to-elmah-io-from-javascript/#intellisense","text":"If installing through npm or similar, Visual Studio should pick up the TypeScript mappings from the elmah.io.javascript package. If not, add the following line at the top of the JavaScript file where you want elmah.io.javascript IntelliSense: /// <reference path=\"/path/to/elmahio.d.ts\" />","title":"IntelliSense"},{"location":"logging-to-elmah-io-from-javascript/#message-reference","text":"This is an example of the elmah.io.javascript Message object that is used in various callbacks, etc.: { title: 'this.remove is not a function', detail: 'TypeError: this.remove is not a function\\n at (undefined:12:14)', source: 'index.js', severity: 'Error', type: 'TypeError', url: '/current/page', method: 'GET', application: 'MyApp', hostname: 'WOPR', statusCode: 400, user: 'DavidLightman', version: '1.0.0', cookies: [ { key: '_ga', value: 'GA1.3.1580453215.1783132008' } ], queryString: [ { key: 'id', value: '42' } ], data: [ { key: 'User-Language', value: 'en-US' }, { key: 'Color-Depth', value: '24' } ], serverVariables: [ { key: 'User-Agent', value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' } ], breadcrumbs: [ { severity: 'Information', event: 'click', message: 'A user clicked' } ] } For a complete definition, check out the Message interface in the elmah.io.javascript TypeScript mappings .","title":"Message reference"},{"location":"logging-to-elmah-io-from-jsnlog/","text":"Logging to elmah.io from JSNLog While logging through JSNLog still works, we recommend using our native integration with JavaScript: Logging to elmah.io from JavaScript Using JSNLog you will be able to log JavaScript errors to elmah.io. In this sample, we will focus on logging JavaScript errors from an ASP.NET MVC web application, but you can use JSNLog to log anything to elmah.io, so please check out their documentation. Start by installing the JSNLog.Elmah package: Package Manager .NET CLI PackageReference Paket CLI Install-Package JSNLog.Elmah dotnet add package JSNLog.Elmah <PackageReference Include=\"JSNLog.Elmah\" Version=\"2.*\" /> paket add JSNLog.Elmah This installs and setup JSNLog into your project, using ELMAH as an appender. Then, install Elmah.Io : Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Add the JSNLog code before any script imports in your _Layout.cshtml file: @Html.Raw(JSNLog.JavascriptLogging.Configure()) You are ready to log errors from JavaScript to elmah.io. To test that everything is installed correctly, launch your web application and execute the following JavaScript using Chrome Developer Tools or similar: JL().fatal(\"log message\"); Navigate to your log at elmah.io and observe the new error. As you can see, logging JavaScript errors is now extremely simple and can be built into any try-catch, jQuery fail handlers, and pretty much anywhere else. To log every JavaScript error, add the following to the bottom of the _Layout.cshtml file: <script> window.onerror = function (errorMsg, url, lineNumber, column, errorObj) { // Send object with all data to server side log, using severity fatal, // from logger \"onerrorLogger\" JL(\"onerrorLogger\").fatalException({ \"msg\": \"Exception!\", \"errorMsg\": errorMsg, \"url\": url, \"line number\": lineNumber, \"column\": column }, errorObj); // Tell browser to run its own error handler as well return false; } </script>","title":"Logging from JSNLog"},{"location":"logging-to-elmah-io-from-jsnlog/#logging-to-elmahio-from-jsnlog","text":"While logging through JSNLog still works, we recommend using our native integration with JavaScript: Logging to elmah.io from JavaScript Using JSNLog you will be able to log JavaScript errors to elmah.io. In this sample, we will focus on logging JavaScript errors from an ASP.NET MVC web application, but you can use JSNLog to log anything to elmah.io, so please check out their documentation. Start by installing the JSNLog.Elmah package: Package Manager .NET CLI PackageReference Paket CLI Install-Package JSNLog.Elmah dotnet add package JSNLog.Elmah <PackageReference Include=\"JSNLog.Elmah\" Version=\"2.*\" /> paket add JSNLog.Elmah This installs and setup JSNLog into your project, using ELMAH as an appender. Then, install Elmah.Io : Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Add the JSNLog code before any script imports in your _Layout.cshtml file: @Html.Raw(JSNLog.JavascriptLogging.Configure()) You are ready to log errors from JavaScript to elmah.io. To test that everything is installed correctly, launch your web application and execute the following JavaScript using Chrome Developer Tools or similar: JL().fatal(\"log message\"); Navigate to your log at elmah.io and observe the new error. As you can see, logging JavaScript errors is now extremely simple and can be built into any try-catch, jQuery fail handlers, and pretty much anywhere else. To log every JavaScript error, add the following to the bottom of the _Layout.cshtml file: <script> window.onerror = function (errorMsg, url, lineNumber, column, errorObj) { // Send object with all data to server side log, using severity fatal, // from logger \"onerrorLogger\" JL(\"onerrorLogger\").fatalException({ \"msg\": \"Exception!\", \"errorMsg\": errorMsg, \"url\": url, \"line number\": lineNumber, \"column\": column }, errorObj); // Tell browser to run its own error handler as well return false; } </script>","title":"Logging to elmah.io from JSNLog"},{"location":"logging-to-elmah-io-from-log4net/","text":"Logging to elmah.io from log4net Logging to elmah.io from log4net Logging custom properties Setting category Message hooks Decorating log messages Include source code Specify API key and log ID in appSettings ASP.NET Core Troubleshooting System.IO.FileLoadException: Could not load file or assembly 'log4net ... In this tutorial we'll add logging to elmah.io from a .NET application with log4net. Install the elmah.io appender: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Log4Net dotnet add package Elmah.Io.Log4Net <PackageReference Include=\"Elmah.Io.Log4Net\" Version=\"5.*\" /> paket add Elmah.Io.Log4Net Add the following to your AssemblyInfo.cs file: [assembly: log4net.Config.XmlConfigurator(Watch = true)] Add the following config section to your web/app.config file: <section name=\"log4net\" type=\"log4net.Config.Log4NetConfigurationSectionHandler, log4net\" /> Finally, add the log4net configuration element to web/app.config : <log4net> <appender name=\"ElmahIoAppender\" type=\"elmah.io.log4net.ElmahIoAppender, elmah.io.log4net\"> <logId value=\"LOG_ID\" /> <apiKey value=\"API_KEY\" /> </appender> <root> <level value=\"Info\" /> <appender-ref ref=\"ElmahIoAppender\" /> </root> </log4net> That\u2019s it! log4net is now configured and log messages to elmah.io. Remember to replace API_KEY ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with your actual log Id. To start logging, write your usual log4net log statements: var log = log4net.LogManager.GetLogger(typeof(HomeController)); try { log.Info(\"Trying something\"); throw new ApplicationException(); } catch (ApplicationException ex) { log.Error(\"Error happening\", ex); } Logging custom properties log4net offers a feature called context properties. With context properties, you can log additional key/value pairs with every log message. The elmah.io appender for log4net, supports context properties as well. Context properties are handled like custom properties in the elmah.io UI. Let's utilize two different hooks in log4net, to add context properties to elmah.io: log4net.GlobalContext.Properties[\"ApplicationIdentifier\"] = \"MyCoolApp\"; log4net.ThreadContext.Properties[\"ThreadId\"] = Thread.CurrentThread.ManagedThreadId; log.Info(\"This is a message with custom properties\"); Basically, we set two custom properties on contextual classes provided by log4net. To read more about the choices in log4net, check out the log4net manual . When looking up the log message in elmah.io, we see the context properties in the Data tab. Besides the two custom variables that we set through GlobalContext and ThreadContext , we see a couple of build-in properties in log4net, both prefixed with log4net: . In addition, Elmah.Io.Log4Net provides a range of reserved property names, that can be used to fill in data in the correct fields on the elmah.io UI. Let's say you want to fill the User field: var properties = new PropertiesDictionary(); properties[\"User\"] = \"Arnold Schwarzenegger\"; log.Logger.Log(new LoggingEvent(new LoggingEventData { Level = Level.Error, TimeStampUtc = DateTime.UtcNow, Properties = properties, Message = \"Hasta la vista, baby\", })); This will fill in the value Arnold Schwarzenegger in the User field, as well as add a key/value pair to the Data tab on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage . Setting category elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to log4net's LoggerName field automatically when using Elmah.Io.Log4Net . The category field can be overwritten using one of the context features available in log4net: log4net.ThreadContext.Properties[\"Category\"] = \"The category\"; log.Info(\"This is an information message with custom category\"); Message hooks Decorating log messages In case you want to set one or more core properties on each elmah.io message logged, using message hooks may be a better solution. In that case you will need to add a bit of log4net magic. An example could be setting the Version property on all log messages. In the following code, we set a hard-coded version number on all log messages, but the value could come from assembly info, a text file, or similar: Hierarchy hier = log4net.LogManager.GetRepository(Assembly.GetEntryAssembly()) as Hierarchy; var elmahIoAppender = (ElmahIoAppender)(hier?.GetAppenders()) .FirstOrDefault(appender => appender.Name .Equals(\"ElmahIoAppender\", StringComparison.InvariantCultureIgnoreCase)); elmahIoAppender.ActivateOptions(); elmahIoAppender.Client.Messages.OnMessage += (sender, a) => { a.Message.Version = \"1.0.0\"; }; This rather ugly piece of code would go into an initalization block, depending on the project type. The code starts by getting the configured elmah.io appender (typically set up in web/app.config or log4net.config ). With the appender, you can access the underlying elmah.io client and subscribe to the OnMessage event. This let you trigger a small piece of code, just before sending log messages to elmah.io. In this case, we set the Version property to 1.0.0 . Remember to call the ActiveOptions method, to make sure that the Client property is initialized. Include source code You can use the OnMessage event to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage event handler: elmahIoAppender.Client.Messages.OnMessage += (sender, a) => { a.Message.WithSourceCodeFromPdb(); }; Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward. Specify API key and log ID in appSettings You may prefer storing the API key and log ID in the appSettings element over having the values embedded into the appender element. This can be the case for easy config transformation, overwriting values on Azure, or similar. log4net provides a feature named pattern strings to address just that: <?xml version=\"1.0\" encoding=\"utf-8\"?> <configuration> <configSections> <section name=\"log4net\" type=\"log4net.Config.Log4NetConfigurationSectionHandler, log4net\" /> </configSections> <appSettings> <add key=\"logId\" value=\"LOG_ID\"/> <add key=\"apiKey\" value=\"API_KEY\"/> </appSettings> <log4net> <root> <level value=\"ALL\" /> <appender-ref ref=\"ElmahIoAppender\" /> </root> <appender name=\"ElmahIoAppender\" type=\"elmah.io.log4net.ElmahIoAppender, elmah.io.log4net\"> <logId type=\"log4net.Util.PatternString\" value=\"%appSetting{logId}\" /> <apiKey type=\"log4net.Util.PatternString\" value=\"%appSetting{apiKey}\" /> </appender> </log4net> </configuration> The logId and apiKey elements underneath the elmah.io appender have been extended to include type=\"log4net.Util.PatternString\" . This allows for complex patterns in the value attribute. In this example, I reference an app setting from its name, by adding a value of %appSetting{logId} where logId is a reference to the app setting key specified above. ASP.NET Core Like other logging frameworks, logging through log4net from ASP.NET Core is also supported. We have a sample to show you how to set it up. The required NuGet packages and configuration are documented in this section. To start logging to elmah.io from Microsoft.Extensions.Logging (through log4net), install the Elmah.Io.Log4Net and Microsoft.Extensions.Logging.Log4Net.AspNetCore NuGet packages: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Log4Net Install-Package Microsoft.Extensions.Logging.Log4Net.AspNetCore dotnet add package Elmah.Io.Log4Net dotnet add package Microsoft.Extensions.Logging.Log4Net.AspNetCore <PackageReference Include=\"Elmah.Io.Log4Net\" Version=\"5.*\" /> <PackageReference Include=\"Microsoft.Extensions.Logging.Log4Net.AspNetCore\" Version=\"6.*\" /> paket add Elmah.Io.Log4Net paket add Microsoft.Extensions.Logging.Log4Net.AspNetCore The version of Microsoft.Extensions.Logging.Log4Net.AspNetCore should match the version of .NET you are targeting. Include a log4net config file to the root of the project: <?xml version=\"1.0\" encoding=\"utf-8\" ?> <log4net> <root> <level value=\"WARN\" /> <appender-ref ref=\"ElmahIoAppender\" /> <appender-ref ref=\"ConsoleAppender\" /> </root> <appender name=\"ElmahIoAppender\" type=\"elmah.io.log4net.ElmahIoAppender, elmah.io.log4net\"> <logId value=\"LOG_ID\" /> <apiKey value=\"API_KEY\" /> <!--<application value=\"My app\" />--> </appender> <appender name=\"ConsoleAppender\" type=\"log4net.Appender.ConsoleAppender\"> <layout type=\"log4net.Layout.PatternLayout\"> <conversionPattern value=\"%date [%thread] %-5level %logger [%property{NDC}] - %message%newline\" /> </layout> </appender> </log4net> In the Program.cs file, make sure to set up log4net: builder.Logging.AddLog4Net(); All internal logging from ASP.NET Core, as well as manual logging you create through the ILogger interface, now goes directly into elmah.io. A common request is to include all of the HTTP contextual information you usually get logged when using a package like Elmah.Io.AspNetCore . We have developed a specialized NuGet package to include cookies, server variables, etc. when logging through log4net from ASP.NET Core. To set it up, install the Elmah.Io.AspNetCore.Log4Net NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.Log4Net dotnet add package Elmah.Io.AspNetCore.Log4Net <PackageReference Include=\"Elmah.Io.AspNetCore.Log4Net\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.Log4Net Finally, make sure to call the UseElmahIoLog4Net method in the Program.cs file: // ... Exception handling middleware app.UseElmahIoLog4Net(); // ... UseMvc etc. Troubleshooting Here are some things to try out if logging from log4net to elmah.io doesn't work: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure that you have the newest Elmah.Io.Log4Net and Elmah.Io.Client packages installed. Make sure to include all of the configuration from the example above. That includes both the <root> and <appender> element. Make sure that the API key is valid and allow the Messages | Write permission . Make sure to include a valid log ID. Make sure that you have sufficient log messages in your subscription and that you didn't disable logging to the log or include any ignore filters/rules. Enable and inspect log4net's internal debug log by including the following code in your web/app.config file to reveal any exceptions happening inside the log4net engine room or one of the appenders: <?xml version=\"1.0\" encoding=\"utf-8\"?> <configuration> <appSettings> <add key=\"log4net.Internal.Debug\" value=\"true\"/> </appSettings> <system.diagnostics> <trace autoflush=\"true\"> <listeners> <add name=\"textWriterTraceListener\" type=\"System.Diagnostics.TextWriterTraceListener\" initializeData=\"C:\\temp\\log4net-debug.log\" /> </listeners> </trace> </system.diagnostics> </configuration> System.IO.FileLoadException: Could not load file or assembly 'log4net ... In case you get the following exception while trying to log messages to elmah.io: System.IO.FileLoadException: Could not load file or assembly 'log4net, Version=2.0.8.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040) This indicates they either the log4net.dll file is missing or there's a problem with assembly bindings. You can include a binding redirect to the newest version of log4net by including the following code in your web/app.config file: <?xml version=\"1.0\" encoding=\"utf-8\"?> <configuration> <runtime> <assemblyBinding xmlns=\"urn:schemas-microsoft-com:asm.v1\"> <dependentAssembly> <assemblyIdentity name=\"log4net\" publicKeyToken=\"669e0ddf0bb1aa2a\" culture=\"neutral\"/> <bindingRedirect oldVersion=\"0.0.0.0-2.0.12.0\" newVersion=\"2.0.12.0\"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration>","title":"Logging from log4net"},{"location":"logging-to-elmah-io-from-log4net/#logging-to-elmahio-from-log4net","text":"Logging to elmah.io from log4net Logging custom properties Setting category Message hooks Decorating log messages Include source code Specify API key and log ID in appSettings ASP.NET Core Troubleshooting System.IO.FileLoadException: Could not load file or assembly 'log4net ... In this tutorial we'll add logging to elmah.io from a .NET application with log4net. Install the elmah.io appender: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Log4Net dotnet add package Elmah.Io.Log4Net <PackageReference Include=\"Elmah.Io.Log4Net\" Version=\"5.*\" /> paket add Elmah.Io.Log4Net Add the following to your AssemblyInfo.cs file: [assembly: log4net.Config.XmlConfigurator(Watch = true)] Add the following config section to your web/app.config file: <section name=\"log4net\" type=\"log4net.Config.Log4NetConfigurationSectionHandler, log4net\" /> Finally, add the log4net configuration element to web/app.config : <log4net> <appender name=\"ElmahIoAppender\" type=\"elmah.io.log4net.ElmahIoAppender, elmah.io.log4net\"> <logId value=\"LOG_ID\" /> <apiKey value=\"API_KEY\" /> </appender> <root> <level value=\"Info\" /> <appender-ref ref=\"ElmahIoAppender\" /> </root> </log4net> That\u2019s it! log4net is now configured and log messages to elmah.io. Remember to replace API_KEY ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with your actual log Id. To start logging, write your usual log4net log statements: var log = log4net.LogManager.GetLogger(typeof(HomeController)); try { log.Info(\"Trying something\"); throw new ApplicationException(); } catch (ApplicationException ex) { log.Error(\"Error happening\", ex); }","title":"Logging to elmah.io from log4net"},{"location":"logging-to-elmah-io-from-log4net/#logging-custom-properties","text":"log4net offers a feature called context properties. With context properties, you can log additional key/value pairs with every log message. The elmah.io appender for log4net, supports context properties as well. Context properties are handled like custom properties in the elmah.io UI. Let's utilize two different hooks in log4net, to add context properties to elmah.io: log4net.GlobalContext.Properties[\"ApplicationIdentifier\"] = \"MyCoolApp\"; log4net.ThreadContext.Properties[\"ThreadId\"] = Thread.CurrentThread.ManagedThreadId; log.Info(\"This is a message with custom properties\"); Basically, we set two custom properties on contextual classes provided by log4net. To read more about the choices in log4net, check out the log4net manual . When looking up the log message in elmah.io, we see the context properties in the Data tab. Besides the two custom variables that we set through GlobalContext and ThreadContext , we see a couple of build-in properties in log4net, both prefixed with log4net: . In addition, Elmah.Io.Log4Net provides a range of reserved property names, that can be used to fill in data in the correct fields on the elmah.io UI. Let's say you want to fill the User field: var properties = new PropertiesDictionary(); properties[\"User\"] = \"Arnold Schwarzenegger\"; log.Logger.Log(new LoggingEvent(new LoggingEventData { Level = Level.Error, TimeStampUtc = DateTime.UtcNow, Properties = properties, Message = \"Hasta la vista, baby\", })); This will fill in the value Arnold Schwarzenegger in the User field, as well as add a key/value pair to the Data tab on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage .","title":"Logging custom properties"},{"location":"logging-to-elmah-io-from-log4net/#setting-category","text":"elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to log4net's LoggerName field automatically when using Elmah.Io.Log4Net . The category field can be overwritten using one of the context features available in log4net: log4net.ThreadContext.Properties[\"Category\"] = \"The category\"; log.Info(\"This is an information message with custom category\");","title":"Setting category"},{"location":"logging-to-elmah-io-from-log4net/#message-hooks","text":"","title":"Message hooks"},{"location":"logging-to-elmah-io-from-log4net/#decorating-log-messages","text":"In case you want to set one or more core properties on each elmah.io message logged, using message hooks may be a better solution. In that case you will need to add a bit of log4net magic. An example could be setting the Version property on all log messages. In the following code, we set a hard-coded version number on all log messages, but the value could come from assembly info, a text file, or similar: Hierarchy hier = log4net.LogManager.GetRepository(Assembly.GetEntryAssembly()) as Hierarchy; var elmahIoAppender = (ElmahIoAppender)(hier?.GetAppenders()) .FirstOrDefault(appender => appender.Name .Equals(\"ElmahIoAppender\", StringComparison.InvariantCultureIgnoreCase)); elmahIoAppender.ActivateOptions(); elmahIoAppender.Client.Messages.OnMessage += (sender, a) => { a.Message.Version = \"1.0.0\"; }; This rather ugly piece of code would go into an initalization block, depending on the project type. The code starts by getting the configured elmah.io appender (typically set up in web/app.config or log4net.config ). With the appender, you can access the underlying elmah.io client and subscribe to the OnMessage event. This let you trigger a small piece of code, just before sending log messages to elmah.io. In this case, we set the Version property to 1.0.0 . Remember to call the ActiveOptions method, to make sure that the Client property is initialized.","title":"Decorating log messages"},{"location":"logging-to-elmah-io-from-log4net/#include-source-code","text":"You can use the OnMessage event to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage event handler: elmahIoAppender.Client.Messages.OnMessage += (sender, a) => { a.Message.WithSourceCodeFromPdb(); }; Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.","title":"Include source code"},{"location":"logging-to-elmah-io-from-log4net/#specify-api-key-and-log-id-in-appsettings","text":"You may prefer storing the API key and log ID in the appSettings element over having the values embedded into the appender element. This can be the case for easy config transformation, overwriting values on Azure, or similar. log4net provides a feature named pattern strings to address just that: <?xml version=\"1.0\" encoding=\"utf-8\"?> <configuration> <configSections> <section name=\"log4net\" type=\"log4net.Config.Log4NetConfigurationSectionHandler, log4net\" /> </configSections> <appSettings> <add key=\"logId\" value=\"LOG_ID\"/> <add key=\"apiKey\" value=\"API_KEY\"/> </appSettings> <log4net> <root> <level value=\"ALL\" /> <appender-ref ref=\"ElmahIoAppender\" /> </root> <appender name=\"ElmahIoAppender\" type=\"elmah.io.log4net.ElmahIoAppender, elmah.io.log4net\"> <logId type=\"log4net.Util.PatternString\" value=\"%appSetting{logId}\" /> <apiKey type=\"log4net.Util.PatternString\" value=\"%appSetting{apiKey}\" /> </appender> </log4net> </configuration> The logId and apiKey elements underneath the elmah.io appender have been extended to include type=\"log4net.Util.PatternString\" . This allows for complex patterns in the value attribute. In this example, I reference an app setting from its name, by adding a value of %appSetting{logId} where logId is a reference to the app setting key specified above.","title":"Specify API key and log ID in appSettings"},{"location":"logging-to-elmah-io-from-log4net/#aspnet-core","text":"Like other logging frameworks, logging through log4net from ASP.NET Core is also supported. We have a sample to show you how to set it up. The required NuGet packages and configuration are documented in this section. To start logging to elmah.io from Microsoft.Extensions.Logging (through log4net), install the Elmah.Io.Log4Net and Microsoft.Extensions.Logging.Log4Net.AspNetCore NuGet packages: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Log4Net Install-Package Microsoft.Extensions.Logging.Log4Net.AspNetCore dotnet add package Elmah.Io.Log4Net dotnet add package Microsoft.Extensions.Logging.Log4Net.AspNetCore <PackageReference Include=\"Elmah.Io.Log4Net\" Version=\"5.*\" /> <PackageReference Include=\"Microsoft.Extensions.Logging.Log4Net.AspNetCore\" Version=\"6.*\" /> paket add Elmah.Io.Log4Net paket add Microsoft.Extensions.Logging.Log4Net.AspNetCore The version of Microsoft.Extensions.Logging.Log4Net.AspNetCore should match the version of .NET you are targeting. Include a log4net config file to the root of the project: <?xml version=\"1.0\" encoding=\"utf-8\" ?> <log4net> <root> <level value=\"WARN\" /> <appender-ref ref=\"ElmahIoAppender\" /> <appender-ref ref=\"ConsoleAppender\" /> </root> <appender name=\"ElmahIoAppender\" type=\"elmah.io.log4net.ElmahIoAppender, elmah.io.log4net\"> <logId value=\"LOG_ID\" /> <apiKey value=\"API_KEY\" /> <!--<application value=\"My app\" />--> </appender> <appender name=\"ConsoleAppender\" type=\"log4net.Appender.ConsoleAppender\"> <layout type=\"log4net.Layout.PatternLayout\"> <conversionPattern value=\"%date [%thread] %-5level %logger [%property{NDC}] - %message%newline\" /> </layout> </appender> </log4net> In the Program.cs file, make sure to set up log4net: builder.Logging.AddLog4Net(); All internal logging from ASP.NET Core, as well as manual logging you create through the ILogger interface, now goes directly into elmah.io. A common request is to include all of the HTTP contextual information you usually get logged when using a package like Elmah.Io.AspNetCore . We have developed a specialized NuGet package to include cookies, server variables, etc. when logging through log4net from ASP.NET Core. To set it up, install the Elmah.Io.AspNetCore.Log4Net NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.Log4Net dotnet add package Elmah.Io.AspNetCore.Log4Net <PackageReference Include=\"Elmah.Io.AspNetCore.Log4Net\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.Log4Net Finally, make sure to call the UseElmahIoLog4Net method in the Program.cs file: // ... Exception handling middleware app.UseElmahIoLog4Net(); // ... UseMvc etc.","title":"ASP.NET Core"},{"location":"logging-to-elmah-io-from-log4net/#troubleshooting","text":"Here are some things to try out if logging from log4net to elmah.io doesn't work: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure that you have the newest Elmah.Io.Log4Net and Elmah.Io.Client packages installed. Make sure to include all of the configuration from the example above. That includes both the <root> and <appender> element. Make sure that the API key is valid and allow the Messages | Write permission . Make sure to include a valid log ID. Make sure that you have sufficient log messages in your subscription and that you didn't disable logging to the log or include any ignore filters/rules. Enable and inspect log4net's internal debug log by including the following code in your web/app.config file to reveal any exceptions happening inside the log4net engine room or one of the appenders: <?xml version=\"1.0\" encoding=\"utf-8\"?> <configuration> <appSettings> <add key=\"log4net.Internal.Debug\" value=\"true\"/> </appSettings> <system.diagnostics> <trace autoflush=\"true\"> <listeners> <add name=\"textWriterTraceListener\" type=\"System.Diagnostics.TextWriterTraceListener\" initializeData=\"C:\\temp\\log4net-debug.log\" /> </listeners> </trace> </system.diagnostics> </configuration>","title":"Troubleshooting"},{"location":"logging-to-elmah-io-from-log4net/#systemiofileloadexception-could-not-load-file-or-assembly-log4net","text":"In case you get the following exception while trying to log messages to elmah.io: System.IO.FileLoadException: Could not load file or assembly 'log4net, Version=2.0.8.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040) This indicates they either the log4net.dll file is missing or there's a problem with assembly bindings. You can include a binding redirect to the newest version of log4net by including the following code in your web/app.config file: <?xml version=\"1.0\" encoding=\"utf-8\"?> <configuration> <runtime> <assemblyBinding xmlns=\"urn:schemas-microsoft-com:asm.v1\"> <dependentAssembly> <assemblyIdentity name=\"log4net\" publicKeyToken=\"669e0ddf0bb1aa2a\" culture=\"neutral\"/> <bindingRedirect oldVersion=\"0.0.0.0-2.0.12.0\" newVersion=\"2.0.12.0\"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration>","title":"System.IO.FileLoadException: Could not load file or assembly 'log4net ..."},{"location":"logging-to-elmah-io-from-logary/","text":"Logging to elmah.io from Logary Logary is a semantic logging framework like Serilog and Microsoft Semantic Logging. Combining semantic logs with elmah.io is a perfect fit since elmah.io has been designed with semantics from the ground up. In this tutorial, we\u2019ll add Logary to a Console application, but the process is almost identical to other project types. Create a new console application and add the elmah.io target for Logary: Package Manager .NET CLI PackageReference Paket CLI Install-Package Logary.Targets.ElmahIO dotnet add package Logary.Targets.ElmahIO <PackageReference Include=\"Logary.Targets.ElmahIO\" Version=\"5.*\" /> paket add Logary.Targets.ElmahIO Configuration in F# Configure elmah.io just like you would any normal target: withTargets [ // ... ElmahIO.create { logId = Guid.Parse \"LOG_ID\"; apiKey = \"API_KEY\" } \"elmah.io\" ] >> withRules [ // ... Rule.createForTarget \"elmah.io\" ] where LOG_ID is the id of your log ( Where is my log ID? ). Configuration in C# Configuration in C# is just as easy: .Target<ElmahIO.Builder>( \"elmah.io\", conf => conf.Target.SendTo(logId: \"LOG_ID\", apiKey: \"API_KEY\")) where API_KEY is your API key and LOG_ID is the ID of your log. Logging To start logging messages to elmah.io, you can use the following F# code: let logger = logary.getLogger (PointName [| \"Logary\"; \"Samples\"; \"main\" |]) Message.event Info \"User logged in\" |> Message.setField \"userName\" \"haf\" |> Logger.logSimple logger or in C#: var logger = logary.GetLogger(\"Logary.CSharpExample\"); logger.LogEventFormat(LogLevel.Fatal, \"Unhandled {exception}!\", e);","title":"Logging from Logary"},{"location":"logging-to-elmah-io-from-logary/#logging-to-elmahio-from-logary","text":"Logary is a semantic logging framework like Serilog and Microsoft Semantic Logging. Combining semantic logs with elmah.io is a perfect fit since elmah.io has been designed with semantics from the ground up. In this tutorial, we\u2019ll add Logary to a Console application, but the process is almost identical to other project types. Create a new console application and add the elmah.io target for Logary: Package Manager .NET CLI PackageReference Paket CLI Install-Package Logary.Targets.ElmahIO dotnet add package Logary.Targets.ElmahIO <PackageReference Include=\"Logary.Targets.ElmahIO\" Version=\"5.*\" /> paket add Logary.Targets.ElmahIO","title":"Logging to elmah.io from Logary"},{"location":"logging-to-elmah-io-from-logary/#configuration-in-f","text":"Configure elmah.io just like you would any normal target: withTargets [ // ... ElmahIO.create { logId = Guid.Parse \"LOG_ID\"; apiKey = \"API_KEY\" } \"elmah.io\" ] >> withRules [ // ... Rule.createForTarget \"elmah.io\" ] where LOG_ID is the id of your log ( Where is my log ID? ).","title":"Configuration in F#"},{"location":"logging-to-elmah-io-from-logary/#configuration-in-c","text":"Configuration in C# is just as easy: .Target<ElmahIO.Builder>( \"elmah.io\", conf => conf.Target.SendTo(logId: \"LOG_ID\", apiKey: \"API_KEY\")) where API_KEY is your API key and LOG_ID is the ID of your log.","title":"Configuration in C#"},{"location":"logging-to-elmah-io-from-logary/#logging","text":"To start logging messages to elmah.io, you can use the following F# code: let logger = logary.getLogger (PointName [| \"Logary\"; \"Samples\"; \"main\" |]) Message.event Info \"User logged in\" |> Message.setField \"userName\" \"haf\" |> Logger.logSimple logger or in C#: var logger = logary.GetLogger(\"Logary.CSharpExample\"); logger.LogEventFormat(LogLevel.Fatal, \"Unhandled {exception}!\", e);","title":"Logging"},{"location":"logging-to-elmah-io-from-maui/","text":"Logging to elmah.io from MAUI We just started looking into .NET MAUI. If you want to go for the stable choice, check out the integration with Xamarin (the NuGet package is also in prerelease but more stable than the integration described on this page). A quick way to get started with logging from MAUI to elmah.io is by installing the Elmah.Io.Blazor.Wasm NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Blazor.Wasm -IncludePrerelease dotnet add package Elmah.Io.Blazor.Wasm --prerelease <PackageReference Include=\"Elmah.Io.Blazor.Wasm\" Version=\"4.*\" /> paket add Elmah.Io.Blazor.Wasm The name is, admittedly, misleading here. The Elmah.Io.Blazor.Wasm package contains a simplified version of the Elmah.Io.Extensions.Logging package that doesn't make use of the Elmah.Io.Client package to communicate with the elmah.io API. Finally, configure the elmah.io logger in the MauiProgram.cs file: public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp<App>() // ... .Logging .AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); return builder.Build(); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). The logging initialization code will log all exceptions logged through ILogger both manually and by MAUI itself. MAUI supports dependency injection of the ILogger like most other web and mobile frameworks built on top of .NET. Some exceptions are not logged through Microsoft.Extensions.Logging why you need to handle these manually. If you have worked with Xamarin, you probably know about the various exceptions-related events (like UnhandledException ). Which event you need to subscribe to depends on the current platform. An easy approach is to use Matt Johnson-Pint's MauiExceptions class available here . When the class is added, logging uncaught exceptions is using a few lines of code: // Replace with an instance of the ILogger ILogger logger; // Subscribe to the UnhandledException event and log it through ILogger MauiExceptions.UnhandledException += (sender, args) => { var exception = (Exception)args.ExceptionObject; logger.LogError(exception, exception.GetBaseException().Message); }","title":"Logging from MAUI"},{"location":"logging-to-elmah-io-from-maui/#logging-to-elmahio-from-maui","text":"We just started looking into .NET MAUI. If you want to go for the stable choice, check out the integration with Xamarin (the NuGet package is also in prerelease but more stable than the integration described on this page). A quick way to get started with logging from MAUI to elmah.io is by installing the Elmah.Io.Blazor.Wasm NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Blazor.Wasm -IncludePrerelease dotnet add package Elmah.Io.Blazor.Wasm --prerelease <PackageReference Include=\"Elmah.Io.Blazor.Wasm\" Version=\"4.*\" /> paket add Elmah.Io.Blazor.Wasm The name is, admittedly, misleading here. The Elmah.Io.Blazor.Wasm package contains a simplified version of the Elmah.Io.Extensions.Logging package that doesn't make use of the Elmah.Io.Client package to communicate with the elmah.io API. Finally, configure the elmah.io logger in the MauiProgram.cs file: public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp<App>() // ... .Logging .AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); return builder.Build(); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). The logging initialization code will log all exceptions logged through ILogger both manually and by MAUI itself. MAUI supports dependency injection of the ILogger like most other web and mobile frameworks built on top of .NET. Some exceptions are not logged through Microsoft.Extensions.Logging why you need to handle these manually. If you have worked with Xamarin, you probably know about the various exceptions-related events (like UnhandledException ). Which event you need to subscribe to depends on the current platform. An easy approach is to use Matt Johnson-Pint's MauiExceptions class available here . When the class is added, logging uncaught exceptions is using a few lines of code: // Replace with an instance of the ILogger ILogger logger; // Subscribe to the UnhandledException event and log it through ILogger MauiExceptions.UnhandledException += (sender, args) => { var exception = (Exception)args.ExceptionObject; logger.LogError(exception, exception.GetBaseException().Message); }","title":"Logging to elmah.io from MAUI"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/","text":"Logging to elmah.io from Microsoft.Extensions.Logging Logging to elmah.io from Microsoft.Extensions.Logging Logging from ASP.NET Core Include HTTP context Logging from a console application Logging custom properties Setting category Include source code Options Setting application name appsettings.json configuration Filtering log messages Logging through a proxy Troubleshooting Microsoft.Extensions.Logging is both a logging framework itself and a logging abstraction on top of other logging frameworks like log4net and Serilog. Start by installing the Elmah.Io.Extensions.Logging package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Locate your API key ( Where is my API key? ) and log ID. The two values will be referenced as API_KEY and LOG_ID ( Where is my log ID? ) in the following. Logging from ASP.NET Core In the Program.cs file, add a new using statement: using Elmah.Io.Extensions.Logging; Then call the AddElmahIo -method as shown here: builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); builder.Logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); By calling, the AddFilter -method, you ensure that only warnings and up are logged to elmah.io. The API key and log ID can also be configured in appsettings.json : { // ... \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } } To tell Microsoft.Extensions.Logging to use configuration from the appsettings.json file, include the following code in Program.cs : builder.Services.Configure<ElmahIoProviderOptions>(builder.Configuration.GetSection(\"ElmahIo\")); builder.Logging.AddConfiguration(builder.Configuration.GetSection(\"Logging\")); builder.Logging.AddElmahIo(); The first line fetches elmah.io configuration from the ElmahIo object in the appsettings.json file. The second line configures log levels in Microsoft.Extensions.Logging from the Logging object in appsettings.json . The third line adds the elmah.io logger to Microsoft.Extensions.Logging. Notice how the overload without any options object is called since options are already loaded from appsettings.json . Start logging messages by injecting an ILogger in your controllers: public class HomeController : Controller { private readonly ILogger<HomeController> logger; public HomeController(ILogger<HomeController> logger) { this.logger = logger; } public IActionResult Index() { logger.LogWarning(\"Request to index\"); return View(); } } For an example of configuring elmah.io with ASP.NET Core 3.1, check out this sample . Include HTTP context A common use case for using Microsoft.Extensions.Logging is part of an ASP.NET Core project. When combining the two, you would expect the log messages to contain relevant information from the HTTP context (like URL, status code, cookies, etc.). This is not the case out of the box, since Microsoft.Extensions.Logging doesn't know which project type includes it. Logging HTTP context requires Elmah.Io.Extensions.Logging version 3.6.x or newer. To add HTTP context properties to log messages when logging from ASP.NET Core, install the Elmah.Io.AspNetCore.ExtensionsLogging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.ExtensionsLogging dotnet add package Elmah.Io.AspNetCore.ExtensionsLogging <PackageReference Include=\"Elmah.Io.AspNetCore.ExtensionsLogging\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.ExtensionsLogging Then call the UseElmahIoExtensionsLogging method in the Program.cs file: // ... Exception handling middleware app.UseElmahIoExtensionsLogging(); // ... UseMvc etc. It's important to call the UseElmahIoExtensionsLogging method after any calls to UseElmahIo , UseAuthentication , and other exception handling middleware but before UseMvc and UseEndpoints . If you experience logged errors without the HTTP context, try moving the UseElmahIoExtensionsLogging method as the first call in the Configure method. Logging from a console application Choose the right framework version: .NET >= 6 .NET Core 3 Configure logging to elmah.io using a new or existing ServiceCollection : var services = new ServiceCollection(); services.AddLogging(logging => logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); })); Get a reference to the LoggerFactory : using var serviceProvider = services.BuildServiceProvider(); var loggerFactory = serviceProvider.GetService<ILoggerFactory>(); Create a new LoggerFactory and configure it to use elmah.io: using var loggerFactory = LoggerFactory.Create(builder => builder .AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); })); Adding the using keyword is important to let elmah.io store messages before exiting the application. Finally, create a new logger and start logging exceptions: var logger = factory.CreateLogger(\"MyLog\"); logger.LogError(ex, \"Unexpected error\"); Logging custom properties Elmah.Io.Extensions.Logging support Microsoft.Extensions.Logging scopes from version 3.6.x . In short, scopes are a way to decorate your log messages like enrichers in Serilog and context in NLog and log4net. By including properties in a scope, these properties automatically go into the Data tab on elmah.io. To define a new scope, wrap your logging code in a using : using (logger.BeginScope(new Dictionary<string, object> { { \"UserId\", 42 } })) { logger.LogInformation(\"Someone says hello\"); } In the example above, the UserId key will be added on the Data tab with the value of 42 . Like the other logging framework integrations, Elmah.Io.Extensions.Logging supports a range of known keys that can be used to insert value in the correct fields on the elmah.io UI. using (logger.BeginScope(new Dictionary<string, object> { { \"statuscode\", 500 }, { \"method\", \"GET\" } })) { logger.LogError(\"Request to {url} caused an error\", \"/profile\"); } In this example, a log message with the template Request to {url} caused an error to be logged. The use of the variable names statuscode , method , and url will fill in those values in the correct fields on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage . An alternative is to use the OnMessage action. As an example, we'll add a version number to all messages: logging .AddElmahIo(options => { // ... options.OnMessage = msg => { msg.Version = \"2.0.0\"; }; }); You can even access the current HTTP context in the OnMessage action. To do so, start by creating a new class named DecorateElmahIoMessages : public class DecorateElmahIoMessages : IConfigureOptions<ElmahIoProviderOptions> { private readonly IHttpContextAccessor httpContextAccessor; public DecorateElmahIoMessages(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public void Configure(ElmahIoProviderOptions options) { options.OnMessage = msg => { var context = httpContextAccessor.HttpContext; if (context == null) return; msg.User = context.User?.Identity?.Name; }; } } Then register IHttpContextAccessor and the new class in the Program.cs file: builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton<IConfigureOptions<ElmahIoProviderOptions>, DecorateElmahIoMessages>(); Setting category elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to Microsoft.Extensions.Logging field of the same name. The category field is automatically set when using a typed logger: ILogger<MyType> logger = ...; logger.LogInformation(\"This is an information message with category\"); The category can be overwritten using a property named category on either the log message or a new scope: using (logger.BeginScope(new Dictionary<string, object> { { \"category\", \"The category\" } })) { logger.LogInformation(\"This is an information message with category\"); } Include source code You can use the OnMessage action already described to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: logging .AddElmahIo(options => { // ... options.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; }); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward. Options Setting application name If logging to the same log from multiple applications it is a good idea to set unique application names from each app. This will let you search and filter errors on the elmah.io UI. To set an application name, add the following code to the options: logging.AddElmahIo(options => { // ... options.Application = \"MyApp\"; }); The application name can also be configured through appsettings.json : { // ... \"ElmahIo\": { // ... \"Application\": \"MyApp\" } } appsettings.json configuration Some of the configuration for Elmah.Io.Extensions.Logging can be done through the appsettings.json file when using ASP.NET Core 2.x or above. To configure the minimum log level, add a new logger named ElmahIo to the settings file: { \"Logging\": { // ... \"ElmahIo\": { \"LogLevel\": { \"Default\": \"Warning\" } } } } Finally, tell the logger to look for this information, by adding a bit of code to Program.cs : builder.Logging.AddConfiguration(builder.Configuration.GetSection(\"Logging\")); Filtering log messages As default, the elmah.io logger for Microsoft.Extensions.Logging only logs warnings, errors, and fatals. The rationale behind this is that we build an error management system and doesn't do much to support millions of debug messages from your code. Sometimes you may want to log non-exception messages, though. To do so, use filters in Microsoft.Extensions.Logging. To log everything from log level Information and up, do the following: Inside Program.cs change the minimum level: builder.Logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Information); In the code sample, every log message with a log level of Information and up will be logged to elmah.io. Logging through a proxy Proxy configuration requires 3.5.49 or newer. You can log through a proxy using options: logging.AddElmahIo(options => { // ... options.WebProxy = new WebProxy(\"localhost\", 8000); }); In this example, the elmah.io client routes all traffic through http://localhost:8000 . Troubleshooting Here are some things to try out if logging from Microsoft.Extensions.Logging to elmah.io doesn't work: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . x message(s) dropped because of queue size limit If you see this message in your log, it means that you are logging a large number of messages to elmah.io through Microsoft.Extensions.Logging within a short period of time. Either turn down the volume using filters: logging.AddElmahIo(options => { /*...*/ }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); or increase the queue size of Elmah.Io.Extensions.Logging : logging.AddElmahIo(options => { // ... options.BackgroundQueueSize = 5000; }); Uncaught errors are logged twice If you have both Elmah.Io.Extensions.Logging and Elmah.Io.AspNetCore installed, you may see a pattern of uncaught errors being logged twice. This is because a range of middleware from Microsoft (and others) log uncaught exceptions through ILogger . When you have Elmah.Io.AspNetCore installed you typically don't want other pieces of middleware to log the same error but with fewer details. To ignore duplicate errors you need to figure out which middleware class that trigger the logging and in which namespace it is located. One or more of the following ignore filters can be added to your Program.cs file: logging.AddFilter<ElmahIoLoggerProvider>( \"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware\", LogLevel.None); logging.AddFilter<ElmahIoLoggerProvider>( \"Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware\", LogLevel.None); logging.AddFilter<ElmahIoLoggerProvider>( \"Microsoft.AspNetCore.Server.IIS.Core\", LogLevel.None); Be aware that these lines will ignore all messages from the specified class or namespace. To ignore specific errors you can implement the OnFilter action as shown previously in this document. Ignoring uncaught errors from IIS would look like this: options.OnFilter = msg => { return msg.TitleTemplate == \"Connection ID \\\"{ConnectionId}\\\", Request ID \\\"{TraceIdentifier}\\\": An unhandled exception was thrown by the application.\"; };","title":"Logging from Microsoft.Extensions.Logging"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#logging-to-elmahio-from-microsoftextensionslogging","text":"Logging to elmah.io from Microsoft.Extensions.Logging Logging from ASP.NET Core Include HTTP context Logging from a console application Logging custom properties Setting category Include source code Options Setting application name appsettings.json configuration Filtering log messages Logging through a proxy Troubleshooting Microsoft.Extensions.Logging is both a logging framework itself and a logging abstraction on top of other logging frameworks like log4net and Serilog. Start by installing the Elmah.Io.Extensions.Logging package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging Locate your API key ( Where is my API key? ) and log ID. The two values will be referenced as API_KEY and LOG_ID ( Where is my log ID? ) in the following.","title":"Logging to elmah.io from Microsoft.Extensions.Logging"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#logging-from-aspnet-core","text":"In the Program.cs file, add a new using statement: using Elmah.Io.Extensions.Logging; Then call the AddElmahIo -method as shown here: builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); builder.Logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); By calling, the AddFilter -method, you ensure that only warnings and up are logged to elmah.io. The API key and log ID can also be configured in appsettings.json : { // ... \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } } To tell Microsoft.Extensions.Logging to use configuration from the appsettings.json file, include the following code in Program.cs : builder.Services.Configure<ElmahIoProviderOptions>(builder.Configuration.GetSection(\"ElmahIo\")); builder.Logging.AddConfiguration(builder.Configuration.GetSection(\"Logging\")); builder.Logging.AddElmahIo(); The first line fetches elmah.io configuration from the ElmahIo object in the appsettings.json file. The second line configures log levels in Microsoft.Extensions.Logging from the Logging object in appsettings.json . The third line adds the elmah.io logger to Microsoft.Extensions.Logging. Notice how the overload without any options object is called since options are already loaded from appsettings.json . Start logging messages by injecting an ILogger in your controllers: public class HomeController : Controller { private readonly ILogger<HomeController> logger; public HomeController(ILogger<HomeController> logger) { this.logger = logger; } public IActionResult Index() { logger.LogWarning(\"Request to index\"); return View(); } } For an example of configuring elmah.io with ASP.NET Core 3.1, check out this sample .","title":"Logging from ASP.NET Core"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#include-http-context","text":"A common use case for using Microsoft.Extensions.Logging is part of an ASP.NET Core project. When combining the two, you would expect the log messages to contain relevant information from the HTTP context (like URL, status code, cookies, etc.). This is not the case out of the box, since Microsoft.Extensions.Logging doesn't know which project type includes it. Logging HTTP context requires Elmah.Io.Extensions.Logging version 3.6.x or newer. To add HTTP context properties to log messages when logging from ASP.NET Core, install the Elmah.Io.AspNetCore.ExtensionsLogging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.ExtensionsLogging dotnet add package Elmah.Io.AspNetCore.ExtensionsLogging <PackageReference Include=\"Elmah.Io.AspNetCore.ExtensionsLogging\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.ExtensionsLogging Then call the UseElmahIoExtensionsLogging method in the Program.cs file: // ... Exception handling middleware app.UseElmahIoExtensionsLogging(); // ... UseMvc etc. It's important to call the UseElmahIoExtensionsLogging method after any calls to UseElmahIo , UseAuthentication , and other exception handling middleware but before UseMvc and UseEndpoints . If you experience logged errors without the HTTP context, try moving the UseElmahIoExtensionsLogging method as the first call in the Configure method.","title":"Include HTTP context"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#logging-from-a-console-application","text":"Choose the right framework version: .NET >= 6 .NET Core 3 Configure logging to elmah.io using a new or existing ServiceCollection : var services = new ServiceCollection(); services.AddLogging(logging => logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); })); Get a reference to the LoggerFactory : using var serviceProvider = services.BuildServiceProvider(); var loggerFactory = serviceProvider.GetService<ILoggerFactory>(); Create a new LoggerFactory and configure it to use elmah.io: using var loggerFactory = LoggerFactory.Create(builder => builder .AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); })); Adding the using keyword is important to let elmah.io store messages before exiting the application. Finally, create a new logger and start logging exceptions: var logger = factory.CreateLogger(\"MyLog\"); logger.LogError(ex, \"Unexpected error\");","title":"Logging from a console application"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#logging-custom-properties","text":"Elmah.Io.Extensions.Logging support Microsoft.Extensions.Logging scopes from version 3.6.x . In short, scopes are a way to decorate your log messages like enrichers in Serilog and context in NLog and log4net. By including properties in a scope, these properties automatically go into the Data tab on elmah.io. To define a new scope, wrap your logging code in a using : using (logger.BeginScope(new Dictionary<string, object> { { \"UserId\", 42 } })) { logger.LogInformation(\"Someone says hello\"); } In the example above, the UserId key will be added on the Data tab with the value of 42 . Like the other logging framework integrations, Elmah.Io.Extensions.Logging supports a range of known keys that can be used to insert value in the correct fields on the elmah.io UI. using (logger.BeginScope(new Dictionary<string, object> { { \"statuscode\", 500 }, { \"method\", \"GET\" } })) { logger.LogError(\"Request to {url} caused an error\", \"/profile\"); } In this example, a log message with the template Request to {url} caused an error to be logged. The use of the variable names statuscode , method , and url will fill in those values in the correct fields on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage . An alternative is to use the OnMessage action. As an example, we'll add a version number to all messages: logging .AddElmahIo(options => { // ... options.OnMessage = msg => { msg.Version = \"2.0.0\"; }; }); You can even access the current HTTP context in the OnMessage action. To do so, start by creating a new class named DecorateElmahIoMessages : public class DecorateElmahIoMessages : IConfigureOptions<ElmahIoProviderOptions> { private readonly IHttpContextAccessor httpContextAccessor; public DecorateElmahIoMessages(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public void Configure(ElmahIoProviderOptions options) { options.OnMessage = msg => { var context = httpContextAccessor.HttpContext; if (context == null) return; msg.User = context.User?.Identity?.Name; }; } } Then register IHttpContextAccessor and the new class in the Program.cs file: builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton<IConfigureOptions<ElmahIoProviderOptions>, DecorateElmahIoMessages>();","title":"Logging custom properties"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#setting-category","text":"elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to Microsoft.Extensions.Logging field of the same name. The category field is automatically set when using a typed logger: ILogger<MyType> logger = ...; logger.LogInformation(\"This is an information message with category\"); The category can be overwritten using a property named category on either the log message or a new scope: using (logger.BeginScope(new Dictionary<string, object> { { \"category\", \"The category\" } })) { logger.LogInformation(\"This is an information message with category\"); }","title":"Setting category"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#include-source-code","text":"You can use the OnMessage action already described to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: logging .AddElmahIo(options => { // ... options.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; }); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.","title":"Include source code"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#options","text":"","title":"Options"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#setting-application-name","text":"If logging to the same log from multiple applications it is a good idea to set unique application names from each app. This will let you search and filter errors on the elmah.io UI. To set an application name, add the following code to the options: logging.AddElmahIo(options => { // ... options.Application = \"MyApp\"; }); The application name can also be configured through appsettings.json : { // ... \"ElmahIo\": { // ... \"Application\": \"MyApp\" } }","title":"Setting application name"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#appsettingsjson-configuration","text":"Some of the configuration for Elmah.Io.Extensions.Logging can be done through the appsettings.json file when using ASP.NET Core 2.x or above. To configure the minimum log level, add a new logger named ElmahIo to the settings file: { \"Logging\": { // ... \"ElmahIo\": { \"LogLevel\": { \"Default\": \"Warning\" } } } } Finally, tell the logger to look for this information, by adding a bit of code to Program.cs : builder.Logging.AddConfiguration(builder.Configuration.GetSection(\"Logging\"));","title":"appsettings.json configuration"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#filtering-log-messages","text":"As default, the elmah.io logger for Microsoft.Extensions.Logging only logs warnings, errors, and fatals. The rationale behind this is that we build an error management system and doesn't do much to support millions of debug messages from your code. Sometimes you may want to log non-exception messages, though. To do so, use filters in Microsoft.Extensions.Logging. To log everything from log level Information and up, do the following: Inside Program.cs change the minimum level: builder.Logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Information); In the code sample, every log message with a log level of Information and up will be logged to elmah.io.","title":"Filtering log messages"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#logging-through-a-proxy","text":"Proxy configuration requires 3.5.49 or newer. You can log through a proxy using options: logging.AddElmahIo(options => { // ... options.WebProxy = new WebProxy(\"localhost\", 8000); }); In this example, the elmah.io client routes all traffic through http://localhost:8000 .","title":"Logging through a proxy"},{"location":"logging-to-elmah-io-from-microsoft-extensions-logging/#troubleshooting","text":"Here are some things to try out if logging from Microsoft.Extensions.Logging to elmah.io doesn't work: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . x message(s) dropped because of queue size limit If you see this message in your log, it means that you are logging a large number of messages to elmah.io through Microsoft.Extensions.Logging within a short period of time. Either turn down the volume using filters: logging.AddElmahIo(options => { /*...*/ }); logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); or increase the queue size of Elmah.Io.Extensions.Logging : logging.AddElmahIo(options => { // ... options.BackgroundQueueSize = 5000; }); Uncaught errors are logged twice If you have both Elmah.Io.Extensions.Logging and Elmah.Io.AspNetCore installed, you may see a pattern of uncaught errors being logged twice. This is because a range of middleware from Microsoft (and others) log uncaught exceptions through ILogger . When you have Elmah.Io.AspNetCore installed you typically don't want other pieces of middleware to log the same error but with fewer details. To ignore duplicate errors you need to figure out which middleware class that trigger the logging and in which namespace it is located. One or more of the following ignore filters can be added to your Program.cs file: logging.AddFilter<ElmahIoLoggerProvider>( \"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware\", LogLevel.None); logging.AddFilter<ElmahIoLoggerProvider>( \"Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware\", LogLevel.None); logging.AddFilter<ElmahIoLoggerProvider>( \"Microsoft.AspNetCore.Server.IIS.Core\", LogLevel.None); Be aware that these lines will ignore all messages from the specified class or namespace. To ignore specific errors you can implement the OnFilter action as shown previously in this document. Ignoring uncaught errors from IIS would look like this: options.OnFilter = msg => { return msg.TitleTemplate == \"Connection ID \\\"{ConnectionId}\\\", Request ID \\\"{TraceIdentifier}\\\": An unhandled exception was thrown by the application.\"; };","title":"Troubleshooting"},{"location":"logging-to-elmah-io-from-nancy/","text":"Logging to elmah.io from Nancy Nancy is no longer maintained. While the installation steps on this page (probably) still work, we no longer test the Nancy.Elmah package. As with MVC and WebAPI, Nancy already provides ELMAH support out of the box. Start by installing the Elmah.Io NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). To integrate Nancy and ELMAH, Christian Westman already did a great job with his Nancy.Elmah package. Install it using NuGet: Package Manager .NET CLI PackageReference Paket CLI Install-Package Nancy.Elmah dotnet add package Nancy.Elmah <PackageReference Include=\"Nancy.Elmah\" Version=\"0.*\" /> paket add Nancy.Elmah You must install the Elmah.Io package before Nancy.Elmah , because both packages like to add the ELMAH configuration to the web.config file. If you install it the other way around, you will need to add the elmah.io ErrorLog element manually. For Nancy to know how to log errors to Elmah, you need to add an override of the DefaultNancyBootstrapper. Create a new class in the root named Bootstrapper: using Nancy; using Nancy.Bootstrapper; using Nancy.Elmah; using Nancy.TinyIoc; namespace Elmah.Io.NancyExample { public class Bootstrapper : DefaultNancyBootstrapper { protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) { base.ApplicationStartup(container, pipelines); Elmahlogging.Enable(pipelines, \"elmah\"); } } } The important thing in the code sample is line 13, where we tell Nancy.Elmah to hook into the pipeline of Nancy for it to catch and log HTTP errors. The second parameter for the Enable-method, lets us define a URL for the ELMAH error page, which can be used as an alternative to elmah.io for quick viewing of errors.","title":"Logging from Nancy"},{"location":"logging-to-elmah-io-from-nancy/#logging-to-elmahio-from-nancy","text":"Nancy is no longer maintained. While the installation steps on this page (probably) still work, we no longer test the Nancy.Elmah package. As with MVC and WebAPI, Nancy already provides ELMAH support out of the box. Start by installing the Elmah.Io NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). To integrate Nancy and ELMAH, Christian Westman already did a great job with his Nancy.Elmah package. Install it using NuGet: Package Manager .NET CLI PackageReference Paket CLI Install-Package Nancy.Elmah dotnet add package Nancy.Elmah <PackageReference Include=\"Nancy.Elmah\" Version=\"0.*\" /> paket add Nancy.Elmah You must install the Elmah.Io package before Nancy.Elmah , because both packages like to add the ELMAH configuration to the web.config file. If you install it the other way around, you will need to add the elmah.io ErrorLog element manually. For Nancy to know how to log errors to Elmah, you need to add an override of the DefaultNancyBootstrapper. Create a new class in the root named Bootstrapper: using Nancy; using Nancy.Bootstrapper; using Nancy.Elmah; using Nancy.TinyIoc; namespace Elmah.Io.NancyExample { public class Bootstrapper : DefaultNancyBootstrapper { protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) { base.ApplicationStartup(container, pipelines); Elmahlogging.Enable(pipelines, \"elmah\"); } } } The important thing in the code sample is line 13, where we tell Nancy.Elmah to hook into the pipeline of Nancy for it to catch and log HTTP errors. The second parameter for the Enable-method, lets us define a URL for the ELMAH error page, which can be used as an alternative to elmah.io for quick viewing of errors.","title":"Logging to elmah.io from Nancy"},{"location":"logging-to-elmah-io-from-nlog/","text":"Logging to elmah.io from NLog Logging to elmah.io from NLog Configuration in .NET Specify API key and log ID in appSettings IntelliSense Configuration in appsettings.json elmah.io configuration outside the NLog section IntelliSense Configuration in code Logging custom properties Setting application name Setting version number Setting category Message hooks Decorating log messages Include source code Handle errors Error filtering Include HTTP context in ASP.NET and ASP.NET Core Troubleshooting System.IO.FileLoadException: Could not load file or assembly 'Newtonsoft.Json' NLog is one of the most popular logging frameworks for .NET. With an active history of almost 10 years, the possibilities with NLog are many and it\u2019s easy to find documentation on how to use it. To start logging messages from NLog to elmah.io, you need to install the Elmah.Io.NLog NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.NLog dotnet add package Elmah.Io.NLog <PackageReference Include=\"Elmah.Io.NLog\" Version=\"5.*\" /> paket add Elmah.Io.NLog Please don't use NLog 4.6.0 since that version contains a bug that causes the elmah.io target to not load correctly. 4.5.11 , 4.6.1 , or newer. Configuration in .NET To configure the elmah.io target, add the following configuration to your app.config/web.config/nlog.config depending on what kind of project you\u2019ve created: Elmah.Io.NLog 3.x/4.x Elmah.Io.NLog >= 5 <extensions> <add assembly=\"Elmah.Io.NLog\"/> </extensions> <targets> <target name=\"elmahio\" type=\"elmah.io\" apiKey=\"API_KEY\" logId=\"LOG_ID\"/> </targets> <rules> <logger name=\"*\" minlevel=\"Info\" writeTo=\"elmahio\" /> </rules> <targets> <target name=\"elmahio\" type=\"elmah.io, Elmah.Io.NLog\" apiKey=\"API_KEY\" logId=\"LOG_ID\"/> </targets> <rules> <logger name=\"*\" minlevel=\"Info\" writeTo=\"elmahio\" /> </rules> Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). In the example, we specify the level minimum as Info. This tells NLog to log only information, warning, error, and fatal messages. You may adjust this, but be aware that your elmah.io log may run full pretty fast, especially if you log thousands and thousands of trace and debug messages. Log messages to elmah.io, just as with every other target and NLog: log.Warn(\"This is a warning message\"); log.Error(new Exception(), \"This is an error message\"); Specify API key and log ID in appSettings If you are already using elmah.io, you may have your API key and log ID in the appSettings element already. To use these settings from withing the NLog target configuration you can use an NLog layout formatter: <targets> <target name=\"elmahio\" type=\"elmah.io\" apiKey=\"${appsetting:item=apiKey}\" logId=\"${appsetting:item=logId}\"/> </targets> By using the layout ${appsetting:item=apiKey} you tell NLog that the value for this attribute is in an appSettings element named elmahKey : <appSettings> <add key=\"apiKey\" value=\"API_KEY\" /> <add key=\"logId\" value=\"LOG_ID\" /> </appSettings> The appSettings layout formatter only works when targeting .NET Full Framework and requires Elmah.Io.NLog version 3.3.x or above and NLog version 4.6.x or above. IntelliSense There is support for adding IntelliSense in Visual Studio for the NLog.config file. Extend the nlog root element like this: <?xml version=\"1.0\" encoding=\"utf-8\" ?> <nlog xmlns=\"http://www.nlog-project.org/schemas/NLog.xsd\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:elmahio=\"http://www.nlog-project.org/schemas/NLog.Targets.Elmah.Io.xsd\" xsi:schemaLocation=\"http://www.nlog-project.org/schemas/NLog.Targets.Elmah.Io.xsd http://www.nlog-project.org/schemas/NLog.Targets.Elmah.Io.xsd\"> <!-- ... --> </nlog> Then change the type attribute in the target to xsi:type : <target xsi:type=\"elmahio:elmah.io\" /> Configuration in appsettings.json .NET 5 (previously Core) and newer switched from declaring XML configuration in app/web/nlog.config files to JSON configuration in an appsettings.json file. To configure elmah.io in JSON, install the NLog.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package NLog.Extensions.Logging dotnet add package NLog.Extensions.Logging <PackageReference Include=\"NLog.Extensions.Logging\" Version=\"1.*\" /> paket add NLog.Extensions.Logging Extend the appsettings.json file with a new NLog section: Elmah.Io.NLog 3.x/4.x Elmah.Io.NLog >= 5 { \"NLog\": { \"throwConfigExceptions\": true, \"extensions\": [ { \"assembly\": \"Elmah.Io.NLog\" } ], \"targets\": { \"elmahio\": { \"type\": \"elmah.io\", \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } }, \"rules\": [ { \"logger\": \"*\", \"minLevel\": \"Info\", \"writeTo\": \"elmahio\" } ] } } { \"NLog\": { \"throwConfigExceptions\": true, \"targets\": { \"elmahio\": { \"type\": \"elmah.io, Elmah.Io.NLog\", \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } }, \"rules\": [ { \"logger\": \"*\", \"minLevel\": \"Info\", \"writeTo\": \"elmahio\" } ] } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). If you haven't already loaded the configuration in your application, make sure to do so: var config = new ConfigurationBuilder() .AddJsonFile(\"appsettings.json\", optional: true, reloadOnChange: true) .Build(); Finally, tell NLog how to load the NLog configuration section: LogManager.Configuration = new NLogLoggingConfiguration(config.GetSection(\"NLog\")); elmah.io configuration outside the NLog section You might not want the elmah.io API key and log Id inside the NLog section or already have an ElmahIo section defined and want to reuse that. Splitting up configuration like that is supported through NLog layout renderers: { \"NLog\": { // ... \"targets\": { \"elmahio\": { \"type\": \"elmah.io\", \"apiKey\": \"${configsetting:item=ElmahIo.ApiKey}\", \"logId\": \"${configsetting:item=ElmahIo.LogId}\" } }, // ... }, \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } } Notice how the value of the apiKey and logId parameters have been replaced with ${configsetting:item=ElmahIo.*} . At the bottom, the ElmahIo section wraps the API key and log Id. To make this work, you will need an additional line of C# when setting up NLog logging: var config = new ConfigurationBuilder() .AddJsonFile(\"appsettings.json\", optional: true, reloadOnChange: true) .Build(); ConfigSettingLayoutRenderer.DefaultConfiguration = config; // \ud83d\udc48 add this line LogManager.Configuration = new NLogLoggingConfiguration(config.GetSection(\"NLog\")); IntelliSense There is support for adding IntelliSense in Visual Studio for the NLog section in the appsettings.json file. Copy and paste the following link into the Schema textbox above the file content: https://nlog-project.org/schemas/appsettings.schema.json Configuration in code The elmah.io target can be configured from C# code if you prefer or need to access the built-in events (see more later). The following adds logging to elmah.io: var config = new LoggingConfiguration(); var elmahIoTarget = new ElmahIoTarget(); elmahIoTarget.Name = \"elmahio\"; elmahIoTarget.ApiKey = \"API_KEY\"; elmahIoTarget.LogId = \"LOG_ID\"; config.AddTarget(elmahIoTarget); config.AddRuleForAllLevels(elmahIoTarget); LogManager.Configuration = config; The example will log all log levels to elmah.io. For more information about how to configure individual log levels, check out the NLog documentation on GitHub . Logging custom properties NLog supports logging custom properties in multiple ways. If you want to include a property (like a version number) in all log messages, you might want to look into the OnMessage feature on Elmah.Io.NLog . With custom properties, you can log additional key/value pairs with every log message. The elmah.io target for NLog supports custom properties as well. Properties are persisted alongside every log message in elmah.io and searchable if named correctly . One way to log custom properties with NLog and elmah.io is to use the overload of each logging-method that takes a LogEventInfo object as a parameter: var infoMessage = new LogEventInfo(LogLevel.Info, \"\", \"This is an information message\"); infoMessage.Properties.Add(\"Some Property Key\", \"Some Property Value\"); log.Info(infoMessage); This saves the information message in elmah.io with a custom property with key Some Property Key and value Some Property Value . As of NLog 4.5, structured logging is supported as well. To log a property as part of the log message, use the new syntax as shown here: log.Warn(\"Property named {FirstName}\", \"Donald\"); In the example, NLog will log the message Property named \"Donald\" , but the key ( FirstName ) and value ( Donald ), will also be available in the Data tab inside elmah.io. Elmah.Io.NLog provides a range of reserved property names, that can be used to fill in data in the correct fields on the elmah.io UI. Let's say you want to fill the User field using structured logging only: log.Info(\"{Quote} from {User}\", \"Hasta la vista, baby\", \"T-800\"); This will fill in the value T-800 in the User field, as well as add two key/value pairs ( Quote and User ) to the Data tab on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage . NLog also provides a fluent API (available in the NLog.Fluent namespace) that some might find more readable: logger.Info() .Message(\"I'll be back\") .Property(\"User\", \"T-800\") .Write(); If you want to use the normal logging methods like Info and Error , you can do so injunction with the MappedDiagnosticsLogicalContext class, also provided by NLog: using (MappedDiagnosticsLogicalContext.SetScoped(\"User\", \"T-800\")) { logger.Info(\"I'll be back\"); } This will create the same result as the example above. In NLog 5 MappedDiagnosticsLogicalContext has been deprecated in favor of a scope context: using (logger.PushScopeProperty(\"User\", \"T-800\")) { logger.Info(\"I'll be back\"); } Setting application name The application field on elmah.io can be set globally using NLog's global context: GlobalDiagnosticsContext.Set(\"Application\", \"My application name\"); Setting version number The version field on elmah.io can be set globally using NLog's global context: GlobalDiagnosticsContext.Set(\"Version\", \"1.2.3\"); Setting category elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to NLog's logger field automatically when using Elmah.Io.NLog . The category field can be overwritten using a scoped property (NLog 5): using (logger.PushScopeProperty(\"category\", \"The category\")) { logger.Info(\"This is an information message with category\"); } Message hooks Elmah.Io.NLog provides message hooks similar to the integrations with ASP.NET and ASP.NET Core. Message hooks need to be implemented in C#. Either configure the elmah.io target in C# or fetch the target already configured in XML: var elmahIoTarget = (ElmahIoTarget)LogManager.Configuration.FindTargetByName(\"elmahio\"); You also need to install the most recent version of the Elmah.Io.Client NuGet package to use message hooks. Decorating log messages To include additional information on log messages, you can use the OnMessage action: elmahIoTarget.OnMessage = msg => { msg.Version = \"1.0.0\"; }; The example above includes a version number on all errors. Include source code You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: elmahIoTarget.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward. Handle errors To handle any errors happening while processing a log message, you can use the OnError event when initializing the elmah.io target: elmahIoTarget.OnError = (msg, err) => { // Do something here }; The example implements a callback if logging to elmah.io fails. How you choose to implement this is entirely up to your application and tech stack. Error filtering To ignore specific errors based on their content, you can use the OnFilter event when initializing the elmah.io target: elmahIoTarget.OnFilter = msg => { return msg.Title.Contains(\"trace\"); }; The example above ignores any log messages with the word trace in the title. Include HTTP context in ASP.NET and ASP.NET Core When logging through NLog from a web application, you may want to include HTTP contextual information like the current URL, status codes, server variables, etc. NLog provides two web-packages to include this information. For ASP.NET, MVC, and Web API you can install the NLog.Web NuGet package and include the following code in web.config : <system.webServer> <modules runAllManagedModulesForAllRequests=\"true\"> <add name=\"NLog\" type=\"NLog.Web.NLogHttpModule, NLog.Web\" /> </modules> </system.webServer> For ASP.NET Core you can install the NLog.Web.AspNetCore NuGet package. When installed, the elmah.io NLog target automatically picks up the HTTP context and fills in all possible fields on messages sent to elmah.io. Troubleshooting Here are some things to try out if logging from NLog to elmah.io doesn't work: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure that you have the newest Elmah.Io.NLog and Elmah.Io.Client packages installed. Make sure to include all of the configuration from the example above. That includes both the <extensions> (Only needed in Elmah.Io.NLog 3 and 4), <targets> , and <rules> element. Make sure that the API key is valid and allow the Messages | Write permission . Make sure to include a valid log ID. Make sure that you have sufficient log messages in your subscription and that you didn't disable logging to the log or include any ignore filters/rules. Always make sure to call LogManager.Shutdown() before exiting the application to make sure that all log messages are flushed. Extend the nlog element with internalLogLevel=\"Warn\" internalLogFile=\"c:\\temp\\nlog-internal.log and inspect that log file for any internal NLog errors. System.IO.FileLoadException: Could not load file or assembly 'Newtonsoft.Json' If you see this error in the internal NLog file it means that there's a problem with multiple assemblies referencing different versions of Newtonsoft.Json (also known as NuGet dependency hell ). This can be fixed by redirecting to the installed version in the App.config or Web.config file: <runtime> <assemblyBinding xmlns=\"urn:schemas-microsoft-com:asm.v1\"> <dependentAssembly> <assemblyIdentity name=\"Newtonsoft.Json\" publicKeyToken=\"30ad4fe6b2a6aeed\" culture=\"neutral\" /> <bindingRedirect oldVersion=\"0.0.0.0-13.0.1.0\" newVersion=\"13.0.1.0\"/> </dependentAssembly> </assemblyBinding> </runtime>","title":"Logging from NLog"},{"location":"logging-to-elmah-io-from-nlog/#logging-to-elmahio-from-nlog","text":"Logging to elmah.io from NLog Configuration in .NET Specify API key and log ID in appSettings IntelliSense Configuration in appsettings.json elmah.io configuration outside the NLog section IntelliSense Configuration in code Logging custom properties Setting application name Setting version number Setting category Message hooks Decorating log messages Include source code Handle errors Error filtering Include HTTP context in ASP.NET and ASP.NET Core Troubleshooting System.IO.FileLoadException: Could not load file or assembly 'Newtonsoft.Json' NLog is one of the most popular logging frameworks for .NET. With an active history of almost 10 years, the possibilities with NLog are many and it\u2019s easy to find documentation on how to use it. To start logging messages from NLog to elmah.io, you need to install the Elmah.Io.NLog NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.NLog dotnet add package Elmah.Io.NLog <PackageReference Include=\"Elmah.Io.NLog\" Version=\"5.*\" /> paket add Elmah.Io.NLog Please don't use NLog 4.6.0 since that version contains a bug that causes the elmah.io target to not load correctly. 4.5.11 , 4.6.1 , or newer.","title":"Logging to elmah.io from NLog"},{"location":"logging-to-elmah-io-from-nlog/#configuration-in-net","text":"To configure the elmah.io target, add the following configuration to your app.config/web.config/nlog.config depending on what kind of project you\u2019ve created: Elmah.Io.NLog 3.x/4.x Elmah.Io.NLog >= 5 <extensions> <add assembly=\"Elmah.Io.NLog\"/> </extensions> <targets> <target name=\"elmahio\" type=\"elmah.io\" apiKey=\"API_KEY\" logId=\"LOG_ID\"/> </targets> <rules> <logger name=\"*\" minlevel=\"Info\" writeTo=\"elmahio\" /> </rules> <targets> <target name=\"elmahio\" type=\"elmah.io, Elmah.Io.NLog\" apiKey=\"API_KEY\" logId=\"LOG_ID\"/> </targets> <rules> <logger name=\"*\" minlevel=\"Info\" writeTo=\"elmahio\" /> </rules> Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). In the example, we specify the level minimum as Info. This tells NLog to log only information, warning, error, and fatal messages. You may adjust this, but be aware that your elmah.io log may run full pretty fast, especially if you log thousands and thousands of trace and debug messages. Log messages to elmah.io, just as with every other target and NLog: log.Warn(\"This is a warning message\"); log.Error(new Exception(), \"This is an error message\");","title":"Configuration in .NET"},{"location":"logging-to-elmah-io-from-nlog/#specify-api-key-and-log-id-in-appsettings","text":"If you are already using elmah.io, you may have your API key and log ID in the appSettings element already. To use these settings from withing the NLog target configuration you can use an NLog layout formatter: <targets> <target name=\"elmahio\" type=\"elmah.io\" apiKey=\"${appsetting:item=apiKey}\" logId=\"${appsetting:item=logId}\"/> </targets> By using the layout ${appsetting:item=apiKey} you tell NLog that the value for this attribute is in an appSettings element named elmahKey : <appSettings> <add key=\"apiKey\" value=\"API_KEY\" /> <add key=\"logId\" value=\"LOG_ID\" /> </appSettings> The appSettings layout formatter only works when targeting .NET Full Framework and requires Elmah.Io.NLog version 3.3.x or above and NLog version 4.6.x or above.","title":"Specify API key and log ID in appSettings"},{"location":"logging-to-elmah-io-from-nlog/#intellisense","text":"There is support for adding IntelliSense in Visual Studio for the NLog.config file. Extend the nlog root element like this: <?xml version=\"1.0\" encoding=\"utf-8\" ?> <nlog xmlns=\"http://www.nlog-project.org/schemas/NLog.xsd\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:elmahio=\"http://www.nlog-project.org/schemas/NLog.Targets.Elmah.Io.xsd\" xsi:schemaLocation=\"http://www.nlog-project.org/schemas/NLog.Targets.Elmah.Io.xsd http://www.nlog-project.org/schemas/NLog.Targets.Elmah.Io.xsd\"> <!-- ... --> </nlog> Then change the type attribute in the target to xsi:type : <target xsi:type=\"elmahio:elmah.io\" />","title":"IntelliSense"},{"location":"logging-to-elmah-io-from-nlog/#configuration-in-appsettingsjson","text":".NET 5 (previously Core) and newer switched from declaring XML configuration in app/web/nlog.config files to JSON configuration in an appsettings.json file. To configure elmah.io in JSON, install the NLog.Extensions.Logging NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package NLog.Extensions.Logging dotnet add package NLog.Extensions.Logging <PackageReference Include=\"NLog.Extensions.Logging\" Version=\"1.*\" /> paket add NLog.Extensions.Logging Extend the appsettings.json file with a new NLog section: Elmah.Io.NLog 3.x/4.x Elmah.Io.NLog >= 5 { \"NLog\": { \"throwConfigExceptions\": true, \"extensions\": [ { \"assembly\": \"Elmah.Io.NLog\" } ], \"targets\": { \"elmahio\": { \"type\": \"elmah.io\", \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } }, \"rules\": [ { \"logger\": \"*\", \"minLevel\": \"Info\", \"writeTo\": \"elmahio\" } ] } } { \"NLog\": { \"throwConfigExceptions\": true, \"targets\": { \"elmahio\": { \"type\": \"elmah.io, Elmah.Io.NLog\", \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } }, \"rules\": [ { \"logger\": \"*\", \"minLevel\": \"Info\", \"writeTo\": \"elmahio\" } ] } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). If you haven't already loaded the configuration in your application, make sure to do so: var config = new ConfigurationBuilder() .AddJsonFile(\"appsettings.json\", optional: true, reloadOnChange: true) .Build(); Finally, tell NLog how to load the NLog configuration section: LogManager.Configuration = new NLogLoggingConfiguration(config.GetSection(\"NLog\"));","title":"Configuration in appsettings.json"},{"location":"logging-to-elmah-io-from-nlog/#elmahio-configuration-outside-the-nlog-section","text":"You might not want the elmah.io API key and log Id inside the NLog section or already have an ElmahIo section defined and want to reuse that. Splitting up configuration like that is supported through NLog layout renderers: { \"NLog\": { // ... \"targets\": { \"elmahio\": { \"type\": \"elmah.io\", \"apiKey\": \"${configsetting:item=ElmahIo.ApiKey}\", \"logId\": \"${configsetting:item=ElmahIo.LogId}\" } }, // ... }, \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } } Notice how the value of the apiKey and logId parameters have been replaced with ${configsetting:item=ElmahIo.*} . At the bottom, the ElmahIo section wraps the API key and log Id. To make this work, you will need an additional line of C# when setting up NLog logging: var config = new ConfigurationBuilder() .AddJsonFile(\"appsettings.json\", optional: true, reloadOnChange: true) .Build(); ConfigSettingLayoutRenderer.DefaultConfiguration = config; // \ud83d\udc48 add this line LogManager.Configuration = new NLogLoggingConfiguration(config.GetSection(\"NLog\"));","title":"elmah.io configuration outside the NLog section"},{"location":"logging-to-elmah-io-from-nlog/#intellisense_1","text":"There is support for adding IntelliSense in Visual Studio for the NLog section in the appsettings.json file. Copy and paste the following link into the Schema textbox above the file content: https://nlog-project.org/schemas/appsettings.schema.json","title":"IntelliSense"},{"location":"logging-to-elmah-io-from-nlog/#configuration-in-code","text":"The elmah.io target can be configured from C# code if you prefer or need to access the built-in events (see more later). The following adds logging to elmah.io: var config = new LoggingConfiguration(); var elmahIoTarget = new ElmahIoTarget(); elmahIoTarget.Name = \"elmahio\"; elmahIoTarget.ApiKey = \"API_KEY\"; elmahIoTarget.LogId = \"LOG_ID\"; config.AddTarget(elmahIoTarget); config.AddRuleForAllLevels(elmahIoTarget); LogManager.Configuration = config; The example will log all log levels to elmah.io. For more information about how to configure individual log levels, check out the NLog documentation on GitHub .","title":"Configuration in code"},{"location":"logging-to-elmah-io-from-nlog/#logging-custom-properties","text":"NLog supports logging custom properties in multiple ways. If you want to include a property (like a version number) in all log messages, you might want to look into the OnMessage feature on Elmah.Io.NLog . With custom properties, you can log additional key/value pairs with every log message. The elmah.io target for NLog supports custom properties as well. Properties are persisted alongside every log message in elmah.io and searchable if named correctly . One way to log custom properties with NLog and elmah.io is to use the overload of each logging-method that takes a LogEventInfo object as a parameter: var infoMessage = new LogEventInfo(LogLevel.Info, \"\", \"This is an information message\"); infoMessage.Properties.Add(\"Some Property Key\", \"Some Property Value\"); log.Info(infoMessage); This saves the information message in elmah.io with a custom property with key Some Property Key and value Some Property Value . As of NLog 4.5, structured logging is supported as well. To log a property as part of the log message, use the new syntax as shown here: log.Warn(\"Property named {FirstName}\", \"Donald\"); In the example, NLog will log the message Property named \"Donald\" , but the key ( FirstName ) and value ( Donald ), will also be available in the Data tab inside elmah.io. Elmah.Io.NLog provides a range of reserved property names, that can be used to fill in data in the correct fields on the elmah.io UI. Let's say you want to fill the User field using structured logging only: log.Info(\"{Quote} from {User}\", \"Hasta la vista, baby\", \"T-800\"); This will fill in the value T-800 in the User field, as well as add two key/value pairs ( Quote and User ) to the Data tab on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage . NLog also provides a fluent API (available in the NLog.Fluent namespace) that some might find more readable: logger.Info() .Message(\"I'll be back\") .Property(\"User\", \"T-800\") .Write(); If you want to use the normal logging methods like Info and Error , you can do so injunction with the MappedDiagnosticsLogicalContext class, also provided by NLog: using (MappedDiagnosticsLogicalContext.SetScoped(\"User\", \"T-800\")) { logger.Info(\"I'll be back\"); } This will create the same result as the example above. In NLog 5 MappedDiagnosticsLogicalContext has been deprecated in favor of a scope context: using (logger.PushScopeProperty(\"User\", \"T-800\")) { logger.Info(\"I'll be back\"); }","title":"Logging custom properties"},{"location":"logging-to-elmah-io-from-nlog/#setting-application-name","text":"The application field on elmah.io can be set globally using NLog's global context: GlobalDiagnosticsContext.Set(\"Application\", \"My application name\");","title":"Setting application name"},{"location":"logging-to-elmah-io-from-nlog/#setting-version-number","text":"The version field on elmah.io can be set globally using NLog's global context: GlobalDiagnosticsContext.Set(\"Version\", \"1.2.3\");","title":"Setting version number"},{"location":"logging-to-elmah-io-from-nlog/#setting-category","text":"elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to NLog's logger field automatically when using Elmah.Io.NLog . The category field can be overwritten using a scoped property (NLog 5): using (logger.PushScopeProperty(\"category\", \"The category\")) { logger.Info(\"This is an information message with category\"); }","title":"Setting category"},{"location":"logging-to-elmah-io-from-nlog/#message-hooks","text":"Elmah.Io.NLog provides message hooks similar to the integrations with ASP.NET and ASP.NET Core. Message hooks need to be implemented in C#. Either configure the elmah.io target in C# or fetch the target already configured in XML: var elmahIoTarget = (ElmahIoTarget)LogManager.Configuration.FindTargetByName(\"elmahio\"); You also need to install the most recent version of the Elmah.Io.Client NuGet package to use message hooks.","title":"Message hooks"},{"location":"logging-to-elmah-io-from-nlog/#decorating-log-messages","text":"To include additional information on log messages, you can use the OnMessage action: elmahIoTarget.OnMessage = msg => { msg.Version = \"1.0.0\"; }; The example above includes a version number on all errors.","title":"Decorating log messages"},{"location":"logging-to-elmah-io-from-nlog/#include-source-code","text":"You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: elmahIoTarget.OnMessage = msg => { msg.WithSourceCodeFromPdb(); }; Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.","title":"Include source code"},{"location":"logging-to-elmah-io-from-nlog/#handle-errors","text":"To handle any errors happening while processing a log message, you can use the OnError event when initializing the elmah.io target: elmahIoTarget.OnError = (msg, err) => { // Do something here }; The example implements a callback if logging to elmah.io fails. How you choose to implement this is entirely up to your application and tech stack.","title":"Handle errors"},{"location":"logging-to-elmah-io-from-nlog/#error-filtering","text":"To ignore specific errors based on their content, you can use the OnFilter event when initializing the elmah.io target: elmahIoTarget.OnFilter = msg => { return msg.Title.Contains(\"trace\"); }; The example above ignores any log messages with the word trace in the title.","title":"Error filtering"},{"location":"logging-to-elmah-io-from-nlog/#include-http-context-in-aspnet-and-aspnet-core","text":"When logging through NLog from a web application, you may want to include HTTP contextual information like the current URL, status codes, server variables, etc. NLog provides two web-packages to include this information. For ASP.NET, MVC, and Web API you can install the NLog.Web NuGet package and include the following code in web.config : <system.webServer> <modules runAllManagedModulesForAllRequests=\"true\"> <add name=\"NLog\" type=\"NLog.Web.NLogHttpModule, NLog.Web\" /> </modules> </system.webServer> For ASP.NET Core you can install the NLog.Web.AspNetCore NuGet package. When installed, the elmah.io NLog target automatically picks up the HTTP context and fills in all possible fields on messages sent to elmah.io.","title":"Include HTTP context in ASP.NET and ASP.NET Core"},{"location":"logging-to-elmah-io-from-nlog/#troubleshooting","text":"Here are some things to try out if logging from NLog to elmah.io doesn't work: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure that you have the newest Elmah.Io.NLog and Elmah.Io.Client packages installed. Make sure to include all of the configuration from the example above. That includes both the <extensions> (Only needed in Elmah.Io.NLog 3 and 4), <targets> , and <rules> element. Make sure that the API key is valid and allow the Messages | Write permission . Make sure to include a valid log ID. Make sure that you have sufficient log messages in your subscription and that you didn't disable logging to the log or include any ignore filters/rules. Always make sure to call LogManager.Shutdown() before exiting the application to make sure that all log messages are flushed. Extend the nlog element with internalLogLevel=\"Warn\" internalLogFile=\"c:\\temp\\nlog-internal.log and inspect that log file for any internal NLog errors.","title":"Troubleshooting"},{"location":"logging-to-elmah-io-from-nlog/#systemiofileloadexception-could-not-load-file-or-assembly-newtonsoftjson","text":"If you see this error in the internal NLog file it means that there's a problem with multiple assemblies referencing different versions of Newtonsoft.Json (also known as NuGet dependency hell ). This can be fixed by redirecting to the installed version in the App.config or Web.config file: <runtime> <assemblyBinding xmlns=\"urn:schemas-microsoft-com:asm.v1\"> <dependentAssembly> <assemblyIdentity name=\"Newtonsoft.Json\" publicKeyToken=\"30ad4fe6b2a6aeed\" culture=\"neutral\" /> <bindingRedirect oldVersion=\"0.0.0.0-13.0.1.0\" newVersion=\"13.0.1.0\"/> </dependentAssembly> </assemblyBinding> </runtime>","title":"System.IO.FileLoadException: Could not load file or assembly 'Newtonsoft.Json'"},{"location":"logging-to-elmah-io-from-orchard/","text":"Logging to elmah.io from Orchard CMS Orchard CMS is a free, open-source community-focused content management system built on the ASP.NET MVC and ASP.NET Core platforms. This tutorial is written for the ASP.NET Core version of Orchard. If you want to log to elmah.io from the MVC version, you should follow our tutorial for MVC . To start logging to elmah.io, install the Elmah.Io.Client and Elmah.Io.AspNetCore NuGet packages: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client Install-Package Elmah.Io.AspNetCore dotnet add package Elmah.Io.Client dotnet add package Elmah.Io.AspNetCore <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> <PackageReference Include=\"Elmah.Io.AspNetCore\" Version=\"5.*\" /> paket add Elmah.Io.Client paket add Elmah.Io.AspNetCore Then modify your Startup.cs file: public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddElmahIo(o => { o.ApiKey = \"API_KEY\"; o.LogId = new Guid(\"LOG_ID\"); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseElmahIo(); // ... } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged. Like with any other ASP.NET Core application, it's important to call the UseElmahIo -method after setting up other middleware handling exceptions (like UseDeveloperExceptionPage ). Orchard uses NLog as the internal logging framework. Hooking into this pipeline is a great way to log warnings and errors through NLog to elmah.io as well. Install the Elmah.Io.Nlog NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.NLog dotnet add package Elmah.Io.NLog <PackageReference Include=\"Elmah.Io.NLog\" Version=\"5.*\" /> paket add Elmah.Io.NLog Add the elmah.io target to the NLog.config -file: <?xml version=\"1.0\" encoding=\"utf-8\" ?> <nlog> <extensions> <!-- ... --> <add assembly=\"Elmah.Io.NLog\"/> </extensions> <targets> <!-- ... --> <target name=\"elmahio\" type=\"elmah.io\" apiKey=\"API_KEY\" logId=\"LOG_ID\"/> </targets> <rules> <!-- ... --> <logger name=\"*\" minlevel=\"Warn\" writeTo=\"elmahio\" /> </rules> </nlog> Make sure not to log Trace and Debug messages to elmah.io, which will quickly use up the included storage.","title":"Logging from Orchard"},{"location":"logging-to-elmah-io-from-orchard/#logging-to-elmahio-from-orchard-cms","text":"Orchard CMS is a free, open-source community-focused content management system built on the ASP.NET MVC and ASP.NET Core platforms. This tutorial is written for the ASP.NET Core version of Orchard. If you want to log to elmah.io from the MVC version, you should follow our tutorial for MVC . To start logging to elmah.io, install the Elmah.Io.Client and Elmah.Io.AspNetCore NuGet packages: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client Install-Package Elmah.Io.AspNetCore dotnet add package Elmah.Io.Client dotnet add package Elmah.Io.AspNetCore <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> <PackageReference Include=\"Elmah.Io.AspNetCore\" Version=\"5.*\" /> paket add Elmah.Io.Client paket add Elmah.Io.AspNetCore Then modify your Startup.cs file: public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddElmahIo(o => { o.ApiKey = \"API_KEY\"; o.LogId = new Guid(\"LOG_ID\"); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseElmahIo(); // ... } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged. Like with any other ASP.NET Core application, it's important to call the UseElmahIo -method after setting up other middleware handling exceptions (like UseDeveloperExceptionPage ). Orchard uses NLog as the internal logging framework. Hooking into this pipeline is a great way to log warnings and errors through NLog to elmah.io as well. Install the Elmah.Io.Nlog NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.NLog dotnet add package Elmah.Io.NLog <PackageReference Include=\"Elmah.Io.NLog\" Version=\"5.*\" /> paket add Elmah.Io.NLog Add the elmah.io target to the NLog.config -file: <?xml version=\"1.0\" encoding=\"utf-8\" ?> <nlog> <extensions> <!-- ... --> <add assembly=\"Elmah.Io.NLog\"/> </extensions> <targets> <!-- ... --> <target name=\"elmahio\" type=\"elmah.io\" apiKey=\"API_KEY\" logId=\"LOG_ID\"/> </targets> <rules> <!-- ... --> <logger name=\"*\" minlevel=\"Warn\" writeTo=\"elmahio\" /> </rules> </nlog> Make sure not to log Trace and Debug messages to elmah.io, which will quickly use up the included storage.","title":"Logging to elmah.io from Orchard CMS"},{"location":"logging-to-elmah-io-from-piranha-cms/","text":"Logging to elmah.io from Piranha CMS Piranha CMS is a popular headless CMS written in ASP.NET Core. elmah.io works with Piranha CMS out of the box. This document contains a quick installation guide for setting up elmah.io logging in Piranha CMS. For the full overview of logging from ASP.NET Core, check out Logging to elmah.io from ASP.NET Core . To start logging to elmah.io, install the Elmah.Io.AspNetCore NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore dotnet add package Elmah.Io.AspNetCore <PackageReference Include=\"Elmah.Io.AspNetCore\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore Then modify your Startup.cs file: public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddElmahIo(o => { o.ApiKey = \"API_KEY\"; o.LogId = new Guid(\"LOG_ID\"); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseElmahIo(); // ... } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged. Make sure to call the UseElmahIo -method after setting up other middleware handling exceptions (like UseDeveloperExceptionPage ), but before the call to UsePiranha . To use structured logging and the ILogger interface with Piranha CMS and elmah.io, set up Microsoft.Extensions.Logging as explained here: Logging to elmah.io from Microsoft.Extensions.Logging .","title":"Logging from Piranha CMS"},{"location":"logging-to-elmah-io-from-piranha-cms/#logging-to-elmahio-from-piranha-cms","text":"Piranha CMS is a popular headless CMS written in ASP.NET Core. elmah.io works with Piranha CMS out of the box. This document contains a quick installation guide for setting up elmah.io logging in Piranha CMS. For the full overview of logging from ASP.NET Core, check out Logging to elmah.io from ASP.NET Core . To start logging to elmah.io, install the Elmah.Io.AspNetCore NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore dotnet add package Elmah.Io.AspNetCore <PackageReference Include=\"Elmah.Io.AspNetCore\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore Then modify your Startup.cs file: public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddElmahIo(o => { o.ApiKey = \"API_KEY\"; o.LogId = new Guid(\"LOG_ID\"); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseElmahIo(); // ... } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged. Make sure to call the UseElmahIo -method after setting up other middleware handling exceptions (like UseDeveloperExceptionPage ), but before the call to UsePiranha . To use structured logging and the ILogger interface with Piranha CMS and elmah.io, set up Microsoft.Extensions.Logging as explained here: Logging to elmah.io from Microsoft.Extensions.Logging .","title":"Logging to elmah.io from Piranha CMS"},{"location":"logging-to-elmah-io-from-powershell/","text":"Logging to elmah.io from PowerShell Logging to elmah.io from PowerShell Log through the API Log through PoShLog.Sinks.ElmahIo Log through Elmah.Io.Client Examples Log error on low remaining disk Troubleshooting There are a couple of options for logging to elmah.io from PowerShell. If you need to log a few messages, using the API is the easiest. Log through the API Logging to elmah.io from PowerShell is easy using built-in cmdlets: $apiKey = \"API_KEY\" $logId = \"LOG_ID\" $url = \"https://api.elmah.io/v3/messages/$logId/?api_key=$apiKey\" $body = @{ title = \"Error from PowerShell\" severity = \"Error\" detail = \"This is an error message logged from PowerShell\" hostname = hostname } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). Log through PoShLog.Sinks.ElmahIo PoShLog is a PowerShell logging module built on top of Serilog. To log to elmah.io using PoShLog, install the following packages: Install-Module -Name PoShLog Install-Module -Name PoShLog.Sinks.ElmahIo Logging messages can now be done using Write-*Log : Import-Module PoShLog Import-Module PoShLog.Sinks.ElmahIo New-Logger | Add-SinkElmahIo -ApiKey 'API_KEY' -LogId 'LOG_ID' | Start-Logger Write-ErrorLog 'Say My Name' # Don't forget to close the logger Close-Logger Log through Elmah.Io.Client If you prefer to use the Elmah.Io.Client NuGet package, you can do this in PowerShell too. First of all, you will need to include elmah.io.client.dll . How you do this is entirely up to you. You can include this assembly in your script location or you can download it through NuGet on every execution. To download the package through NuGet, you will need nuget.exe : $source = \"https://dist.nuget.org/win-x86-commandline/latest/nuget.exe\" $target = \".\\nuget.exe\" Invoke-WebRequest $source -OutFile $target Set-Alias nuget $target -Scope Global This script will download the latest version of the NuGet command-line tool and make it available through the command nuget . To install Elmah.Io.Client run nuget.exe : nuget install Elmah.Io.Client This will create an Elmah.Io.Client-version folder containing the latest stable version of the Elmah.Io.Client package. You now have Elmah.Io.Client.dll loaded into your shell and everything is set up to log to elmah.io. To do so, add try-catch around critical code: $logger = [Elmah.Io.Client.ElmahioAPI]::Create(\"API_KEY\") Try { # some code that may throw exceptions } Catch { $logger.Messages.Error([guid]::new(\"LOG_ID\"), $_.Exception, \"Oh no\") } In the first line, we create a new logger object. Then, in the Catch block, the catched exception is shipped off to the elmah.io log specified in LOG_ID together with a custom message. Examples For inspiration, here's a list of examples of common scenarios where you'd want to log to elmah.io from PowerShell. Log error on low remaining disk You can monitor when a server is running low on disk space like this: $cdrive = Get-Volume -DriveLetter C $sizeremainingingb = $cdrive.SizeRemaining/1024/1024/1024 if ($sizeremainingingb -lt 10) { $apiKey = \"API_KEY\" $logId = \"LOG_ID\" $url = \"https://api.elmah.io/v3/messages/$logId/?api_key=$apiKey\" $body = @{ title = \"Disk storage less than 10 gb\" severity = \"Error\" detail = \"Remaining storage in gb: $sizeremainingingb\" hostname = hostname } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" } Troubleshooting Elmah.Io.Client.ElmahioAPI cannot be loaded If PowerShell complains about Elmah.Io.Client.ElmahioAPI not being found, try adding the following lines to the script after installing the Elmah.Io.Client NuGet package: $elmahIoClientPath = Get-ChildItem -Path . -Filter Elmah.Io.Client.dll -Recurse ` | Where-Object {$_.FullName -match \"net45\"} [Reflection.Assembly]::LoadFile($elmahIoClientPath.FullName) $jsonNetPath = Get-ChildItem -Path . -Filter Newtonsoft.Json.dll -Recurse ` | Where-Object {$_.FullName -match \"net45\" -and $_.FullName -notmatch \"portable\"} [Reflection.Assembly]::LoadFile($jsonNetPath.FullName) You may need to include additional assemblies if PowerShell keeps complaining. The catch block is not invoked even though a cmdlet failed Most errors in PowerShell are non-terminating meaning that they are handled internally in the cmdlet. To force a cmdlet to use terminating errors use the -ErrorAction parameter: Try { My-Cmdlet -ErrorAction Stop } Catch { // Log to elmah.io }","title":"Logging from PowerShell"},{"location":"logging-to-elmah-io-from-powershell/#logging-to-elmahio-from-powershell","text":"Logging to elmah.io from PowerShell Log through the API Log through PoShLog.Sinks.ElmahIo Log through Elmah.Io.Client Examples Log error on low remaining disk Troubleshooting There are a couple of options for logging to elmah.io from PowerShell. If you need to log a few messages, using the API is the easiest.","title":"Logging to elmah.io from PowerShell"},{"location":"logging-to-elmah-io-from-powershell/#log-through-the-api","text":"Logging to elmah.io from PowerShell is easy using built-in cmdlets: $apiKey = \"API_KEY\" $logId = \"LOG_ID\" $url = \"https://api.elmah.io/v3/messages/$logId/?api_key=$apiKey\" $body = @{ title = \"Error from PowerShell\" severity = \"Error\" detail = \"This is an error message logged from PowerShell\" hostname = hostname } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ).","title":"Log through the API"},{"location":"logging-to-elmah-io-from-powershell/#log-through-poshlogsinkselmahio","text":"PoShLog is a PowerShell logging module built on top of Serilog. To log to elmah.io using PoShLog, install the following packages: Install-Module -Name PoShLog Install-Module -Name PoShLog.Sinks.ElmahIo Logging messages can now be done using Write-*Log : Import-Module PoShLog Import-Module PoShLog.Sinks.ElmahIo New-Logger | Add-SinkElmahIo -ApiKey 'API_KEY' -LogId 'LOG_ID' | Start-Logger Write-ErrorLog 'Say My Name' # Don't forget to close the logger Close-Logger","title":"Log through PoShLog.Sinks.ElmahIo"},{"location":"logging-to-elmah-io-from-powershell/#log-through-elmahioclient","text":"If you prefer to use the Elmah.Io.Client NuGet package, you can do this in PowerShell too. First of all, you will need to include elmah.io.client.dll . How you do this is entirely up to you. You can include this assembly in your script location or you can download it through NuGet on every execution. To download the package through NuGet, you will need nuget.exe : $source = \"https://dist.nuget.org/win-x86-commandline/latest/nuget.exe\" $target = \".\\nuget.exe\" Invoke-WebRequest $source -OutFile $target Set-Alias nuget $target -Scope Global This script will download the latest version of the NuGet command-line tool and make it available through the command nuget . To install Elmah.Io.Client run nuget.exe : nuget install Elmah.Io.Client This will create an Elmah.Io.Client-version folder containing the latest stable version of the Elmah.Io.Client package. You now have Elmah.Io.Client.dll loaded into your shell and everything is set up to log to elmah.io. To do so, add try-catch around critical code: $logger = [Elmah.Io.Client.ElmahioAPI]::Create(\"API_KEY\") Try { # some code that may throw exceptions } Catch { $logger.Messages.Error([guid]::new(\"LOG_ID\"), $_.Exception, \"Oh no\") } In the first line, we create a new logger object. Then, in the Catch block, the catched exception is shipped off to the elmah.io log specified in LOG_ID together with a custom message.","title":"Log through Elmah.Io.Client"},{"location":"logging-to-elmah-io-from-powershell/#examples","text":"For inspiration, here's a list of examples of common scenarios where you'd want to log to elmah.io from PowerShell.","title":"Examples"},{"location":"logging-to-elmah-io-from-powershell/#log-error-on-low-remaining-disk","text":"You can monitor when a server is running low on disk space like this: $cdrive = Get-Volume -DriveLetter C $sizeremainingingb = $cdrive.SizeRemaining/1024/1024/1024 if ($sizeremainingingb -lt 10) { $apiKey = \"API_KEY\" $logId = \"LOG_ID\" $url = \"https://api.elmah.io/v3/messages/$logId/?api_key=$apiKey\" $body = @{ title = \"Disk storage less than 10 gb\" severity = \"Error\" detail = \"Remaining storage in gb: $sizeremainingingb\" hostname = hostname } Invoke-RestMethod -Method Post -Uri $url -Body ($body|ConvertTo-Json) -ContentType \"application/json-patch+json\" }","title":"Log error on low remaining disk"},{"location":"logging-to-elmah-io-from-powershell/#troubleshooting","text":"Elmah.Io.Client.ElmahioAPI cannot be loaded If PowerShell complains about Elmah.Io.Client.ElmahioAPI not being found, try adding the following lines to the script after installing the Elmah.Io.Client NuGet package: $elmahIoClientPath = Get-ChildItem -Path . -Filter Elmah.Io.Client.dll -Recurse ` | Where-Object {$_.FullName -match \"net45\"} [Reflection.Assembly]::LoadFile($elmahIoClientPath.FullName) $jsonNetPath = Get-ChildItem -Path . -Filter Newtonsoft.Json.dll -Recurse ` | Where-Object {$_.FullName -match \"net45\" -and $_.FullName -notmatch \"portable\"} [Reflection.Assembly]::LoadFile($jsonNetPath.FullName) You may need to include additional assemblies if PowerShell keeps complaining. The catch block is not invoked even though a cmdlet failed Most errors in PowerShell are non-terminating meaning that they are handled internally in the cmdlet. To force a cmdlet to use terminating errors use the -ErrorAction parameter: Try { My-Cmdlet -ErrorAction Stop } Catch { // Log to elmah.io }","title":"Troubleshooting"},{"location":"logging-to-elmah-io-from-react/","text":"Logging to elmah.io from React To log all errors from a React application, install the elmah.io.javascript npm package as described in Logging from JavaScript . Then modify the index.js or index.tsx file: // ... import Elmahio from 'elmah.io.javascript'; new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); // After this the ReactDOM.render etc. will be included When launching your React app, elmah.io is configured and all errors happening in the application are logged. Check out the elmahio-react and elmahio-react-typescript samples for some real working code. React have a known bug/feature in DEV mode where errors are submitted twice. For better error handling in React, you should look into Error Boundaries .","title":"Logging from React"},{"location":"logging-to-elmah-io-from-react/#logging-to-elmahio-from-react","text":"To log all errors from a React application, install the elmah.io.javascript npm package as described in Logging from JavaScript . Then modify the index.js or index.tsx file: // ... import Elmahio from 'elmah.io.javascript'; new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); // After this the ReactDOM.render etc. will be included When launching your React app, elmah.io is configured and all errors happening in the application are logged. Check out the elmahio-react and elmahio-react-typescript samples for some real working code. React have a known bug/feature in DEV mode where errors are submitted twice. For better error handling in React, you should look into Error Boundaries .","title":"Logging to elmah.io from React"},{"location":"logging-to-elmah-io-from-serilog/","text":"Logging to elmah.io from Serilog Logging to elmah.io from Serilog Logging custom properties Message hooks Decorating log messages Include source code Handle errors Error filtering ASP.NET Core ASP.NET Config using appsettings.json Extended exception details with Serilog.Exceptions Remove sensitive data Setting a category Troubleshooting PeriodicBatchingSink is marked as sealed Serilog is a great addition to the flowering .NET logging community, described as \u201cA no-nonsense logging library for the NoSQL era\u201d on their project page. Serilog works just like other logging frameworks such as log4net and NLog but offers a great fluent API and the concept of sinks (a bit like appenders in log4net). Sinks are superior to appenders because they threat errors as objects rather than strings, a perfect fit for elmah.io which itself is built on NoSQL. Serilog already comes with native support for elmah.io, which makes it easy to integrate with any application using Serilog. Adding this type of logging to your windows and console applications is just as easy. Add the Serilog.Sinks.ElmahIo NuGet package to your project: Package Manager .NET CLI PackageReference Paket CLI Install-Package Serilog.Sinks.ElmahIo dotnet add package Serilog.Sinks.ElmahIo <PackageReference Include=\"Serilog.Sinks.ElmahIo\" Version=\"5.*\" /> paket add Serilog.Sinks.ElmahIo To configure Serilog, add the following code to the Application_Start method in global.asax.cs: var log = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\"))) .CreateLogger(); Log.Logger = log; Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). First, we create a new LoggerConfiguration and tell it to write to elmah.io. The log object can be used to log errors and you should register this in your IoC container. In this case, we don't use IoC, that is why the log object is set as the public static Logger property, which makes it accessible from everywhere. To log exceptions to elmah.io through Serilog use the Log class provided by Serilog: try { // Do some stuff that may cause an exception } catch (Exception e) { Log.Error(e, \"The actual error message\"); } The Error method tells Serilog to log the error in the configured sinks, which in our case logs to elmah.io. Simple and beautiful. Always call Log.CloseAndFlush(); before your program terminates. Logging custom properties Serilog supports logging custom properties in three ways: As part of the log message, through enrichers, and using LogContext . All three types of properties are implemented in the elmah.io sink as part of the Data dictionary to elmah.io. The following example shows how to log all three types of properties: var logger = new LoggerConfiguration() .Enrich.WithProperty(\"ApplicationIdentifier\", \"MyCoolApp\") .Enrich.FromLogContext() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\"))) .CreateLogger(); using (LogContext.PushProperty(\"ThreadId\", Thread.CurrentThread.ManagedThreadId)) { logger.Error(\"This is a message with {Type} properties\", \"custom\"); } Beneath the Data tab on the logged message details, the ApplicationIdentifier , ThreadId , and Type properties can be found. Serilog.Sinks.ElmahIo provides a range of reserved property names, that can be used to fill in data in the correct fields on the elmah.io UI. Let's say you want to fill the User field using structured logging only: logger.Information(\"{Quote} from {User}\", \"Hasta la vista, baby\", \"Arnold Schwarzenegger\"); This will fill in the value Arnold Schwarzenegger in the User field, as well as add two key/value pairs (Quote and User) to the Data tab on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage . Message hooks Serilog.Sinks.ElmahIo provides message hooks similar to the integrations with ASP.NET and ASP.NET Core. Message hooks require Serilog.Sinks.ElmahIo version 3.3.0 or newer. Decorating log messages To include additional information on log messages, you can use the OnMessage event when initializing the elmah.io target: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnMessage = msg => { msg.Version = \"1.0.0\"; } }) .CreateLogger(); The example above includes a version number on all errors. Since the elmah.io sink also picks up enrichers specified with Serilog, this example could be implemented by enriching all log messages with a field named version . Include source code You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnMessage = msg => { msg.WithSourceCodeFromPdb(); } }) .CreateLogger(); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward. Handle errors To handle any errors happening while processing a log message, you can use the OnError event when initializing the elmah.io sink: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnError = (msg, ex) => { Console.Error.WriteLine(ex.Message); } }) .CreateLogger(); The example implements a callback if logging to elmah.io fails. How you choose to implement this is entirely up to your application and tech stack. Error filtering To ignore specific messages based on their content, you can use the OnFilter event when initializing the elmah.io sink: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnFilter = msg => { return msg.Title.Contains(\"trace\"); } }) .CreateLogger(); The example above ignores any log message with the word trace in the title. ASP.NET Core Serilog provides a package for ASP.NET Core, that routes log messages from inside the framework through Serilog. We recommend using this package together with the elmah.io sink, in order to capture warnings and errors happening inside ASP.NET Core. To use this, install the following packages: Package Manager .NET CLI PackageReference Paket CLI Install-Package Serilog.AspNetCore Install-Package Serilog.Sinks.ElmahIo dotnet add package Serilog.AspNetCore dotnet add package Serilog.Sinks.ElmahIo <PackageReference Include=\"Serilog.AspNetCore\" Version=\"6.*\" /> <PackageReference Include=\"Serilog.Sinks.ElmahIo\" Version=\"5.*\" /> paket add Serilog.AspNetCore paket add Serilog.Sinks.ElmahIo Configure Serilog using the UseSerilog method in the Program.cs file: builder.Host.UseSerilog((ctx, lc) => lc .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { MinimumLogEventLevel = LogEventLevel.Information, })); } Now, all warnings, errors, and fatals happening inside ASP.NET Core are logged to elmah.io. A common request is to include all of the HTTP contextual information you usually get logged when using a package like Elmah.Io.AspNetCore . We have developed a specialized NuGet package to include cookies, server variables, etc. when logging through Serilog from ASP.NET Core. To set it up, install the Elmah.Io.AspNetCore.Serilog NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.Serilog dotnet add package Elmah.Io.AspNetCore.Serilog <PackageReference Include=\"Elmah.Io.AspNetCore.Serilog\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.Serilog Then, call the UseElmahIoSerilog method in Program.cs file: // ... Exception handling middleware app.UseElmahIoSerilog(); // ... UseMvc etc. The middleware uses Serilog's LogContext feature to enrich each log message with additional properties. To turn on the log context, extend your Serilog config: builder.Host.UseSerilog((ctx, lc) => lc .WriteTo.ElmahIo(/*...*/) .Enrich.FromLogContext() // <-- add this line ); There's a problem with this approach when an endpoint throws an uncaught exception. Microsoft.Extensions.Logging logs all uncaught exceptions as errors, but the LogContext is already popped when doing so. The recommended approach is to ignore these errors in the elmah.io sink and install the Elmah.Io.AspNetCore package to log uncaught errors to elmah.io (as explained in Logging from ASP.NET Core ). The specific error message can be ignored in the sink by providing the following filter during initialization of Serilog: .WriteTo.ElmahIo(/*...*/) { // ... OnFilter = msg => { return msg != null && msg.TitleTemplate != null && msg.TitleTemplate.Equals( \"An unhandled exception has occurred while executing the request.\", StringComparison.InvariantCultureIgnoreCase); } }) ASP.NET Messages logged through Serilog in an ASP.NET WebForms, MVC, or Web API application can be enriched with a range of HTTP contextual information using the SerilogWeb.Classic NuGet package. Start by installing the package: Package Manager .NET CLI PackageReference Paket CLI Install-Package SerilogWeb.Classic dotnet add package SerilogWeb.Classic <PackageReference Include=\"SerilogWeb.Classic\" Version=\"5.*\" /> paket add SerilogWeb.Classic The package includes automatic HTTP request and response logging as well as some Serilog enrichers. Unless you are trying to debug a specific problem with your website, we recommend disabling HTTP logging since that will produce a lot of messages (depending on the traffic on your website). HTTP logging can be disabled by including the following code in the Global.asax.cs file: protected void Application_Start() { SerilogWebClassic.Configure(cfg => cfg .Disable() ); // ... } To enrich log messages with HTTP contextual information you can configure one or more enrichers in the same place as you configure the elmah.io sink: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\"))) .Enrich.WithHttpRequestClientHostIP() .Enrich.WithHttpRequestRawUrl() .Enrich.WithHttpRequestType() .Enrich.WithHttpRequestUrl() .Enrich.WithHttpRequestUserAgent() .Enrich.WithUserName(anonymousUsername:null) .CreateLogger(); This will automatically fill in fields on elmah.io like URL, method, client IP, and UserAgent. Check out this full sample for more details. Config using appsettings.json While Serilog provides a great fluent C# API, some prefer to configure Serilog using an appsettings.json file. To configure the elmah.io sink this way, you will need to install the Serilog.Settings.Configuration NuGet package. Then configure elmah.io in your appsettings.json file: { // ... \"Serilog\":{ \"Using\":[ \"Serilog.Sinks.ElmahIo\" ], \"MinimumLevel\": \"Warning\", \"WriteTo\":[ { \"Name\": \"ElmahIo\", \"Args\":{ \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } } ] } } Make sure to specify the apiKey and logId arguments with the first character in lowercase. Finally, tell Serilog to read the configuration from the appsettings.json file: var configuration = new ConfigurationBuilder() .AddJsonFile(\"appsettings.json\") .Build(); var logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .CreateLogger(); Extended exception details with Serilog.Exceptions The more information you have on an error, the easier it is to find out what went wrong. Muhammad Rehan Saeed made a nice enrichment package for Serilog named Serilog.Exceptions . The package uses reflection on a logged exception to log additional information depending on the concrete exception type. You can install the package through NuGet: Package Manager .NET CLI PackageReference Paket CLI Install-Package Serilog.Exceptions dotnet add package Serilog.Exceptions <PackageReference Include=\"Serilog.Exceptions\" Version=\"5.*\" /> paket add Serilog.Exceptions And configure it in C# code: var logger = new LoggerConfiguration() .Enrich.WithExceptionDetails() .WriteTo.ElmahIo(/*...*/) .CreateLogger(); The elmah.io sink will automatically pick up the additional information and show them in the extended message details overlay. To navigate to this view, click an error on the search view. Then click the button in the upper right corner to open extended message details. The information logged by Serilog.Exceptions are available beneath the Data tab. Remove sensitive data Structured logging with Serilog is a great way to store a lot of contextual information about a log message. In some cases, it may result in sensitive data being stored in your log, though. We recommend you remove any sensitive data from your log messages before storing them on elmah.io and anywhere else. To implement this, you can use the OnMessage event as already shown previously in the document: OnMessage = msg => { foreach (var d in msg.Data) { if (d.Key.Equals(\"Password\")) { d.Value = \"****\"; } } } An alternative to replacing sensitive values manually is to use a custom destructuring package for Serilog. The following example shows how to achieve this using the Destructurama.Attributed package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Destructurama.Attributed dotnet add package Destructurama.Attributed <PackageReference Include=\"Destructurama.Attributed\" Version=\"2.*\" /> paket add Destructurama.Attributed Set up destructuring from attributes: Log.Logger = new LoggerConfiguration() .Destructure.UsingAttributes() .WriteTo.ElmahIo(/*...*/) .CreateLogger(); Make sure to decorate any properties including sensitive data with the NotLogged attribute: public class LoginModel { public string Username { get; set; } [NotLogged] public string Password { get; set; } } Setting a category elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to Serilog's SourceContext automatically when using Serilog.Sinks.ElmahIo . To make sure that the category is correctly populated, either use the Forcontext method: Log.ForContext<Program>().Information(\"This is an information message with context\"); Log.ForContext(\"The context\").Information(\"This is another information message with context\"); Or set a SourceContext or Category property using LogContext : using (LogContext.PushProperty(\"SourceContext\", \"The context\")) Log.Information(\"This is an information message with context\"); using (LogContext.PushProperty(\"Category\", \"The context\")) Log.Information(\"This is another information message with context\"); When using Serilog through Microsoft.Extensions.Logging's ILogger<T> interface, the source context will automatically be set by Serilog. Troubleshooting Here are some things to try out if logging from Serilog to elmah.io doesn't work: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure that you have the newest Serilog.Sinks.ElmahIo and Elmah.Io.Client packages installed. Make sure to include all of the configuration from the example above. Make sure that the API key is valid and allow the Messages | Write permission . Make sure to include a valid log ID. Make sure that you have sufficient log messages in your subscription and that you didn't disable logging to the log or include any ignore filters/rules. Always make sure to call Log.CloseAndFlush() before exiting the application to make sure that all log messages are flushed. Set up Serilog's SelfLog to inspect any errors happening inside Serilog or the elmah.io sink: Serilog.Debugging.SelfLog.Enable(msg => Debug.WriteLine(msg)); . Implement the OnError action and put a breakpoint in the handler to inspect if any errors are thrown while logging to the elmah.io API. PeriodicBatchingSink is marked as sealed If you get a runtime error stating that the PeriodicBatchingSink class is sealed and cannot be extended, make sure to manually install version 3.1.0 or newer of the Serilog.Sinks.PeriodicBatching NuGet package. The bug has also been fixed in the Serilog.Sinks.ElmahIo NuGet package from version 4.3.29 and forward.","title":"Logging from Serilog"},{"location":"logging-to-elmah-io-from-serilog/#logging-to-elmahio-from-serilog","text":"Logging to elmah.io from Serilog Logging custom properties Message hooks Decorating log messages Include source code Handle errors Error filtering ASP.NET Core ASP.NET Config using appsettings.json Extended exception details with Serilog.Exceptions Remove sensitive data Setting a category Troubleshooting PeriodicBatchingSink is marked as sealed Serilog is a great addition to the flowering .NET logging community, described as \u201cA no-nonsense logging library for the NoSQL era\u201d on their project page. Serilog works just like other logging frameworks such as log4net and NLog but offers a great fluent API and the concept of sinks (a bit like appenders in log4net). Sinks are superior to appenders because they threat errors as objects rather than strings, a perfect fit for elmah.io which itself is built on NoSQL. Serilog already comes with native support for elmah.io, which makes it easy to integrate with any application using Serilog. Adding this type of logging to your windows and console applications is just as easy. Add the Serilog.Sinks.ElmahIo NuGet package to your project: Package Manager .NET CLI PackageReference Paket CLI Install-Package Serilog.Sinks.ElmahIo dotnet add package Serilog.Sinks.ElmahIo <PackageReference Include=\"Serilog.Sinks.ElmahIo\" Version=\"5.*\" /> paket add Serilog.Sinks.ElmahIo To configure Serilog, add the following code to the Application_Start method in global.asax.cs: var log = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\"))) .CreateLogger(); Log.Logger = log; Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the ID of the log you want messages sent to ( Where is my log ID? ). First, we create a new LoggerConfiguration and tell it to write to elmah.io. The log object can be used to log errors and you should register this in your IoC container. In this case, we don't use IoC, that is why the log object is set as the public static Logger property, which makes it accessible from everywhere. To log exceptions to elmah.io through Serilog use the Log class provided by Serilog: try { // Do some stuff that may cause an exception } catch (Exception e) { Log.Error(e, \"The actual error message\"); } The Error method tells Serilog to log the error in the configured sinks, which in our case logs to elmah.io. Simple and beautiful. Always call Log.CloseAndFlush(); before your program terminates.","title":"Logging to elmah.io from Serilog"},{"location":"logging-to-elmah-io-from-serilog/#logging-custom-properties","text":"Serilog supports logging custom properties in three ways: As part of the log message, through enrichers, and using LogContext . All three types of properties are implemented in the elmah.io sink as part of the Data dictionary to elmah.io. The following example shows how to log all three types of properties: var logger = new LoggerConfiguration() .Enrich.WithProperty(\"ApplicationIdentifier\", \"MyCoolApp\") .Enrich.FromLogContext() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\"))) .CreateLogger(); using (LogContext.PushProperty(\"ThreadId\", Thread.CurrentThread.ManagedThreadId)) { logger.Error(\"This is a message with {Type} properties\", \"custom\"); } Beneath the Data tab on the logged message details, the ApplicationIdentifier , ThreadId , and Type properties can be found. Serilog.Sinks.ElmahIo provides a range of reserved property names, that can be used to fill in data in the correct fields on the elmah.io UI. Let's say you want to fill the User field using structured logging only: logger.Information(\"{Quote} from {User}\", \"Hasta la vista, baby\", \"Arnold Schwarzenegger\"); This will fill in the value Arnold Schwarzenegger in the User field, as well as add two key/value pairs (Quote and User) to the Data tab on elmah.io. For a reference of all possible property names, check out the property names on CreateMessage .","title":"Logging custom properties"},{"location":"logging-to-elmah-io-from-serilog/#message-hooks","text":"Serilog.Sinks.ElmahIo provides message hooks similar to the integrations with ASP.NET and ASP.NET Core. Message hooks require Serilog.Sinks.ElmahIo version 3.3.0 or newer.","title":"Message hooks"},{"location":"logging-to-elmah-io-from-serilog/#decorating-log-messages","text":"To include additional information on log messages, you can use the OnMessage event when initializing the elmah.io target: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnMessage = msg => { msg.Version = \"1.0.0\"; } }) .CreateLogger(); The example above includes a version number on all errors. Since the elmah.io sink also picks up enrichers specified with Serilog, this example could be implemented by enriching all log messages with a field named version .","title":"Decorating log messages"},{"location":"logging-to-elmah-io-from-serilog/#include-source-code","text":"You can use the OnMessage action to include source code to log messages. This will require a stack trace in the Detail property with filenames and line numbers in it. There are multiple ways of including source code to log messages. In short, you will need to install the Elmah.Io.Client.Extensions.SourceCode NuGet package and call the WithSourceCodeFromPdb method in the OnMessage action: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnMessage = msg => { msg.WithSourceCodeFromPdb(); } }) .CreateLogger(); Check out How to include source code in log messages for additional requirements to make source code show up on elmah.io. Including source code on log messages is available in the Elmah.Io.Client v4 package and forward.","title":"Include source code"},{"location":"logging-to-elmah-io-from-serilog/#handle-errors","text":"To handle any errors happening while processing a log message, you can use the OnError event when initializing the elmah.io sink: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnError = (msg, ex) => { Console.Error.WriteLine(ex.Message); } }) .CreateLogger(); The example implements a callback if logging to elmah.io fails. How you choose to implement this is entirely up to your application and tech stack.","title":"Handle errors"},{"location":"logging-to-elmah-io-from-serilog/#error-filtering","text":"To ignore specific messages based on their content, you can use the OnFilter event when initializing the elmah.io sink: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { OnFilter = msg => { return msg.Title.Contains(\"trace\"); } }) .CreateLogger(); The example above ignores any log message with the word trace in the title.","title":"Error filtering"},{"location":"logging-to-elmah-io-from-serilog/#aspnet-core","text":"Serilog provides a package for ASP.NET Core, that routes log messages from inside the framework through Serilog. We recommend using this package together with the elmah.io sink, in order to capture warnings and errors happening inside ASP.NET Core. To use this, install the following packages: Package Manager .NET CLI PackageReference Paket CLI Install-Package Serilog.AspNetCore Install-Package Serilog.Sinks.ElmahIo dotnet add package Serilog.AspNetCore dotnet add package Serilog.Sinks.ElmahIo <PackageReference Include=\"Serilog.AspNetCore\" Version=\"6.*\" /> <PackageReference Include=\"Serilog.Sinks.ElmahIo\" Version=\"5.*\" /> paket add Serilog.AspNetCore paket add Serilog.Sinks.ElmahIo Configure Serilog using the UseSerilog method in the Program.cs file: builder.Host.UseSerilog((ctx, lc) => lc .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\")) { MinimumLogEventLevel = LogEventLevel.Information, })); } Now, all warnings, errors, and fatals happening inside ASP.NET Core are logged to elmah.io. A common request is to include all of the HTTP contextual information you usually get logged when using a package like Elmah.Io.AspNetCore . We have developed a specialized NuGet package to include cookies, server variables, etc. when logging through Serilog from ASP.NET Core. To set it up, install the Elmah.Io.AspNetCore.Serilog NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.Serilog dotnet add package Elmah.Io.AspNetCore.Serilog <PackageReference Include=\"Elmah.Io.AspNetCore.Serilog\" Version=\"5.*\" /> paket add Elmah.Io.AspNetCore.Serilog Then, call the UseElmahIoSerilog method in Program.cs file: // ... Exception handling middleware app.UseElmahIoSerilog(); // ... UseMvc etc. The middleware uses Serilog's LogContext feature to enrich each log message with additional properties. To turn on the log context, extend your Serilog config: builder.Host.UseSerilog((ctx, lc) => lc .WriteTo.ElmahIo(/*...*/) .Enrich.FromLogContext() // <-- add this line ); There's a problem with this approach when an endpoint throws an uncaught exception. Microsoft.Extensions.Logging logs all uncaught exceptions as errors, but the LogContext is already popped when doing so. The recommended approach is to ignore these errors in the elmah.io sink and install the Elmah.Io.AspNetCore package to log uncaught errors to elmah.io (as explained in Logging from ASP.NET Core ). The specific error message can be ignored in the sink by providing the following filter during initialization of Serilog: .WriteTo.ElmahIo(/*...*/) { // ... OnFilter = msg => { return msg != null && msg.TitleTemplate != null && msg.TitleTemplate.Equals( \"An unhandled exception has occurred while executing the request.\", StringComparison.InvariantCultureIgnoreCase); } })","title":"ASP.NET Core"},{"location":"logging-to-elmah-io-from-serilog/#aspnet","text":"Messages logged through Serilog in an ASP.NET WebForms, MVC, or Web API application can be enriched with a range of HTTP contextual information using the SerilogWeb.Classic NuGet package. Start by installing the package: Package Manager .NET CLI PackageReference Paket CLI Install-Package SerilogWeb.Classic dotnet add package SerilogWeb.Classic <PackageReference Include=\"SerilogWeb.Classic\" Version=\"5.*\" /> paket add SerilogWeb.Classic The package includes automatic HTTP request and response logging as well as some Serilog enrichers. Unless you are trying to debug a specific problem with your website, we recommend disabling HTTP logging since that will produce a lot of messages (depending on the traffic on your website). HTTP logging can be disabled by including the following code in the Global.asax.cs file: protected void Application_Start() { SerilogWebClassic.Configure(cfg => cfg .Disable() ); // ... } To enrich log messages with HTTP contextual information you can configure one or more enrichers in the same place as you configure the elmah.io sink: Log.Logger = new LoggerConfiguration() .WriteTo.ElmahIo(new ElmahIoSinkOptions(\"API_KEY\", new Guid(\"LOG_ID\"))) .Enrich.WithHttpRequestClientHostIP() .Enrich.WithHttpRequestRawUrl() .Enrich.WithHttpRequestType() .Enrich.WithHttpRequestUrl() .Enrich.WithHttpRequestUserAgent() .Enrich.WithUserName(anonymousUsername:null) .CreateLogger(); This will automatically fill in fields on elmah.io like URL, method, client IP, and UserAgent. Check out this full sample for more details.","title":"ASP.NET"},{"location":"logging-to-elmah-io-from-serilog/#config-using-appsettingsjson","text":"While Serilog provides a great fluent C# API, some prefer to configure Serilog using an appsettings.json file. To configure the elmah.io sink this way, you will need to install the Serilog.Settings.Configuration NuGet package. Then configure elmah.io in your appsettings.json file: { // ... \"Serilog\":{ \"Using\":[ \"Serilog.Sinks.ElmahIo\" ], \"MinimumLevel\": \"Warning\", \"WriteTo\":[ { \"Name\": \"ElmahIo\", \"Args\":{ \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } } ] } } Make sure to specify the apiKey and logId arguments with the first character in lowercase. Finally, tell Serilog to read the configuration from the appsettings.json file: var configuration = new ConfigurationBuilder() .AddJsonFile(\"appsettings.json\") .Build(); var logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .CreateLogger();","title":"Config using appsettings.json"},{"location":"logging-to-elmah-io-from-serilog/#extended-exception-details-with-serilogexceptions","text":"The more information you have on an error, the easier it is to find out what went wrong. Muhammad Rehan Saeed made a nice enrichment package for Serilog named Serilog.Exceptions . The package uses reflection on a logged exception to log additional information depending on the concrete exception type. You can install the package through NuGet: Package Manager .NET CLI PackageReference Paket CLI Install-Package Serilog.Exceptions dotnet add package Serilog.Exceptions <PackageReference Include=\"Serilog.Exceptions\" Version=\"5.*\" /> paket add Serilog.Exceptions And configure it in C# code: var logger = new LoggerConfiguration() .Enrich.WithExceptionDetails() .WriteTo.ElmahIo(/*...*/) .CreateLogger(); The elmah.io sink will automatically pick up the additional information and show them in the extended message details overlay. To navigate to this view, click an error on the search view. Then click the button in the upper right corner to open extended message details. The information logged by Serilog.Exceptions are available beneath the Data tab.","title":"Extended exception details with Serilog.Exceptions"},{"location":"logging-to-elmah-io-from-serilog/#remove-sensitive-data","text":"Structured logging with Serilog is a great way to store a lot of contextual information about a log message. In some cases, it may result in sensitive data being stored in your log, though. We recommend you remove any sensitive data from your log messages before storing them on elmah.io and anywhere else. To implement this, you can use the OnMessage event as already shown previously in the document: OnMessage = msg => { foreach (var d in msg.Data) { if (d.Key.Equals(\"Password\")) { d.Value = \"****\"; } } } An alternative to replacing sensitive values manually is to use a custom destructuring package for Serilog. The following example shows how to achieve this using the Destructurama.Attributed package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Destructurama.Attributed dotnet add package Destructurama.Attributed <PackageReference Include=\"Destructurama.Attributed\" Version=\"2.*\" /> paket add Destructurama.Attributed Set up destructuring from attributes: Log.Logger = new LoggerConfiguration() .Destructure.UsingAttributes() .WriteTo.ElmahIo(/*...*/) .CreateLogger(); Make sure to decorate any properties including sensitive data with the NotLogged attribute: public class LoginModel { public string Username { get; set; } [NotLogged] public string Password { get; set; } }","title":"Remove sensitive data"},{"location":"logging-to-elmah-io-from-serilog/#setting-a-category","text":"elmah.io provide a field named Category to better group log messages by class name, namespace, or similar. Category maps to Serilog's SourceContext automatically when using Serilog.Sinks.ElmahIo . To make sure that the category is correctly populated, either use the Forcontext method: Log.ForContext<Program>().Information(\"This is an information message with context\"); Log.ForContext(\"The context\").Information(\"This is another information message with context\"); Or set a SourceContext or Category property using LogContext : using (LogContext.PushProperty(\"SourceContext\", \"The context\")) Log.Information(\"This is an information message with context\"); using (LogContext.PushProperty(\"Category\", \"The context\")) Log.Information(\"This is another information message with context\"); When using Serilog through Microsoft.Extensions.Logging's ILogger<T> interface, the source context will automatically be set by Serilog.","title":"Setting a category"},{"location":"logging-to-elmah-io-from-serilog/#troubleshooting","text":"Here are some things to try out if logging from Serilog to elmah.io doesn't work: Run the diagnose command with the elmah.io CLI as shown here: Diagnose potential problems with an elmah.io installation . Make sure that you have the newest Serilog.Sinks.ElmahIo and Elmah.Io.Client packages installed. Make sure to include all of the configuration from the example above. Make sure that the API key is valid and allow the Messages | Write permission . Make sure to include a valid log ID. Make sure that you have sufficient log messages in your subscription and that you didn't disable logging to the log or include any ignore filters/rules. Always make sure to call Log.CloseAndFlush() before exiting the application to make sure that all log messages are flushed. Set up Serilog's SelfLog to inspect any errors happening inside Serilog or the elmah.io sink: Serilog.Debugging.SelfLog.Enable(msg => Debug.WriteLine(msg)); . Implement the OnError action and put a breakpoint in the handler to inspect if any errors are thrown while logging to the elmah.io API.","title":"Troubleshooting"},{"location":"logging-to-elmah-io-from-serilog/#periodicbatchingsink-is-marked-as-sealed","text":"If you get a runtime error stating that the PeriodicBatchingSink class is sealed and cannot be extended, make sure to manually install version 3.1.0 or newer of the Serilog.Sinks.PeriodicBatching NuGet package. The bug has also been fixed in the Serilog.Sinks.ElmahIo NuGet package from version 4.3.29 and forward.","title":"PeriodicBatchingSink is marked as sealed"},{"location":"logging-to-elmah-io-from-servicestack/","text":"Logging to elmah.io from ServiceStack Logging errors to elmah.io from ServiceStack is almost as easy as installing in MVC and Web API. The folks over at ServiceStack provide you with a NuGet package named ServiceStack.Logging.Elmah. Like Web API you need to tell ServiceStack to use ELMAH as the logging framework for errors, besides adding the standard ELMAH configuration in web.config. Start by installing both ServiceStack.Logging.Elmah and Elmah.Io into your ServiceStack web project: Package Manager .NET CLI PackageReference Paket CLI Install-Package ServiceStack.Logging.Elmah Install-Package Elmah.Io dotnet add package ServiceStack.Logging.Elmah dotnet add package Elmah.Io <PackageReference Include=\"ServiceStack.Logging.Elmah\" Version=\"5.*\" /> <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add ServiceStack.Logging.Elmah paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Once installed, add the following line to your AppHost: LogManager.LogFactory = new ElmahLogFactory(new NLogFactory()); The above example assumes that you are already using NLog as the existing framework for logging. Wrapping different logger factories in each other and makes it possible to log errors through ELMAH and other types of messages like warnings and info messages through another logging framework. If you don\u2019t need anything other than ELMAH logging, use the NullLogFactory instead of NLogFactory. That\u2019s it! By installing both the ServiceStack.Logging.Elmah and elmah.io packages, you should have sufficient configuration in your web.config to start logging errors.","title":"Logging from ServiceStack"},{"location":"logging-to-elmah-io-from-servicestack/#logging-to-elmahio-from-servicestack","text":"Logging errors to elmah.io from ServiceStack is almost as easy as installing in MVC and Web API. The folks over at ServiceStack provide you with a NuGet package named ServiceStack.Logging.Elmah. Like Web API you need to tell ServiceStack to use ELMAH as the logging framework for errors, besides adding the standard ELMAH configuration in web.config. Start by installing both ServiceStack.Logging.Elmah and Elmah.Io into your ServiceStack web project: Package Manager .NET CLI PackageReference Paket CLI Install-Package ServiceStack.Logging.Elmah Install-Package Elmah.Io dotnet add package ServiceStack.Logging.Elmah dotnet add package Elmah.Io <PackageReference Include=\"ServiceStack.Logging.Elmah\" Version=\"5.*\" /> <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add ServiceStack.Logging.Elmah paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Once installed, add the following line to your AppHost: LogManager.LogFactory = new ElmahLogFactory(new NLogFactory()); The above example assumes that you are already using NLog as the existing framework for logging. Wrapping different logger factories in each other and makes it possible to log errors through ELMAH and other types of messages like warnings and info messages through another logging framework. If you don\u2019t need anything other than ELMAH logging, use the NullLogFactory instead of NLogFactory. That\u2019s it! By installing both the ServiceStack.Logging.Elmah and elmah.io packages, you should have sufficient configuration in your web.config to start logging errors.","title":"Logging to elmah.io from ServiceStack"},{"location":"logging-to-elmah-io-from-signalr/","text":"Logging to elmah.io from SignalR Logging from SignalR is supported through our Elmah.Io.Extensions.Logging package. For details not included in this article, check out Logging from Microsoft.Extensions.Logging . Start by installing the Elmah.Io.Extensions.Logging package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging In the Program.cs file, add a new using statement: using Elmah.Io.Extensions.Logging; Then configure elmah.io like shown here: builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); builder.Logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. The code only logs Warning , Error , and Fatal messages. To change that you can change the LogLevel filter specified in the line calling the AddFilter method. You may also need to change log levels for SignalR itself: logging.AddFilter(\"Microsoft.AspNetCore.SignalR\", LogLevel.Debug); logging.AddFilter(\"Microsoft.AspNetCore.Http.Connections\", LogLevel.Debug); Be aware that changing log levels to Debug or lower will cause a lot of messages to be stored in elmah.io. Log levels can be specified through the appsettings.json file as with any other ASP.NET Core application. Check out appsettings.json configuration for more details.","title":"Logging from SignalR"},{"location":"logging-to-elmah-io-from-signalr/#logging-to-elmahio-from-signalr","text":"Logging from SignalR is supported through our Elmah.Io.Extensions.Logging package. For details not included in this article, check out Logging from Microsoft.Extensions.Logging . Start by installing the Elmah.Io.Extensions.Logging package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Extensions.Logging dotnet add package Elmah.Io.Extensions.Logging <PackageReference Include=\"Elmah.Io.Extensions.Logging\" Version=\"5.*\" /> paket add Elmah.Io.Extensions.Logging In the Program.cs file, add a new using statement: using Elmah.Io.Extensions.Logging; Then configure elmah.io like shown here: builder.Logging.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); builder.Logging.AddFilter<ElmahIoLoggerProvider>(null, LogLevel.Warning); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. The code only logs Warning , Error , and Fatal messages. To change that you can change the LogLevel filter specified in the line calling the AddFilter method. You may also need to change log levels for SignalR itself: logging.AddFilter(\"Microsoft.AspNetCore.SignalR\", LogLevel.Debug); logging.AddFilter(\"Microsoft.AspNetCore.Http.Connections\", LogLevel.Debug); Be aware that changing log levels to Debug or lower will cause a lot of messages to be stored in elmah.io. Log levels can be specified through the appsettings.json file as with any other ASP.NET Core application. Check out appsettings.json configuration for more details.","title":"Logging to elmah.io from SignalR"},{"location":"logging-to-elmah-io-from-sitefinity/","text":"Logging to elmah.io from Sitefinity Sitefinity is a CMS from Telerik, implemented on top of ASP.NET. Like other content management systems build on top of ASP.NET, ELMAH is supported out of the box. To install elmah.io in a Sitefinity website, start by opening the website in Visual Studio by selecting File | Open Web Site... and navigate to the Sitefinity projects folder (something similar to this: C:\\Program Files (x86)\\Telerik\\Sitefinity\\Projects\\Default ). Right-click the website and install the Elmah.Io NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During installation, you will be prompted for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). That's it! Uncaught errors in Sitefinity are logged to your elmah.io log. To test that the integration works, right-click the website and add a new Web Form named ELMAH.aspx. In the code-behind file add the following code: protected void Page_Load(object sender, EventArgs e) { throw new ApplicationException(); } Start the website and navigate to the ELMAH.aspx page. If everything works as intended, you will see the yellow screen of death, and a new error will pop up on elmah.io.","title":"Logging from Sitefinity"},{"location":"logging-to-elmah-io-from-sitefinity/#logging-to-elmahio-from-sitefinity","text":"Sitefinity is a CMS from Telerik, implemented on top of ASP.NET. Like other content management systems build on top of ASP.NET, ELMAH is supported out of the box. To install elmah.io in a Sitefinity website, start by opening the website in Visual Studio by selecting File | Open Web Site... and navigate to the Sitefinity projects folder (something similar to this: C:\\Program Files (x86)\\Telerik\\Sitefinity\\Projects\\Default ). Right-click the website and install the Elmah.Io NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During installation, you will be prompted for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). That's it! Uncaught errors in Sitefinity are logged to your elmah.io log. To test that the integration works, right-click the website and add a new Web Form named ELMAH.aspx. In the code-behind file add the following code: protected void Page_Load(object sender, EventArgs e) { throw new ApplicationException(); } Start the website and navigate to the ELMAH.aspx page. If everything works as intended, you will see the yellow screen of death, and a new error will pop up on elmah.io.","title":"Logging to elmah.io from Sitefinity"},{"location":"logging-to-elmah-io-from-sveltekit/","text":"Logging to elmah.io from SvelteKit To log all errors from a SvelteKit application, install the elmah.io.javascript npm package as described in Logging from JavaScript . Then add the following code to the hooks.client.js file. If the file does not exist, make sure to create it in the src folder since SvelteKit will automatically load it from there. import Elmahio from 'elmah.io.javascript'; var logger = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); /** @type {import('@sveltejs/kit').HandleClientError} */ export function handleError({ error, event }) { if (error && error.message) { logger.error(error.message, error); } else { logger.error('Error in application', error); } } When launching your SvelteKit app, elmah.io is configured and all errors happening in the application are logged. For now, elmah.io.javascript only supports SvelteKit apps running inside the browser, why implementing the HandleServerError is not supported. Check out the elmahio-sveltekit sample for some real working code.","title":"Logging from SvelteKit"},{"location":"logging-to-elmah-io-from-sveltekit/#logging-to-elmahio-from-sveltekit","text":"To log all errors from a SvelteKit application, install the elmah.io.javascript npm package as described in Logging from JavaScript . Then add the following code to the hooks.client.js file. If the file does not exist, make sure to create it in the src folder since SvelteKit will automatically load it from there. import Elmahio from 'elmah.io.javascript'; var logger = new Elmahio({ apiKey: 'API_KEY', logId: 'LOG_ID' }); /** @type {import('@sveltejs/kit').HandleClientError} */ export function handleError({ error, event }) { if (error && error.message) { logger.error(error.message, error); } else { logger.error('Error in application', error); } } When launching your SvelteKit app, elmah.io is configured and all errors happening in the application are logged. For now, elmah.io.javascript only supports SvelteKit apps running inside the browser, why implementing the HandleServerError is not supported. Check out the elmahio-sveltekit sample for some real working code.","title":"Logging to elmah.io from SvelteKit"},{"location":"logging-to-elmah-io-from-system-diagnostics/","text":"Logging to elmah.io from System.Diagnostics Logging through System.Diagnostics have been deprecated. Please use the Elmah.Io.Client package to log trace messages to elmah.io. .NET comes with its own tracing/logging feature located in the System.Diagnostics namespaces. A core part of System.Diagnostics is the Trace class, but that namespace contains utilities for performance counters, working with the event log, and a lot of other features. In this article, we will focus on logging to elmah.io from System.Diagnostics.Trace . To start logging, install the Elmah.Io.Trace package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Trace dotnet add package Elmah.Io.Trace <PackageReference Include=\"Elmah.Io.Trace\" Version=\"3.*\" /> paket add Elmah.Io.Trace As default, Trace logs to the Win32 OutputDebugString function, but it is possible to log to multiple targets (like appenders in log4net). To do so, tell Trace about elmah.io: System.Diagnostics.Trace.Listeners.Add( new ElmahIoTraceListener(\"API_KEY\", new Guid(\"LOG_ID\"))); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with your log id ( Where is my log ID? ). To start logging, call the Trace API: try { System.Diagnostics.Trace.Write(\"Starting something dangerous\"); // ... } catch (Exception e) { System.Diagnostics.Trace.Fail(e.Message, e.ToString()); } In the example, we write an information message with the message Starting something dangerous and log any thrown exception to elmah.io.","title":"Logging from System.Diagnostics"},{"location":"logging-to-elmah-io-from-system-diagnostics/#logging-to-elmahio-from-systemdiagnostics","text":"Logging through System.Diagnostics have been deprecated. Please use the Elmah.Io.Client package to log trace messages to elmah.io. .NET comes with its own tracing/logging feature located in the System.Diagnostics namespaces. A core part of System.Diagnostics is the Trace class, but that namespace contains utilities for performance counters, working with the event log, and a lot of other features. In this article, we will focus on logging to elmah.io from System.Diagnostics.Trace . To start logging, install the Elmah.Io.Trace package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Trace dotnet add package Elmah.Io.Trace <PackageReference Include=\"Elmah.Io.Trace\" Version=\"3.*\" /> paket add Elmah.Io.Trace As default, Trace logs to the Win32 OutputDebugString function, but it is possible to log to multiple targets (like appenders in log4net). To do so, tell Trace about elmah.io: System.Diagnostics.Trace.Listeners.Add( new ElmahIoTraceListener(\"API_KEY\", new Guid(\"LOG_ID\"))); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with your log id ( Where is my log ID? ). To start logging, call the Trace API: try { System.Diagnostics.Trace.Write(\"Starting something dangerous\"); // ... } catch (Exception e) { System.Diagnostics.Trace.Fail(e.Message, e.ToString()); } In the example, we write an information message with the message Starting something dangerous and log any thrown exception to elmah.io.","title":"Logging to elmah.io from System.Diagnostics"},{"location":"logging-to-elmah-io-from-umbraco/","text":"Logging to elmah.io from Umbraco Logging to elmah.io from Umbraco Umbraco >= 9 Umbraco 8 Configuration Different environments Umbraco 7 Umbraco Cloud Umbraco Uno elmah.io offer great support for all newer Umbraco versions. Umbraco has been in rapid development in the last few years, so the installation instructions are very different depending on which major version you are using. Make sure to select the right version below since newer versions of the Elmah.Io.Umbraco package don't work with older versions of Umbraco and vice versa. To learn more about the elmah.io integration with Umbraco and an overall introduction to the included features, make sure to check out the elmah.io and Umbraco page. During the installation steps described below, you will need your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). elmah.io integrates with Umbraco's Health Checks feature too. To learn more about how to set it up, visit Logging heartbeats from Umbraco . Umbraco >= 9 Umbraco 9 is targeting .NET 5.0 which is no longer supported by Microsoft. This is why we have chosen to support Umbraco 10 and up only. To install elmah.io in your Umbraco >= v10 site, install the Elmah.Io.Umbraco NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco dotnet add package Elmah.Io.Umbraco <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"5.*\" /> paket add Elmah.Io.Umbraco After installing the NuGet package add the following to the Startup.cs file: public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); // ... } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseElmahIo(); // ... } } Make sure to call the UseElmahIo -method after installation of other pieces of middleware handling exceptions and auth (like UseDeveloperExceptionPage , UseExceptionHandler , UseAuthentication , and UseAuthorization ), but before the call to UseUmbraco . This will log all uncaught errors to elmah.io. If you want to hook into Umbraco's logging through Serilog, extend the configuration in the appsettings.json file with the following JSON: { \"Serilog\": { ... \"WriteTo\": [ { \"Name\": \"ElmahIo\", \"Args\": { \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } } ] }, ... } This will configure elmah.io's Serilog sink in Umbraco. You may experience logging not coming through when running locally. In this case, it might help to remove the WriteTo action from the appsettings.Development.json file. Umbraco 8 To install elmah.io in your Umbraco v8 site, install the Elmah.Io.Umbraco v4 package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco -Version 4.2.21 dotnet add package Elmah.Io.Umbraco --version 4.2.21 <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"4.2.21\" /> paket add Elmah.Io.Umbraco --version 4.2.21 During the installation, you will be presented with a dialog asking for your API key and log ID. Hit F5 and watch messages start flowing into elmah.io. Unless serious security issues in the Elmah.Io.Umbraco v4 package are found, new features will be added to the v5 package only (supporting Umbraco 10 and newer). Configuration If you are running on the default Umbraco template, all necessary configuration is added during the installation of the Elmah.Io.Umbraco NuGet package. If your web.config file for some reason isn't updated during installation, you can configure elmah.io manually: Configure elmah.io manually . Likewise, the installer configures the elmah.io sink for Serilog in your config\\serilog.user.config file. Different environments You may have different environments like Staging and Production . At least you have two: Localhost and Production . If you want to log to different error logs depending on the current environment, check out Use multiple logs for different environments . Web.config transformations work on the Web.config file only but you may have other config files that need transformation as well. In terms of elmah.io, the serilog.user.config file also includes elmah.io configuration that you may want to disable on localhost and include on production. If you are running on Umbraco Cloud this is natively supported as explained here: Config Transforms . Even in self-hosted environments, you can achieve something similar using the SlowCheetah extension. Check out this question on Our for details: Deploying different umbracoSettings.config for different environments . Umbraco 7 We still support Umbraco 7 through the Elmah.Io.Umbraco package version 3.2.35 : Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco -Version 3.2.35 dotnet add package Elmah.Io.Umbraco --version 3.2.35 <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"3.2.35\" /> paket add Elmah.Io.Umbraco --version 3.2.35 New features will be added to the updated package for Umbraco 10 and newer only. Umbraco Cloud When using Umbraco Cloud, you may not have a local clone of the source code. To install elmah.io on Umbraco Cloud, follow these steps: Clone your Umbraco Cloud project to a local folder as explained here: Working with a Local Clone . Where you need to install and configure the Elmah.Io.Umbraco package depends on the Umbraco major version you are running on Umbraco Cloud. For Umbraco 7-8, all changes should be made in the *.Web project only and all commits from within that folder as well. Don't commit and push anything in the root folder. For Umbraco versions above 8, all changes should be made in the src\\UmbracoProject folder. Follow the installation steps for your Umbraco version as specified in the beginning of this document. Commit and push all changes to the git repository. This will add elmah.io logging to your remote Umbraco Cloud project. In case you want logging to different elmah.io logs from each Umbraco Cloud environment, please check out Umbraco's support for config transformations here: Config transforms . Umbraco Uno Umbraco Uno has been discontinued. Installing elmah.io in Umbraco Uno follows the process of installing it onto Umbraco Cloud. To modify code and configuration in Uno you will need a Umbraco Uno Standard plan or higher. Also, you need to enable Custom Code to clone the code locally. This can be done from Uno by clicking the Enable custom code button: After enabling Custom Code you can create a Development environment and follow the steps in the Umbraco Cloud documentation.","title":"Logging from Umbraco"},{"location":"logging-to-elmah-io-from-umbraco/#logging-to-elmahio-from-umbraco","text":"Logging to elmah.io from Umbraco Umbraco >= 9 Umbraco 8 Configuration Different environments Umbraco 7 Umbraco Cloud Umbraco Uno elmah.io offer great support for all newer Umbraco versions. Umbraco has been in rapid development in the last few years, so the installation instructions are very different depending on which major version you are using. Make sure to select the right version below since newer versions of the Elmah.Io.Umbraco package don't work with older versions of Umbraco and vice versa. To learn more about the elmah.io integration with Umbraco and an overall introduction to the included features, make sure to check out the elmah.io and Umbraco page. During the installation steps described below, you will need your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). elmah.io integrates with Umbraco's Health Checks feature too. To learn more about how to set it up, visit Logging heartbeats from Umbraco .","title":"Logging to elmah.io from Umbraco"},{"location":"logging-to-elmah-io-from-umbraco/#umbraco-9","text":"Umbraco 9 is targeting .NET 5.0 which is no longer supported by Microsoft. This is why we have chosen to support Umbraco 10 and up only. To install elmah.io in your Umbraco >= v10 site, install the Elmah.Io.Umbraco NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco dotnet add package Elmah.Io.Umbraco <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"5.*\" /> paket add Elmah.Io.Umbraco After installing the NuGet package add the following to the Startup.cs file: public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddElmahIo(options => { options.ApiKey = \"API_KEY\"; options.LogId = new Guid(\"LOG_ID\"); }); // ... } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseElmahIo(); // ... } } Make sure to call the UseElmahIo -method after installation of other pieces of middleware handling exceptions and auth (like UseDeveloperExceptionPage , UseExceptionHandler , UseAuthentication , and UseAuthorization ), but before the call to UseUmbraco . This will log all uncaught errors to elmah.io. If you want to hook into Umbraco's logging through Serilog, extend the configuration in the appsettings.json file with the following JSON: { \"Serilog\": { ... \"WriteTo\": [ { \"Name\": \"ElmahIo\", \"Args\": { \"apiKey\": \"API_KEY\", \"logId\": \"LOG_ID\" } } ] }, ... } This will configure elmah.io's Serilog sink in Umbraco. You may experience logging not coming through when running locally. In this case, it might help to remove the WriteTo action from the appsettings.Development.json file.","title":"Umbraco >= 9"},{"location":"logging-to-elmah-io-from-umbraco/#umbraco-8","text":"To install elmah.io in your Umbraco v8 site, install the Elmah.Io.Umbraco v4 package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco -Version 4.2.21 dotnet add package Elmah.Io.Umbraco --version 4.2.21 <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"4.2.21\" /> paket add Elmah.Io.Umbraco --version 4.2.21 During the installation, you will be presented with a dialog asking for your API key and log ID. Hit F5 and watch messages start flowing into elmah.io. Unless serious security issues in the Elmah.Io.Umbraco v4 package are found, new features will be added to the v5 package only (supporting Umbraco 10 and newer).","title":"Umbraco 8"},{"location":"logging-to-elmah-io-from-umbraco/#configuration","text":"If you are running on the default Umbraco template, all necessary configuration is added during the installation of the Elmah.Io.Umbraco NuGet package. If your web.config file for some reason isn't updated during installation, you can configure elmah.io manually: Configure elmah.io manually . Likewise, the installer configures the elmah.io sink for Serilog in your config\\serilog.user.config file.","title":"Configuration"},{"location":"logging-to-elmah-io-from-umbraco/#different-environments","text":"You may have different environments like Staging and Production . At least you have two: Localhost and Production . If you want to log to different error logs depending on the current environment, check out Use multiple logs for different environments . Web.config transformations work on the Web.config file only but you may have other config files that need transformation as well. In terms of elmah.io, the serilog.user.config file also includes elmah.io configuration that you may want to disable on localhost and include on production. If you are running on Umbraco Cloud this is natively supported as explained here: Config Transforms . Even in self-hosted environments, you can achieve something similar using the SlowCheetah extension. Check out this question on Our for details: Deploying different umbracoSettings.config for different environments .","title":"Different environments"},{"location":"logging-to-elmah-io-from-umbraco/#umbraco-7","text":"We still support Umbraco 7 through the Elmah.Io.Umbraco package version 3.2.35 : Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Umbraco -Version 3.2.35 dotnet add package Elmah.Io.Umbraco --version 3.2.35 <PackageReference Include=\"Elmah.Io.Umbraco\" Version=\"3.2.35\" /> paket add Elmah.Io.Umbraco --version 3.2.35 New features will be added to the updated package for Umbraco 10 and newer only.","title":"Umbraco 7"},{"location":"logging-to-elmah-io-from-umbraco/#umbraco-cloud","text":"When using Umbraco Cloud, you may not have a local clone of the source code. To install elmah.io on Umbraco Cloud, follow these steps: Clone your Umbraco Cloud project to a local folder as explained here: Working with a Local Clone . Where you need to install and configure the Elmah.Io.Umbraco package depends on the Umbraco major version you are running on Umbraco Cloud. For Umbraco 7-8, all changes should be made in the *.Web project only and all commits from within that folder as well. Don't commit and push anything in the root folder. For Umbraco versions above 8, all changes should be made in the src\\UmbracoProject folder. Follow the installation steps for your Umbraco version as specified in the beginning of this document. Commit and push all changes to the git repository. This will add elmah.io logging to your remote Umbraco Cloud project. In case you want logging to different elmah.io logs from each Umbraco Cloud environment, please check out Umbraco's support for config transformations here: Config transforms .","title":"Umbraco Cloud"},{"location":"logging-to-elmah-io-from-umbraco/#umbraco-uno","text":"Umbraco Uno has been discontinued. Installing elmah.io in Umbraco Uno follows the process of installing it onto Umbraco Cloud. To modify code and configuration in Uno you will need a Umbraco Uno Standard plan or higher. Also, you need to enable Custom Code to clone the code locally. This can be done from Uno by clicking the Enable custom code button: After enabling Custom Code you can create a Development environment and follow the steps in the Umbraco Cloud documentation.","title":"Umbraco Uno"},{"location":"logging-to-elmah-io-from-uno/","text":"Logging to elmah.io from Uno The Uno integration for elmah.io is currently in prerelease. Integrating Uno with elmah.io is done by installing the Elmah.Io.Uno NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Uno -IncludePrerelease dotnet add package Elmah.Io.Uno --prerelease <PackageReference Include=\"Elmah.Io.Uno\" Version=\"4.0.19-pre\" /> paket add Elmah.Io.Uno While configured in the shared project, the NuGet package will need to be installed in all platform projects. Elmah.Io.Uno comes with a logger for Microsoft.Extensions.Logging. To configure the logger, open the App.xaml.cs file and locate the InitializeLogging method. Here you will see the logging configuration for your application. Include logging to elmah.io by calling the AddElmahIo method: var factory = LoggerFactory.Create(builder => { // ... builder.AddElmahIo(\"API_KEY\", new Guid(\"LOG_ID\")); // ... }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. elmah.io will now automatically log all warning, error, and fatal messages to elmah.io. Uno log messages internally, but you can also do manual logging like this: this.Log().LogWarning(\"Oh no\"); Logging with Uno's log helper will require you to add the following usings: using Microsoft.Extensions.Logging; using Uno.Extensions;","title":"Logging from Uno"},{"location":"logging-to-elmah-io-from-uno/#logging-to-elmahio-from-uno","text":"The Uno integration for elmah.io is currently in prerelease. Integrating Uno with elmah.io is done by installing the Elmah.Io.Uno NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Uno -IncludePrerelease dotnet add package Elmah.Io.Uno --prerelease <PackageReference Include=\"Elmah.Io.Uno\" Version=\"4.0.19-pre\" /> paket add Elmah.Io.Uno While configured in the shared project, the NuGet package will need to be installed in all platform projects. Elmah.Io.Uno comes with a logger for Microsoft.Extensions.Logging. To configure the logger, open the App.xaml.cs file and locate the InitializeLogging method. Here you will see the logging configuration for your application. Include logging to elmah.io by calling the AddElmahIo method: var factory = LoggerFactory.Create(builder => { // ... builder.AddElmahIo(\"API_KEY\", new Guid(\"LOG_ID\")); // ... }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. elmah.io will now automatically log all warning, error, and fatal messages to elmah.io. Uno log messages internally, but you can also do manual logging like this: this.Log().LogWarning(\"Oh no\"); Logging with Uno's log helper will require you to add the following usings: using Microsoft.Extensions.Logging; using Uno.Extensions;","title":"Logging to elmah.io from Uno"},{"location":"logging-to-elmah-io-from-vue/","text":"Logging to elmah.io from Vue To log all errors from a Vue.js application, install the elmah.io.javascript npm package as described in Logging from JavaScript or include it with a direct <script> include: <script src=\"https://cdn.jsdelivr.net/gh/elmahio/elmah.io.javascript@latest/dist/elmahio.min.js\" type=\"text/javascript\"></script> Before initializing the application, include the following code: var logger = new Elmahio({ apiKey: \"API_KEY\", logId: \"LOG_ID\" }); Vue.config.errorHandler = function (err, vm, info) { logger.error(err.message, err); }; Vue.config.warnHandler = function (msg, vm, trace) { logger.warning(msg); }; elmah.io.javascript will automatically log all errors raised through window.onerror and log additional errors and warnings from Vue.js through the errorHandler and warnHandler functions. If you want to exclude warnings, simply remove the warnHandler function. Check out the Elmah.Io.JavaScript.VueJs sample for some real working code.","title":"Logging from Vue"},{"location":"logging-to-elmah-io-from-vue/#logging-to-elmahio-from-vue","text":"To log all errors from a Vue.js application, install the elmah.io.javascript npm package as described in Logging from JavaScript or include it with a direct <script> include: <script src=\"https://cdn.jsdelivr.net/gh/elmahio/elmah.io.javascript@latest/dist/elmahio.min.js\" type=\"text/javascript\"></script> Before initializing the application, include the following code: var logger = new Elmahio({ apiKey: \"API_KEY\", logId: \"LOG_ID\" }); Vue.config.errorHandler = function (err, vm, info) { logger.error(err.message, err); }; Vue.config.warnHandler = function (msg, vm, trace) { logger.warning(msg); }; elmah.io.javascript will automatically log all errors raised through window.onerror and log additional errors and warnings from Vue.js through the errorHandler and warnHandler functions. If you want to exclude warnings, simply remove the warnHandler function. Check out the Elmah.Io.JavaScript.VueJs sample for some real working code.","title":"Logging to elmah.io from Vue"},{"location":"logging-to-elmah-io-from-wcf/","text":"Logging to elmah.io from WCF ELMAH (the open-source project) and WCF aren't exactly known to go hand in hand. But, with a bit of custom code, logging exceptions from WCF to elmah.io is possible. Let's get started. Install elmah.io into your WCF project using NuGet: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Add a new class named HttpErrorHandler : public class HttpErrorHandler : IErrorHandler { public bool HandleError(Exception error) { return false; } public void ProvideFault(Exception error, MessageVersion version, ref Message fault) { if (error != null) { Elmah.ErrorSignal.FromCurrentContext().Raise(error); } } } This is an implementation of WCF's IErrorHandler that instructs WCF to log any errors to ELMAH, using the Raise -method on ErrorSignal . Then create an attribute named ServiceErrorBehaviourAttribute : public class ServiceErrorBehaviourAttribute : Attribute, IServiceBehavior { Type errorHandlerType; public ServiceErrorBehaviourAttribute(Type errorHandlerType) { this.errorHandlerType = errorHandlerType; } public void Validate(ServiceDescription description, ServiceHostBase serviceHostBase) { } public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase) { IErrorHandler errorHandler; errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType); foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers) { ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher; channelDispatcher.ErrorHandlers.Add(errorHandler); } } } We'll use the ServiceErrorBehaviourAttribute class for decorating endpoints which we want logging uncaught errors to ELMAH. Add the new attribute to your service implementation like this: [ServiceErrorBehaviour(typeof(HttpErrorHandler))] public class Service1 : IService1 { // ... } That's it. Services decorated with the ServiceErrorBehaviourAttribute now logs exceptions to ELMAH.","title":"Logging from WCF"},{"location":"logging-to-elmah-io-from-wcf/#logging-to-elmahio-from-wcf","text":"ELMAH (the open-source project) and WCF aren't exactly known to go hand in hand. But, with a bit of custom code, logging exceptions from WCF to elmah.io is possible. Let's get started. Install elmah.io into your WCF project using NuGet: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Add a new class named HttpErrorHandler : public class HttpErrorHandler : IErrorHandler { public bool HandleError(Exception error) { return false; } public void ProvideFault(Exception error, MessageVersion version, ref Message fault) { if (error != null) { Elmah.ErrorSignal.FromCurrentContext().Raise(error); } } } This is an implementation of WCF's IErrorHandler that instructs WCF to log any errors to ELMAH, using the Raise -method on ErrorSignal . Then create an attribute named ServiceErrorBehaviourAttribute : public class ServiceErrorBehaviourAttribute : Attribute, IServiceBehavior { Type errorHandlerType; public ServiceErrorBehaviourAttribute(Type errorHandlerType) { this.errorHandlerType = errorHandlerType; } public void Validate(ServiceDescription description, ServiceHostBase serviceHostBase) { } public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase) { IErrorHandler errorHandler; errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType); foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers) { ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher; channelDispatcher.ErrorHandlers.Add(errorHandler); } } } We'll use the ServiceErrorBehaviourAttribute class for decorating endpoints which we want logging uncaught errors to ELMAH. Add the new attribute to your service implementation like this: [ServiceErrorBehaviour(typeof(HttpErrorHandler))] public class Service1 : IService1 { // ... } That's it. Services decorated with the ServiceErrorBehaviourAttribute now logs exceptions to ELMAH.","title":"Logging to elmah.io from WCF"},{"location":"logging-to-elmah-io-from-web-api/","text":"Logging to elmah.io from Web API Web API provides its own mechanism for handling errors, why ELMAH\u2019s modules and handlers don't work there. Luckily, Richard Dingwall created the Elmah.Contrib.WebApi NuGet package to fix this. We've built a package for ASP.NET Web API exclusively, which installs all the necessary packages. To start logging exceptions from Web API, install the Elmah.Io.WebApi NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.WebApi dotnet add package Elmah.Io.WebApi <PackageReference Include=\"Elmah.Io.WebApi\" Version=\"5.*\" /> paket add Elmah.Io.WebApi During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Web API 2.x Web API 1.x Add the following code to your WebApiConfig.cs file: public static class WebApiConfig { public static void Register(HttpConfiguration config) { // ... config.Services.Add(typeof(IExceptionLogger), new ElmahExceptionLogger()); // ... } } The registered IExceptionLogger intercepts all thrown exceptions, even errors in controller constructors and routing errors. Add the following code to your Global.asax.cs file: protected void Application_Start() { // ... GlobalConfiguration.Configuration.Filters.Add(new ElmahHandleErrorApiAttribute()); // ... } In this case you register a new global filter with Web API. The downside of this approach is, that only errors thrown in controller actions are logged. All uncaught exceptions in ASP.NET Web API are now logged to elmah.io Logging from exception/action filters It's a widely used Web API approach, to handle all exceptions in a global exception/action filter and return a nicely formatted JSON/XML error response to the client. This is a nice approach to avoid throwing internal server errors, but it also puts ELMAH out of the game. When catching any exception manually and converting it to a response message, errors won't be logged in elmah.io. To overcome this, errors should be logged manually from your global exception/action filter: public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext context) { ErrorSignal.FromCurrentContext().Raise(context.Exception); // Now generate the result to the client } }","title":"Logging from Web API"},{"location":"logging-to-elmah-io-from-web-api/#logging-to-elmahio-from-web-api","text":"Web API provides its own mechanism for handling errors, why ELMAH\u2019s modules and handlers don't work there. Luckily, Richard Dingwall created the Elmah.Contrib.WebApi NuGet package to fix this. We've built a package for ASP.NET Web API exclusively, which installs all the necessary packages. To start logging exceptions from Web API, install the Elmah.Io.WebApi NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.WebApi dotnet add package Elmah.Io.WebApi <PackageReference Include=\"Elmah.Io.WebApi\" Version=\"5.*\" /> paket add Elmah.Io.WebApi During the installation, you will be asked for your API key ( Where is my API key? ) and log ID ( Where is my log ID? ). Web API 2.x Web API 1.x Add the following code to your WebApiConfig.cs file: public static class WebApiConfig { public static void Register(HttpConfiguration config) { // ... config.Services.Add(typeof(IExceptionLogger), new ElmahExceptionLogger()); // ... } } The registered IExceptionLogger intercepts all thrown exceptions, even errors in controller constructors and routing errors. Add the following code to your Global.asax.cs file: protected void Application_Start() { // ... GlobalConfiguration.Configuration.Filters.Add(new ElmahHandleErrorApiAttribute()); // ... } In this case you register a new global filter with Web API. The downside of this approach is, that only errors thrown in controller actions are logged. All uncaught exceptions in ASP.NET Web API are now logged to elmah.io","title":"Logging to elmah.io from Web API"},{"location":"logging-to-elmah-io-from-web-api/#logging-from-exceptionaction-filters","text":"It's a widely used Web API approach, to handle all exceptions in a global exception/action filter and return a nicely formatted JSON/XML error response to the client. This is a nice approach to avoid throwing internal server errors, but it also puts ELMAH out of the game. When catching any exception manually and converting it to a response message, errors won't be logged in elmah.io. To overcome this, errors should be logged manually from your global exception/action filter: public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext context) { ErrorSignal.FromCurrentContext().Raise(context.Exception); // Now generate the result to the client } }","title":"Logging from exception/action filters"},{"location":"logging-to-elmah-io-from-web-pages/","text":"Logging to elmah.io from Web Pages Logging from ASP.NET Web Pages uses our integration with ELMAH (the open-source project). This means that the installation is identical to installing elmah.io in ASP.NET Web Forms. For a full guide to installing the Elmah.Io NuGet package in your Web Pages project go to: Logging to elmah.io from ASP.NET / WebForms . If the dialog requesting you to input an API key and a Log ID is not shown during the installation of the NuGet package, ELMAH configuration needs to be checked manually as explained here: Configure elmah.io manually .","title":"Logging from Web Pages"},{"location":"logging-to-elmah-io-from-web-pages/#logging-to-elmahio-from-web-pages","text":"Logging from ASP.NET Web Pages uses our integration with ELMAH (the open-source project). This means that the installation is identical to installing elmah.io in ASP.NET Web Forms. For a full guide to installing the Elmah.Io NuGet package in your Web Pages project go to: Logging to elmah.io from ASP.NET / WebForms . If the dialog requesting you to input an API key and a Log ID is not shown during the installation of the NuGet package, ELMAH configuration needs to be checked manually as explained here: Configure elmah.io manually .","title":"Logging to elmah.io from Web Pages"},{"location":"logging-to-elmah-io-from-winforms/","text":"Logging to elmah.io from Windows Forms elmah.io logging can be easily added to Windows Forms applications. We don't provide a package specific for WinForms, but the Elmah.Io.Client package, combined with a bit of code, will achieve just the same. To start logging to elmah.io, install the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Add the following usings to the Program.cs file: using Elmah.Io.Client; using System.Security.Principal; using System.Threading; Add an event handler to the ThreadException event in the Main method: Application.ThreadException += Application_ThreadException; Finally, add the Application_ThreadException method: static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { var logger = ElmahioAPI.Create(\"API_KEY\"); var exception = e.Exception; var baseException = exception.GetBaseException(); logger.Messages.Create(\"LOG_ID\", new CreateMessage { DateTime = DateTime.UtcNow, Detail = exception?.ToString(), Type = baseException?.GetType().FullName, Title = baseException?.Message ?? \"An error occurred\", Data = exception.ToDataList(), Severity = \"Error\", Source = baseException?.Source, User = WindowsIdentity.GetCurrent().Name, }); Application.Exit(); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged. This example closes the application when an error occurs. If you only want to log the error, make sure to re-use the logger object: private static IElmahioAPI logger; static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { if (logger == null) { logger = ElmahioAPI.Create(\"API_KEY\"); } // ... }","title":"Logging from Windows Forms"},{"location":"logging-to-elmah-io-from-winforms/#logging-to-elmahio-from-windows-forms","text":"elmah.io logging can be easily added to Windows Forms applications. We don't provide a package specific for WinForms, but the Elmah.Io.Client package, combined with a bit of code, will achieve just the same. To start logging to elmah.io, install the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Add the following usings to the Program.cs file: using Elmah.Io.Client; using System.Security.Principal; using System.Threading; Add an event handler to the ThreadException event in the Main method: Application.ThreadException += Application_ThreadException; Finally, add the Application_ThreadException method: static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { var logger = ElmahioAPI.Create(\"API_KEY\"); var exception = e.Exception; var baseException = exception.GetBaseException(); logger.Messages.Create(\"LOG_ID\", new CreateMessage { DateTime = DateTime.UtcNow, Detail = exception?.ToString(), Type = baseException?.GetType().FullName, Title = baseException?.Message ?? \"An error occurred\", Data = exception.ToDataList(), Severity = \"Error\", Source = baseException?.Source, User = WindowsIdentity.GetCurrent().Name, }); Application.Exit(); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged. This example closes the application when an error occurs. If you only want to log the error, make sure to re-use the logger object: private static IElmahioAPI logger; static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { if (logger == null) { logger = ElmahioAPI.Create(\"API_KEY\"); } // ... }","title":"Logging to elmah.io from Windows Forms"},{"location":"logging-to-elmah-io-from-wpf/","text":"Logging to elmah.io from WPF Logging to elmah.io from WPF Logging exceptions manually Breadcrumbs Additional options Setting application name Hooks Legacy elmah.io logging can be easily added to WPF applications. To start logging to elmah.io, install the Elmah.Io.Wpf NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Wpf dotnet add package Elmah.Io.Wpf <PackageReference Include=\"Elmah.Io.Wpf\" Version=\"5.*\" /> paket add Elmah.Io.Wpf Next, initialize elmah.io in the App.xaml.cs file: public partial class App : Application { public App() { ElmahIoWpf.Init(new ElmahIoWpfOptions { ApiKey = \"API_KEY\", LogId = new Guid(\"LOG_ID\") }); } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged. Remember to generate a new API key with messages_write permission only. This makes it easy to revoke the API key if someone starts sending messages to your log with your key. That's it. All uncaught exceptions are now logged to elmah.io. Logging exceptions manually Once initialized using the Init call, exceptions can be logged manually: ElmahIoWpf.Log(new Exception()); Breadcrumbs The Elmah.Io.Wpf package automatically records breadcrumbs when clicking buttons and opening/closing windows. To manually include a breadcrumb you can include the following code: ElmahIoWpf.AddBreadcrumb(new Client.Breadcrumb(DateTime.UtcNow, severity:\"Information\", action:\"Save\", message:\"Record save\")); severity can be set to Verbose , Debug , Information , Warning , Error , or Fatal . The value of action is a string of your choice. If using one of the following values, the action will get a special icon in the elmah.io UI: click , submit , navigation , request , error , warning , fatal . The message field can be used to describe the breadcrumb in more detail and/or include IDs or similar related to the breadcrumb. The number of breadcrumbs to store in memory is 10 as a default. If you want to lower or increase this number, set the MaximumBreadcrumbs property during initialization: ElmahIoWpf.Init(new ElmahIoWpfOptions { // ... MaximumBreadcrumbs = 20, }); Additional options Setting application name The application name can be set on all logged messages by setting the Application property on ElmahIoWpfOptions during initialization: ElmahIoWpf.Init(new ElmahIoWpfOptions { // ... Application = \"WPF on .NET 6\", }); Hooks The ElmahIoWpfOptions class also supports a range of actions to hook into various stages of logging errors. Hooks are registered as actions when installing Elmah.Io.Wpf : ElmahIoWpf.Init(new ElmahIoWpfOptions { // ... OnFilter = msg => { return msg.Type.Equals(\"System.NullReferenceException\"); }, OnMessage = msg => { msg.Version = \"42\"; }, OnError = (msg, ex) => { // Log somewhere else } }); The OnFilter action can be used to ignore/filter specific errors. In this example, all errors of type System.NullReferenceException is ignored. The OnMessage action can be used to decorate/enrich all errors with different information. In this example, all errors get a version number of 42 . The OnError action can be used to handle if the elmah.io API is down. While this doesn't happen frequently, you might want to log errors elsewhere. Legacy Before the Elmah.Io.Wpf package was developed, this was the recommended way of installing elmah.io in WPF. To start logging to elmah.io, install the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Add the following usings to the App.xaml.cs file: using Elmah.Io.Client; using System.Diagnostics; using System.Security.Principal; using System.Threading.Tasks; Add the following code: private IElmahioAPI logger; public App() { logger = ElmahioAPI.Create(\"API_KEY\"); AppDomain.CurrentDomain.UnhandledException += (sender, args) => LogException(args.ExceptionObject as Exception); TaskScheduler.UnobservedTaskException += (sender, args) => LogException(args.Exception); Dispatcher.UnhandledException += (sender, args) => { if (!Debugger.IsAttached) LogException(args.Exception); }; } private void LogException(Exception exception) { var baseException = exception.GetBaseException(); logger.Messages.Create(\"LOG_ID\", new CreateMessage { DateTime = DateTime.UtcNow, Detail = exception?.ToString(), Type = baseException?.GetType().FullName, Title = baseException?.Message ?? \"An error occurred\", Data = exception.ToDataList(), Severity = \"Error\", Source = baseException?.Source, User = WindowsIdentity.GetCurrent().Name, }); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged.","title":"Logging from WPF"},{"location":"logging-to-elmah-io-from-wpf/#logging-to-elmahio-from-wpf","text":"Logging to elmah.io from WPF Logging exceptions manually Breadcrumbs Additional options Setting application name Hooks Legacy elmah.io logging can be easily added to WPF applications. To start logging to elmah.io, install the Elmah.Io.Wpf NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Wpf dotnet add package Elmah.Io.Wpf <PackageReference Include=\"Elmah.Io.Wpf\" Version=\"5.*\" /> paket add Elmah.Io.Wpf Next, initialize elmah.io in the App.xaml.cs file: public partial class App : Application { public App() { ElmahIoWpf.Init(new ElmahIoWpfOptions { ApiKey = \"API_KEY\", LogId = new Guid(\"LOG_ID\") }); } } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged. Remember to generate a new API key with messages_write permission only. This makes it easy to revoke the API key if someone starts sending messages to your log with your key. That's it. All uncaught exceptions are now logged to elmah.io.","title":"Logging to elmah.io from WPF"},{"location":"logging-to-elmah-io-from-wpf/#logging-exceptions-manually","text":"Once initialized using the Init call, exceptions can be logged manually: ElmahIoWpf.Log(new Exception());","title":"Logging exceptions manually"},{"location":"logging-to-elmah-io-from-wpf/#breadcrumbs","text":"The Elmah.Io.Wpf package automatically records breadcrumbs when clicking buttons and opening/closing windows. To manually include a breadcrumb you can include the following code: ElmahIoWpf.AddBreadcrumb(new Client.Breadcrumb(DateTime.UtcNow, severity:\"Information\", action:\"Save\", message:\"Record save\")); severity can be set to Verbose , Debug , Information , Warning , Error , or Fatal . The value of action is a string of your choice. If using one of the following values, the action will get a special icon in the elmah.io UI: click , submit , navigation , request , error , warning , fatal . The message field can be used to describe the breadcrumb in more detail and/or include IDs or similar related to the breadcrumb. The number of breadcrumbs to store in memory is 10 as a default. If you want to lower or increase this number, set the MaximumBreadcrumbs property during initialization: ElmahIoWpf.Init(new ElmahIoWpfOptions { // ... MaximumBreadcrumbs = 20, });","title":"Breadcrumbs"},{"location":"logging-to-elmah-io-from-wpf/#additional-options","text":"","title":"Additional options"},{"location":"logging-to-elmah-io-from-wpf/#setting-application-name","text":"The application name can be set on all logged messages by setting the Application property on ElmahIoWpfOptions during initialization: ElmahIoWpf.Init(new ElmahIoWpfOptions { // ... Application = \"WPF on .NET 6\", });","title":"Setting application name"},{"location":"logging-to-elmah-io-from-wpf/#hooks","text":"The ElmahIoWpfOptions class also supports a range of actions to hook into various stages of logging errors. Hooks are registered as actions when installing Elmah.Io.Wpf : ElmahIoWpf.Init(new ElmahIoWpfOptions { // ... OnFilter = msg => { return msg.Type.Equals(\"System.NullReferenceException\"); }, OnMessage = msg => { msg.Version = \"42\"; }, OnError = (msg, ex) => { // Log somewhere else } }); The OnFilter action can be used to ignore/filter specific errors. In this example, all errors of type System.NullReferenceException is ignored. The OnMessage action can be used to decorate/enrich all errors with different information. In this example, all errors get a version number of 42 . The OnError action can be used to handle if the elmah.io API is down. While this doesn't happen frequently, you might want to log errors elsewhere.","title":"Hooks"},{"location":"logging-to-elmah-io-from-wpf/#legacy","text":"Before the Elmah.Io.Wpf package was developed, this was the recommended way of installing elmah.io in WPF. To start logging to elmah.io, install the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Add the following usings to the App.xaml.cs file: using Elmah.Io.Client; using System.Diagnostics; using System.Security.Principal; using System.Threading.Tasks; Add the following code: private IElmahioAPI logger; public App() { logger = ElmahioAPI.Create(\"API_KEY\"); AppDomain.CurrentDomain.UnhandledException += (sender, args) => LogException(args.ExceptionObject as Exception); TaskScheduler.UnobservedTaskException += (sender, args) => LogException(args.Exception); Dispatcher.UnhandledException += (sender, args) => { if (!Debugger.IsAttached) LogException(args.Exception); }; } private void LogException(Exception exception) { var baseException = exception.GetBaseException(); logger.Messages.Create(\"LOG_ID\", new CreateMessage { DateTime = DateTime.UtcNow, Detail = exception?.ToString(), Type = baseException?.GetType().FullName, Title = baseException?.Message ?? \"An error occurred\", Data = exception.ToDataList(), Severity = \"Error\", Source = baseException?.Source, User = WindowsIdentity.GetCurrent().Name, }); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with the id of the log ( Where is my log ID? ) where you want errors logged.","title":"Legacy"},{"location":"logging-to-elmah-io-from-xamarin/","text":"Logging to elmah.io from Xamarin Logging to elmah.io from Xamarin Log exceptions manually Breadcrumbs Additional configuration Application Version Decorating all messages Filtering log messages Handling errors Legacy integration The Xamarin integration for elmah.io is currently in prerelease. Integrating Xamarin with elmah.io is done by installing the Elmah.Io.Xamarin NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Xamarin -IncludePrerelease dotnet add package Elmah.Io.Xamarin --prerelease <PackageReference Include=\"Elmah.Io.Xamarin\" Version=\"4.0.30-pre\" /> paket add Elmah.Io.Xamarin For each platform (Android and iOS) you will need to set up elmah.io as illustrated in the following sections. The code is the same for both Xamarin and Xamarin.Forms. Android iOS Open the MainActivity.cs file and add the following using statements: using System; using Elmah.Io.Xamarin; Locate the OnCreate method and add the following code before all other lines: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { ApiKey = \"API_KEY\", LogId = new Guid(\"LOG_ID\"), }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Calling the Init method will initialize elmah.io. For more configuration options see the Additional configuration section. Open the Main.cs file and add the following using statements: using System; using Elmah.Io.Xamarin; Locate the Main method and add the following code after the call to UIApplication.Main : ElmahIoXamarin.Init(new ElmahIoXamarinOptions { ApiKey = \"API_KEY\", LogId = new Guid(\"LOG_ID\"), }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Calling the Init method will initialize elmah.io. For more configuration options see the Additional configuration section. Log exceptions manually Once the ElmahIoXamarin.Init method has been configured during initialization of the app, any exception can be logged manually using the Log methods available in the Elmah.Io.Xamarin namespace: try { // Code that may break } catch (Exception e) { // Log the exception with the Log extension method: e.Log(); // or use the Log method on ElmahIoXamarin: ElmahIoXamarin.Log(e); } Breadcrumbs Breadcrumbs can be a great help when needing to figure out how a user ended up with an error. To log breadcrumbs, you can use the AddBreadcrumb method on ElmahIoXamarin . The following is a sample for Android which will log breadcrumbs on interesting events: public class MainActivity : AppCompatActivity, BottomNavigationView.IOnNavigationItemSelectedListener { public override void OnBackPressed() { ElmahIoXamarin.AddBreadcrumb(\"OnBackPressed\", DateTime.UtcNow, action: \"Navigation\"); base.OnBackPressed(); } protected override void OnPause() { ElmahIoXamarin.AddBreadcrumb(\"OnPause\", DateTime.UtcNow); base.OnPause(); } // ... public bool OnNavigationItemSelected(IMenuItem item) { switch (item.ItemId) { case Resource.Id.navigation_home: ElmahIoXamarin.AddBreadcrumb(\"Navigate to Home\", DateTime.UtcNow, action: \"Navigation\"); textMessage.SetText(Resource.String.title_home); return true; case Resource.Id.navigation_dashboard: ElmahIoXamarin.AddBreadcrumb(\"Navigate to Dashboard\", DateTime.UtcNow, action: \"Navigation\"); textMessage.SetText(Resource.String.title_dashboard); return true; case Resource.Id.navigation_notifications: ElmahIoXamarin.AddBreadcrumb(\"Navigate to Notifications\", DateTime.UtcNow, action: \"Navigation\"); textMessage.SetText(Resource.String.title_notifications); return true; } return false; } } Additional configuration Besides the mandatory properties ApiKey and LogId the ElmahIoXamarinOptions provide a range of other configuration parameters. Application The elmah.io integration for Xamarin will automatically use the package name for the Application field. To override this you can set the application name in settings: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... Application = \"MyApp\" }); Version The elmah.io integration for Xamarin will automatically use the package version for the Version field. To override this you can set the version string in settings: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... Version = \"1.0.2\" }); Decorating all messages All log messages logged through this integration can be decorated with the OnMessage action: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... OnMessage = msg => { msg.Source = \"Custom source\"; } }); Filtering log messages Log messages can be filtered directly on the device to avoid specific log messages from being sent to elmah.io with the OnFilter function: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... OnFilter = msg => msg.Title.Contains(\"foo\") }); This code will automatically ignore all log messages with the text foo in the title. Handling errors You may want to handle the scenario where the device cannot communicate with the elmah.io API. You can use the OnError action: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... OnError = (msg, ex) => { // Do something with ex } }); Legacy integration If you prefer you can configure elmah.io manually without the use of the Elmah.Io.Xamarin package. This is not the recommended way to integrate with elmah.io from Xamarin and this approach will be discontinued. Start by installing the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client If you are targeting a single platform, you can install the package directly in the startup project. If you are targeting multiple platforms, you can either install the package in all platform-specific projects or a shared project. Additional steps will vary from platform to platform. Android iOS Locate your main activity class and look for the OnCreate method. Here, you'd want to set up event handlers for when uncaught exceptions happen: protected override void OnCreate(Bundle savedInstanceState) { // ... AndroidEnvironment.UnhandledExceptionRaiser += AndroidEnvironment_UnhandledExceptionRaiser; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; // ... LoadApplication(new App()); etc. } Next, implement a method that can log an exception to elmah.io: private void LogExceptionToElmahIo(Exception exception) { if (exception == null) return; if (elmahIoClient == null) { elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); } var packageInfo = PackageManager.GetPackageInfo(PackageName, PackageInfoFlags.MetaData); var baseException = exception?.GetBaseException(); var errorMessage = baseException?.Message ?? \"Unhandled exception\"; try { elmahIoClient.Messages.Create(\"LOG_ID\", new CreateMessage { Data = exception?.ToDataList(), DateTime = DateTime.UtcNow, Detail = exception?.ToString(), Severity = \"Error\", Source = baseException?.Source, Title = errorMessage, Type = baseException?.GetType().FullName, Version = packageInfo.VersionName, Application = packageInfo.PackageName, }); } catch (Exception inner) { Android.Util.Log.Error(\"elmahio\", inner.Message); } // Log to Android Device Logging. Android.Util.Log.Error(\"crash\", errorMessage); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Finally, implement the three event handlers that we added in the first step: private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { LogExceptionToElmahIo(e.Exception); e.SetObserved(); } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { LogExceptionToElmahIo(e.ExceptionObject as Exception); } private void AndroidEnvironment_UnhandledExceptionRaiser(object sender, RaiseThrowableEventArgs e) { LogExceptionToElmahIo(e.Exception); e.Handled = true; } Locate your main application class and look for the Main method. Here, you'd want to set up event handlers for when uncaught exceptions happen: static void Main(string[] args) { // ... AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; } Next, implement a method that can log an exception to elmah.io: private static void LogExceptionToElmahIo(Exception exception) { if (exception == null) return; if (elmahIoClient == null) { elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); } var baseException = exception?.GetBaseException(); elmahIoClient.Messages.Create(\"LOG_ID\", new CreateMessage { Data = exception?.ToDataList(), DateTime = DateTime.UtcNow, Detail = exception?.ToString(), Severity = \"Error\", Source = baseException?.Source, Title = baseException?.Message ?? \"Unhandled exception\", Type = baseException?.GetType().FullName, }); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Finally, implement the two event handlers that we added in the first step: private static void TaskScheduler_UnobservedTaskException( object sender, UnobservedTaskExceptionEventArgs e) { LogExceptionToElmahIo(e.Exception); e.SetObserved(); } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { LogExceptionToElmahIo(e.ExceptionObject as Exception); }","title":"Logging from Xamarin"},{"location":"logging-to-elmah-io-from-xamarin/#logging-to-elmahio-from-xamarin","text":"Logging to elmah.io from Xamarin Log exceptions manually Breadcrumbs Additional configuration Application Version Decorating all messages Filtering log messages Handling errors Legacy integration The Xamarin integration for elmah.io is currently in prerelease. Integrating Xamarin with elmah.io is done by installing the Elmah.Io.Xamarin NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Xamarin -IncludePrerelease dotnet add package Elmah.Io.Xamarin --prerelease <PackageReference Include=\"Elmah.Io.Xamarin\" Version=\"4.0.30-pre\" /> paket add Elmah.Io.Xamarin For each platform (Android and iOS) you will need to set up elmah.io as illustrated in the following sections. The code is the same for both Xamarin and Xamarin.Forms. Android iOS Open the MainActivity.cs file and add the following using statements: using System; using Elmah.Io.Xamarin; Locate the OnCreate method and add the following code before all other lines: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { ApiKey = \"API_KEY\", LogId = new Guid(\"LOG_ID\"), }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Calling the Init method will initialize elmah.io. For more configuration options see the Additional configuration section. Open the Main.cs file and add the following using statements: using System; using Elmah.Io.Xamarin; Locate the Main method and add the following code after the call to UIApplication.Main : ElmahIoXamarin.Init(new ElmahIoXamarinOptions { ApiKey = \"API_KEY\", LogId = new Guid(\"LOG_ID\"), }); Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Calling the Init method will initialize elmah.io. For more configuration options see the Additional configuration section.","title":"Logging to elmah.io from Xamarin"},{"location":"logging-to-elmah-io-from-xamarin/#log-exceptions-manually","text":"Once the ElmahIoXamarin.Init method has been configured during initialization of the app, any exception can be logged manually using the Log methods available in the Elmah.Io.Xamarin namespace: try { // Code that may break } catch (Exception e) { // Log the exception with the Log extension method: e.Log(); // or use the Log method on ElmahIoXamarin: ElmahIoXamarin.Log(e); }","title":"Log exceptions manually"},{"location":"logging-to-elmah-io-from-xamarin/#breadcrumbs","text":"Breadcrumbs can be a great help when needing to figure out how a user ended up with an error. To log breadcrumbs, you can use the AddBreadcrumb method on ElmahIoXamarin . The following is a sample for Android which will log breadcrumbs on interesting events: public class MainActivity : AppCompatActivity, BottomNavigationView.IOnNavigationItemSelectedListener { public override void OnBackPressed() { ElmahIoXamarin.AddBreadcrumb(\"OnBackPressed\", DateTime.UtcNow, action: \"Navigation\"); base.OnBackPressed(); } protected override void OnPause() { ElmahIoXamarin.AddBreadcrumb(\"OnPause\", DateTime.UtcNow); base.OnPause(); } // ... public bool OnNavigationItemSelected(IMenuItem item) { switch (item.ItemId) { case Resource.Id.navigation_home: ElmahIoXamarin.AddBreadcrumb(\"Navigate to Home\", DateTime.UtcNow, action: \"Navigation\"); textMessage.SetText(Resource.String.title_home); return true; case Resource.Id.navigation_dashboard: ElmahIoXamarin.AddBreadcrumb(\"Navigate to Dashboard\", DateTime.UtcNow, action: \"Navigation\"); textMessage.SetText(Resource.String.title_dashboard); return true; case Resource.Id.navigation_notifications: ElmahIoXamarin.AddBreadcrumb(\"Navigate to Notifications\", DateTime.UtcNow, action: \"Navigation\"); textMessage.SetText(Resource.String.title_notifications); return true; } return false; } }","title":"Breadcrumbs"},{"location":"logging-to-elmah-io-from-xamarin/#additional-configuration","text":"Besides the mandatory properties ApiKey and LogId the ElmahIoXamarinOptions provide a range of other configuration parameters.","title":"Additional configuration"},{"location":"logging-to-elmah-io-from-xamarin/#application","text":"The elmah.io integration for Xamarin will automatically use the package name for the Application field. To override this you can set the application name in settings: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... Application = \"MyApp\" });","title":"Application"},{"location":"logging-to-elmah-io-from-xamarin/#version","text":"The elmah.io integration for Xamarin will automatically use the package version for the Version field. To override this you can set the version string in settings: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... Version = \"1.0.2\" });","title":"Version"},{"location":"logging-to-elmah-io-from-xamarin/#decorating-all-messages","text":"All log messages logged through this integration can be decorated with the OnMessage action: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... OnMessage = msg => { msg.Source = \"Custom source\"; } });","title":"Decorating all messages"},{"location":"logging-to-elmah-io-from-xamarin/#filtering-log-messages","text":"Log messages can be filtered directly on the device to avoid specific log messages from being sent to elmah.io with the OnFilter function: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... OnFilter = msg => msg.Title.Contains(\"foo\") }); This code will automatically ignore all log messages with the text foo in the title.","title":"Filtering log messages"},{"location":"logging-to-elmah-io-from-xamarin/#handling-errors","text":"You may want to handle the scenario where the device cannot communicate with the elmah.io API. You can use the OnError action: ElmahIoXamarin.Init(new ElmahIoXamarinOptions { // ... OnError = (msg, ex) => { // Do something with ex } });","title":"Handling errors"},{"location":"logging-to-elmah-io-from-xamarin/#legacy-integration","text":"If you prefer you can configure elmah.io manually without the use of the Elmah.Io.Xamarin package. This is not the recommended way to integrate with elmah.io from Xamarin and this approach will be discontinued. Start by installing the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client If you are targeting a single platform, you can install the package directly in the startup project. If you are targeting multiple platforms, you can either install the package in all platform-specific projects or a shared project. Additional steps will vary from platform to platform. Android iOS Locate your main activity class and look for the OnCreate method. Here, you'd want to set up event handlers for when uncaught exceptions happen: protected override void OnCreate(Bundle savedInstanceState) { // ... AndroidEnvironment.UnhandledExceptionRaiser += AndroidEnvironment_UnhandledExceptionRaiser; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; // ... LoadApplication(new App()); etc. } Next, implement a method that can log an exception to elmah.io: private void LogExceptionToElmahIo(Exception exception) { if (exception == null) return; if (elmahIoClient == null) { elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); } var packageInfo = PackageManager.GetPackageInfo(PackageName, PackageInfoFlags.MetaData); var baseException = exception?.GetBaseException(); var errorMessage = baseException?.Message ?? \"Unhandled exception\"; try { elmahIoClient.Messages.Create(\"LOG_ID\", new CreateMessage { Data = exception?.ToDataList(), DateTime = DateTime.UtcNow, Detail = exception?.ToString(), Severity = \"Error\", Source = baseException?.Source, Title = errorMessage, Type = baseException?.GetType().FullName, Version = packageInfo.VersionName, Application = packageInfo.PackageName, }); } catch (Exception inner) { Android.Util.Log.Error(\"elmahio\", inner.Message); } // Log to Android Device Logging. Android.Util.Log.Error(\"crash\", errorMessage); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Finally, implement the three event handlers that we added in the first step: private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { LogExceptionToElmahIo(e.Exception); e.SetObserved(); } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { LogExceptionToElmahIo(e.ExceptionObject as Exception); } private void AndroidEnvironment_UnhandledExceptionRaiser(object sender, RaiseThrowableEventArgs e) { LogExceptionToElmahIo(e.Exception); e.Handled = true; } Locate your main application class and look for the Main method. Here, you'd want to set up event handlers for when uncaught exceptions happen: static void Main(string[] args) { // ... AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; } Next, implement a method that can log an exception to elmah.io: private static void LogExceptionToElmahIo(Exception exception) { if (exception == null) return; if (elmahIoClient == null) { elmahIoClient = ElmahioAPI.Create(\"API_KEY\"); } var baseException = exception?.GetBaseException(); elmahIoClient.Messages.Create(\"LOG_ID\", new CreateMessage { Data = exception?.ToDataList(), DateTime = DateTime.UtcNow, Detail = exception?.ToString(), Severity = \"Error\", Source = baseException?.Source, Title = baseException?.Message ?? \"Unhandled exception\", Type = baseException?.GetType().FullName, }); } Replace API_KEY with your API key ( Where is my API key? ) and LOG_ID ( Where is my log ID? ) with the log Id of the log you want to log to. Finally, implement the two event handlers that we added in the first step: private static void TaskScheduler_UnobservedTaskException( object sender, UnobservedTaskExceptionEventArgs e) { LogExceptionToElmahIo(e.Exception); e.SetObserved(); } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { LogExceptionToElmahIo(e.ExceptionObject as Exception); }","title":"Legacy integration"},{"location":"logging-to-multiple-elmah-logs/","text":"Logging to multiple ELMAH logs Unfortunately, ELMAH (the open source project) doesn't support multiple log targets like other logging frameworks like Serilog. This makes logging to multiple logs a bit tricky but in no way impossible. Let's say that you're using ELMAH in your web application and configured it to log everything in SQL Server. If you look through your web.config file, you will have code looking like this somewhere: <elmah> <errorLog type=\"Elmah.SqlErrorLog, Elmah\" connectionStringName=\"elmah\"/> </elmah> As you probably know, this tells ELMAH to log all unhandled errors in SQL Server with the connection string \u201celmah\u201d. You cannot add more <errorLog> elements, why logging into a second log seems impossible. Meet ELMAH's Logged event, which is a great hook to log to multiple targets. Install the Elmah.Io NuGet package and add the following code to your global.asax.cs file: void ErrorLog_Logged(object sender, Elmah.ErrorLoggedEventArgs args) { var elmahIoLog = new Elmah.Io.ErrorLog(ElmahioAPI.Create(\"API_KEY\"), new Guid(\"LOG_ID\")); elmahIoLog.Log(args.Entry.Error); } In the above code, we listen for the Logged event by simply declaring a method named ErrorLog_Logged . When called, we create a new (Elmah.Io.)ErrorLog instance with an IElmahioAPI object and the log ID. Remember to replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with your log ID ( Where is my log ID? ). You may want to share the ElmahioAPI object between requests by declaring it as a private member. Next, we simply call the Log method with a new Error object. Bam! The error is logged both in SQL Server and in elmah.io. If you only want to log certain types of errors in elmah.io, but everything to your normal log, you can extend your code like this: void ErrorLog_Logged(object sender, Elmah.ErrorLoggedEventArgs args) { if (args.Entry.Error.StatusCode == 500) { var elmahIoLog = new Elmah.Io.ErrorLog(/*...*/); elmahIoLog.Log(args.Entry.Error); } } This time we only begin logging to elmah.io, if the thrown exception is of type HttpException and contains an HTTP status code of 500 . This example only logs errors with status code 500 in elmah.io and all errors in your normal error log. If you want to create this filter on all logs, you should use the ErrorLog_Filtering method instead. This method is called before ErrorLog_Logged and before actually logging the error to your normal error log.","title":"Logging to multiple logs"},{"location":"logging-to-multiple-elmah-logs/#logging-to-multiple-elmah-logs","text":"Unfortunately, ELMAH (the open source project) doesn't support multiple log targets like other logging frameworks like Serilog. This makes logging to multiple logs a bit tricky but in no way impossible. Let's say that you're using ELMAH in your web application and configured it to log everything in SQL Server. If you look through your web.config file, you will have code looking like this somewhere: <elmah> <errorLog type=\"Elmah.SqlErrorLog, Elmah\" connectionStringName=\"elmah\"/> </elmah> As you probably know, this tells ELMAH to log all unhandled errors in SQL Server with the connection string \u201celmah\u201d. You cannot add more <errorLog> elements, why logging into a second log seems impossible. Meet ELMAH's Logged event, which is a great hook to log to multiple targets. Install the Elmah.Io NuGet package and add the following code to your global.asax.cs file: void ErrorLog_Logged(object sender, Elmah.ErrorLoggedEventArgs args) { var elmahIoLog = new Elmah.Io.ErrorLog(ElmahioAPI.Create(\"API_KEY\"), new Guid(\"LOG_ID\")); elmahIoLog.Log(args.Entry.Error); } In the above code, we listen for the Logged event by simply declaring a method named ErrorLog_Logged . When called, we create a new (Elmah.Io.)ErrorLog instance with an IElmahioAPI object and the log ID. Remember to replace API_KEY with your API key ( Where is my API key? ) and LOG_ID with your log ID ( Where is my log ID? ). You may want to share the ElmahioAPI object between requests by declaring it as a private member. Next, we simply call the Log method with a new Error object. Bam! The error is logged both in SQL Server and in elmah.io. If you only want to log certain types of errors in elmah.io, but everything to your normal log, you can extend your code like this: void ErrorLog_Logged(object sender, Elmah.ErrorLoggedEventArgs args) { if (args.Entry.Error.StatusCode == 500) { var elmahIoLog = new Elmah.Io.ErrorLog(/*...*/); elmahIoLog.Log(args.Entry.Error); } } This time we only begin logging to elmah.io, if the thrown exception is of type HttpException and contains an HTTP status code of 500 . This example only logs errors with status code 500 in elmah.io and all errors in your normal error log. If you want to create this filter on all logs, you should use the ErrorLog_Filtering method instead. This method is called before ErrorLog_Logged and before actually logging the error to your normal error log.","title":"Logging to multiple ELMAH logs"},{"location":"managing-environments/","text":"Managing Environments Environments is a grouping feature on elmah.io that will help you group similar logs. By default you will have four environments in your organization: Development, Test, Staging, and Production. While this is the default view, additional environments can be added or environments named differently. The names don't need to match environments if you prefer a different naming scheme. Other ways to use environments would be to group logs by customers, departments, or servers. When creating a new log from the elmah.io Dashboard, you can pick an environment in the dropdown: Once logs are added to environments, they are nicely grouped in the left menu and on the list of log boxes on the dashboard: Logs can be ordered using the drag-and-drop button at the top but only inside the same environment. If you need to move a log to another environment, click Edit (the pencil icon on the log box) and select the new environment from the dropdown. If you want to show logs within one or more specific environments on the dashboard, you can use the Environments dropdown: The list of environments can be managed from your organization settings page. To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard: On the organization settings page, select the Environments tab: The order of environments can be changed using drag and drop. New environments can be created and existing ones deleted.","title":"Managing Environments"},{"location":"managing-environments/#managing-environments","text":"Environments is a grouping feature on elmah.io that will help you group similar logs. By default you will have four environments in your organization: Development, Test, Staging, and Production. While this is the default view, additional environments can be added or environments named differently. The names don't need to match environments if you prefer a different naming scheme. Other ways to use environments would be to group logs by customers, departments, or servers. When creating a new log from the elmah.io Dashboard, you can pick an environment in the dropdown: Once logs are added to environments, they are nicely grouped in the left menu and on the list of log boxes on the dashboard: Logs can be ordered using the drag-and-drop button at the top but only inside the same environment. If you need to move a log to another environment, click Edit (the pencil icon on the log box) and select the new environment from the dropdown. If you want to show logs within one or more specific environments on the dashboard, you can use the Environments dropdown: The list of environments can be managed from your organization settings page. To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard: On the organization settings page, select the Environments tab: The order of environments can be changed using drag and drop. New environments can be created and existing ones deleted.","title":"Managing Environments"},{"location":"managing-organisations-and-users/","text":"Managing Organizations and Users Managing Organizations and Users Adding existing users to an organization Invite new users to an organization Control security Chances are that you are not the only one needing to access your logs. Luckily, elmah.io offers great features to manage the users in your organization and to specify who should be allowed access to what. This guide is also available as a short video tutorial here: To manage access, you will need to know about the concepts of users and organizations . A user represents a person wanting to access one or more logs. Each user logs in using a username/password or a social provider of choice. A user can be added to one or more organizations. Each user has an access level within the organization as well as an access level on each log. The access level on the organization and the logs doesn't need to be the same. An organization is a collection of users and their roles inside the organization. You will typically only need a single organization, representing all of the users in your company needing to access one or more logs on elmah.io. Your elmah.io subscription is attached to your organization and everyone with administrator access to the organization will be able to manage the subscription. Adding existing users to an organization To assign users to a log, you will need to add them to the organization first. When hovering the organization name in either the left menu or on the dashboard, you will see a small gear icon. When clicking the icon, you will be taken to the organization settings page: At first, the user creating the organization will be the only one on the list. To add a new user to the list, click the Add user button and input the user's email or name in the textbox. The dropdown will show a list of users on elmah.io matching your query. Each user needs to sign up on elmah.io before being visible through Add user . Jump to Invite new users to an organization to learn how to invite new users. When the new user is visible in the dropdown, click the user and select an access level. The chosen access level decides what the new user is allowed to do inside the organization. Read users are only allowed to view the organization, while Administrator users are allowed to add new users and delete the entire organization and all logs beneath it. The access level set for the user in the organization will become the user's access level on all new logs inside that organization as well. To change the access level on an added user, click one of the grouped buttons to the right of the user's name. Changing a user's access level on the organization won't change the user's access level on each log. To delete a user from the organization, click the red delete button to the far right. When a user is added to an organization, the user will automatically have access to all new logs created in that organization. For security reasons, a new user added to the organization, will not have access to existing logs in the organization. To assign the new user to existing logs, assign an access level on each log by clicking the settings button to the right of the user: Awarding a user Administrator on a log doesn't give them Administrator rights to the organization. To assign a user to all logs, click the None , Read , Write , or Administrator buttons in the table header above the list of logs. Likewise, organization administrators can assign users to different emails for each log in the organization: This view is similar to the one the user has on the profile when signing into elmah.io. Make sure to notify users when you assign emails to them to avoid them marking emails as spam. Invite new users to an organization If someone not already created as a user on elmah.io needs access to your organization, you can use the Invite feature. Inviting users will send them an email telling them to sign up for elmah.io and automatically add them to your organization. To invite a user click the Invite user button and input the new user's email. Select an organization access level and click the green Invite user button. This will add the new user to the organization and display it as \"Invited\" until the user signs up. Control security You may have requirements of using two-factor authentication or against using social sign-ins in your company. These requirements can be configured on elmah.io as well. Click the Security button above the user's list to set it up: Using this view you can allow or disallow sign-ins using: An elmah.io username and password Twitter Facebook Microsoft Google Notice that disallowing different sign-in types will still allow users in your organization to sign into elmah.io. As soon as a disallowed user type is trying to access pages inside the organization and/or logs a page telling them which sign-in type or required settings is shown.","title":"Managing Organisations and Users"},{"location":"managing-organisations-and-users/#managing-organizations-and-users","text":"Managing Organizations and Users Adding existing users to an organization Invite new users to an organization Control security Chances are that you are not the only one needing to access your logs. Luckily, elmah.io offers great features to manage the users in your organization and to specify who should be allowed access to what. This guide is also available as a short video tutorial here: To manage access, you will need to know about the concepts of users and organizations . A user represents a person wanting to access one or more logs. Each user logs in using a username/password or a social provider of choice. A user can be added to one or more organizations. Each user has an access level within the organization as well as an access level on each log. The access level on the organization and the logs doesn't need to be the same. An organization is a collection of users and their roles inside the organization. You will typically only need a single organization, representing all of the users in your company needing to access one or more logs on elmah.io. Your elmah.io subscription is attached to your organization and everyone with administrator access to the organization will be able to manage the subscription.","title":"Managing Organizations and Users"},{"location":"managing-organisations-and-users/#adding-existing-users-to-an-organization","text":"To assign users to a log, you will need to add them to the organization first. When hovering the organization name in either the left menu or on the dashboard, you will see a small gear icon. When clicking the icon, you will be taken to the organization settings page: At first, the user creating the organization will be the only one on the list. To add a new user to the list, click the Add user button and input the user's email or name in the textbox. The dropdown will show a list of users on elmah.io matching your query. Each user needs to sign up on elmah.io before being visible through Add user . Jump to Invite new users to an organization to learn how to invite new users. When the new user is visible in the dropdown, click the user and select an access level. The chosen access level decides what the new user is allowed to do inside the organization. Read users are only allowed to view the organization, while Administrator users are allowed to add new users and delete the entire organization and all logs beneath it. The access level set for the user in the organization will become the user's access level on all new logs inside that organization as well. To change the access level on an added user, click one of the grouped buttons to the right of the user's name. Changing a user's access level on the organization won't change the user's access level on each log. To delete a user from the organization, click the red delete button to the far right. When a user is added to an organization, the user will automatically have access to all new logs created in that organization. For security reasons, a new user added to the organization, will not have access to existing logs in the organization. To assign the new user to existing logs, assign an access level on each log by clicking the settings button to the right of the user: Awarding a user Administrator on a log doesn't give them Administrator rights to the organization. To assign a user to all logs, click the None , Read , Write , or Administrator buttons in the table header above the list of logs. Likewise, organization administrators can assign users to different emails for each log in the organization: This view is similar to the one the user has on the profile when signing into elmah.io. Make sure to notify users when you assign emails to them to avoid them marking emails as spam.","title":"Adding existing users to an organization"},{"location":"managing-organisations-and-users/#invite-new-users-to-an-organization","text":"If someone not already created as a user on elmah.io needs access to your organization, you can use the Invite feature. Inviting users will send them an email telling them to sign up for elmah.io and automatically add them to your organization. To invite a user click the Invite user button and input the new user's email. Select an organization access level and click the green Invite user button. This will add the new user to the organization and display it as \"Invited\" until the user signs up.","title":"Invite new users to an organization"},{"location":"managing-organisations-and-users/#control-security","text":"You may have requirements of using two-factor authentication or against using social sign-ins in your company. These requirements can be configured on elmah.io as well. Click the Security button above the user's list to set it up: Using this view you can allow or disallow sign-ins using: An elmah.io username and password Twitter Facebook Microsoft Google Notice that disallowing different sign-in types will still allow users in your organization to sign into elmah.io. As soon as a disallowed user type is trying to access pages inside the organization and/or logs a page telling them which sign-in type or required settings is shown.","title":"Control security"},{"location":"missing-server-side-information-on-uptime-errors/","text":"Missing server-side information on uptime errors To decorate uptime errors with server-side error information, you will need a few things: The monitored website should be configured to log errors to elmah.io. The uptime check needs to be created on the same log as the monitored website. When installed in a previous version, errors generated by Uptime Monitoring are ignored by the BotBuster app (since Uptime Monitoring is also a bot). Uninstall the BotBuster app and enable the Filter Crawlers filter instead.","title":"Missing server-side information on uptime errors"},{"location":"missing-server-side-information-on-uptime-errors/#missing-server-side-information-on-uptime-errors","text":"To decorate uptime errors with server-side error information, you will need a few things: The monitored website should be configured to log errors to elmah.io. The uptime check needs to be created on the same log as the monitored website. When installed in a previous version, errors generated by Uptime Monitoring are ignored by the BotBuster app (since Uptime Monitoring is also a bot). Uninstall the BotBuster app and enable the Filter Crawlers filter instead.","title":"Missing server-side information on uptime errors"},{"location":"query-messages-using-full-text-search/","text":"Query messages using full-text search All messages sent to elmah.io, are indexed in Elasticsearch. Storing messages in a database like Elasticsearch opens up a world of possibilities. This article explains how to query your log messages using full-text search, Search Filters, and Lucene Query Syntax. Query messages using full-text search Full-text search Search Filters Lucene Query Syntax Field specification Full-text search The easiest approach to start searching your log messages is by inputting search terms in the Search field on elmah.io: We don't want to get into too much detail on how full-text works in Elasticsearch. In short, Elasticsearch breaks the query into the terms nominavi and voluptatibus and tries to match all log messages including those terms. Full-text search work on analyzed fields in Elasticsearch, which means that wildcards and other constructs are fully supported. Full-text queries work great. when you want to do a quick search for some keywords like part of an exception message or stack trace. Remember that the entire log message is search, why a search for 500 would hit both log messages with status code 500 and the term 500 in the stack trace. Search Filters Search filters are built exclusively for elmah.io. They are built on top of Lucene Query Syntax (which we'll discuss in a minute), but much easier to write. Search filters are available through either the Add filter button below the search field or using various links and icons on the elmah.io UI. Let's say we want to find all errors with a status code of 500: Adding the two filters is possible using a few clicks. As mentioned previously, search filters are available throughout the UI too. In this example, a filter is used to find messages not matching a specified URL: Search filters can be used in combination with full-text queries for greater flexibility. Lucene Query Syntax Elasticsearch is implemented on top of Lucene; a high-performance search engine, written entirely in Java. While Elasticsearch supports a lot of nice abstractions on top of Lucene, sometimes you just want close to the metal. This is when we need to introduce you to Lucene Query Syntax. The query syntax is a query language similar to the WHERE part of a SQL statement. Unlike SQL, the query syntax supports both filters (similar to SQL) and full-text queries. All Lucene queries are made up of strings containing full-text search strings and/or terms combined with operators. A simple full-text query simply looks like this: values to search for This will search log messages for 'values', 'to', 'search', and 'for'. For exact searches you can use quotes: \"values to search for\" This will only find log messages where that exact string is present somewhere. Queries can also search inside specific fields: field:value This is similar to the WHERE part in SQL and will, in this example, search for the term 'value' inside the field named 'field'. Both full-text queries and field-based queries can be combined with other queries using AND , OR , and NOT : field1:value1 AND field2:value2 AND NOT field3:value3 You can use operators known from C# if you prefer that syntax: field1:value1 && field2:value2 && !field3:value3 Full-text and field-based queries can be combined into complex queries: field1:value1 && field2:\"exact value\" || (field1:value2 && \"a full text query\") Examples are worth a thousand words, why the rest of this document is examples of frequently used queries. If you think that examples are missing or have a problem with custom queries, let us know. We will extend this tutorial with the examples you need. Find messages with type type:\"System.Web.HttpException\" Find messages with status codes statusCode:[500 TO 599] Find messages with URL and method url:\"/tester/\" AND method:get Find messages with URL starting with url:\\/.well-known* The forward slash, in the beginning, needs to be escaped, since Lucene will understand it as the start of a regex otherwise. Find messages by IP remoteAddr:192.168.0.1 Find messages by IP's remoteAddr:192.68.0.* The examples above can be achieved using Search Filters as well. We recommend using Search Filters where possible and falling back to Lucene Query Syntax when something isn't supported through filters. An example is using OR which currently isn't possible using filters. Field specification As already illustrated through multiple examples in this document, all log messages consist of a range of fields. Here's a full overview of all fields, data types, etc. The .raw column requires a bit of explanation if you are not familiar with Elasticsearch. Fields that are marked as having .raw are indexed for full-text queries. This means that the values are tokenized and optimized for full-text search and not querying by exact values. In this case, a special raw field is created for supporting SQL like WHERE queries. To illustrate, searching for a part of a user agent would look like this: userAgent:chrome And searching for a specific user agent would look like this: userAgent.raw:\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36\" Here is the full set of fields: Name Type .raw Description applicationName string \u2705 Used to identify which application logged this message. You can use this if you have multiple applications and services logging into the same log. assignedTo string The id of the user assigned to the log message. elmah.io user IDs are not something that is published on the UI anywhere why this field is intended for the My errors dashboard only. browser string \u2705 A short string classifying each log message to a browser. The value can be one of the following: chrome, safari, edge, firefox, opera, ie, other. category string \u2705 The log message category. Category can be a string of choice but typically contain a logging category set by a logging framework like NLog or Serilog. When logging through a logging framework, this field will be provided by the framework and not something that needs to be set manually. correlationId string CorrelationId can be used to group similar log messages into a single discoverable batch. A correlation ID could be a session ID from ASP.NET Core, a unique string spanning multiple microservices handling the same request, or similar. country string An ISO 3166 two-letter country code in case we could resolve a country from the log message. detail string A longer description of the message. For errors, this could be a stack trace, but it's really up to you what to log in there. domain string The domain name if it could be resolved from the server variables. hash string A unique hash for a log message. The hash is used for multiple things on elmah.io like the new detection. hidden boolean A boolean indicating if a log message has been hidden through the UI, the API, or a hide rule. hostName string \u2705 The hostname of the server logging the message. isBot boolean A boolean indicating is a log message is generated by an automated bot or crawler. This flag is set manually through the UI. isBotSuggestion boolean A boolean indicating if the log message looks to be generated by an automated bot or crawler. Unlike the isBot field, this field is automatically set using machine learning and is available on the Enterprise plan only. isBurst boolean A boolean indicating if the log message is a burst. Log messages are automatically marked as burst if we have seen it more than 50 times during the retention period of the purchased plan. isFixed boolean A boolean indicating if the log message has been marked as fixed. Log messages can be marked as fixed from the UI. isHeartbeat boolean A boolean indicating if the log message is logged from the elmah.io Heartbeats feature. isNew boolean A boolean indicating if we have seen this unique log message before. isSpike boolean A boolean indicating if the log message is logged from the elmah.io Spike feature. isUptime boolean A boolean indicating if the log message is logged from the elmah.io Uptime Monitoring feature. message string \u2705 The textual title or headline of the message to log. messageTemplate string The title template of the message to log. This property can be used from logging frameworks that support structured logging like: \"{user} says {quote}\". In the example, titleTemplate will be this string and the title will be \"Gilfoyle says It's not magic. It's talent and sweat\". method string \u2705 If the log message relates to an HTTP request, you may send the HTTP method of that request. If you don't provide us with a method, we will try to find a key named REQUEST_METHOD in serverVariables. os string \u2705 A short string classifying each log message to an operating system. The value can be one of the following: ios, windows, android, macos, linux, other. remoteAddr string \u2705 The IP address of the user generating this log message if it can be resolved from server variables. severity string An enum value representing the severity of this message. The following values are allowed: Verbose, Debug, Information, Warning, Error, Fatal. source string \u2705 The source of the code logging the message. This could be the assembly name. statusCode number If the message logged relates to an HTTP status code, you can put the code in this property. This would probably only be relevant for errors but could be used for logging successful status codes as well. time date The date and time in UTC of the message. If you don't provide us with a value, this will be set the current date and time in UTC. type string \u2705 The type of message. If logging an error, the type of the exception would go into type but you can put anything in there, that makes sense for your domain. url string If the log message relates to an HTTP request, you may send the URL of that request. If you don't provide us with an URL, we will try to find a key named URL in serverVariables. user string \u2705 An identification of the user triggering this message. You can put the user's email address or your user key into this property. userAgent string \u2705 The user agent of the user causing the log message if it can be resolved from server variables. version string \u2705 Versions can be used to distinguish messages from different versions of your software. The value of the version can be a SemVer compliant string or any other syntax that you are using as your version numbering scheme.","title":"Query messages using full-text search"},{"location":"query-messages-using-full-text-search/#query-messages-using-full-text-search","text":"All messages sent to elmah.io, are indexed in Elasticsearch. Storing messages in a database like Elasticsearch opens up a world of possibilities. This article explains how to query your log messages using full-text search, Search Filters, and Lucene Query Syntax. Query messages using full-text search Full-text search Search Filters Lucene Query Syntax Field specification","title":"Query messages using full-text search"},{"location":"query-messages-using-full-text-search/#full-text-search","text":"The easiest approach to start searching your log messages is by inputting search terms in the Search field on elmah.io: We don't want to get into too much detail on how full-text works in Elasticsearch. In short, Elasticsearch breaks the query into the terms nominavi and voluptatibus and tries to match all log messages including those terms. Full-text search work on analyzed fields in Elasticsearch, which means that wildcards and other constructs are fully supported. Full-text queries work great. when you want to do a quick search for some keywords like part of an exception message or stack trace. Remember that the entire log message is search, why a search for 500 would hit both log messages with status code 500 and the term 500 in the stack trace.","title":"Full-text search"},{"location":"query-messages-using-full-text-search/#search-filters","text":"Search filters are built exclusively for elmah.io. They are built on top of Lucene Query Syntax (which we'll discuss in a minute), but much easier to write. Search filters are available through either the Add filter button below the search field or using various links and icons on the elmah.io UI. Let's say we want to find all errors with a status code of 500: Adding the two filters is possible using a few clicks. As mentioned previously, search filters are available throughout the UI too. In this example, a filter is used to find messages not matching a specified URL: Search filters can be used in combination with full-text queries for greater flexibility.","title":"Search Filters"},{"location":"query-messages-using-full-text-search/#lucene-query-syntax","text":"Elasticsearch is implemented on top of Lucene; a high-performance search engine, written entirely in Java. While Elasticsearch supports a lot of nice abstractions on top of Lucene, sometimes you just want close to the metal. This is when we need to introduce you to Lucene Query Syntax. The query syntax is a query language similar to the WHERE part of a SQL statement. Unlike SQL, the query syntax supports both filters (similar to SQL) and full-text queries. All Lucene queries are made up of strings containing full-text search strings and/or terms combined with operators. A simple full-text query simply looks like this: values to search for This will search log messages for 'values', 'to', 'search', and 'for'. For exact searches you can use quotes: \"values to search for\" This will only find log messages where that exact string is present somewhere. Queries can also search inside specific fields: field:value This is similar to the WHERE part in SQL and will, in this example, search for the term 'value' inside the field named 'field'. Both full-text queries and field-based queries can be combined with other queries using AND , OR , and NOT : field1:value1 AND field2:value2 AND NOT field3:value3 You can use operators known from C# if you prefer that syntax: field1:value1 && field2:value2 && !field3:value3 Full-text and field-based queries can be combined into complex queries: field1:value1 && field2:\"exact value\" || (field1:value2 && \"a full text query\") Examples are worth a thousand words, why the rest of this document is examples of frequently used queries. If you think that examples are missing or have a problem with custom queries, let us know. We will extend this tutorial with the examples you need. Find messages with type type:\"System.Web.HttpException\" Find messages with status codes statusCode:[500 TO 599] Find messages with URL and method url:\"/tester/\" AND method:get Find messages with URL starting with url:\\/.well-known* The forward slash, in the beginning, needs to be escaped, since Lucene will understand it as the start of a regex otherwise. Find messages by IP remoteAddr:192.168.0.1 Find messages by IP's remoteAddr:192.68.0.* The examples above can be achieved using Search Filters as well. We recommend using Search Filters where possible and falling back to Lucene Query Syntax when something isn't supported through filters. An example is using OR which currently isn't possible using filters.","title":"Lucene Query Syntax"},{"location":"query-messages-using-full-text-search/#field-specification","text":"As already illustrated through multiple examples in this document, all log messages consist of a range of fields. Here's a full overview of all fields, data types, etc. The .raw column requires a bit of explanation if you are not familiar with Elasticsearch. Fields that are marked as having .raw are indexed for full-text queries. This means that the values are tokenized and optimized for full-text search and not querying by exact values. In this case, a special raw field is created for supporting SQL like WHERE queries. To illustrate, searching for a part of a user agent would look like this: userAgent:chrome And searching for a specific user agent would look like this: userAgent.raw:\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36\" Here is the full set of fields: Name Type .raw Description applicationName string \u2705 Used to identify which application logged this message. You can use this if you have multiple applications and services logging into the same log. assignedTo string The id of the user assigned to the log message. elmah.io user IDs are not something that is published on the UI anywhere why this field is intended for the My errors dashboard only. browser string \u2705 A short string classifying each log message to a browser. The value can be one of the following: chrome, safari, edge, firefox, opera, ie, other. category string \u2705 The log message category. Category can be a string of choice but typically contain a logging category set by a logging framework like NLog or Serilog. When logging through a logging framework, this field will be provided by the framework and not something that needs to be set manually. correlationId string CorrelationId can be used to group similar log messages into a single discoverable batch. A correlation ID could be a session ID from ASP.NET Core, a unique string spanning multiple microservices handling the same request, or similar. country string An ISO 3166 two-letter country code in case we could resolve a country from the log message. detail string A longer description of the message. For errors, this could be a stack trace, but it's really up to you what to log in there. domain string The domain name if it could be resolved from the server variables. hash string A unique hash for a log message. The hash is used for multiple things on elmah.io like the new detection. hidden boolean A boolean indicating if a log message has been hidden through the UI, the API, or a hide rule. hostName string \u2705 The hostname of the server logging the message. isBot boolean A boolean indicating is a log message is generated by an automated bot or crawler. This flag is set manually through the UI. isBotSuggestion boolean A boolean indicating if the log message looks to be generated by an automated bot or crawler. Unlike the isBot field, this field is automatically set using machine learning and is available on the Enterprise plan only. isBurst boolean A boolean indicating if the log message is a burst. Log messages are automatically marked as burst if we have seen it more than 50 times during the retention period of the purchased plan. isFixed boolean A boolean indicating if the log message has been marked as fixed. Log messages can be marked as fixed from the UI. isHeartbeat boolean A boolean indicating if the log message is logged from the elmah.io Heartbeats feature. isNew boolean A boolean indicating if we have seen this unique log message before. isSpike boolean A boolean indicating if the log message is logged from the elmah.io Spike feature. isUptime boolean A boolean indicating if the log message is logged from the elmah.io Uptime Monitoring feature. message string \u2705 The textual title or headline of the message to log. messageTemplate string The title template of the message to log. This property can be used from logging frameworks that support structured logging like: \"{user} says {quote}\". In the example, titleTemplate will be this string and the title will be \"Gilfoyle says It's not magic. It's talent and sweat\". method string \u2705 If the log message relates to an HTTP request, you may send the HTTP method of that request. If you don't provide us with a method, we will try to find a key named REQUEST_METHOD in serverVariables. os string \u2705 A short string classifying each log message to an operating system. The value can be one of the following: ios, windows, android, macos, linux, other. remoteAddr string \u2705 The IP address of the user generating this log message if it can be resolved from server variables. severity string An enum value representing the severity of this message. The following values are allowed: Verbose, Debug, Information, Warning, Error, Fatal. source string \u2705 The source of the code logging the message. This could be the assembly name. statusCode number If the message logged relates to an HTTP status code, you can put the code in this property. This would probably only be relevant for errors but could be used for logging successful status codes as well. time date The date and time in UTC of the message. If you don't provide us with a value, this will be set the current date and time in UTC. type string \u2705 The type of message. If logging an error, the type of the exception would go into type but you can put anything in there, that makes sense for your domain. url string If the log message relates to an HTTP request, you may send the URL of that request. If you don't provide us with an URL, we will try to find a key named URL in serverVariables. user string \u2705 An identification of the user triggering this message. You can put the user's email address or your user key into this property. userAgent string \u2705 The user agent of the user causing the log message if it can be resolved from server variables. version string \u2705 Versions can be used to distinguish messages from different versions of your software. The value of the version can be a SemVer compliant string or any other syntax that you are using as your version numbering scheme.","title":"Field specification"},{"location":"remove-sensitive-form-data/","text":"Remove sensitive form data You may have something like usernames and passwords in form posts on your website. Since elmah.io automatically logs the content of a failing form POST, sensitive data potentially ends up in your log. No one else but you and your company should get to look inside your log, but remember that everyone connected to the Internet, is a potential hacking victim. In this example, we hide the value of a form value named SomeSecretFormField . Add the following code in the Application_Start method in the global.asax.cs file: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = Elmah.Io.ErrorLog.Client; logger.OnMessage += (sender, args) => { var form = args.Message.Form.FirstOrDefault(f => f.Key == \"SomeSecretFormField\"); if (form != null) { form.Value = \"***hidden***\"; } };","title":"Remove sensitive form data"},{"location":"remove-sensitive-form-data/#remove-sensitive-form-data","text":"You may have something like usernames and passwords in form posts on your website. Since elmah.io automatically logs the content of a failing form POST, sensitive data potentially ends up in your log. No one else but you and your company should get to look inside your log, but remember that everyone connected to the Internet, is a potential hacking victim. In this example, we hide the value of a form value named SomeSecretFormField . Add the following code in the Application_Start method in the global.asax.cs file: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = Elmah.Io.ErrorLog.Client; logger.OnMessage += (sender, args) => { var form = args.Message.Form.FirstOrDefault(f => f.Key == \"SomeSecretFormField\"); if (form != null) { form.Value = \"***hidden***\"; } };","title":"Remove sensitive form data"},{"location":"roslyn-analyzers-for-elmah-io-and-aspnet-core/","text":"Roslyn analyzers for elmah.io and ASP.NET Core Roslyn analyzers for elmah.io and ASP.NET Core Installation and usage EIO1000 ConfigureServices must call AddElmahIo EIO1001 Configure must call UseElmahIo EIO1002 UseElmahIo must be called before/after Use* The Roslyn analyzers for elmah.io and ASP.NET Core has reached end of life. The analyzers are no longer updated and won't work for top-level statements or when configuring Elmah.Io.AspNetCore in the Program.cs file. To validate the installation we recommend running the diagnose command as explained here: Diagnose potential problems with an elmah.io installation . To help to install elmah.io in ASP.NET Core (by using the Elmah.Io.AspNetCore NuGet package) we have developed a range of Roslyn analyzers. Analyzers run inside Visual Studio and make it possible to validate your Startup.cs file during development. Installation and usage The analyzers can be installed in two ways. As a NuGet package or a Visual Studio extension. To install it from NuGet: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.Analyzers dotnet add package Elmah.Io.AspNetCore.Analyzers <PackageReference Include=\"Elmah.Io.AspNetCore.Analyzers\" Version=\"0.*\" /> paket add Elmah.Io.AspNetCore.Analyzers The package is installed as a private asset, which means that it is not distributed as part of your build. You can keep the package installed after you have used it to inspect any warnings generated or uninstall it. To install it as a Visual Studio extension, navigate to Extensions | Manage extensions | Online and search for Elmah.Io.AspNetCore.Analyzers . Then click the Download button and restart Visual Studio. As an alternative, you can download the extension directly from the Visual Studio Marketplace. Once installed, analyzers will help you add or move elmah.io-related setup code: All issues are listed as warnings in the Error list as well. The following is an explanation of possible warnings. EIO1000 ConfigureServices must call AddElmahIo AddElmahIo needs to be added as part of the ConfigureServices method: public void ConfigureServices(IServiceCollection services) { services.AddElmahIo(/*...*/); //\ud83d\udc48 } EIO1001 Configure must call UseElmahIo UseElmahIo needs to be added as part of the Configure method: public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseElmahIo(); //\ud83d\udc48 } EIO1002 UseElmahIo must be called before/after Use* UseElmahIo needs to be called after any calls to UseDeveloperExceptionPage , UseExceptionHandler , UseAuthorization , and UseAuthentication but before any calls to UseEndpoints and UseMvc : public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(/*...*/); } app.UseAuthentication(); app.UseAuthorization(); app.UseElmahIo(); //\ud83d\udc48 app.UseEndpoints(); app.UseMvc(/*...*/); }","title":"Roslyn analyzers for elmah.io and ASP.NET Core"},{"location":"roslyn-analyzers-for-elmah-io-and-aspnet-core/#roslyn-analyzers-for-elmahio-and-aspnet-core","text":"Roslyn analyzers for elmah.io and ASP.NET Core Installation and usage EIO1000 ConfigureServices must call AddElmahIo EIO1001 Configure must call UseElmahIo EIO1002 UseElmahIo must be called before/after Use* The Roslyn analyzers for elmah.io and ASP.NET Core has reached end of life. The analyzers are no longer updated and won't work for top-level statements or when configuring Elmah.Io.AspNetCore in the Program.cs file. To validate the installation we recommend running the diagnose command as explained here: Diagnose potential problems with an elmah.io installation . To help to install elmah.io in ASP.NET Core (by using the Elmah.Io.AspNetCore NuGet package) we have developed a range of Roslyn analyzers. Analyzers run inside Visual Studio and make it possible to validate your Startup.cs file during development.","title":"Roslyn analyzers for elmah.io and ASP.NET Core"},{"location":"roslyn-analyzers-for-elmah-io-and-aspnet-core/#installation-and-usage","text":"The analyzers can be installed in two ways. As a NuGet package or a Visual Studio extension. To install it from NuGet: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.AspNetCore.Analyzers dotnet add package Elmah.Io.AspNetCore.Analyzers <PackageReference Include=\"Elmah.Io.AspNetCore.Analyzers\" Version=\"0.*\" /> paket add Elmah.Io.AspNetCore.Analyzers The package is installed as a private asset, which means that it is not distributed as part of your build. You can keep the package installed after you have used it to inspect any warnings generated or uninstall it. To install it as a Visual Studio extension, navigate to Extensions | Manage extensions | Online and search for Elmah.Io.AspNetCore.Analyzers . Then click the Download button and restart Visual Studio. As an alternative, you can download the extension directly from the Visual Studio Marketplace. Once installed, analyzers will help you add or move elmah.io-related setup code: All issues are listed as warnings in the Error list as well. The following is an explanation of possible warnings.","title":"Installation and usage"},{"location":"roslyn-analyzers-for-elmah-io-and-aspnet-core/#eio1000-configureservices-must-call-addelmahio","text":"AddElmahIo needs to be added as part of the ConfigureServices method: public void ConfigureServices(IServiceCollection services) { services.AddElmahIo(/*...*/); //\ud83d\udc48 }","title":"EIO1000 ConfigureServices must call AddElmahIo"},{"location":"roslyn-analyzers-for-elmah-io-and-aspnet-core/#eio1001-configure-must-call-useelmahio","text":"UseElmahIo needs to be added as part of the Configure method: public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseElmahIo(); //\ud83d\udc48 }","title":"EIO1001 Configure must call UseElmahIo"},{"location":"roslyn-analyzers-for-elmah-io-and-aspnet-core/#eio1002-useelmahio-must-be-called-beforeafter-use","text":"UseElmahIo needs to be called after any calls to UseDeveloperExceptionPage , UseExceptionHandler , UseAuthorization , and UseAuthentication but before any calls to UseEndpoints and UseMvc : public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(/*...*/); } app.UseAuthentication(); app.UseAuthorization(); app.UseElmahIo(); //\ud83d\udc48 app.UseEndpoints(); app.UseMvc(/*...*/); }","title":"EIO1002 UseElmahIo must be called before/after Use*"},{"location":"setting-application-name/","text":"Setting application name If logging to the same log from multiple applications, it might be a good idea to set an application name on all log messages. This will let you search for errors generated by a specific application through the elmah.io UI. Also, ELMAH automatically sets the application name to the Guid of the application inside IIS, why it often looks better to either clear this or set something meaningful. The application name can be easily set from the web.config file as part of the elmah.io configuration: <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" applicationName=\"MyApp\" /> This will decorate all errors logged to elmah.io with the application name MyApp . A common use of Web.config Transformations is to have a transformation per environment, customer, application, etc. If this is the case, you can replace the application name as part of the config transformation. Check out Use multiple logs for different environments for more information about how to update the errorLog element. Our Web.config transformations - The definitive syntax guide contains general information about how transformations work and you can use the Web.config Transformation Tester to validate transformation files. If you, for some reason, cannot set the application name in the web.config file, it can be specified from C# by including the following code in the Application_Start method in the global.asax.cs file: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = Elmah.Io.ErrorLog.Client; logger.OnMessage += (sender, args) => { args.Message.Application = \"MyApp\"; };","title":"Setting application name"},{"location":"setting-application-name/#setting-application-name","text":"If logging to the same log from multiple applications, it might be a good idea to set an application name on all log messages. This will let you search for errors generated by a specific application through the elmah.io UI. Also, ELMAH automatically sets the application name to the Guid of the application inside IIS, why it often looks better to either clear this or set something meaningful. The application name can be easily set from the web.config file as part of the elmah.io configuration: <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" applicationName=\"MyApp\" /> This will decorate all errors logged to elmah.io with the application name MyApp . A common use of Web.config Transformations is to have a transformation per environment, customer, application, etc. If this is the case, you can replace the application name as part of the config transformation. Check out Use multiple logs for different environments for more information about how to update the errorLog element. Our Web.config transformations - The definitive syntax guide contains general information about how transformations work and you can use the Web.config Transformation Tester to validate transformation files. If you, for some reason, cannot set the application name in the web.config file, it can be specified from C# by including the following code in the Application_Start method in the global.asax.cs file: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = Elmah.Io.ErrorLog.Client; logger.OnMessage += (sender, args) => { args.Message.Application = \"MyApp\"; };","title":"Setting application name"},{"location":"setup-deployment-tracking/","text":"Set Up Deployment Tracking Set Up Deployment Tracking Generate a new API key Tell elmah.io when you release Decorate your messages with a version number Versioning Different Services Enable/Disable logging while deploying Deployment tracking creates an overview of the different versions of your software and shows you how well each version performed. With this integration in place, you will be able to see when you released and if some of your releases caused more errors than others. While most pages on elmah.io support everything from verbose to fatal messages, the context on deployment tracking is around warnings and errors. To set up deployment tracking, you will need to tell elmah.io when you release, using our REST API or one of the integrations: PowerShell Octopus Deploy Azure DevOps GitHub Actions Umbraco Cloud Bitbucket Pipelines Bamboo Deployments are as default created on all of your logs, but this can be tweaked. More about this later. For a complete overview of the possibilities with deployment tracking, watch this video tutorial: Generate a new API key While you can use the same API key for everything, we recommend you create an API key specific to deployment tracking. To do so, go to the organization settings page by clicking the gears icon next to the organization name on either the dashboard or in the left menu. Select the API Keys tabs and click Add API Key . Fill in a name of choice and enable the Deployments > Write permission only: Click the save button and copy the API key for later use. Tell elmah.io when you release When you create a release of your software either manually or with the help of a tool like Octopus, you need to tell elmah.io about it. The elmah.io REST API v3 provides an endpoint named deployments , which you can call when creating releases. After calling the endpoint, all new messages to your logs will automatically be decorated with the most recent version number. If you release your software manually, creating the new release manually is easy using Swagger UI. Swagger UI is a graphical client for calling a Swagger-enabled endpoint (much like Postman). Navigate to https://api.elmah.io/swagger/index.html , expand the Deployments node and click the POST request: To create the release, input your API key ( Where is my API key? ) in the top right corner and click the JSON beneath Model Schema . This copies the example JSON to the deployment parameter. A minimal deployment would look like the following, but adding more information makes the experience within elmah.io even better: { \"version\": \"1.42.7\" } The version string in the example conforms to SemVer, but the content can be anything. The date of the release is automatically added if not specified in the JSON body. Click the Try it out! button and the deployment is created. We support a range of different integrations to avoid you manually having to use Swagger UI every time you release. Click one of the products below for instructions: PowerShell Octopus Deploy Kudu Azure DevOps Pipelines Azure DevOps Releases GitHub Actions Umbraco Cloud Bitbucket Pipelines Atlassian Bamboo Decorate your messages with a version number As default, all messages are decorated with the most recent deployment version. If you want to override this behavior, check out Adding Version Information for details. Versioning Different Services Chances are that your software consists of multiple services released independently and with different version numbers. This is a common pattern when splitting up a software system in microservices. How you choose to split your elmah.io logs is entirely up to you, but we almost always recommend having a separate log for each service. When doing so, you only want deployment tracking to show the releases from the service you are currently looking at. The problem here is that deployments on elmah.io are shown on all logs as default. To make sure that only deployments related to the service you are looking at are shown, you need to decorate each deployment with the log ID where it belongs. The deployments API supports this through an optional logId property. If set, the new deployment is only shown on the specified log. Enable/Disable logging while deploying Some websites and services are not built to work properly while deploying a new version. This may cause errors logged from Uptime Monitoring and similar while you deploy a new version of your software. In this case, you might consider disabling logging while deploying and enable logging once the new version is deployed. Disabling logging can be done in two ways: Through the elmah.io UI on the log settings page: By calling the Disable and Enable endpoints on the API (either manually or automatically). Powershell examples for reference: # Disable logging Invoke-RestMethod -Method Post -Uri 'https://api.elmah.io/v3/logs/LOG_ID/_disable?api_key=API_KEY' # Enable logging Invoke-RestMethod -Method Post -Uri 'https://api.elmah.io/v3/logs/LOG_ID/_enable?api_key=API_KEY'","title":"Set up Deployment Tracking"},{"location":"setup-deployment-tracking/#set-up-deployment-tracking","text":"Set Up Deployment Tracking Generate a new API key Tell elmah.io when you release Decorate your messages with a version number Versioning Different Services Enable/Disable logging while deploying Deployment tracking creates an overview of the different versions of your software and shows you how well each version performed. With this integration in place, you will be able to see when you released and if some of your releases caused more errors than others. While most pages on elmah.io support everything from verbose to fatal messages, the context on deployment tracking is around warnings and errors. To set up deployment tracking, you will need to tell elmah.io when you release, using our REST API or one of the integrations: PowerShell Octopus Deploy Azure DevOps GitHub Actions Umbraco Cloud Bitbucket Pipelines Bamboo Deployments are as default created on all of your logs, but this can be tweaked. More about this later. For a complete overview of the possibilities with deployment tracking, watch this video tutorial:","title":"Set Up Deployment Tracking"},{"location":"setup-deployment-tracking/#generate-a-new-api-key","text":"While you can use the same API key for everything, we recommend you create an API key specific to deployment tracking. To do so, go to the organization settings page by clicking the gears icon next to the organization name on either the dashboard or in the left menu. Select the API Keys tabs and click Add API Key . Fill in a name of choice and enable the Deployments > Write permission only: Click the save button and copy the API key for later use.","title":"Generate a new API key"},{"location":"setup-deployment-tracking/#tell-elmahio-when-you-release","text":"When you create a release of your software either manually or with the help of a tool like Octopus, you need to tell elmah.io about it. The elmah.io REST API v3 provides an endpoint named deployments , which you can call when creating releases. After calling the endpoint, all new messages to your logs will automatically be decorated with the most recent version number. If you release your software manually, creating the new release manually is easy using Swagger UI. Swagger UI is a graphical client for calling a Swagger-enabled endpoint (much like Postman). Navigate to https://api.elmah.io/swagger/index.html , expand the Deployments node and click the POST request: To create the release, input your API key ( Where is my API key? ) in the top right corner and click the JSON beneath Model Schema . This copies the example JSON to the deployment parameter. A minimal deployment would look like the following, but adding more information makes the experience within elmah.io even better: { \"version\": \"1.42.7\" } The version string in the example conforms to SemVer, but the content can be anything. The date of the release is automatically added if not specified in the JSON body. Click the Try it out! button and the deployment is created. We support a range of different integrations to avoid you manually having to use Swagger UI every time you release. Click one of the products below for instructions: PowerShell Octopus Deploy Kudu Azure DevOps Pipelines Azure DevOps Releases GitHub Actions Umbraco Cloud Bitbucket Pipelines Atlassian Bamboo","title":"Tell elmah.io when you release"},{"location":"setup-deployment-tracking/#decorate-your-messages-with-a-version-number","text":"As default, all messages are decorated with the most recent deployment version. If you want to override this behavior, check out Adding Version Information for details.","title":"Decorate your messages with a version number"},{"location":"setup-deployment-tracking/#versioning-different-services","text":"Chances are that your software consists of multiple services released independently and with different version numbers. This is a common pattern when splitting up a software system in microservices. How you choose to split your elmah.io logs is entirely up to you, but we almost always recommend having a separate log for each service. When doing so, you only want deployment tracking to show the releases from the service you are currently looking at. The problem here is that deployments on elmah.io are shown on all logs as default. To make sure that only deployments related to the service you are looking at are shown, you need to decorate each deployment with the log ID where it belongs. The deployments API supports this through an optional logId property. If set, the new deployment is only shown on the specified log.","title":"Versioning Different Services"},{"location":"setup-deployment-tracking/#enabledisable-logging-while-deploying","text":"Some websites and services are not built to work properly while deploying a new version. This may cause errors logged from Uptime Monitoring and similar while you deploy a new version of your software. In this case, you might consider disabling logging while deploying and enable logging once the new version is deployed. Disabling logging can be done in two ways: Through the elmah.io UI on the log settings page: By calling the Disable and Enable endpoints on the API (either manually or automatically). Powershell examples for reference: # Disable logging Invoke-RestMethod -Method Post -Uri 'https://api.elmah.io/v3/logs/LOG_ID/_disable?api_key=API_KEY' # Enable logging Invoke-RestMethod -Method Post -Uri 'https://api.elmah.io/v3/logs/LOG_ID/_enable?api_key=API_KEY'","title":"Enable/Disable logging while deploying"},{"location":"setup-heartbeats/","text":"Set up Heartbeats Set up Heartbeats Additional properties Reason Application and Version Took elmah.io Heartbeats complements the Error Logging and Uptime Monitoring features already available on elmah.io. Where Uptime Monitoring is based on us pinging your public HTTP endpoints, Heartbeats is the other way around. When configured, your services, scheduled tasks, and websites ping the elmah.io in a specified interval. We call these ping Heartbeats, hence the name of the feature. Whether you should use Uptime Monitoring or Heartbeats to monitor your code, depends on a range of variables. Uptime Monitoring is great at making sure that your public endpoints can be reached from multiple locations. Scheduled tasks and services typically don't have public endpoints and are expected to run at a specified interval. With Heartbeats, setting up monitoring on these kinds of services is extremely easy, since elmah.io will automatically detect when an unhealthy heartbeat is received or if no heartbeat is received. Click one of the integrations below or continue reading to learn more about Heartbeats: ASP.NET Core Functions Isolated Functions PowerShell cURL Umbraco Hangfire Worker Services AWS Lambda Scheduled Tasks To better understand Heartbeats, let's create a simple example. For detailed instructions on how to set up Heartbeats in different languages and frameworks, select one of the specific articles in the left menu. In this example, we will extend a C# console application, executed as a Windows Scheduled task, with a heartbeat. The scheduled task is run every 30 minutes. Open a log on elmah.io and navigate to the Heartbeats tab: Click the Add Heartbeat button and fill in a name. For Interval we are selecting 30 minutes since the task is scheduled to run every 30 minutes. For Grace , we select 5 minutes to give the task a chance to complete. Selecting 30 and 5 minutes means that elmah.io will log an error if more than 35 minutes pass since we last heard from the task: To create heartbeats from our task, we will need an API key, a log ID, and a heartbeat ID. Let's start with the API key. Go to the organization settings page and click the API Keys tab. Add a new API key and check the Heartbeats - Write permission only: Copy and store the API key somewhere. Navigate back to your log and click the Instructions link on the newly created Heartbeat. This will reveal the log ID and heartbeat ID. Copy and store both values since we will need them in a minute. Time to do the integration. Like mentioned before, there are multiple ways of invoking the API. For this example, we'll use C#. Install the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Extend your C# with the following code: using Elmah.Io.Client; public class Program { public static void Main() { var logId = new Guid(\"LOG_ID\"); var api = ElmahioAPI.Create(\"API_KEY\"); try { // Your task code goes here api.Heartbeats.Healthy(logId, \"HEARTBEAT_ID\"); } catch (Exception e) { api.Heartbeats.Unhealthy(logId, \"HEARTBEAT_ID\"); } } } Replace LOG_ID , API_KEY , and HEARTBEAT_ID with the values stored in the previous steps. When the code runs without throwing an exception, your task now creates a Healthy heartbeat. If an exception occurs, the code creates an Unhealthy heartbeat and uses the exception text as the reason. There's an additional method named Degraded for logging a degraded heartbeat. Depending on the heartbeat status, a log message can be created in the configured log. Log messages are only created on state changes. This means that if logging two Unhealthy requests, only the first request triggers a new error. If logging a Healthy heartbeat after logging an Unhealthy heartbeat, an information message will be logged. Transitioning to Degraded logs a warning. Additional properties Reason The Healthy , Unhealthy , and Degraded methods (or the CreateHeartbeat class when using the raw Create method) accept an additional parameter named reason . reason can be used to specify why a heartbeat check is either Degraded or Unhealthy . If your service throws an exception, the full exception including its stack trace is a good candidate for the reason parameter. When using integrations like the one with ASP.NET Core Health Checks, the health check report is used as the reason for the failing heartbeat. Application and Version When logging errors through one or more of the integrations, you may already use the Application and/or Version fields to set an application name and software version on all messages logged to elmah.io. Since Heartbeats will do the actual logging of messages, in this case, you can configure it to use the same application name and/or version number as your remaining integrations. api.Heartbeats.Unhealthy(logId, \"HEARTBEAT_ID\", application: \"MyApp\", version: \"1.0.0\"); If an application name is not configured, all messages logged from Heartbeats will get a default value of Heartbeats . If no version number is configured, log messages from Heartbeats will be assigned the latest version created through Deployment Tracking . Took A single performance metric named Took can be logged alongside a heartbeat. The value should be the elapsed time in milliseconds for the job, scheduled task, or whatever code resulting in the heartbeat. For a scheduled task, the Took value would be the time from the scheduled task start until the task ends: var stopwatch = new Stopwatch(); stopwatch.Start(); // run job stopwatch.Stop(); heartbeats.Healthy( logId, heartbeatId, took: stopwatch.ElapsedMilliseconds); The value of the Took property is shown in the History modal on the Heartbeats page on elmah.io.","title":"Set up Heartbeats"},{"location":"setup-heartbeats/#set-up-heartbeats","text":"Set up Heartbeats Additional properties Reason Application and Version Took elmah.io Heartbeats complements the Error Logging and Uptime Monitoring features already available on elmah.io. Where Uptime Monitoring is based on us pinging your public HTTP endpoints, Heartbeats is the other way around. When configured, your services, scheduled tasks, and websites ping the elmah.io in a specified interval. We call these ping Heartbeats, hence the name of the feature. Whether you should use Uptime Monitoring or Heartbeats to monitor your code, depends on a range of variables. Uptime Monitoring is great at making sure that your public endpoints can be reached from multiple locations. Scheduled tasks and services typically don't have public endpoints and are expected to run at a specified interval. With Heartbeats, setting up monitoring on these kinds of services is extremely easy, since elmah.io will automatically detect when an unhealthy heartbeat is received or if no heartbeat is received. Click one of the integrations below or continue reading to learn more about Heartbeats: ASP.NET Core Functions Isolated Functions PowerShell cURL Umbraco Hangfire Worker Services AWS Lambda Scheduled Tasks To better understand Heartbeats, let's create a simple example. For detailed instructions on how to set up Heartbeats in different languages and frameworks, select one of the specific articles in the left menu. In this example, we will extend a C# console application, executed as a Windows Scheduled task, with a heartbeat. The scheduled task is run every 30 minutes. Open a log on elmah.io and navigate to the Heartbeats tab: Click the Add Heartbeat button and fill in a name. For Interval we are selecting 30 minutes since the task is scheduled to run every 30 minutes. For Grace , we select 5 minutes to give the task a chance to complete. Selecting 30 and 5 minutes means that elmah.io will log an error if more than 35 minutes pass since we last heard from the task: To create heartbeats from our task, we will need an API key, a log ID, and a heartbeat ID. Let's start with the API key. Go to the organization settings page and click the API Keys tab. Add a new API key and check the Heartbeats - Write permission only: Copy and store the API key somewhere. Navigate back to your log and click the Instructions link on the newly created Heartbeat. This will reveal the log ID and heartbeat ID. Copy and store both values since we will need them in a minute. Time to do the integration. Like mentioned before, there are multiple ways of invoking the API. For this example, we'll use C#. Install the Elmah.Io.Client NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io.Client dotnet add package Elmah.Io.Client <PackageReference Include=\"Elmah.Io.Client\" Version=\"5.*\" /> paket add Elmah.Io.Client Extend your C# with the following code: using Elmah.Io.Client; public class Program { public static void Main() { var logId = new Guid(\"LOG_ID\"); var api = ElmahioAPI.Create(\"API_KEY\"); try { // Your task code goes here api.Heartbeats.Healthy(logId, \"HEARTBEAT_ID\"); } catch (Exception e) { api.Heartbeats.Unhealthy(logId, \"HEARTBEAT_ID\"); } } } Replace LOG_ID , API_KEY , and HEARTBEAT_ID with the values stored in the previous steps. When the code runs without throwing an exception, your task now creates a Healthy heartbeat. If an exception occurs, the code creates an Unhealthy heartbeat and uses the exception text as the reason. There's an additional method named Degraded for logging a degraded heartbeat. Depending on the heartbeat status, a log message can be created in the configured log. Log messages are only created on state changes. This means that if logging two Unhealthy requests, only the first request triggers a new error. If logging a Healthy heartbeat after logging an Unhealthy heartbeat, an information message will be logged. Transitioning to Degraded logs a warning.","title":"Set up Heartbeats"},{"location":"setup-heartbeats/#additional-properties","text":"","title":"Additional properties"},{"location":"setup-heartbeats/#reason","text":"The Healthy , Unhealthy , and Degraded methods (or the CreateHeartbeat class when using the raw Create method) accept an additional parameter named reason . reason can be used to specify why a heartbeat check is either Degraded or Unhealthy . If your service throws an exception, the full exception including its stack trace is a good candidate for the reason parameter. When using integrations like the one with ASP.NET Core Health Checks, the health check report is used as the reason for the failing heartbeat.","title":"Reason"},{"location":"setup-heartbeats/#application-and-version","text":"When logging errors through one or more of the integrations, you may already use the Application and/or Version fields to set an application name and software version on all messages logged to elmah.io. Since Heartbeats will do the actual logging of messages, in this case, you can configure it to use the same application name and/or version number as your remaining integrations. api.Heartbeats.Unhealthy(logId, \"HEARTBEAT_ID\", application: \"MyApp\", version: \"1.0.0\"); If an application name is not configured, all messages logged from Heartbeats will get a default value of Heartbeats . If no version number is configured, log messages from Heartbeats will be assigned the latest version created through Deployment Tracking .","title":"Application and Version"},{"location":"setup-heartbeats/#took","text":"A single performance metric named Took can be logged alongside a heartbeat. The value should be the elapsed time in milliseconds for the job, scheduled task, or whatever code resulting in the heartbeat. For a scheduled task, the Took value would be the time from the scheduled task start until the task ends: var stopwatch = new Stopwatch(); stopwatch.Start(); // run job stopwatch.Stop(); heartbeats.Healthy( logId, heartbeatId, took: stopwatch.ElapsedMilliseconds); The value of the Took property is shown in the History modal on the Heartbeats page on elmah.io.","title":"Took"},{"location":"setup-uptime-monitoring/","text":"Set Up Uptime Monitoring Set Up Uptime Monitoring Uptime checks SSL certificate expiration checks Domain name expiration checks Lighthouse checks Canonical checks elmah.io Uptime Monitoring is the perfect companion for error logging. When your websites log errors, you are notified through elmah.io. But in the case where your website doesn't even respond to web requests, you will need something else to tell you that something is wrong. This is where Uptime Monitoring comes in. When set up, uptime monitoring automatically pings your websites from 5 different locations every 5 minutes. For a complete overview of the possibilities with uptime monitoring, watch this video tutorial: Uptime checks Uptime checks are automatic HTTP requests that you may already know from Azure, Pingdom, or a similar service. Uptime checks are created from the Uptime tab, directly on each log: SSL certificate expiration checks Expiring SSL certificates cause errors in your user's browser. If you ever tried forgetting to renew an SSL certificate, you know how many problems it can cause. With the SSL check option available when creating a new uptime check, elmah.io automatically validates your SSL certificates daily. When your SSL certificate is up for renewal, we start notifying you through the error logs. Domain name expiration checks Much like SSL checks, Domain name expiration checks, will notify you through your log when your domain names are about to expire. To enable this feature, enable the Domain Expiration toggle when creating a new uptime check. Lighthouse checks When enabling Lighthouse on an uptime check, we will generate Lighthouse results every night and include them on the history tab. Lighthouse is a popular tool for improving websites by running through a range of checks spread across multiple categories like performance and accessibility. Canonical checks Canonical checks are a range of checks implemented by us. It checks the URL of the uptime checks by running through a range of different best practices. Examples are HTTP to HTTPS redirect, correct use of redirect status code, and more checks.","title":"Set up Uptime Monitoring"},{"location":"setup-uptime-monitoring/#set-up-uptime-monitoring","text":"Set Up Uptime Monitoring Uptime checks SSL certificate expiration checks Domain name expiration checks Lighthouse checks Canonical checks elmah.io Uptime Monitoring is the perfect companion for error logging. When your websites log errors, you are notified through elmah.io. But in the case where your website doesn't even respond to web requests, you will need something else to tell you that something is wrong. This is where Uptime Monitoring comes in. When set up, uptime monitoring automatically pings your websites from 5 different locations every 5 minutes. For a complete overview of the possibilities with uptime monitoring, watch this video tutorial:","title":"Set Up Uptime Monitoring"},{"location":"setup-uptime-monitoring/#uptime-checks","text":"Uptime checks are automatic HTTP requests that you may already know from Azure, Pingdom, or a similar service. Uptime checks are created from the Uptime tab, directly on each log:","title":"Uptime checks"},{"location":"setup-uptime-monitoring/#ssl-certificate-expiration-checks","text":"Expiring SSL certificates cause errors in your user's browser. If you ever tried forgetting to renew an SSL certificate, you know how many problems it can cause. With the SSL check option available when creating a new uptime check, elmah.io automatically validates your SSL certificates daily. When your SSL certificate is up for renewal, we start notifying you through the error logs.","title":"SSL certificate expiration checks"},{"location":"setup-uptime-monitoring/#domain-name-expiration-checks","text":"Much like SSL checks, Domain name expiration checks, will notify you through your log when your domain names are about to expire. To enable this feature, enable the Domain Expiration toggle when creating a new uptime check.","title":"Domain name expiration checks"},{"location":"setup-uptime-monitoring/#lighthouse-checks","text":"When enabling Lighthouse on an uptime check, we will generate Lighthouse results every night and include them on the history tab. Lighthouse is a popular tool for improving websites by running through a range of checks spread across multiple categories like performance and accessibility.","title":"Lighthouse checks"},{"location":"setup-uptime-monitoring/#canonical-checks","text":"Canonical checks are a range of checks implemented by us. It checks the URL of the uptime checks by running through a range of different best practices. Examples are HTTP to HTTPS redirect, correct use of redirect status code, and more checks.","title":"Canonical checks"},{"location":"sourcemaps/","text":"Source maps Source maps Publish to your web server Upload to elmah.io Upload through the API Upload from the elmah.io CLI Upload from PowerShell Upload from C# Upload from Azure DevOps Upload from GitHub Actions Upload from Octopus Deploy elmah.io.javascript automatically tries to translate stack traces from minified and/or bundled code into developer-friendly traces using JavaScript source maps. For this to work, you will need to publish a valid .map source map file to either your web server or through the elmah.io API. In this article, we go through each of the possibilities. This is not a guide for generating source map files. There's a range of possibilities to help you do that (like gulp ). Publish to your web server The easiest way for elmah.io.javascript to translate minified/bundled stack traces is to publish a JavaScript source map alongside your minified JavaScript file. When logging a message through elmah.io.javascript , a source map is automatically used if there is a .map reference at the end of the file causing the log message/error: var v = 42; //# sourceMappingURL=/script.map This will require you to serve a source map file named script.map together with the minified/bundled file. The .map file doesn't need to be publicly available over the internet, but the elmah.io.javascript package will need to have access to it. De-minification from a .map file doesn't work in older browsers (like < IE10). Upload to elmah.io Publishing source map files alongside your minified and bundled files is not always the preferred way of doing things. With both the minified JavaScript file and the source map, someone outside of your organization can reverse engineer your JavaScript. In cases where this would be critical or if you want to limit the size of the published files, you can choose to upload the minified/bundled file and the corresponding source map file on the elmah.io API. When elmah.io receives a stack trace from a minified JavaScript file, we try to de-minify it if a corresponding .min.js and .map file have been uploaded. The code in the following sections will use the following bundled and minified stack trace as an example: Error: You cannot copy to the clipboard at window.copyTextToClipboard (https://foo.bar/bundles/sharedbundle.min.js:69:24978) at MyViewModel.n.copy (https://foo.bar/bundles/viewmodels.min.js:37:37385) at HTMLButtonElement.<anonymous> (https://foo.bar/bundles/sharedbundle.min.js:55:109467) To upload a source map to elmah.io you will need an API key with the Source Maps | Write permission enabled. We recommend creating a new API key with this permission enabled only. For more information about API keys and permissions, check out How to configure API key permissions . Upload through the API Go to the API and insert your API key. The upload source map endpoint accepts a couple of parameters that you will need to get right to make de-minification work. The stack trace contains references to two different bundled JavaScript files with each their .map file. Both source map files should be uploaded to the elmah.io API. For both bundled files, fill in the details as explained here: Parameter Description logId A source map will always belong to a specific elmah.io log. Insert the log ID of that log in this field. If an application is logging JavaScript messages to multiple logs, you will need to upload the files to all those logs. Path This is the relative path to the bundled and minified file. For the example above you will need to specify /bundles/sharedbundle.min.js for the first source map and /bundles/viewmodels.min.js for the second one. You can use an absolute path and query parameters on the URL if you prefer, but this will be converted to a relative path by the elmah.io API. SourceMap The source map file representing the minified file in the Path specified above. MinifiedJavaScript The bundled and minified JavaScript file. This will be the content of the Path specified above. Upload from the elmah.io CLI Source maps can be uploaded from the elmah.io CLI. Install the CLI if not already installed: dotnet tool install --global Elmah.Io.Cli Then, upload a source map and minified JavaScript using the sourcemap command: sourcemap --apiKey API_KEY --logId LOG_ID --path \"/bundles/sharedbundle.min.js\" --sourceMap \"c:\\path\\to\\sharedbundle.map\" --minifiedJavaScript \"c:\\path\\to\\sharedbundle.min.js\" Upload from PowerShell Uploading source maps can be built into your CI/CD pipeline using cURL, PowerShell, or similar. Here's an example written in PowerShell: $boundary = [System.Guid]::NewGuid().ToString() $mapPath = \"c:\\path\\to\\sharedbundle.map\" $jsPath = \"c:\\path\\to\\sharedbundle.min.js\" $mapFile = [System.IO.File]::ReadAllBytes($mapPath) $mapContent = [System.Text.Encoding]::UTF8.GetString($mapFile) $jsFile = [System.IO.File]::ReadAllBytes($jsPath) $jsContent = [System.Text.Encoding]::UTF8.GetString($jsFile) $LF = \"`r`n\" $bodyLines = ( \"--$boundary\", \"Content-Disposition: form-data; name=`\"Path`\"$LF\", \"/bundles/sharedbundle.min.js\", \"--$boundary\", \"Content-Disposition: form-data; name=`\"SourceMap`\"; filename=`\"sharedbundle.map`\"\", \"Content-Type: application/json$LF\", $mapContent, \"--$boundary\", \"Content-Disposition: form-data; name=`\"MinifiedJavaScript`\"; filename=`\"sharedbundle.min.js`\"\", \"Content-Type: text/javascript$LF\", $jsContent, \"--$boundary--$LF\" ) -join $LF Invoke-RestMethod 'https://api.elmah.io/v3/sourcemaps/LOG_ID?api_key=API_KEY' -Method POST -ContentType \"multipart/form-data; boundary=`\"$boundary`\"\" -Body $bodyLines Upload from C# Source maps can be uploaded from C# using the Elmah.Io.Client NuGet package: var api = ElmahioAPI.Create(\"API_KEY\"); using var sourceMapStream = File.OpenRead(\"c:\\\\path\\\\to\\\\sharedbundle.map\"); using var scriptStream = File.OpenRead(\"c:\\\\path\\\\to\\\\sharedbundle.min.js\"); api.SourceMaps.CreateOrUpdate( \"LOG_ID\", new Uri(\"/bundles/sharedbundle.min.js\", UriKind.Relative), new FileParameter(sourceMapStream, \"sharedbundle.map\", \"application/json\"), new FileParameter(scriptStream, \"sharedbundle.min.js\", \"text/javascript\")); Upload from Azure DevOps Uploading one or more source maps from Azure DevOps is available using our integration with Pipelines. Here is a guide to help you upload source maps from Azure DevOps. Before you can include the upload source map task, you will need to publish your generated source maps and minified JavaScript files from your build pipeline. Go to the elmah.io Upload Source Map extension on the Azure DevOps Marketplace and click the Get it free button: Select your organization and click the Install button: Go to your Azure DevOps project and add the elmah.io Upload Source Map task. Fill in all fields as shown here: Upload from GitHub Actions Uploading one or more source maps from GitHub Actions is available using our integration with Actions. We recommend adding your API key and log ID as secrets on your GitHub repository, to avoid people outside of your organization getting access to those values. To upload a generated source map from GitHub Actions, include the following in your build YAML file: uses: elmahio/github-upload-source-map-action@v1 with: apiKey: ${{ secrets.ELMAH_IO_API_KEY }} logId: ${{ secrets.ELMAH_IO_LOG_ID }} path: '/bundles/sharedbundle.min.js' sourceMap: 'path/to/sharedbundle.map' minifiedJavaScript: 'path/to/sharedbundle.min.js' Upload from Octopus Deploy Uploading one or more source from Octopus Deploy is available using our step template for Octopus. The step template can be installed in multiple ways as explained on Community step templates . In this document, the step template will be installed directly from the Process Editor : Go to the Process Editor and click the ADD STEP button. In the Choose Step Template section search for 'elmah.io': Hover over the 'elmah.io - Upload Source Map' community template and click the INSTALL AND ADD button. In the Install and add modal click the SAVE button. The step template is now added to the process. Fill in all fields as shown here:","title":"Source maps"},{"location":"sourcemaps/#source-maps","text":"Source maps Publish to your web server Upload to elmah.io Upload through the API Upload from the elmah.io CLI Upload from PowerShell Upload from C# Upload from Azure DevOps Upload from GitHub Actions Upload from Octopus Deploy elmah.io.javascript automatically tries to translate stack traces from minified and/or bundled code into developer-friendly traces using JavaScript source maps. For this to work, you will need to publish a valid .map source map file to either your web server or through the elmah.io API. In this article, we go through each of the possibilities. This is not a guide for generating source map files. There's a range of possibilities to help you do that (like gulp ).","title":"Source maps"},{"location":"sourcemaps/#publish-to-your-web-server","text":"The easiest way for elmah.io.javascript to translate minified/bundled stack traces is to publish a JavaScript source map alongside your minified JavaScript file. When logging a message through elmah.io.javascript , a source map is automatically used if there is a .map reference at the end of the file causing the log message/error: var v = 42; //# sourceMappingURL=/script.map This will require you to serve a source map file named script.map together with the minified/bundled file. The .map file doesn't need to be publicly available over the internet, but the elmah.io.javascript package will need to have access to it. De-minification from a .map file doesn't work in older browsers (like < IE10).","title":"Publish to your web server"},{"location":"sourcemaps/#upload-to-elmahio","text":"Publishing source map files alongside your minified and bundled files is not always the preferred way of doing things. With both the minified JavaScript file and the source map, someone outside of your organization can reverse engineer your JavaScript. In cases where this would be critical or if you want to limit the size of the published files, you can choose to upload the minified/bundled file and the corresponding source map file on the elmah.io API. When elmah.io receives a stack trace from a minified JavaScript file, we try to de-minify it if a corresponding .min.js and .map file have been uploaded. The code in the following sections will use the following bundled and minified stack trace as an example: Error: You cannot copy to the clipboard at window.copyTextToClipboard (https://foo.bar/bundles/sharedbundle.min.js:69:24978) at MyViewModel.n.copy (https://foo.bar/bundles/viewmodels.min.js:37:37385) at HTMLButtonElement.<anonymous> (https://foo.bar/bundles/sharedbundle.min.js:55:109467) To upload a source map to elmah.io you will need an API key with the Source Maps | Write permission enabled. We recommend creating a new API key with this permission enabled only. For more information about API keys and permissions, check out How to configure API key permissions .","title":"Upload to elmah.io"},{"location":"sourcemaps/#upload-through-the-api","text":"Go to the API and insert your API key. The upload source map endpoint accepts a couple of parameters that you will need to get right to make de-minification work. The stack trace contains references to two different bundled JavaScript files with each their .map file. Both source map files should be uploaded to the elmah.io API. For both bundled files, fill in the details as explained here: Parameter Description logId A source map will always belong to a specific elmah.io log. Insert the log ID of that log in this field. If an application is logging JavaScript messages to multiple logs, you will need to upload the files to all those logs. Path This is the relative path to the bundled and minified file. For the example above you will need to specify /bundles/sharedbundle.min.js for the first source map and /bundles/viewmodels.min.js for the second one. You can use an absolute path and query parameters on the URL if you prefer, but this will be converted to a relative path by the elmah.io API. SourceMap The source map file representing the minified file in the Path specified above. MinifiedJavaScript The bundled and minified JavaScript file. This will be the content of the Path specified above.","title":"Upload through the API"},{"location":"sourcemaps/#upload-from-the-elmahio-cli","text":"Source maps can be uploaded from the elmah.io CLI. Install the CLI if not already installed: dotnet tool install --global Elmah.Io.Cli Then, upload a source map and minified JavaScript using the sourcemap command: sourcemap --apiKey API_KEY --logId LOG_ID --path \"/bundles/sharedbundle.min.js\" --sourceMap \"c:\\path\\to\\sharedbundle.map\" --minifiedJavaScript \"c:\\path\\to\\sharedbundle.min.js\"","title":"Upload from the elmah.io CLI"},{"location":"sourcemaps/#upload-from-powershell","text":"Uploading source maps can be built into your CI/CD pipeline using cURL, PowerShell, or similar. Here's an example written in PowerShell: $boundary = [System.Guid]::NewGuid().ToString() $mapPath = \"c:\\path\\to\\sharedbundle.map\" $jsPath = \"c:\\path\\to\\sharedbundle.min.js\" $mapFile = [System.IO.File]::ReadAllBytes($mapPath) $mapContent = [System.Text.Encoding]::UTF8.GetString($mapFile) $jsFile = [System.IO.File]::ReadAllBytes($jsPath) $jsContent = [System.Text.Encoding]::UTF8.GetString($jsFile) $LF = \"`r`n\" $bodyLines = ( \"--$boundary\", \"Content-Disposition: form-data; name=`\"Path`\"$LF\", \"/bundles/sharedbundle.min.js\", \"--$boundary\", \"Content-Disposition: form-data; name=`\"SourceMap`\"; filename=`\"sharedbundle.map`\"\", \"Content-Type: application/json$LF\", $mapContent, \"--$boundary\", \"Content-Disposition: form-data; name=`\"MinifiedJavaScript`\"; filename=`\"sharedbundle.min.js`\"\", \"Content-Type: text/javascript$LF\", $jsContent, \"--$boundary--$LF\" ) -join $LF Invoke-RestMethod 'https://api.elmah.io/v3/sourcemaps/LOG_ID?api_key=API_KEY' -Method POST -ContentType \"multipart/form-data; boundary=`\"$boundary`\"\" -Body $bodyLines","title":"Upload from PowerShell"},{"location":"sourcemaps/#upload-from-c","text":"Source maps can be uploaded from C# using the Elmah.Io.Client NuGet package: var api = ElmahioAPI.Create(\"API_KEY\"); using var sourceMapStream = File.OpenRead(\"c:\\\\path\\\\to\\\\sharedbundle.map\"); using var scriptStream = File.OpenRead(\"c:\\\\path\\\\to\\\\sharedbundle.min.js\"); api.SourceMaps.CreateOrUpdate( \"LOG_ID\", new Uri(\"/bundles/sharedbundle.min.js\", UriKind.Relative), new FileParameter(sourceMapStream, \"sharedbundle.map\", \"application/json\"), new FileParameter(scriptStream, \"sharedbundle.min.js\", \"text/javascript\"));","title":"Upload from C#"},{"location":"sourcemaps/#upload-from-azure-devops","text":"Uploading one or more source maps from Azure DevOps is available using our integration with Pipelines. Here is a guide to help you upload source maps from Azure DevOps. Before you can include the upload source map task, you will need to publish your generated source maps and minified JavaScript files from your build pipeline. Go to the elmah.io Upload Source Map extension on the Azure DevOps Marketplace and click the Get it free button: Select your organization and click the Install button: Go to your Azure DevOps project and add the elmah.io Upload Source Map task. Fill in all fields as shown here:","title":"Upload from Azure DevOps"},{"location":"sourcemaps/#upload-from-github-actions","text":"Uploading one or more source maps from GitHub Actions is available using our integration with Actions. We recommend adding your API key and log ID as secrets on your GitHub repository, to avoid people outside of your organization getting access to those values. To upload a generated source map from GitHub Actions, include the following in your build YAML file: uses: elmahio/github-upload-source-map-action@v1 with: apiKey: ${{ secrets.ELMAH_IO_API_KEY }} logId: ${{ secrets.ELMAH_IO_LOG_ID }} path: '/bundles/sharedbundle.min.js' sourceMap: 'path/to/sharedbundle.map' minifiedJavaScript: 'path/to/sharedbundle.min.js'","title":"Upload from GitHub Actions"},{"location":"sourcemaps/#upload-from-octopus-deploy","text":"Uploading one or more source from Octopus Deploy is available using our step template for Octopus. The step template can be installed in multiple ways as explained on Community step templates . In this document, the step template will be installed directly from the Process Editor : Go to the Process Editor and click the ADD STEP button. In the Choose Step Template section search for 'elmah.io': Hover over the 'elmah.io - Upload Source Map' community template and click the INSTALL AND ADD button. In the Install and add modal click the SAVE button. The step template is now added to the process. Fill in all fields as shown here:","title":"Upload from Octopus Deploy"},{"location":"specify-api-key-and-log-id-through-appsettings/","text":"Specify API key and log ID through appSettings When integrating to elmah.io from ASP.NET, MVC, Web API and similar, we use the open source project ELMAH to log uncaught exceptions. ELMAH requires configuration in web.config , which in the case of elmah.io could look something like this: <elmah> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> </elmah> You'd normally use web.config Transformations to specify different API keys and log IDs for different environments (see Use multiple logs for different environments ). When hosting on Microsoft Azure (and other cloud-based offerings), a better approach is to specify the configuration in the appSettings element and overwrite values through the web app settings in the Portal. The elmah.io clients built for ASP.NET based web frameworks support this scenario through additional attributes on the <errorLog> element: <appSettings> <add key=\"apiKeyRef\" value=\"API_KEY\" /> <add key=\"logIdRef\" value=\"LOG_ID\" /> </appSettings> <!-- ... --> <elmah> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKeyKey=\"apiKeyRef\" logIdKey=\"logIdRef\" /> </elmah> Unlike the first example, the term Key has been appended to both the apiKey and logId attributes. The values of those attributes need to match a key specified in appSettings (in this example apiKeyRef and logIdRef ). How you choose to name these keys is entirely up to you, as long as the names match. elmah.io now picks up your API key ( Where is my API key? ) and log ID ( Where is my log ID? ) from the appSettings element and can be overwritten on your production site on Azure.","title":"Specify API key and log ID through appSettings"},{"location":"specify-api-key-and-log-id-through-appsettings/#specify-api-key-and-log-id-through-appsettings","text":"When integrating to elmah.io from ASP.NET, MVC, Web API and similar, we use the open source project ELMAH to log uncaught exceptions. ELMAH requires configuration in web.config , which in the case of elmah.io could look something like this: <elmah> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> </elmah> You'd normally use web.config Transformations to specify different API keys and log IDs for different environments (see Use multiple logs for different environments ). When hosting on Microsoft Azure (and other cloud-based offerings), a better approach is to specify the configuration in the appSettings element and overwrite values through the web app settings in the Portal. The elmah.io clients built for ASP.NET based web frameworks support this scenario through additional attributes on the <errorLog> element: <appSettings> <add key=\"apiKeyRef\" value=\"API_KEY\" /> <add key=\"logIdRef\" value=\"LOG_ID\" /> </appSettings> <!-- ... --> <elmah> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKeyKey=\"apiKeyRef\" logIdKey=\"logIdRef\" /> </elmah> Unlike the first example, the term Key has been appended to both the apiKey and logId attributes. The values of those attributes need to match a key specified in appSettings (in this example apiKeyRef and logIdRef ). How you choose to name these keys is entirely up to you, as long as the names match. elmah.io now picks up your API key ( Where is my API key? ) and log ID ( Where is my log ID? ) from the appSettings element and can be overwritten on your production site on Azure.","title":"Specify API key and log ID through appSettings"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/","text":"Tips and tricks to stay below your message limit Tips and tricks to stay below your message limit Ignore Rules Filters Ignore future messages like this Disable logs Client-side message filtering Monitor current usage Fix bugs Purchase a top-up Upgrading to the next plan Each plan on elmah.io includes a maximum number of messages per month. The number of messages is calculated from how many times your applications have called our API and successfully stored a message (in most cases messages equal errors). Deleting messages either one by one or in batches is fully supported, but does not result in a decrease in the current message count. Our costs are primarily around receiving, indexing, and notifying about messages, why we cannot allow someone on a lower plan like the Small Business, to log millions and yet millions of messages and then just clean up regularly. We're sure that everyone understands the challenge here. With that said, we want to help you stay within your message limits. Luckily, there are a lot of ways to limit messages. This article contains a list of the most common tactics to stay below your message limit. Ignore Rules Be aware that Ignore rules and filters are only meant as a temporary way of ignoring messages. In case you want to permanently ignore one or more log messages, use client-side filtering . The easiest way to limit logged messages is by ignoring some of them. Ignored messages do not count toward the message limit. Message rules can be configured through the Rules tab on the Log Settings view. Rules consist of a query and an action. The query can either be a full-text query or written using Lucene Query Syntax. To create a new ignore rule, input a query on the Rules tab: All new rules are created with an ignore action as default, why you don't need to click the Then link for this type of rule. In the example above, ignore all messages with a status code of 404 . For more information about the possibilities with rules, check out Creating Rules to Perform Actions on Messages . Filters Filters are Ignore Rules in disguise. With Filters, we have collected the most common ignore rules and made them available as a set of checkboxes. To ignore all messages matching a specific filter, enable one of the checkboxes on the Filters tab on Log Settings: If your website is available for everyone to access, ignoring known crawlers, bots, and spiders is a good idea in most cases. Filtering below warning can also be a good idea unless you are using elmah.io to log all log severities from a logging framework like NLog or Serilog. If staying within the message limit is more important than getting the full picture of the errors generated by your website, there are a couple of filters that will help you with that. The Filter Known filter will make sure that only one instance of each error is logged. If you still want to log multiple instances but stop at some point, the Filter Burst filter will stop logging after 50 instances are logged. Finally, you can set a limit on how many errors you want logged to a specific log each month, by inputting a number in the Filter Limit filter. Please notice that using any of these last three filters will cause inconsistent results in different graphs and features (like spike detection). They can solve the problem of logging too much, but it is a sign that you are logging more data than is included in your plan. A perfectly valid solution is to purchase a larger plan, get your logging under control (maybe even fix some errors?), and then downgrade when you are ready. Ignore future messages like this Sometimes you may find yourself on the Search tab with a search result thinking: \"I don't care about these messages\". By clicking the caret next to the query filters, an Ignore future messages like this option is revealed: Clicking this option automatically ignores any future messages matching your current search result. Disable logs Each log can be disabled from Log Settings: Disabled logs are shown as semi-transparent on the dashboard, to help you remember that you disabled a log. Client-side message filtering Most of our clients support client filtering. All of the filtering options described above filter messages server-side. This means that your application still communicates with elmah.io's API and needs to wait for that to answer (even for ignored messages). Filtering client-side from ASP.NET, MVC, Web API, and other frameworks built on top of ASP.NET, can be done using ELMAH's (the open-source project) filtering feature. To filter messages, create a method named ErrorLog_Filtering in the Global.asax.cs file: void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs args) { var httpContext = args.Context as HttpContext; if (httpContext.Response.StatusCode == 404) { args.Dismiss(); } } If you're using ASP.NET Core, our client supports the OnFilter action: builder.Services.AddElmahIo(o => { // ... o.OnFilter = message => { return message.StatusCode == 404; }; }); To help you set up client-side filtering in commonly used logging frameworks, click the button on the log message details toolbar. This will show a dialog containing extensive help on setting up client-side filtering. Monitor current usage We send you an email when you have used 90% of your limit and again when reaching the limit. Monitoring your usage is a good supplement to the emails since you can react early on (by upgrading, ignoring errors, or something else). There's a usage graph on the Organisation Settings view: By clicking the information icon above the counter, you will be able to see which logs that are taking up space: Fix bugs Seeing the same error over and over again? Maybe the best idea is to fix it :) I mean, that's the whole purpose of elmah.io: to help you fix bugs. And remember, the fewer bugs you have, the cheaper elmah.io gets. The ultimate motivation! Purchase a top-up Sometimes, a spike in errors can be caused by unexpected events like a black hat bot deciding to bombard your site with requests or a junior dev on your team accidentally enabling verbose logging. In these cases, purchasing a top-up may be a better solution than permanently upgrading your plan. Top-ups can be purchased from your subscription when you reach 90% of your included messages. Top-ups are purchased in bundles of 25,000 messages, valid for the rest of the calendar month. Upgrading to the next plan If you constantly go over the limit, you have probably reached a point where you will need to upgrade to a larger plan. You can upgrade and downgrade at any time, so upgrading for a few months (until you get errors under control) and then downgrading again, is perfectly fine.","title":"Tips and tricks to stay below your message limit"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#tips-and-tricks-to-stay-below-your-message-limit","text":"Tips and tricks to stay below your message limit Ignore Rules Filters Ignore future messages like this Disable logs Client-side message filtering Monitor current usage Fix bugs Purchase a top-up Upgrading to the next plan Each plan on elmah.io includes a maximum number of messages per month. The number of messages is calculated from how many times your applications have called our API and successfully stored a message (in most cases messages equal errors). Deleting messages either one by one or in batches is fully supported, but does not result in a decrease in the current message count. Our costs are primarily around receiving, indexing, and notifying about messages, why we cannot allow someone on a lower plan like the Small Business, to log millions and yet millions of messages and then just clean up regularly. We're sure that everyone understands the challenge here. With that said, we want to help you stay within your message limits. Luckily, there are a lot of ways to limit messages. This article contains a list of the most common tactics to stay below your message limit.","title":"Tips and tricks to stay below your message limit"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#ignore-rules","text":"Be aware that Ignore rules and filters are only meant as a temporary way of ignoring messages. In case you want to permanently ignore one or more log messages, use client-side filtering . The easiest way to limit logged messages is by ignoring some of them. Ignored messages do not count toward the message limit. Message rules can be configured through the Rules tab on the Log Settings view. Rules consist of a query and an action. The query can either be a full-text query or written using Lucene Query Syntax. To create a new ignore rule, input a query on the Rules tab: All new rules are created with an ignore action as default, why you don't need to click the Then link for this type of rule. In the example above, ignore all messages with a status code of 404 . For more information about the possibilities with rules, check out Creating Rules to Perform Actions on Messages .","title":"Ignore Rules"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#filters","text":"Filters are Ignore Rules in disguise. With Filters, we have collected the most common ignore rules and made them available as a set of checkboxes. To ignore all messages matching a specific filter, enable one of the checkboxes on the Filters tab on Log Settings: If your website is available for everyone to access, ignoring known crawlers, bots, and spiders is a good idea in most cases. Filtering below warning can also be a good idea unless you are using elmah.io to log all log severities from a logging framework like NLog or Serilog. If staying within the message limit is more important than getting the full picture of the errors generated by your website, there are a couple of filters that will help you with that. The Filter Known filter will make sure that only one instance of each error is logged. If you still want to log multiple instances but stop at some point, the Filter Burst filter will stop logging after 50 instances are logged. Finally, you can set a limit on how many errors you want logged to a specific log each month, by inputting a number in the Filter Limit filter. Please notice that using any of these last three filters will cause inconsistent results in different graphs and features (like spike detection). They can solve the problem of logging too much, but it is a sign that you are logging more data than is included in your plan. A perfectly valid solution is to purchase a larger plan, get your logging under control (maybe even fix some errors?), and then downgrade when you are ready.","title":"Filters"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#ignore-future-messages-like-this","text":"Sometimes you may find yourself on the Search tab with a search result thinking: \"I don't care about these messages\". By clicking the caret next to the query filters, an Ignore future messages like this option is revealed: Clicking this option automatically ignores any future messages matching your current search result.","title":"Ignore future messages like this"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#disable-logs","text":"Each log can be disabled from Log Settings: Disabled logs are shown as semi-transparent on the dashboard, to help you remember that you disabled a log.","title":"Disable logs"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#client-side-message-filtering","text":"Most of our clients support client filtering. All of the filtering options described above filter messages server-side. This means that your application still communicates with elmah.io's API and needs to wait for that to answer (even for ignored messages). Filtering client-side from ASP.NET, MVC, Web API, and other frameworks built on top of ASP.NET, can be done using ELMAH's (the open-source project) filtering feature. To filter messages, create a method named ErrorLog_Filtering in the Global.asax.cs file: void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs args) { var httpContext = args.Context as HttpContext; if (httpContext.Response.StatusCode == 404) { args.Dismiss(); } } If you're using ASP.NET Core, our client supports the OnFilter action: builder.Services.AddElmahIo(o => { // ... o.OnFilter = message => { return message.StatusCode == 404; }; }); To help you set up client-side filtering in commonly used logging frameworks, click the button on the log message details toolbar. This will show a dialog containing extensive help on setting up client-side filtering.","title":"Client-side message filtering"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#monitor-current-usage","text":"We send you an email when you have used 90% of your limit and again when reaching the limit. Monitoring your usage is a good supplement to the emails since you can react early on (by upgrading, ignoring errors, or something else). There's a usage graph on the Organisation Settings view: By clicking the information icon above the counter, you will be able to see which logs that are taking up space:","title":"Monitor current usage"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#fix-bugs","text":"Seeing the same error over and over again? Maybe the best idea is to fix it :) I mean, that's the whole purpose of elmah.io: to help you fix bugs. And remember, the fewer bugs you have, the cheaper elmah.io gets. The ultimate motivation!","title":"Fix bugs"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#purchase-a-top-up","text":"Sometimes, a spike in errors can be caused by unexpected events like a black hat bot deciding to bombard your site with requests or a junior dev on your team accidentally enabling verbose logging. In these cases, purchasing a top-up may be a better solution than permanently upgrading your plan. Top-ups can be purchased from your subscription when you reach 90% of your included messages. Top-ups are purchased in bundles of 25,000 messages, valid for the rest of the calendar month.","title":"Purchase a top-up"},{"location":"tips-and-tricks-to-stay-below-your-message-limit/#upgrading-to-the-next-plan","text":"If you constantly go over the limit, you have probably reached a point where you will need to upgrade to a larger plan. You can upgrade and downgrade at any time, so upgrading for a few months (until you get errors under control) and then downgrading again, is perfectly fine.","title":"Upgrading to the next plan"},{"location":"upgrade-elmah-io-from-v2-to-v3/","text":"Upgrade elmah.io from v2 to v3 When we launched the new version of our API ( v3 ), we used the jump in major version to fix some issues that would require major changes in our client. One of them is a move to netstandard, which makes the new client usable from .NET Core. With interface changes in the client, upgrading from 2.x to 3.x requires more than simply upgrading the NuGet package. This is a guide to upgrading the Elmah.Io package. If you are logging to elmah.io from ASP.NET Core, you are already using the 3.x client. Updating the NuGet package First, you need to upgrade the Elmah.Io NuGet package: Update-Package Elmah.Io This installs the latest 3.x client. After doing so, we recommend updating to the latest Elmah.Io.Client package as well (updating Elmah.Io already updated Elmah.Io.Client but to the lowest possible version): Update-Package Elmah.Io.Client The elmah.io.core package is no longer needed and can be uninstalled: Uninstall-Package elmah.io.core Next, you will need to add your API key to your web.config . Where the 2.x client only required a log ID to log messages to elmah.io, the new API improves security by introducing API keys ( Where is my API key? ). Copy your API key and extend the errorLog -element in web.config : <elmah> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> </elmah> If you didn't utilize elmah.io's code from C#, you are ready to rock and roll. Code changes The 3.x client is auto-generated from our new Swagger endpoint. This means that the code doesn't work like previously. We have tried to create extension methods to make some of the API work like previously, but since the client now supports both Messages, Logs, and Deployments, code changes are needed. If you are using the ErrorSignal class from ELMAH (the open-source project) to log exceptions manually, everything works as previously. If you are using methods from the Elmah.Io.Client package, there's a new API documented here: Logging from Console . Elmah.Io.Mvc and Elmah.Io.WebApi When launching the packages for 3.x, we also decided to create two new proxy packages: Elmah.Io.Mvc and Elmah.Io.WebApi. The reason I call them proxy packages is, that they do nothing more than simply install the dependencies needed to log from each framework. The packages are intended for new installs only, so if your code already logs exceptions to elmah.io, there is no need to install any of these packages.","title":"Upgrade elmah.io from v2 to v3"},{"location":"upgrade-elmah-io-from-v2-to-v3/#upgrade-elmahio-from-v2-to-v3","text":"When we launched the new version of our API ( v3 ), we used the jump in major version to fix some issues that would require major changes in our client. One of them is a move to netstandard, which makes the new client usable from .NET Core. With interface changes in the client, upgrading from 2.x to 3.x requires more than simply upgrading the NuGet package. This is a guide to upgrading the Elmah.Io package. If you are logging to elmah.io from ASP.NET Core, you are already using the 3.x client.","title":"Upgrade elmah.io from v2 to v3"},{"location":"upgrade-elmah-io-from-v2-to-v3/#updating-the-nuget-package","text":"First, you need to upgrade the Elmah.Io NuGet package: Update-Package Elmah.Io This installs the latest 3.x client. After doing so, we recommend updating to the latest Elmah.Io.Client package as well (updating Elmah.Io already updated Elmah.Io.Client but to the lowest possible version): Update-Package Elmah.Io.Client The elmah.io.core package is no longer needed and can be uninstalled: Uninstall-Package elmah.io.core Next, you will need to add your API key to your web.config . Where the 2.x client only required a log ID to log messages to elmah.io, the new API improves security by introducing API keys ( Where is my API key? ). Copy your API key and extend the errorLog -element in web.config : <elmah> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> </elmah> If you didn't utilize elmah.io's code from C#, you are ready to rock and roll.","title":"Updating the NuGet package"},{"location":"upgrade-elmah-io-from-v2-to-v3/#code-changes","text":"The 3.x client is auto-generated from our new Swagger endpoint. This means that the code doesn't work like previously. We have tried to create extension methods to make some of the API work like previously, but since the client now supports both Messages, Logs, and Deployments, code changes are needed. If you are using the ErrorSignal class from ELMAH (the open-source project) to log exceptions manually, everything works as previously. If you are using methods from the Elmah.Io.Client package, there's a new API documented here: Logging from Console .","title":"Code changes"},{"location":"upgrade-elmah-io-from-v2-to-v3/#elmahiomvc-and-elmahiowebapi","text":"When launching the packages for 3.x, we also decided to create two new proxy packages: Elmah.Io.Mvc and Elmah.Io.WebApi. The reason I call them proxy packages is, that they do nothing more than simply install the dependencies needed to log from each framework. The packages are intended for new installs only, so if your code already logs exceptions to elmah.io, there is no need to install any of these packages.","title":"Elmah.Io.Mvc and Elmah.Io.WebApi"},{"location":"upgrade-elmah-io-from-v3-to-v4/","text":"Upgrade elmah.io from v3 to v4 Upgrade elmah.io from v3 to v4 Upgrading Elmah.Io.Client Code changes Upgrading Elmah.Io.AspNetCore Code changes Troubleshooting There is a new major version update named 4.x on all client packages. All integrations are based on the Elmah.Io.Client package which has switched from using AutoRest to NSwag. We have tried supporting most of the existing code from the 3.x packages, but some minor code changes may be needed after upgrading. This guide will go through the changes needed and potential problems when upgrading. You may experience problems where NuGet refuses to upgrade an Elmah.Io.* package. We experienced this when referencing more Elmah.Io.* packages from the same project. Like if you would reference both Elmah.Io.AspNetCore and Elmah.Io.Extensions.Logging from the same project. Upgrading in this scenario can be achieved in one of the following ways: Uninstall all Elmah.Io.* packages and install them one by one. Install the 4.x version of the Elmah.Io.Client NuGet package. Then upgrade the remaining Elmah.Io.* NuGet packages one by one. Finally, uninstall the Elmah.Io.Client package. Upgrading Elmah.Io.Client If you are using the Elmah.Io.Client package directly, you can simply upgrade that package through NuGet: Update-Package Elmah.Io.Client This installs the latest 4.x client. Most people are using one of the integrations with ASP.NET Core, Serilog, NLog, or similar. There are new major versions of these packages available as well. Code changes As mentioned, some code changes may be required on your part after upgrading. Most of the API stayed the same, but NSwag generates output differently from AutoRest in some cases. Here's a list of changes needed: Remove any references to Elmah.Io.Client.Models . All generated classes are now in the Elmah.Io.Client namespace. Replace any reference to IMessages , IHeartbeats , and similar to IMessagesClient , IHeartbeatsClient , and similar. Replace any instances of new ElmahioAPI(new ApiKeyCredentials(apiKey)); with ElmahioAPI.Create(apiKey); . The ApiKeyCredentials class was specific to AutoRest and has therefore been removed. Collection types on messages like Data , ServerVariables , etc. are now of type ICollection and not IList . This means you can no longer use indexers on these properties. To use indexer you can call ToList() on each collection. Properties of type DateTime are replaced with DateTimeOffset . This shouldn't require any changes on your part since you can assign a value of type DateTime to a property of type DateTimeOffset . You no longer need to make the following cast: (ElmahioAPI)ElmahioAPI.Create(apiKey) . The IElmahioAPI interface will have all the properties you need from the client. If you for some reason manually installed the Microsoft.Rest.ClientRuntime package you can remove that after upgrading Elmah.Io.Client to v4. Unless you have other dependencies on Microsoft.Rest.ClientRuntime . The OnMessageFail event, available on the IMessagesClient interface (previously just IMessages ), does now trigger more often. On v3, this event would only trigger on timeouts connecting with the server, when the server returned 500 , and similar. In v4, the event will trigger on every failing request, meaning a request where the response contains a status code >= 400 . This will make it a lot easier to make smart choices on the client when something fails. An example of this could be to implement a circuit breaker pattern or a local processing queue on the client when the elmah.io API starts returning 429 (too many requests). Upgrading Elmah.Io.AspNetCore Upgrading the Elmah.Io.AspNetCore package can be done through NuGet: Update-Package Elmah.Io.AspNetCore Code changes Besides the changes mentioned in the section about the Elmah.Io.Client package, there's a single change in the Elmah.Io.AspNetCore v4 package as well: The ElmahIoExtensions class has been moved from the Elmah.Io.AspNetCore namespace to the Microsoft.Extensions.DependencyInjection namespace. The ElmahIoExtensions class contains the UseElmahIo and AddElmahIo methods that you call in your Startup.cs file. This change should not cause any compile errors. For simple installations without custom options, you can most likely remove the using Elmah.Io.AspNetCore; line from the Startup.cs file. Visual Studio will tell if that line is no longer needed. Troubleshooting Could not load file or assembly 'Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' Elmah.Io.Client requires Newtonsoft.Json version 10.0.3 or newer. Please upgrade to fix this error.","title":"Upgrade elmah.io from v3 to v4"},{"location":"upgrade-elmah-io-from-v3-to-v4/#upgrade-elmahio-from-v3-to-v4","text":"Upgrade elmah.io from v3 to v4 Upgrading Elmah.Io.Client Code changes Upgrading Elmah.Io.AspNetCore Code changes Troubleshooting There is a new major version update named 4.x on all client packages. All integrations are based on the Elmah.Io.Client package which has switched from using AutoRest to NSwag. We have tried supporting most of the existing code from the 3.x packages, but some minor code changes may be needed after upgrading. This guide will go through the changes needed and potential problems when upgrading. You may experience problems where NuGet refuses to upgrade an Elmah.Io.* package. We experienced this when referencing more Elmah.Io.* packages from the same project. Like if you would reference both Elmah.Io.AspNetCore and Elmah.Io.Extensions.Logging from the same project. Upgrading in this scenario can be achieved in one of the following ways: Uninstall all Elmah.Io.* packages and install them one by one. Install the 4.x version of the Elmah.Io.Client NuGet package. Then upgrade the remaining Elmah.Io.* NuGet packages one by one. Finally, uninstall the Elmah.Io.Client package.","title":"Upgrade elmah.io from v3 to v4"},{"location":"upgrade-elmah-io-from-v3-to-v4/#upgrading-elmahioclient","text":"If you are using the Elmah.Io.Client package directly, you can simply upgrade that package through NuGet: Update-Package Elmah.Io.Client This installs the latest 4.x client. Most people are using one of the integrations with ASP.NET Core, Serilog, NLog, or similar. There are new major versions of these packages available as well.","title":"Upgrading Elmah.Io.Client"},{"location":"upgrade-elmah-io-from-v3-to-v4/#code-changes","text":"As mentioned, some code changes may be required on your part after upgrading. Most of the API stayed the same, but NSwag generates output differently from AutoRest in some cases. Here's a list of changes needed: Remove any references to Elmah.Io.Client.Models . All generated classes are now in the Elmah.Io.Client namespace. Replace any reference to IMessages , IHeartbeats , and similar to IMessagesClient , IHeartbeatsClient , and similar. Replace any instances of new ElmahioAPI(new ApiKeyCredentials(apiKey)); with ElmahioAPI.Create(apiKey); . The ApiKeyCredentials class was specific to AutoRest and has therefore been removed. Collection types on messages like Data , ServerVariables , etc. are now of type ICollection and not IList . This means you can no longer use indexers on these properties. To use indexer you can call ToList() on each collection. Properties of type DateTime are replaced with DateTimeOffset . This shouldn't require any changes on your part since you can assign a value of type DateTime to a property of type DateTimeOffset . You no longer need to make the following cast: (ElmahioAPI)ElmahioAPI.Create(apiKey) . The IElmahioAPI interface will have all the properties you need from the client. If you for some reason manually installed the Microsoft.Rest.ClientRuntime package you can remove that after upgrading Elmah.Io.Client to v4. Unless you have other dependencies on Microsoft.Rest.ClientRuntime . The OnMessageFail event, available on the IMessagesClient interface (previously just IMessages ), does now trigger more often. On v3, this event would only trigger on timeouts connecting with the server, when the server returned 500 , and similar. In v4, the event will trigger on every failing request, meaning a request where the response contains a status code >= 400 . This will make it a lot easier to make smart choices on the client when something fails. An example of this could be to implement a circuit breaker pattern or a local processing queue on the client when the elmah.io API starts returning 429 (too many requests).","title":"Code changes"},{"location":"upgrade-elmah-io-from-v3-to-v4/#upgrading-elmahioaspnetcore","text":"Upgrading the Elmah.Io.AspNetCore package can be done through NuGet: Update-Package Elmah.Io.AspNetCore","title":"Upgrading Elmah.Io.AspNetCore"},{"location":"upgrade-elmah-io-from-v3-to-v4/#code-changes_1","text":"Besides the changes mentioned in the section about the Elmah.Io.Client package, there's a single change in the Elmah.Io.AspNetCore v4 package as well: The ElmahIoExtensions class has been moved from the Elmah.Io.AspNetCore namespace to the Microsoft.Extensions.DependencyInjection namespace. The ElmahIoExtensions class contains the UseElmahIo and AddElmahIo methods that you call in your Startup.cs file. This change should not cause any compile errors. For simple installations without custom options, you can most likely remove the using Elmah.Io.AspNetCore; line from the Startup.cs file. Visual Studio will tell if that line is no longer needed.","title":"Code changes"},{"location":"upgrade-elmah-io-from-v3-to-v4/#troubleshooting","text":"Could not load file or assembly 'Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' Elmah.Io.Client requires Newtonsoft.Json version 10.0.3 or newer. Please upgrade to fix this error.","title":"Troubleshooting"},{"location":"upgrade-elmah-io-from-v4-to-v5/","text":"Upgrade elmah.io from v4 to v5 Upgrade elmah.io from v4 to v5 Upgrading Elmah.Io.Client Code changes Upgrading Elmah.Io.AspNetCore Code changes Upgrading Serilog.Sinks.ElmahIo Code changes Upgrading Elmah.Io.NLog Code changes Upgrading Elmah.Io.Umbraco Code changes There is a new major version update named 5.x on all client packages. We have tried supporting most of the existing code from the 4.x packages, but some minor code changes may be needed after upgrading. This guide will go through the changes needed and potential problems when upgrading. You may experience problems where NuGet refuses to upgrade an Elmah.Io.* package. We experienced this when referencing more Elmah.Io.* packages from the same project. Like if you would reference both Elmah.Io.AspNetCore and Elmah.Io.Extensions.Logging from the same project. Upgrading in this scenario can be achieved in one of the following ways: Uninstall all Elmah.Io.* packages and install them one by one. Install the 5.x version of the Elmah.Io.Client NuGet package. Then upgrade the remaining Elmah.Io.* NuGet packages one by one. Finally, uninstall the Elmah.Io.Client package. Upgrading Elmah.Io.Client If you are using the Elmah.Io.Client package directly, you can simply upgrade that package through NuGet: Update-Package Elmah.Io.Client This installs the latest 5.x client. Most people are using one of the integrations with ASP.NET Core, Serilog, NLog, or similar. There are new major versions of these packages available as well. Code changes We used the major version number upgrade to clean up the double methods problem introduced back when adding overloads with CancellationToken on all async methods. This means that async methods now only have a single version looking similar to this: Task<Message> CreateAndNotifyAsync( Guid logId, CreateMessage message, CancellationToken cancellationToken = default) This shouldn't cause any compile problems since the v4 version had the following two overloads: Task<Message> CreateAndNotifyAsync( Guid logId, CreateMessage message) Task<Message> CreateAndNotifyAsync( Guid logId, CreateMessage message, CancellationToken cancellationToken = default) In addition, we removed all code marked as Obsolete . Each section below will describe what has been removed for each package. For the Elmah.Io.Client package, the HttpClient property on the IElmahioAPI interface has been removed. If you need to provide a custom HttpClient for the client, you can use the following overload of the Create method on the ElmahioAPI class: public static IElmahioAPI Create(string apiKey, ElmahIoOptions options, HttpClient httpClient) Upgrading Elmah.Io.AspNetCore Upgrading the Elmah.Io.AspNetCore package can be done through NuGet: Update-Package Elmah.Io.AspNetCore Code changes Besides the changes mentioned in the section about the Elmah.Io.Client package, the following obsolete method has been removed from the IHealthChecksBuilder interface: public static IHealthChecksBuilder AddElmahIoPublisher( this IHealthChecksBuilder builder, string apiKey, Guid logId, string application = null) Instead, use the AddElmahIoPublisher method accepting options: public static IHealthChecksBuilder AddElmahIoPublisher( this IHealthChecksBuilder builder, Action<ElmahIoPublisherOptions> options) Upgrading Serilog.Sinks.ElmahIo Upgrading the Serilog.Sinks.ElmahIo package can be done through NuGet: Update-Package Serilog.Sinks.ElmahIo Code changes v5 of the Serilog.Sinks.ElmahIo package moves to Serilog 3. Serilog 3 contains a lot of changes which are fully documented here . Your code may need updates caused by changes in the Serilog package. Besides the changes mentioned in the section about the Elmah.Io.Client package, the following obsolete methods have been removed from the LoggerSinkConfiguration class: public static LoggerConfiguration ElmahIo( this LoggerSinkConfiguration loggerConfiguration, string apiKey, string logId, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, IFormatProvider formatProvider = null) public static LoggerConfiguration ElmahIo( this LoggerSinkConfiguration loggerConfiguration, string apiKey, Guid logId, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, IFormatProvider formatProvider = null) Instead, use the ElmahIo method accepting options: public static LoggerConfiguration ElmahIo( this LoggerSinkConfiguration loggerConfiguration, ElmahIoSinkOptions options) Upgrading Elmah.Io.NLog Upgrading the Elmah.Io.NLog package can be done through NuGet: Update-Package Elmah.Io.NLog Unlike other packages, Elmah.Io.NLog had a version 5.0.38 that was built on top of Elmah.Io.Client v4 and NLog v5. Going forward we recommend using Elmah.Io.NLog version 5.1.x which is built on top of the v5 version of Elmah.Io.Client . Code changes v5 of the Elmah.Io.NLog package moves to NLog 5. NLog 5 contains a lot of changes that are fully documented here . Your code may need updates caused by changes in the NLog package. Upgrading Elmah.Io.Umbraco Upgrading the Elmah.Io.Umbraco package can be done through NuGet: Update-Package Elmah.Io.Umbraco Code changes v5 of the Elmah.Io.Umbraco package moves to Umbraco 10. For earlier versions of Umbraco, check out our documentation here . Since Umbraco 10 is built on top of ASP.NET Core, pretty much everything in the v5 version of the Elmah.Io.Umbraco package is a breaking change. The package now relies on the Elmah.Io.AspNetCore package documented above. We recommend completely uninstalling any Elmah.Io.* NuGet packages and install the Elmah.Io.Umbraco package from scratch.","title":"Upgrade elmah.io from v4 to v5"},{"location":"upgrade-elmah-io-from-v4-to-v5/#upgrade-elmahio-from-v4-to-v5","text":"Upgrade elmah.io from v4 to v5 Upgrading Elmah.Io.Client Code changes Upgrading Elmah.Io.AspNetCore Code changes Upgrading Serilog.Sinks.ElmahIo Code changes Upgrading Elmah.Io.NLog Code changes Upgrading Elmah.Io.Umbraco Code changes There is a new major version update named 5.x on all client packages. We have tried supporting most of the existing code from the 4.x packages, but some minor code changes may be needed after upgrading. This guide will go through the changes needed and potential problems when upgrading. You may experience problems where NuGet refuses to upgrade an Elmah.Io.* package. We experienced this when referencing more Elmah.Io.* packages from the same project. Like if you would reference both Elmah.Io.AspNetCore and Elmah.Io.Extensions.Logging from the same project. Upgrading in this scenario can be achieved in one of the following ways: Uninstall all Elmah.Io.* packages and install them one by one. Install the 5.x version of the Elmah.Io.Client NuGet package. Then upgrade the remaining Elmah.Io.* NuGet packages one by one. Finally, uninstall the Elmah.Io.Client package.","title":"Upgrade elmah.io from v4 to v5"},{"location":"upgrade-elmah-io-from-v4-to-v5/#upgrading-elmahioclient","text":"If you are using the Elmah.Io.Client package directly, you can simply upgrade that package through NuGet: Update-Package Elmah.Io.Client This installs the latest 5.x client. Most people are using one of the integrations with ASP.NET Core, Serilog, NLog, or similar. There are new major versions of these packages available as well.","title":"Upgrading Elmah.Io.Client"},{"location":"upgrade-elmah-io-from-v4-to-v5/#code-changes","text":"We used the major version number upgrade to clean up the double methods problem introduced back when adding overloads with CancellationToken on all async methods. This means that async methods now only have a single version looking similar to this: Task<Message> CreateAndNotifyAsync( Guid logId, CreateMessage message, CancellationToken cancellationToken = default) This shouldn't cause any compile problems since the v4 version had the following two overloads: Task<Message> CreateAndNotifyAsync( Guid logId, CreateMessage message) Task<Message> CreateAndNotifyAsync( Guid logId, CreateMessage message, CancellationToken cancellationToken = default) In addition, we removed all code marked as Obsolete . Each section below will describe what has been removed for each package. For the Elmah.Io.Client package, the HttpClient property on the IElmahioAPI interface has been removed. If you need to provide a custom HttpClient for the client, you can use the following overload of the Create method on the ElmahioAPI class: public static IElmahioAPI Create(string apiKey, ElmahIoOptions options, HttpClient httpClient)","title":"Code changes"},{"location":"upgrade-elmah-io-from-v4-to-v5/#upgrading-elmahioaspnetcore","text":"Upgrading the Elmah.Io.AspNetCore package can be done through NuGet: Update-Package Elmah.Io.AspNetCore","title":"Upgrading Elmah.Io.AspNetCore"},{"location":"upgrade-elmah-io-from-v4-to-v5/#code-changes_1","text":"Besides the changes mentioned in the section about the Elmah.Io.Client package, the following obsolete method has been removed from the IHealthChecksBuilder interface: public static IHealthChecksBuilder AddElmahIoPublisher( this IHealthChecksBuilder builder, string apiKey, Guid logId, string application = null) Instead, use the AddElmahIoPublisher method accepting options: public static IHealthChecksBuilder AddElmahIoPublisher( this IHealthChecksBuilder builder, Action<ElmahIoPublisherOptions> options)","title":"Code changes"},{"location":"upgrade-elmah-io-from-v4-to-v5/#upgrading-serilogsinkselmahio","text":"Upgrading the Serilog.Sinks.ElmahIo package can be done through NuGet: Update-Package Serilog.Sinks.ElmahIo","title":"Upgrading Serilog.Sinks.ElmahIo"},{"location":"upgrade-elmah-io-from-v4-to-v5/#code-changes_2","text":"v5 of the Serilog.Sinks.ElmahIo package moves to Serilog 3. Serilog 3 contains a lot of changes which are fully documented here . Your code may need updates caused by changes in the Serilog package. Besides the changes mentioned in the section about the Elmah.Io.Client package, the following obsolete methods have been removed from the LoggerSinkConfiguration class: public static LoggerConfiguration ElmahIo( this LoggerSinkConfiguration loggerConfiguration, string apiKey, string logId, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, IFormatProvider formatProvider = null) public static LoggerConfiguration ElmahIo( this LoggerSinkConfiguration loggerConfiguration, string apiKey, Guid logId, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, IFormatProvider formatProvider = null) Instead, use the ElmahIo method accepting options: public static LoggerConfiguration ElmahIo( this LoggerSinkConfiguration loggerConfiguration, ElmahIoSinkOptions options)","title":"Code changes"},{"location":"upgrade-elmah-io-from-v4-to-v5/#upgrading-elmahionlog","text":"Upgrading the Elmah.Io.NLog package can be done through NuGet: Update-Package Elmah.Io.NLog Unlike other packages, Elmah.Io.NLog had a version 5.0.38 that was built on top of Elmah.Io.Client v4 and NLog v5. Going forward we recommend using Elmah.Io.NLog version 5.1.x which is built on top of the v5 version of Elmah.Io.Client .","title":"Upgrading Elmah.Io.NLog"},{"location":"upgrade-elmah-io-from-v4-to-v5/#code-changes_3","text":"v5 of the Elmah.Io.NLog package moves to NLog 5. NLog 5 contains a lot of changes that are fully documented here . Your code may need updates caused by changes in the NLog package.","title":"Code changes"},{"location":"upgrade-elmah-io-from-v4-to-v5/#upgrading-elmahioumbraco","text":"Upgrading the Elmah.Io.Umbraco package can be done through NuGet: Update-Package Elmah.Io.Umbraco","title":"Upgrading Elmah.Io.Umbraco"},{"location":"upgrade-elmah-io-from-v4-to-v5/#code-changes_4","text":"v5 of the Elmah.Io.Umbraco package moves to Umbraco 10. For earlier versions of Umbraco, check out our documentation here . Since Umbraco 10 is built on top of ASP.NET Core, pretty much everything in the v5 version of the Elmah.Io.Umbraco package is a breaking change. The package now relies on the Elmah.Io.AspNetCore package documented above. We recommend completely uninstalling any Elmah.Io.* NuGet packages and install the Elmah.Io.Umbraco package from scratch.","title":"Code changes"},{"location":"uptime-monitoring-troubleshooting/","text":"Uptime Monitoring Troubleshooting Cloudflare Super Bot Fight Mode disallows the elmah.io uptime user-agent We have tried to get Cloudflare to adopt the elmah.io Uptime user-agent as an allowed bot. This is not possible since bots need to come from a fixed set of IPs which is not possible when hosting on Microsoft Azure. To allow the elmah.io Uptime user-agent you can create a new firewall rule. To do so go to Security | WAF and select the Custom rules tab. Click the Create rule button and input the following values:","title":"Uptime Monitoring Troubleshooting"},{"location":"uptime-monitoring-troubleshooting/#uptime-monitoring-troubleshooting","text":"","title":"Uptime Monitoring Troubleshooting"},{"location":"uptime-monitoring-troubleshooting/#cloudflare-super-bot-fight-mode-disallows-the-elmahio-uptime-user-agent","text":"We have tried to get Cloudflare to adopt the elmah.io Uptime user-agent as an allowed bot. This is not possible since bots need to come from a fixed set of IPs which is not possible when hosting on Microsoft Azure. To allow the elmah.io Uptime user-agent you can create a new firewall rule. To do so go to Security | WAF and select the Custom rules tab. Click the Create rule button and input the following values:","title":"Cloudflare Super Bot Fight Mode disallows the elmah.io uptime user-agent"},{"location":"use-extended-user-details-without-email-as-id/","text":"Use extended user details without email as ID Most of our integrations automatically log the user identity as part of the error. To make that happen, packages typically use the identity object on the current thread, which gets set by most authentication frameworks for .NET (like ASP.NET Membership Provider and ASP.NET Core Identity). You may use the user's email as the key or a database identifier. If you are using an email, you are already covered and able to see Extended User Details. If not, you need to provide elmah.io with a little help. To tell elmah.io about the user's email and still keep the identifier in the user field, you can enrich the message with a piece of custom data, before sending it off to elmah.io. By putting the user's email in a Data item named X-ELMAHIO-USEREMAIL Extended User Details will pick this up and show the correct user. How you set the Data item is dependent on the elmah.io NuGet package you are using. For ASP.NET, MVC, or Web API, the code could look like this: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = ErrorLog.Client; logger.OnMessage += (sender, args) => { if (string.IsNullOrWhiteSpace(args.Message.User)) return; var db = /*...*/; var user = db.GetById<User>(args.Message.User); args.Message.Data.Add(new Item {Key = \"X-ELMAHIO-USEREMAIL\", Value = user.Email}); } For ASP.NET Core the code could look like this: builder.Services.AddElmahIo(o => { // ... o.OnMessage = message => { if (string.IsNullOrWhiteSpace(message.User)) return; var db = /*...*/; var user = db.GetById<User>(message.User); message.Data.Add(new Item {Key = \"X-ELMAHIO-USEREMAIL\", Value = user.Email}); }; }); OnMessage event handlers are executed just before a message is sent to elmah.io. In the body of the event handler, the user's email is fetched from the database by calling the GetById method. How you will be able to convert the user ID to an email depends on your tech stack, but you get the picture. That's it! A few lines of code and you can watch every little detail about the users experiencing problems on your website:","title":"Use extended user details without email as ID"},{"location":"use-extended-user-details-without-email-as-id/#use-extended-user-details-without-email-as-id","text":"Most of our integrations automatically log the user identity as part of the error. To make that happen, packages typically use the identity object on the current thread, which gets set by most authentication frameworks for .NET (like ASP.NET Membership Provider and ASP.NET Core Identity). You may use the user's email as the key or a database identifier. If you are using an email, you are already covered and able to see Extended User Details. If not, you need to provide elmah.io with a little help. To tell elmah.io about the user's email and still keep the identifier in the user field, you can enrich the message with a piece of custom data, before sending it off to elmah.io. By putting the user's email in a Data item named X-ELMAHIO-USEREMAIL Extended User Details will pick this up and show the correct user. How you set the Data item is dependent on the elmah.io NuGet package you are using. For ASP.NET, MVC, or Web API, the code could look like this: Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client var logger = ErrorLog.Client; logger.OnMessage += (sender, args) => { if (string.IsNullOrWhiteSpace(args.Message.User)) return; var db = /*...*/; var user = db.GetById<User>(args.Message.User); args.Message.Data.Add(new Item {Key = \"X-ELMAHIO-USEREMAIL\", Value = user.Email}); } For ASP.NET Core the code could look like this: builder.Services.AddElmahIo(o => { // ... o.OnMessage = message => { if (string.IsNullOrWhiteSpace(message.User)) return; var db = /*...*/; var user = db.GetById<User>(message.User); message.Data.Add(new Item {Key = \"X-ELMAHIO-USEREMAIL\", Value = user.Email}); }; }); OnMessage event handlers are executed just before a message is sent to elmah.io. In the body of the event handler, the user's email is fetched from the database by calling the GetById method. How you will be able to convert the user ID to an email depends on your tech stack, but you get the picture. That's it! A few lines of code and you can watch every little detail about the users experiencing problems on your website:","title":"Use extended user details without email as ID"},{"location":"use-multiple-logs-for-different-environments/","text":"Use multiple logs for different environments We bet that you use at least two environments for hosting your website: localhost and a production environment. You probably need to log website errors on all your environments, but you don\u2019t want to mix errors from different environments in the same error log. Lucky for you, Microsoft provides a great way of differentiating configuration for different environments called Web Config transformation . To avoid spending numerous hours of debugging, remember that Web Config transformations are only run on deploy and not on build. In other words, deploy your website using Visual Studio, MSBuild, or third for the transformations to replace the right ELMAH config. Whether or not you want errors from localhost logged on elmah.io, start by installing the Elmah.Io NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io Then choose one of the two paths below. Our Web.config transformations - The definitive syntax guide contains general information about how transformations work and you can use the Web.config Transformation Tester to validate transformation files. Logging to elmah.io from both localhost and production Create two new logs at the elmah.io website called something like \u201cMy website\u201d and \u201cMy website development\u201d. The naming isn\u2019t really important, so pick something telling. During installation of the elmah.io package, NuGet will ask you for your elmah.io log id. In this dialog input the log id from the log named \u201cMy website development\u201d. The default configuration is used when running your website locally. When installed open the web.release.config file and add the following code: <elmah xdt:Transform=\"Replace\"> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> <security allowRemoteAccess=\"false\" /> </elmah> Replace the API_KEY with your API key ( Where is my API key? ) and LOG_ID with your log ID ( Where is my log ID? ). For more information about Web.config transformations, check out the blog post Web.config transformations - The definitive syntax guide . For help debugging problems, we have created the Web.config Transformation Tester . That\u2019s it! You can now build and deploy your website using different configurations. When nothing is changed, Visual Studio will build your website using the Debug configuration. This configuration looks for the ELMAH code in the web.debug.config file. We didn\u2019t add any ELMAH configuration to this file, why the default values from web.config are used. When selecting the Release configuration, Web. Config transformations will replace the default values in web.config with the new ELMAH configuration from web.release.config . Logging to elmah.io from production only During the installation, NuGet will ask you for your elmah.io log id. You don't need to write anything in this dialog since we will remove the default elmah.io config in a moment. When installed open the web.config file and locate the <elmah> element. Remove the <errorLog> element and set the allowRemoveAccess attribute to true . Your configuration should look like this now: <elmah> <security allowRemoteAccess=\"true\" /> </elmah> Open the web.release.config file and insert the following code: <elmah xdt:Transform=\"Replace\"> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> <security allowRemoteAccess=\"false\" /> </elmah> Like above, replace API_KEY and LOG_ID with the correct values. Errors happening on your local machine will be logged using ELMAH's default error logger (in-memory) and errors happening in production will be logged to elmah.io.","title":"Use multiple logs for different environments"},{"location":"use-multiple-logs-for-different-environments/#use-multiple-logs-for-different-environments","text":"We bet that you use at least two environments for hosting your website: localhost and a production environment. You probably need to log website errors on all your environments, but you don\u2019t want to mix errors from different environments in the same error log. Lucky for you, Microsoft provides a great way of differentiating configuration for different environments called Web Config transformation . To avoid spending numerous hours of debugging, remember that Web Config transformations are only run on deploy and not on build. In other words, deploy your website using Visual Studio, MSBuild, or third for the transformations to replace the right ELMAH config. Whether or not you want errors from localhost logged on elmah.io, start by installing the Elmah.Io NuGet package: Package Manager .NET CLI PackageReference Paket CLI Install-Package Elmah.Io dotnet add package Elmah.Io <PackageReference Include=\"Elmah.Io\" Version=\"5.*\" /> paket add Elmah.Io Then choose one of the two paths below. Our Web.config transformations - The definitive syntax guide contains general information about how transformations work and you can use the Web.config Transformation Tester to validate transformation files.","title":"Use multiple logs for different environments"},{"location":"use-multiple-logs-for-different-environments/#logging-to-elmahio-from-both-localhost-and-production","text":"Create two new logs at the elmah.io website called something like \u201cMy website\u201d and \u201cMy website development\u201d. The naming isn\u2019t really important, so pick something telling. During installation of the elmah.io package, NuGet will ask you for your elmah.io log id. In this dialog input the log id from the log named \u201cMy website development\u201d. The default configuration is used when running your website locally. When installed open the web.release.config file and add the following code: <elmah xdt:Transform=\"Replace\"> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> <security allowRemoteAccess=\"false\" /> </elmah> Replace the API_KEY with your API key ( Where is my API key? ) and LOG_ID with your log ID ( Where is my log ID? ). For more information about Web.config transformations, check out the blog post Web.config transformations - The definitive syntax guide . For help debugging problems, we have created the Web.config Transformation Tester . That\u2019s it! You can now build and deploy your website using different configurations. When nothing is changed, Visual Studio will build your website using the Debug configuration. This configuration looks for the ELMAH code in the web.debug.config file. We didn\u2019t add any ELMAH configuration to this file, why the default values from web.config are used. When selecting the Release configuration, Web. Config transformations will replace the default values in web.config with the new ELMAH configuration from web.release.config .","title":"Logging to elmah.io from both localhost and production"},{"location":"use-multiple-logs-for-different-environments/#logging-to-elmahio-from-production-only","text":"During the installation, NuGet will ask you for your elmah.io log id. You don't need to write anything in this dialog since we will remove the default elmah.io config in a moment. When installed open the web.config file and locate the <elmah> element. Remove the <errorLog> element and set the allowRemoveAccess attribute to true . Your configuration should look like this now: <elmah> <security allowRemoteAccess=\"true\" /> </elmah> Open the web.release.config file and insert the following code: <elmah xdt:Transform=\"Replace\"> <errorLog type=\"Elmah.Io.ErrorLog, Elmah.Io\" apiKey=\"API_KEY\" logId=\"LOG_ID\" /> <security allowRemoteAccess=\"false\" /> </elmah> Like above, replace API_KEY and LOG_ID with the correct values. Errors happening on your local machine will be logged using ELMAH's default error logger (in-memory) and errors happening in production will be logged to elmah.io.","title":"Logging to elmah.io from production only"},{"location":"using-different-logs-per-environment-in-aspnet-core/","text":"Using different logs per environment in ASP.NET Core We are often asked the question: Should I create a log per environment and how do I set it up with ASP.NET Core? Creating a log per environment (staging, production, etc.) is a good idea since you probably want different notification rules and/or user access depending on the environment. This document provides a range of possibilities for setting up a log per environment. This document is intended for the Elmah.Io.AspNetCore package only. You can use the same approach for Elmah.Io.Extensions.Logging but we recommend using log filtering in the appsettings.json file or through C# instead. Using appsettings.{Environment}.json All ASP.NET Core websites read an environment variable named ASPNETCORE_ENVIRONMENT . The value can be used to set config variables depending on the current environment. The feature works a bit like Web.config transformations that you may remember from the good old ASP.NET days. The value of ASPNETCORE_ENVIRONMENT can be tailored to your need but the following three values are provided out of the box: Development , Staging , and Production . To only add elmah.io when on staging or production, you can add the following code when setting up the elmah.io middleware: if (builder.Environment.IsProduction() || builder.Environment.IsStaging()) { app.UseElmahIo(); } To create errors in different logs depending on the current environment, create two new files: appsettings.staging.json and appsettings.production.json . Add the ElmahIo config section to both files: { \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } } Inside each file, replace API_KEY with your API key and LOG_ID with the different log IDs. The downside of this approach is that you have your production API key and log ID in source control. Using Azure Application settings If you are hosting on Azure (other cloud hosting platforms have a similar feature), you can utilize the built-in Application settings feature to avoid adding API keys and log IDs to source control. Using application settings requires you to specify your elmah.io configuration in the appsettings.json file or one of the environment-specific files as shown above. To replace the values inside the ApiKey and LogId properties, use the colon syntax as shown here: (replace API_KEY and LOG_ID with your staging or production values depending on which environment you are configuring)","title":"Using different logs per environment in ASP.NET Core"},{"location":"using-different-logs-per-environment-in-aspnet-core/#using-different-logs-per-environment-in-aspnet-core","text":"We are often asked the question: Should I create a log per environment and how do I set it up with ASP.NET Core? Creating a log per environment (staging, production, etc.) is a good idea since you probably want different notification rules and/or user access depending on the environment. This document provides a range of possibilities for setting up a log per environment. This document is intended for the Elmah.Io.AspNetCore package only. You can use the same approach for Elmah.Io.Extensions.Logging but we recommend using log filtering in the appsettings.json file or through C# instead.","title":"Using different logs per environment in ASP.NET Core"},{"location":"using-different-logs-per-environment-in-aspnet-core/#using-appsettingsenvironmentjson","text":"All ASP.NET Core websites read an environment variable named ASPNETCORE_ENVIRONMENT . The value can be used to set config variables depending on the current environment. The feature works a bit like Web.config transformations that you may remember from the good old ASP.NET days. The value of ASPNETCORE_ENVIRONMENT can be tailored to your need but the following three values are provided out of the box: Development , Staging , and Production . To only add elmah.io when on staging or production, you can add the following code when setting up the elmah.io middleware: if (builder.Environment.IsProduction() || builder.Environment.IsStaging()) { app.UseElmahIo(); } To create errors in different logs depending on the current environment, create two new files: appsettings.staging.json and appsettings.production.json . Add the ElmahIo config section to both files: { \"ElmahIo\": { \"ApiKey\": \"API_KEY\", \"LogId\": \"LOG_ID\" } } Inside each file, replace API_KEY with your API key and LOG_ID with the different log IDs. The downside of this approach is that you have your production API key and log ID in source control.","title":"Using appsettings.{Environment}.json"},{"location":"using-different-logs-per-environment-in-aspnet-core/#using-azure-application-settings","text":"If you are hosting on Azure (other cloud hosting platforms have a similar feature), you can utilize the built-in Application settings feature to avoid adding API keys and log IDs to source control. Using application settings requires you to specify your elmah.io configuration in the appsettings.json file or one of the environment-specific files as shown above. To replace the values inside the ApiKey and LogId properties, use the colon syntax as shown here: (replace API_KEY and LOG_ID with your staging or production values depending on which environment you are configuring)","title":"Using Azure Application settings"},{"location":"using-the-elmah-io-extension-for-visual-studio/","text":"Using the elmah.io extension for Visual Studio Being able to focus on .NET developers makes it possible to do all kinds of cool things. Like building an elmah.io extension for Visual Studio. That's exactly what we've done and here's how to use it. Installation Download the elmah.io extension for Visual Studio \u2264 2019 or Visual Studio 2022 from the Visual Studio Marketplace. Don't use the Extensions and Updates feature inside Visual Studio, since Visual Studio causes a problem with installing extensions in previous versions. Double/click the downloaded VSIX and enable elmah.io in the versions of Visual Studio of your choice. The extension supports Visual Studio 2015, 2017, 2019, and 2022. Usage Inside Visual Studio navigate to View | Other Windows | elmah.io or simply search for elmah.io in the Quick Launcher ( Ctrl + Q ). You'll see the elmah.io window somewhere. Click the Sign In button and sign in with a username/password or one of the social providers: If you are part of multiple elmah.io organizations, select the one you want to access: If this is the first time someone is browsing the chosen organization from Visual Studio, authorize the creation of a new API key: Once logged in, the list of logs is populated with all of your logs defined at elmah.io. Select a log and click the search icon: Log messages can be filtered by date range, full-text search, and using Lucene query language as already known from the elmah.io UI. To inspect a single message, double-click it and the message details window will open: The toolbar at the top provides a couple of options: View the message on elmah.io, hide the message and delete the message.","title":"Using the elmah.io extension for Visual Studio"},{"location":"using-the-elmah-io-extension-for-visual-studio/#using-the-elmahio-extension-for-visual-studio","text":"Being able to focus on .NET developers makes it possible to do all kinds of cool things. Like building an elmah.io extension for Visual Studio. That's exactly what we've done and here's how to use it.","title":"Using the elmah.io extension for Visual Studio"},{"location":"using-the-elmah-io-extension-for-visual-studio/#installation","text":"Download the elmah.io extension for Visual Studio \u2264 2019 or Visual Studio 2022 from the Visual Studio Marketplace. Don't use the Extensions and Updates feature inside Visual Studio, since Visual Studio causes a problem with installing extensions in previous versions. Double/click the downloaded VSIX and enable elmah.io in the versions of Visual Studio of your choice. The extension supports Visual Studio 2015, 2017, 2019, and 2022.","title":"Installation"},{"location":"using-the-elmah-io-extension-for-visual-studio/#usage","text":"Inside Visual Studio navigate to View | Other Windows | elmah.io or simply search for elmah.io in the Quick Launcher ( Ctrl + Q ). You'll see the elmah.io window somewhere. Click the Sign In button and sign in with a username/password or one of the social providers: If you are part of multiple elmah.io organizations, select the one you want to access: If this is the first time someone is browsing the chosen organization from Visual Studio, authorize the creation of a new API key: Once logged in, the list of logs is populated with all of your logs defined at elmah.io. Select a log and click the search icon: Log messages can be filtered by date range, full-text search, and using Lucene query language as already known from the elmah.io UI. To inspect a single message, double-click it and the message details window will open: The toolbar at the top provides a couple of options: View the message on elmah.io, hide the message and delete the message.","title":"Usage"},{"location":"using-the-rest-api/","text":"Using the REST API Using the REST API Security Messages Creating messages Getting a message Searching messages Deleting a message Deleting messages Hiding a message Fixing a message Under the hood, everything related to communicating with elmah.io happens through our REST API. In this article, we will present the possibilities of using the API in a use case-driven approach. For a detailed reference of the various endpoints, visit the API V3 documentation . Security Security is implemented using API keys ( Where is my API key? ). When creating a new organization, a default API key is automatically created. You can create new keys and revoke an existing key if you suspect that the key has been compromised. The API key acts as a secret and should not be available to people outside your team/organization. All requests to the elmah.io API needs the API key as either an HTTP header or query string parameter named api_key like this: GET https://api.elmah.io/v3/messages/LOG_ID?api_key=MY_API_KEY Messages Creating messages Before doing anything, we will need some messages to play with. The Create Message endpoint does just that. To create a simple message, POST to: POST https://api.elmah.io/v3/messages/LOG_ID with a JSON body: { \"title\": \"This is a test message\" } (replace LOG_ID with your log ID): The title field is the only required field on a message, but fields for specifying severity, timestamp, etc. are there. For more information, check out the documentation . If everything where successful, the API returns an HTTP status code of 201 and a location to where to fetch the new message. If the endpoint fails, the response will contain a description of what went wrong. Forgetting to set Content-Length , Content-Type and similar, will result in an invalid request. Getting a message In the example above, the API returned the URL for getting the newly created message: GET https://api.elmah.io/v3/messages/LOG_ID/81C7C282C9FDAEA3 By making a GET request to this URL, we get back the message details: { \"id\": \"99CDEA3D6A631F09\", \"title\": \"This is a test message\", \"dateTime\": \"2016-07-03T14:25:46.087857Z\", \"severity\": \"Information\" } As shown in the returned body, elmah.io automatically inserted some missing fields like a timestamp and a severity. If no severity is specified during creating, a message is treated as information. Searching messages For the demo, we have inserted a couple of additional messages, which leads us to the next endpoint: searching messages. The search endpoint shares the root path with the get message endpoint but only takes a log ID. The simplest possible configuration queries the API for a list of the 15 most recent messages by calling: GET https://api.elmah.io/v3/messages/LOG_ID The response body looks like this: { \"messages\": [ { \"id\": \"81C7C282C9FDAEA3\", \"title\": \"This is another test message\", \"dateTime\": \"2016-07-03T14:31:45.053851Z\", \"severity\": \"Information\" }, { \"id\": \"99CDEA3D6A631F09\", \"title\": \"This is a test message\", \"dateTime\": \"2016-07-03T14:25:46.087857Z\", \"severity\": \"Information\" }, // ... ], \"total\": 42 } For simplicity, the response has been simplified by not showing all of the results. The important thing to notice here is the list of messages and the total count. messages contain 15 messages, which is the default page size in the search endpoint. To increase the number of returned messages, set the pagesize parameter in the URL (max 100 messages per request). The total count tells you if more messages are matching your search. To select messages from the next page, use the pageindex parameter (or use the searchAfter property as shown later). Returning all messages may be fine, but being able to search by terms is even more fun. To search, use the query , from , and to parameters as shown here: GET https://api.elmah.io/v3/messages/LOG_ID?query=another Searching for another will return the following response: { \"messages\": [ { \"id\": \"81C7C282C9FDAEA3\", \"title\": \"This is another test message\", \"dateTime\": \"2016-07-03T14:25:46.087857Z\", \"severity\": \"Information\" } ], \"total\": 1 } Now only 81C7C282C9FDAEA3 shows up since that message contains the text another in the title field. Like specifying the query parameter, you can limit the number of messages using the from , to , and pageSize parameters. There is a limitation of using the pageSize and pageIndex parameters. The data is stored in Elasticsearch which doesn't allow pagination in more than 10,000 documents. If you need to fetch more than 10,000 documents from your log, we recommend breaking this up into weekly, daily, or hourly jobs instead of changing your job to fetch more than 10,000 messages. In case this is not possible, you can switch from using the pageIndex parameter to searchAfter . Each search result will return a value in the searchAfter property: { \"messages\": [ // ... ], \"searchAfter\": \"1694180633270\", \"total\": 42 } To fetch the next list of messages you can provide the search endpoint with the value of searchAfter : GET https://api.elmah.io/v3/messages/LOG_ID?searchAfter=1694180633270 You will need to use the same set of parameters in query , from , and to as in the previous request for this to work. Deleting a message When fixing the bug causing an error logged at elmah.io, you may want to delete the error. Deleting a single error is as easy as fetching it. Create a DELETE request to the errors unique URL: DELETE https://api.elmah.io/v3/messages/LOG_ID/81C7C282C9FDAEA3 When successfully deleted, the delete endpoint returns an HTTP status code of 200 . Deleting messages Deleting messages one by one can be tedious work. To delete multiple errors, you can utilize the Delete Messages endpoint by creating a DELETE request to: DELETE https://api.elmah.io/v3/messages/LOG_ID The request must contain a body with at least a query: { \"query\": \"test\" } An option for deleting messages by date range is available as well. Check out the API documentation for details. Hiding a message Depending on your use case, you may want to hide a message, rather than delete it. Hidden messages are shown as default through neither the UI nor the REST API. But you will be able to search for them by enabling the Hidden checkbox on the UI. To hide a message, use the _hide endpoint like this: POST https://api.elmah.io/v3/messages/LOG_ID/99CDEA3D6A631F09/_hide If successful, the endpoint returns an HTTP status code of 200 . Fixing a message When you have fixed a bug in your code, it's a good idea to mark any instances of this error in elmah.io as fixed. This gives a better overview of what to fix and ensures that you are notified if the error happens again. To mark a message as fixed, use the _fix endpoint like this: POST https://api.elmah.io/v3/messages/LOG_ID/99CDEA3D6A631F09/_fix If successful, the endpoint returns an HTTP status code of 200 . This will mark a single message as fixed. In case you want to mark all instances of this message as fixe, include the markAllAsFixed parameter: POST https://api.elmah.io/v3/messages/LOG_ID/99CDEA3D6A631F09/_fix?markAllAsFixed=true","title":"Using the REST API"},{"location":"using-the-rest-api/#using-the-rest-api","text":"Using the REST API Security Messages Creating messages Getting a message Searching messages Deleting a message Deleting messages Hiding a message Fixing a message Under the hood, everything related to communicating with elmah.io happens through our REST API. In this article, we will present the possibilities of using the API in a use case-driven approach. For a detailed reference of the various endpoints, visit the API V3 documentation .","title":"Using the REST API"},{"location":"using-the-rest-api/#security","text":"Security is implemented using API keys ( Where is my API key? ). When creating a new organization, a default API key is automatically created. You can create new keys and revoke an existing key if you suspect that the key has been compromised. The API key acts as a secret and should not be available to people outside your team/organization. All requests to the elmah.io API needs the API key as either an HTTP header or query string parameter named api_key like this: GET https://api.elmah.io/v3/messages/LOG_ID?api_key=MY_API_KEY","title":"Security"},{"location":"using-the-rest-api/#messages","text":"","title":"Messages"},{"location":"using-the-rest-api/#creating-messages","text":"Before doing anything, we will need some messages to play with. The Create Message endpoint does just that. To create a simple message, POST to: POST https://api.elmah.io/v3/messages/LOG_ID with a JSON body: { \"title\": \"This is a test message\" } (replace LOG_ID with your log ID): The title field is the only required field on a message, but fields for specifying severity, timestamp, etc. are there. For more information, check out the documentation . If everything where successful, the API returns an HTTP status code of 201 and a location to where to fetch the new message. If the endpoint fails, the response will contain a description of what went wrong. Forgetting to set Content-Length , Content-Type and similar, will result in an invalid request.","title":"Creating messages"},{"location":"using-the-rest-api/#getting-a-message","text":"In the example above, the API returned the URL for getting the newly created message: GET https://api.elmah.io/v3/messages/LOG_ID/81C7C282C9FDAEA3 By making a GET request to this URL, we get back the message details: { \"id\": \"99CDEA3D6A631F09\", \"title\": \"This is a test message\", \"dateTime\": \"2016-07-03T14:25:46.087857Z\", \"severity\": \"Information\" } As shown in the returned body, elmah.io automatically inserted some missing fields like a timestamp and a severity. If no severity is specified during creating, a message is treated as information.","title":"Getting a message"},{"location":"using-the-rest-api/#searching-messages","text":"For the demo, we have inserted a couple of additional messages, which leads us to the next endpoint: searching messages. The search endpoint shares the root path with the get message endpoint but only takes a log ID. The simplest possible configuration queries the API for a list of the 15 most recent messages by calling: GET https://api.elmah.io/v3/messages/LOG_ID The response body looks like this: { \"messages\": [ { \"id\": \"81C7C282C9FDAEA3\", \"title\": \"This is another test message\", \"dateTime\": \"2016-07-03T14:31:45.053851Z\", \"severity\": \"Information\" }, { \"id\": \"99CDEA3D6A631F09\", \"title\": \"This is a test message\", \"dateTime\": \"2016-07-03T14:25:46.087857Z\", \"severity\": \"Information\" }, // ... ], \"total\": 42 } For simplicity, the response has been simplified by not showing all of the results. The important thing to notice here is the list of messages and the total count. messages contain 15 messages, which is the default page size in the search endpoint. To increase the number of returned messages, set the pagesize parameter in the URL (max 100 messages per request). The total count tells you if more messages are matching your search. To select messages from the next page, use the pageindex parameter (or use the searchAfter property as shown later). Returning all messages may be fine, but being able to search by terms is even more fun. To search, use the query , from , and to parameters as shown here: GET https://api.elmah.io/v3/messages/LOG_ID?query=another Searching for another will return the following response: { \"messages\": [ { \"id\": \"81C7C282C9FDAEA3\", \"title\": \"This is another test message\", \"dateTime\": \"2016-07-03T14:25:46.087857Z\", \"severity\": \"Information\" } ], \"total\": 1 } Now only 81C7C282C9FDAEA3 shows up since that message contains the text another in the title field. Like specifying the query parameter, you can limit the number of messages using the from , to , and pageSize parameters. There is a limitation of using the pageSize and pageIndex parameters. The data is stored in Elasticsearch which doesn't allow pagination in more than 10,000 documents. If you need to fetch more than 10,000 documents from your log, we recommend breaking this up into weekly, daily, or hourly jobs instead of changing your job to fetch more than 10,000 messages. In case this is not possible, you can switch from using the pageIndex parameter to searchAfter . Each search result will return a value in the searchAfter property: { \"messages\": [ // ... ], \"searchAfter\": \"1694180633270\", \"total\": 42 } To fetch the next list of messages you can provide the search endpoint with the value of searchAfter : GET https://api.elmah.io/v3/messages/LOG_ID?searchAfter=1694180633270 You will need to use the same set of parameters in query , from , and to as in the previous request for this to work.","title":"Searching messages"},{"location":"using-the-rest-api/#deleting-a-message","text":"When fixing the bug causing an error logged at elmah.io, you may want to delete the error. Deleting a single error is as easy as fetching it. Create a DELETE request to the errors unique URL: DELETE https://api.elmah.io/v3/messages/LOG_ID/81C7C282C9FDAEA3 When successfully deleted, the delete endpoint returns an HTTP status code of 200 .","title":"Deleting a message"},{"location":"using-the-rest-api/#deleting-messages","text":"Deleting messages one by one can be tedious work. To delete multiple errors, you can utilize the Delete Messages endpoint by creating a DELETE request to: DELETE https://api.elmah.io/v3/messages/LOG_ID The request must contain a body with at least a query: { \"query\": \"test\" } An option for deleting messages by date range is available as well. Check out the API documentation for details.","title":"Deleting messages"},{"location":"using-the-rest-api/#hiding-a-message","text":"Depending on your use case, you may want to hide a message, rather than delete it. Hidden messages are shown as default through neither the UI nor the REST API. But you will be able to search for them by enabling the Hidden checkbox on the UI. To hide a message, use the _hide endpoint like this: POST https://api.elmah.io/v3/messages/LOG_ID/99CDEA3D6A631F09/_hide If successful, the endpoint returns an HTTP status code of 200 .","title":"Hiding a message"},{"location":"using-the-rest-api/#fixing-a-message","text":"When you have fixed a bug in your code, it's a good idea to mark any instances of this error in elmah.io as fixed. This gives a better overview of what to fix and ensures that you are notified if the error happens again. To mark a message as fixed, use the _fix endpoint like this: POST https://api.elmah.io/v3/messages/LOG_ID/99CDEA3D6A631F09/_fix If successful, the endpoint returns an HTTP status code of 200 . This will mark a single message as fixed. In case you want to mark all instances of this message as fixe, include the markAllAsFixed parameter: POST https://api.elmah.io/v3/messages/LOG_ID/99CDEA3D6A631F09/_fix?markAllAsFixed=true","title":"Fixing a message"},{"location":"where-is-my-api-key/","text":"Where is my API key? API keys are a new concept introduced with version 3.x of our API . API keys are located on the organization settings page. To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard: When on the organization settings page, click the API Keys tab and copy your API key: This view also lets you generate new API keys and revoke an existing key if you believe that the key is compromised.","title":"Where is my API key"},{"location":"where-is-my-api-key/#where-is-my-api-key","text":"API keys are a new concept introduced with version 3.x of our API . API keys are located on the organization settings page. To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard: When on the organization settings page, click the API Keys tab and copy your API key: This view also lets you generate new API keys and revoke an existing key if you believe that the key is compromised.","title":"Where is my API key?"},{"location":"where-is-my-invoice-receipt/","text":"Where is my invoice/receipt? Invoices are located on the organization settings page. To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard: When on the organization settings page, click the Invoices tab: There's a row per invoice in the table on this page. You can open or download the invoice using the links to the right. If you want invoices emailed to you (or your accountant) every time a payment is made, click the Email invoices button and input an email address.","title":"Where is my invoice / receipt"},{"location":"where-is-my-invoice-receipt/#where-is-my-invoicereceipt","text":"Invoices are located on the organization settings page. To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard: When on the organization settings page, click the Invoices tab: There's a row per invoice in the table on this page. You can open or download the invoice using the links to the right. If you want invoices emailed to you (or your accountant) every time a payment is made, click the Email invoices button and input an email address.","title":"Where is my invoice/receipt?"},{"location":"where-is-my-log-id/","text":"Where is my log ID? A log ID represents a log on elmah.io. A log is a container for log messages. How you choose to split up your logs is totally up to you, but creating a log per application/service is what most users do. The log ID is located on the log settings page. To open log settings, click the gears icon next to the log name in the menu or in the log box on the dashboard: When clicking on the icon, you are taken directly to the Install tab:","title":"Where is my log ID"},{"location":"where-is-my-log-id/#where-is-my-log-id","text":"A log ID represents a log on elmah.io. A log is a container for log messages. How you choose to split up your logs is totally up to you, but creating a log per application/service is what most users do. The log ID is located on the log settings page. To open log settings, click the gears icon next to the log name in the menu or in the log box on the dashboard: When clicking on the icon, you are taken directly to the Install tab:","title":"Where is my log ID?"},{"location":"where-is-the-permalink-button/","text":"Where is the permalink button A permalink is a link to a specific log message on elmah.io. When generating a permalink, the log message will be shown in expanded form on the log search page. To get the permalink, go to the log search page and click a log message to expand the details. Beneath the log message, there's a permalink button that will open the message in a new tab: You can also get a permalink directly from the search result:","title":"Where is the permalink button"},{"location":"where-is-the-permalink-button/#where-is-the-permalink-button","text":"A permalink is a link to a specific log message on elmah.io. When generating a permalink, the log message will be shown in expanded form on the log search page. To get the permalink, go to the log search page and click a log message to expand the details. Beneath the log message, there's a permalink button that will open the message in a new tab: You can also get a permalink directly from the search result:","title":"Where is the permalink button"}]} \ No newline at end of file diff --git a/search/worker.js b/search/worker.js new file mode 100644 index 0000000000..8628dbce94 --- /dev/null +++ b/search/worker.js @@ -0,0 +1,133 @@ +var base_path = 'function' === typeof importScripts ? '.' : '/search/'; +var allowSearch = false; +var index; +var documents = {}; +var lang = ['en']; +var data; + +function getScript(script, callback) { + console.log('Loading script: ' + script); + $.getScript(base_path + script).done(function () { + callback(); + }).fail(function (jqxhr, settings, exception) { + console.log('Error: ' + exception); + }); +} + +function getScriptsInOrder(scripts, callback) { + if (scripts.length === 0) { + callback(); + return; + } + getScript(scripts[0], function() { + getScriptsInOrder(scripts.slice(1), callback); + }); +} + +function loadScripts(urls, callback) { + if( 'function' === typeof importScripts ) { + importScripts.apply(null, urls); + callback(); + } else { + getScriptsInOrder(urls, callback); + } +} + +function onJSONLoaded () { + data = JSON.parse(this.responseText); + var scriptsToLoad = ['lunr.js']; + if (data.config && data.config.lang && data.config.lang.length) { + lang = data.config.lang; + } + if (lang.length > 1 || lang[0] !== "en") { + scriptsToLoad.push('lunr.stemmer.support.js'); + if (lang.length > 1) { + scriptsToLoad.push('lunr.multi.js'); + } + if (lang.includes("ja") || lang.includes("jp")) { + scriptsToLoad.push('tinyseg.js'); + } + for (var i=0; i < lang.length; i++) { + if (lang[i] != 'en') { + scriptsToLoad.push(['lunr', lang[i], 'js'].join('.')); + } + } + } + loadScripts(scriptsToLoad, onScriptsLoaded); +} + +function onScriptsLoaded () { + console.log('All search scripts loaded, building Lunr index...'); + if (data.config && data.config.separator && data.config.separator.length) { + lunr.tokenizer.separator = new RegExp(data.config.separator); + } + + if (data.index) { + index = lunr.Index.load(data.index); + data.docs.forEach(function (doc) { + documents[doc.location] = doc; + }); + console.log('Lunr pre-built index loaded, search ready'); + } else { + index = lunr(function () { + if (lang.length === 1 && lang[0] !== "en" && lunr[lang[0]]) { + this.use(lunr[lang[0]]); + } else if (lang.length > 1) { + this.use(lunr.multiLanguage.apply(null, lang)); // spread operator not supported in all browsers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Browser_compatibility + } + this.field('title'); + this.field('text'); + this.ref('location'); + + for (var i=0; i < data.docs.length; i++) { + var doc = data.docs[i]; + this.add(doc); + documents[doc.location] = doc; + } + }); + console.log('Lunr index built, search ready'); + } + allowSearch = true; + postMessage({config: data.config}); + postMessage({allowSearch: allowSearch}); +} + +function init () { + var oReq = new XMLHttpRequest(); + oReq.addEventListener("load", onJSONLoaded); + var index_path = base_path + '/search_index.json'; + if( 'function' === typeof importScripts ){ + index_path = 'search_index.json'; + } + oReq.open("GET", index_path); + oReq.send(); +} + +function search (query) { + if (!allowSearch) { + console.error('Assets for search still loading'); + return; + } + + var resultDocuments = []; + var results = index.search(query); + for (var i=0; i < results.length; i++){ + var result = results[i]; + doc = documents[result.ref]; + doc.summary = doc.text.substring(0, 200); + resultDocuments.push(doc); + } + return resultDocuments; +} + +if( 'function' === typeof importScripts ) { + onmessage = function (e) { + if (e.data.init) { + init(); + } else if (e.data.query) { + postMessage({ results: search(e.data.query) }); + } else { + console.error("Worker - Unrecognized message: " + e); + } + }; +} diff --git a/setting-application-name/index.html b/setting-application-name/index.html new file mode 100644 index 0000000000..a8f8e0c3f1 --- /dev/null +++ b/setting-application-name/index.html @@ -0,0 +1,678 @@ +<!doctype html> +<html lang="en"> + <head> + <!-- Required meta tags --> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="description" content="When multiple applications log to the same elmah.io log we recommend setting the app name. Learn how to set it up in ASP.NET, MVC, and Web API."> + <meta name="author" content="Thomas Ardal"> + <meta name="twitter:card" content="summary_large_image"> + <meta name="twitter:site" content="@elmah_io"> + <meta name="twitter:creator" content="@elmah_io"> + <meta name="twitter:title" content="Setting application name | elmah.io"> + <meta name="twitter:description" content="When multiple applications log to the same elmah.io log we recommend setting the app name. Learn how to set it up in ASP.NET, MVC, and Web API."> + <meta name="twitter:image" content="https://docs.elmah.io/../assets/img/open-graph-elmahio.png"> + <meta property="og:site_name" content="elmah.io Documentation" /> + <meta property="og:title" content="Setting application name"> + <meta property="og:description" content="When multiple applications log to the same elmah.io log we recommend setting the app name. Learn how to set it up in ASP.NET, MVC, and Web API."> + <meta property="og:type" content="article"> + <meta property="og:url" content="https://docs.elmah.io/setting-application-name/"> + <meta property="og:image" content="https://docs.elmah.io/../assets/img/open-graph-elmahio.png"> + <meta property="article:published_time" content="2023-11-29"> + <meta property="article:modified_time" content="2023-11-29"> + <meta property="article:publisher" content="https://www.facebook.com/elmahdotio/" /> + <meta property="article:author" content="elmah.io" /> + <meta property="fb:app_id" content="218008838347208" /> + <title>Setting application name + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Setting application name

    +

    If logging to the same log from multiple applications, it might be a good idea to set an application name on all log messages. This will let you search for errors generated by a specific application through the elmah.io UI. Also, ELMAH automatically sets the application name to the Guid of the application inside IIS, why it often looks better to either clear this or set something meaningful.

    +

    The application name can be easily set from the web.config file as part of the elmah.io configuration:

    +

    <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="API_KEY" logId="LOG_ID" applicationName="MyApp" />
    +

    +

    This will decorate all errors logged to elmah.io with the application name MyApp.

    +

    A common use of Web.config Transformations is to have a transformation per environment, customer, application, etc. If this is the case, you can replace the application name as part of the config transformation. Check out Use multiple logs for different environments for more information about how to update the errorLog element.

    +
    +
    +
    +
    +
    +
    Our Web.config transformations - The definitive syntax guide contains general information about how transformations work and you can use the Web.config Transformation Tester to validate transformation files.
    +
    +
    + +

    If you, for some reason, cannot set the application name in the web.config file, it can be specified from C# by including the following code in the Application_Start method in the global.asax.cs file:

    +

    Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client
    +var logger = Elmah.Io.ErrorLog.Client;
    +logger.OnMessage += (sender, args) =>
    +{
    +    args.Message.Application = "MyApp";
    +};
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup-deployment-tracking/index.html b/setup-deployment-tracking/index.html new file mode 100644 index 0000000000..a6d084a54f --- /dev/null +++ b/setup-deployment-tracking/index.html @@ -0,0 +1,797 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Set Up Deployment Tracking + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Set Up Deployment Tracking

    + +

    Deployment tracking creates an overview of the different versions of your software and shows you how well each version performed. With this integration in place, you will be able to see when you released and if some of your releases caused more errors than others. While most pages on elmah.io support everything from verbose to fatal messages, the context on deployment tracking is around warnings and errors.

    +

    To set up deployment tracking, you will need to tell elmah.io when you release, using our REST API or one of the integrations:

    + + +

    Deployments are as default created on all of your logs, but this can be tweaked. More about this later.

    +

    For a complete overview of the possibilities with deployment tracking, watch this video tutorial:

    +

    + deployment-tracking + +

    +

    Generate a new API key

    +

    While you can use the same API key for everything, we recommend you create an API key specific to deployment tracking. To do so, go to the organization settings page by clicking the gears icon next to the organization name on either the dashboard or in the left menu. Select the API Keys tabs and click Add API Key. Fill in a name of choice and enable the Deployments > Write permission only:

    +

    Deployment tracking API key

    +

    Click the save button and copy the API key for later use.

    +

    Tell elmah.io when you release

    +

    When you create a release of your software either manually or with the help of a tool like Octopus, you need to tell elmah.io about it. The elmah.io REST API v3 provides an endpoint named deployments, which you can call when creating releases. After calling the endpoint, all new messages to your logs will automatically be decorated with the most recent version number.

    +

    If you release your software manually, creating the new release manually is easy using Swagger UI. Swagger UI is a graphical client for calling a Swagger-enabled endpoint (much like Postman). Navigate to https://api.elmah.io/swagger/index.html, expand the Deployments node and click the POST request:

    +

    Deployments POST

    +

    To create the release, input your API key (Where is my API key?) in the top right corner and click the JSON beneath Model Schema. This copies the example JSON to the deployment parameter. A minimal deployment would look like the following, but adding more information makes the experience within elmah.io even better:

    +

    {
    +  "version": "1.42.7"
    +}
    +

    +

    The version string in the example conforms to SemVer, but the content can be anything. The date of the release is automatically added if not specified in the JSON body.

    +

    Click the Try it out! button and the deployment is created.

    +

    We support a range of different integrations to avoid you manually having to use Swagger UI every time you release. Click one of the products below for instructions:

    + +

    Decorate your messages with a version number

    +

    As default, all messages are decorated with the most recent deployment version. If you want to override this behavior, check out Adding Version Information for details.

    +

    Versioning Different Services

    +

    Chances are that your software consists of multiple services released independently and with different version numbers. This is a common pattern when splitting up a software system in microservices. How you choose to split your elmah.io logs is entirely up to you, but we almost always recommend having a separate log for each service. When doing so, you only want deployment tracking to show the releases from the service you are currently looking at. The problem here is that deployments on elmah.io are shown on all logs as default.

    +

    To make sure that only deployments related to the service you are looking at are shown, you need to decorate each deployment with the log ID where it belongs. The deployments API supports this through an optional logId property. If set, the new deployment is only shown on the specified log.

    +

    Enable/Disable logging while deploying

    +

    Some websites and services are not built to work properly while deploying a new version. This may cause errors logged from Uptime Monitoring and similar while you deploy a new version of your software. In this case, you might consider disabling logging while deploying and enable logging once the new version is deployed. Disabling logging can be done in two ways:

    +
      +
    1. +

      Through the elmah.io UI on the log settings page:

      +

      Enable/disable log

      +
    2. +
    3. +

      By calling the Disable and Enable endpoints on the API (either manually or automatically). Powershell examples for reference:

      +
    4. +
    +

    # Disable logging
    +Invoke-RestMethod -Method Post -Uri 'https://api.elmah.io/v3/logs/LOG_ID/_disable?api_key=API_KEY'
    +
    +# Enable logging
    +Invoke-RestMethod -Method Post -Uri 'https://api.elmah.io/v3/logs/LOG_ID/_enable?api_key=API_KEY'
    +

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup-heartbeats/index.html b/setup-heartbeats/index.html new file mode 100644 index 0000000000..9c6bb129ab --- /dev/null +++ b/setup-heartbeats/index.html @@ -0,0 +1,834 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Set up Heartbeats + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Set up Heartbeats

    + +

    elmah.io Heartbeats complements the Error Logging and Uptime Monitoring features already available on elmah.io. Where Uptime Monitoring is based on us pinging your public HTTP endpoints, Heartbeats is the other way around. When configured, your services, scheduled tasks, and websites ping the elmah.io in a specified interval. We call these ping Heartbeats, hence the name of the feature. Whether you should use Uptime Monitoring or Heartbeats to monitor your code, depends on a range of variables. Uptime Monitoring is great at making sure that your public endpoints can be reached from multiple locations. Scheduled tasks and services typically don't have public endpoints and are expected to run at a specified interval. With Heartbeats, setting up monitoring on these kinds of services is extremely easy, since elmah.io will automatically detect when an unhealthy heartbeat is received or if no heartbeat is received.

    +

    Click one of the integrations below or continue reading to learn more about Heartbeats:

    + + +

    To better understand Heartbeats, let's create a simple example. For detailed instructions on how to set up Heartbeats in different languages and frameworks, select one of the specific articles in the left menu.

    +

    In this example, we will extend a C# console application, executed as a Windows Scheduled task, with a heartbeat. The scheduled task is run every 30 minutes.

    +

    Open a log on elmah.io and navigate to the Heartbeats tab:

    +

    No heartbeats

    +

    Click the Add Heartbeat button and fill in a name. For Interval we are selecting 30 minutes since the task is scheduled to run every 30 minutes. For Grace, we select 5 minutes to give the task a chance to complete. Selecting 30 and 5 minutes means that elmah.io will log an error if more than 35 minutes pass since we last heard from the task:

    +

    Create heartbeat

    +

    To create heartbeats from our task, we will need an API key, a log ID, and a heartbeat ID. Let's start with the API key. Go to the organization settings page and click the API Keys tab. Add a new API key and check the Heartbeats - Write permission only:

    +

    Create Heartbeats API key

    +

    Copy and store the API key somewhere. Navigate back to your log and click the Instructions link on the newly created Heartbeat. This will reveal the log ID and heartbeat ID. Copy and store both values since we will need them in a minute.

    +

    Time to do the integration. Like mentioned before, there are multiple ways of invoking the API. For this example, we'll use C#. Install the Elmah.Io.Client NuGet package:

    +
    Install-Package Elmah.Io.Client
    +
    dotnet add package Elmah.Io.Client
    +
    <PackageReference Include="Elmah.Io.Client" Version="5.*" />
    +
    paket add Elmah.Io.Client
    +
    +

    Extend your C# with the following code:

    +

    using Elmah.Io.Client;
    +
    +public class Program
    +{
    +    public static void Main()
    +    {
    +        var logId = new Guid("LOG_ID");
    +        var api = ElmahioAPI.Create("API_KEY");
    +        try
    +        {
    +            // Your task code goes here
    +
    +            api.Heartbeats.Healthy(logId, "HEARTBEAT_ID");
    +        }
    +        catch (Exception e)
    +        {
    +            api.Heartbeats.Unhealthy(logId, "HEARTBEAT_ID");
    +        }
    +    }
    +}
    +

    +

    Replace LOG_ID, API_KEY, and HEARTBEAT_ID with the values stored in the previous steps.

    +

    When the code runs without throwing an exception, your task now creates a Healthy heartbeat. If an exception occurs, the code creates an Unhealthy heartbeat and uses the exception text as the reason. There's an additional method named Degraded for logging a degraded heartbeat.

    +

    Depending on the heartbeat status, a log message can be created in the configured log. Log messages are only created on state changes. This means that if logging two Unhealthy requests, only the first request triggers a new error. If logging a Healthy heartbeat after logging an Unhealthy heartbeat, an information message will be logged. Transitioning to Degraded logs a warning.

    +

    Additional properties

    +

    Reason

    +

    The Healthy, Unhealthy, and Degraded methods (or the CreateHeartbeat class when using the raw Create method) accept an additional parameter named reason.

    +

    reason can be used to specify why a heartbeat check is either Degraded or Unhealthy. If your service throws an exception, the full exception including its stack trace is a good candidate for the reason parameter. When using integrations like the one with ASP.NET Core Health Checks, the health check report is used as the reason for the failing heartbeat.

    +

    Application and Version

    +

    When logging errors through one or more of the integrations, you may already use the Application and/or Version fields to set an application name and software version on all messages logged to elmah.io. Since Heartbeats will do the actual logging of messages, in this case, you can configure it to use the same application name and/or version number as your remaining integrations.

    +

    api.Heartbeats.Unhealthy(logId, "HEARTBEAT_ID", application: "MyApp", version: "1.0.0");
    +

    +

    If an application name is not configured, all messages logged from Heartbeats will get a default value of Heartbeats. If no version number is configured, log messages from Heartbeats will be assigned the latest version created through Deployment Tracking.

    +

    Took

    +

    A single performance metric named Took can be logged alongside a heartbeat. The value should be the elapsed time in milliseconds for the job, scheduled task, or whatever code resulting in the heartbeat. For a scheduled task, the Took value would be the time from the scheduled task start until the task ends:

    +

    var stopwatch = new Stopwatch();
    +stopwatch.Start();
    +// run job
    +stopwatch.Stop();
    +heartbeats.Healthy(
    +    logId,
    +    heartbeatId,
    +    took: stopwatch.ElapsedMilliseconds);
    +

    +

    The value of the Took property is shown in the History modal on the Heartbeats page on elmah.io.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup-uptime-monitoring/index.html b/setup-uptime-monitoring/index.html new file mode 100644 index 0000000000..08f12b3324 --- /dev/null +++ b/setup-uptime-monitoring/index.html @@ -0,0 +1,685 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Set Up Uptime Monitoring + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Set Up Uptime Monitoring

    + +

    elmah.io Uptime Monitoring is the perfect companion for error logging. When your websites log errors, you are notified through elmah.io. But in the case where your website doesn't even respond to web requests, you will need something else to tell you that something is wrong. This is where Uptime Monitoring comes in. When set up, uptime monitoring automatically pings your websites from 5 different locations every 5 minutes.

    +

    For a complete overview of the possibilities with uptime monitoring, watch this video tutorial:

    +

    + uptime-monitoring + +

    +

    Uptime checks

    +

    Uptime checks are automatic HTTP requests that you may already know from Azure, Pingdom, or a similar service. Uptime checks are created from the Uptime tab, directly on each log:

    +

    Uptime checks

    +

    SSL certificate expiration checks

    +

    Expiring SSL certificates cause errors in your user's browser. If you ever tried forgetting to renew an SSL certificate, you know how many problems it can cause. With the SSL check option available when creating a new uptime check, elmah.io automatically validates your SSL certificates daily.

    +

    When your SSL certificate is up for renewal, we start notifying you through the error logs.

    +

    Domain name expiration checks

    +

    Much like SSL checks, Domain name expiration checks, will notify you through your log when your domain names are about to expire. To enable this feature, enable the Domain Expiration toggle when creating a new uptime check.

    +

    Lighthouse checks

    +

    When enabling Lighthouse on an uptime check, we will generate Lighthouse results every night and include them on the history tab. Lighthouse is a popular tool for improving websites by running through a range of checks spread across multiple categories like performance and accessibility.

    +

    Canonical checks

    +

    Canonical checks are a range of checks implemented by us. It checks the URL of the uptime checks by running through a range of different best practices. Examples are HTTP to HTTPS redirect, correct use of redirect status code, and more checks.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000000..1454c13b5b --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,793 @@ + + + + https://docs.elmah.io/ + 2023-11-29 + daily + + + https://docs.elmah.io/adding-version-information/ + 2023-11-29 + daily + + + https://docs.elmah.io/allowing-elmah-io-uptime-agents/ + 2023-11-29 + daily + + + https://docs.elmah.io/asp-net-core-troubleshooting/ + 2023-11-29 + daily + + + https://docs.elmah.io/asp-net-troubleshooting/ + 2023-11-29 + daily + + + https://docs.elmah.io/authentication/ + 2023-11-29 + daily + + + https://docs.elmah.io/bot-detection/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-clear/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-dataloader/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-deployment/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-diagnose/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-export/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-import/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-log/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-overview/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-sourcemap/ + 2023-11-29 + daily + + + https://docs.elmah.io/cli-tail/ + 2023-11-29 + daily + + + https://docs.elmah.io/configure-elmah-io-from-code/ + 2023-11-29 + daily + + + https://docs.elmah.io/configure-elmah-io-manually/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-atlassian-bamboo/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-azure-devops-pipelines/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-azure-devops-releases/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-bitbucket-pipelines/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-cli/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-github-actions/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-kudu/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-octopus-deploy/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-powershell/ + 2023-11-29 + daily + + + https://docs.elmah.io/create-deployments-from-umbraco-cloud/ + 2023-11-29 + daily + + + https://docs.elmah.io/creating-rules-to-perform-actions-on-messages/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-and-custom-errors/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-and-elmah-io-differences/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-azure-boards/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-bitbucket/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-botbuster/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-chatgpt/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-clickup/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-github/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-gitlab/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-hipchat/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-ipfilter/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-jira/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-mailman/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-pagerduty/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-request-a-new-integration/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-slack/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-teams/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-trello/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-twilio/ + 2023-11-29 + daily + + + https://docs.elmah.io/elmah-io-apps-youtrack/ + 2023-11-29 + daily + + + https://docs.elmah.io/email-troubleshooting/ + 2023-11-29 + daily + + + https://docs.elmah.io/handle-elmah-io-downtime/ + 2023-11-29 + daily + + + https://docs.elmah.io/heartbeats-troubleshooting/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-does-the-new-detection-work/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-prices-are-calculated/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-avoid-emails-getting-classified-as-spam/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-configure-api-key-permissions/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-correlate-messages-across-services/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-enable-two-factor-login/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-get-elmah-io-to-resolve-the-correct-client-ip/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-get-the-sql-tab-to-show-up/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-include-source-code-in-log-messages/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-manage-subscriptions-update-credit-cards-etc/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-rename-a-log/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-run-elmah-io-in-dark-mode/ + 2023-11-29 + daily + + + https://docs.elmah.io/how-to-search-custom-data/ + 2023-11-29 + daily + + + https://docs.elmah.io/include-filename-and-line-number-in-stacktraces/ + 2023-11-29 + daily + + + https://docs.elmah.io/integrate-elmah-io-with-pipedream/ + 2023-11-29 + daily + + + https://docs.elmah.io/integrate-elmah-io-with-zapier/ + 2023-11-29 + daily + + + https://docs.elmah.io/integrations-high-level-overview/ + 2023-11-29 + daily + + + https://docs.elmah.io/javascript-troubleshooting/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-breadcrumbs-from-asp-net-core/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-custom-data/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-errors-programmatically/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-from-a-custom-http-module/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-asp-net-core/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-aws-lambdas/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-azure-functions/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-curl/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-hangfire/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-isolated-azure-functions/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-net-core-worker-services/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-powershell/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-umbraco/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-heartbeats-from-windows-scheduled-tasks/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-through-a-http-proxy/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-a-running-website-on-azure/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-a-running-website-on-iis/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-angular/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-aspnet-core/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-aspnet-mvc/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-aws-beanstalk/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-aws-lambdas/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-azure-functions/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-azure-webjobs/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-blazor/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-blogengine-net/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-console-application/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-corewcf/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-devexpress/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-elmah/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-entity-framework-core/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-google-cloud-functions/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-isolated-azure-functions/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-javascript/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-jsnlog/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-log4net/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-logary/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-maui/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-microsoft-extensions-logging/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-nancy/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-nlog/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-orchard/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-piranha-cms/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-powershell/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-react/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-serilog/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-servicestack/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-signalr/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-sitefinity/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-sveltekit/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-system-diagnostics/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-umbraco/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-uno/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-vue/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-wcf/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-web-api/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-web-pages/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-winforms/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-wpf/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-elmah-io-from-xamarin/ + 2023-11-29 + daily + + + https://docs.elmah.io/logging-to-multiple-elmah-logs/ + 2023-11-29 + daily + + + https://docs.elmah.io/managing-environments/ + 2023-11-29 + daily + + + https://docs.elmah.io/managing-organisations-and-users/ + 2023-11-29 + daily + + + https://docs.elmah.io/missing-server-side-information-on-uptime-errors/ + 2023-11-29 + daily + + + https://docs.elmah.io/query-messages-using-full-text-search/ + 2023-11-29 + daily + + + https://docs.elmah.io/remove-sensitive-form-data/ + 2023-11-29 + daily + + + https://docs.elmah.io/roslyn-analyzers-for-elmah-io-and-aspnet-core/ + 2023-11-29 + daily + + + https://docs.elmah.io/setting-application-name/ + 2023-11-29 + daily + + + https://docs.elmah.io/setup-deployment-tracking/ + 2023-11-29 + daily + + + https://docs.elmah.io/setup-heartbeats/ + 2023-11-29 + daily + + + https://docs.elmah.io/setup-uptime-monitoring/ + 2023-11-29 + daily + + + https://docs.elmah.io/sourcemaps/ + 2023-11-29 + daily + + + https://docs.elmah.io/specify-api-key-and-log-id-through-appsettings/ + 2023-11-29 + daily + + + https://docs.elmah.io/tips-and-tricks-to-stay-below-your-message-limit/ + 2023-11-29 + daily + + + https://docs.elmah.io/upgrade-elmah-io-from-v2-to-v3/ + 2023-11-29 + daily + + + https://docs.elmah.io/upgrade-elmah-io-from-v3-to-v4/ + 2023-11-29 + daily + + + https://docs.elmah.io/upgrade-elmah-io-from-v4-to-v5/ + 2023-11-29 + daily + + + https://docs.elmah.io/uptime-monitoring-troubleshooting/ + 2023-11-29 + daily + + + https://docs.elmah.io/use-extended-user-details-without-email-as-id/ + 2023-11-29 + daily + + + https://docs.elmah.io/use-multiple-logs-for-different-environments/ + 2023-11-29 + daily + + + https://docs.elmah.io/using-different-logs-per-environment-in-aspnet-core/ + 2023-11-29 + daily + + + https://docs.elmah.io/using-the-elmah-io-extension-for-visual-studio/ + 2023-11-29 + daily + + + https://docs.elmah.io/using-the-rest-api/ + 2023-11-29 + daily + + + https://docs.elmah.io/where-is-my-api-key/ + 2023-11-29 + daily + + + https://docs.elmah.io/where-is-my-invoice-receipt/ + 2023-11-29 + daily + + + https://docs.elmah.io/where-is-my-log-id/ + 2023-11-29 + daily + + + https://docs.elmah.io/where-is-the-permalink-button/ + 2023-11-29 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000000..29e3bdfb3a Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/sourcemaps/index.html b/sourcemaps/index.html new file mode 100644 index 0000000000..2d37ddb983 --- /dev/null +++ b/sourcemaps/index.html @@ -0,0 +1,815 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Source maps - De-minify JavaScript source maps with elmah.io + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Source maps

    + +

    elmah.io.javascript automatically tries to translate stack traces from minified and/or bundled code into developer-friendly traces using JavaScript source maps. For this to work, you will need to publish a valid .map source map file to either your web server or through the elmah.io API. In this article, we go through each of the possibilities.

    +

    This is not a guide for generating source map files. There's a range of possibilities to help you do that (like gulp).

    +

    Publish to your web server

    +

    The easiest way for elmah.io.javascript to translate minified/bundled stack traces is to publish a JavaScript source map alongside your minified JavaScript file. When logging a message through elmah.io.javascript, a source map is automatically used if there is a .map reference at the end of the file causing the log message/error:

    +

    var v = 42;
    +//# sourceMappingURL=/script.map
    +

    +

    This will require you to serve a source map file named script.map together with the minified/bundled file. The .map file doesn't need to be publicly available over the internet, but the elmah.io.javascript package will need to have access to it.

    +
    +

    De-minification from a .map file doesn't work in older browsers (like < IE10).

    +
    +

    Upload to elmah.io

    +

    Publishing source map files alongside your minified and bundled files is not always the preferred way of doing things. With both the minified JavaScript file and the source map, someone outside of your organization can reverse engineer your JavaScript. In cases where this would be critical or if you want to limit the size of the published files, you can choose to upload the minified/bundled file and the corresponding source map file on the elmah.io API. When elmah.io receives a stack trace from a minified JavaScript file, we try to de-minify it if a corresponding .min.js and .map file have been uploaded.

    +

    The code in the following sections will use the following bundled and minified stack trace as an example:

    +

    Error: You cannot copy to the clipboard
    +    at window.copyTextToClipboard (https://foo.bar/bundles/sharedbundle.min.js:69:24978)
    +    at MyViewModel.n.copy (https://foo.bar/bundles/viewmodels.min.js:37:37385)
    +    at HTMLButtonElement.<anonymous> (https://foo.bar/bundles/sharedbundle.min.js:55:109467)
    +

    +

    To upload a source map to elmah.io you will need an API key with the Source Maps | Write permission enabled. We recommend creating a new API key with this permission enabled only. For more information about API keys and permissions, check out How to configure API key permissions.

    +

    Upload through the API

    +

    Go to the API and insert your API key. The upload source map endpoint accepts a couple of parameters that you will need to get right to make de-minification work.

    +

    The stack trace contains references to two different bundled JavaScript files with each their .map file. Both source map files should be uploaded to the elmah.io API.

    +

    For both bundled files, fill in the details as explained here:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParameterDescription
    logIdA source map will always belong to a specific elmah.io log. Insert the log ID of that log in this field. If an application is logging JavaScript messages to multiple logs, you will need to upload the files to all those logs.
    PathThis is the relative path to the bundled and minified file. For the example above you will need to specify /bundles/sharedbundle.min.js for the first source map and /bundles/viewmodels.min.js for the second one. You can use an absolute path and query parameters on the URL if you prefer, but this will be converted to a relative path by the elmah.io API.
    SourceMapThe source map file representing the minified file in the Path specified above.
    MinifiedJavaScriptThe bundled and minified JavaScript file. This will be the content of the Path specified above.
    +

    Upload from the elmah.io CLI

    +

    Source maps can be uploaded from the elmah.io CLI. Install the CLI if not already installed:

    +

    dotnet tool install --global Elmah.Io.Cli
    +

    +

    Then, upload a source map and minified JavaScript using the sourcemap command:

    +

    sourcemap --apiKey API_KEY --logId LOG_ID --path "/bundles/sharedbundle.min.js" --sourceMap "c:\path\to\sharedbundle.map" --minifiedJavaScript "c:\path\to\sharedbundle.min.js"
    +

    +

    Upload from PowerShell

    +

    Uploading source maps can be built into your CI/CD pipeline using cURL, PowerShell, or similar. Here's an example written in PowerShell:

    +

    $boundary = [System.Guid]::NewGuid().ToString()
    +$mapPath = "c:\path\to\sharedbundle.map"
    +$jsPath = "c:\path\to\sharedbundle.min.js"
    +
    +$mapFile = [System.IO.File]::ReadAllBytes($mapPath)
    +$mapContent = [System.Text.Encoding]::UTF8.GetString($mapFile)
    +$jsFile = [System.IO.File]::ReadAllBytes($jsPath)
    +$jsContent = [System.Text.Encoding]::UTF8.GetString($jsFile)
    +
    +$LF = "`r`n"
    +$bodyLines = (
    +    "--$boundary",
    +    "Content-Disposition: form-data; name=`"Path`"$LF",
    +    "/bundles/sharedbundle.min.js",
    +    "--$boundary",
    +    "Content-Disposition: form-data; name=`"SourceMap`"; filename=`"sharedbundle.map`"",
    +    "Content-Type: application/json$LF",
    +    $mapContent,
    +    "--$boundary",
    +    "Content-Disposition: form-data; name=`"MinifiedJavaScript`"; filename=`"sharedbundle.min.js`"",
    +    "Content-Type: text/javascript$LF",
    +    $jsContent,
    +    "--$boundary--$LF"
    +) -join $LF
    +
    +Invoke-RestMethod 'https://api.elmah.io/v3/sourcemaps/LOG_ID?api_key=API_KEY' -Method POST -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyLines
    +

    +

    Upload from C#

    +

    Source maps can be uploaded from C# using the Elmah.Io.Client NuGet package:

    +

    var api = ElmahioAPI.Create("API_KEY");
    +
    +using var sourceMapStream = File.OpenRead("c:\\path\\to\\sharedbundle.map");
    +using var scriptStream = File.OpenRead("c:\\path\\to\\sharedbundle.min.js");
    +
    +api.SourceMaps.CreateOrUpdate(
    +    "LOG_ID",
    +    new Uri("/bundles/sharedbundle.min.js", UriKind.Relative),
    +    new FileParameter(sourceMapStream, "sharedbundle.map", "application/json"),
    +    new FileParameter(scriptStream, "sharedbundle.min.js", "text/javascript"));
    +

    +

    Upload from Azure DevOps

    +

    Uploading one or more source maps from Azure DevOps is available using our integration with Pipelines. Here is a guide to help you upload source maps from Azure DevOps. Before you can include the upload source map task, you will need to publish your generated source maps and minified JavaScript files from your build pipeline.

    +
      +
    1. Go to the elmah.io Upload Source Map extension on the Azure DevOps Marketplace and click the Get it free button:
    2. +
    +

    Install the extension

    +
      +
    1. Select your organization and click the Install button:
    2. +
    +

    Select organization

    +
      +
    1. Go to your Azure DevOps project and add the elmah.io Upload Source Map task. Fill in all fields as shown here:
    2. +
    +

    Add the task

    +

    Upload from GitHub Actions

    +

    Uploading one or more source maps from GitHub Actions is available using our integration with Actions. We recommend adding your API key and log ID as secrets on your GitHub repository, to avoid people outside of your organization getting access to those values.

    +

    To upload a generated source map from GitHub Actions, include the following in your build YAML file:

    +

    uses: elmahio/github-upload-source-map-action@v1
    +with:
    +  apiKey: ${{ secrets.ELMAH_IO_API_KEY }}
    +  logId: ${{ secrets.ELMAH_IO_LOG_ID }}
    +  path: '/bundles/sharedbundle.min.js'
    +  sourceMap: 'path/to/sharedbundle.map'
    +  minifiedJavaScript: 'path/to/sharedbundle.min.js'
    +

    +

    Upload from Octopus Deploy

    +

    Uploading one or more source from Octopus Deploy is available using our step template for Octopus. The step template can be installed in multiple ways as explained on Community step templates. In this document, the step template will be installed directly from the Process Editor:

    +
      +
    1. +

      Go to the Process Editor and click the ADD STEP button. In the Choose Step Template section search for 'elmah.io': +Search step template

      +
    2. +
    3. +

      Hover over the 'elmah.io - Upload Source Map' community template and click the INSTALL AND ADD button.

      +
    4. +
    5. +

      In the Install and add modal click the SAVE button.

      +
    6. +
    7. +

      The step template is now added to the process. Fill in all fields as shown here: +Fill in step template

      +
    8. +
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/specify-api-key-and-log-id-through-appsettings/index.html b/specify-api-key-and-log-id-through-appsettings/index.html new file mode 100644 index 0000000000..1649fcb66d --- /dev/null +++ b/specify-api-key-and-log-id-through-appsettings/index.html @@ -0,0 +1,673 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Specify API key and log ID through appSettings + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Specify API key and log ID through appSettings

    +

    When integrating to elmah.io from ASP.NET, MVC, Web API and similar, we use the open source project ELMAH to log uncaught exceptions. ELMAH requires configuration in web.config, which in the case of elmah.io could look something like this:

    +

    <elmah>
    +    <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="API_KEY" logId="LOG_ID" />
    +</elmah>
    +

    +

    You'd normally use web.config Transformations to specify different API keys and log IDs for different environments (see Use multiple logs for different environments). When hosting on Microsoft Azure (and other cloud-based offerings), a better approach is to specify the configuration in the appSettings element and overwrite values through the web app settings in the Portal.

    +

    The elmah.io clients built for ASP.NET based web frameworks support this scenario through additional attributes on the <errorLog> element:

    +

    <appSettings>
    +    <add key="apiKeyRef" value="API_KEY" />
    +    <add key="logIdRef" value="LOG_ID" />
    +</appSettings>
    +<!-- ... -->
    +<elmah>
    +    <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKeyKey="apiKeyRef" logIdKey="logIdRef" />
    +</elmah>
    +

    +

    Unlike the first example, the term Key has been appended to both the apiKey and logId attributes. The values of those attributes need to match a key specified in appSettings (in this example apiKeyRef and logIdRef). How you choose to name these keys is entirely up to you, as long as the names match.

    +

    elmah.io now picks up your API key (Where is my API key?) and log ID (Where is my log ID?) from the appSettings element and can be overwritten on your production site on Azure.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000000..4fd9e47f01 --- /dev/null +++ b/style.css @@ -0,0 +1,1794 @@ +/*! + * Bootstrap v3.3.5 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +mark { + background: #ff0; + color: #000; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} +legend { + border: 0; + padding: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +td, +th { + padding: 0; +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica neue", Helvetica, Arial, sans-serif, Arial; + font-size: 14px; + line-height: 1.42857143; + color: #333333; + background-color: #ffffff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #0da58e; + text-decoration: none; +} +a:hover, +a:focus { + color: #075e51; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + padding: 4px; + line-height: 1.42857143; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + display: inline-block; + max-width: 100%; + height: auto; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eeeeee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +[role="button"] { + cursor: pointer; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: "Helvetica neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; + color: #333333; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777777; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 80px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 18px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +mark, +.mark { + background-color: #f8d9ac; + padding: .2em; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #777777; +} +.text-primary { + color: #0da58e; +} +a.text-primary:hover { + color: #097665; +} +.text-success { + color: #5cb85c; +} +a.text-success:hover { + color: #449d44; +} +.text-info { + color: #349dcf; +} +a.text-info:hover { + color: #287fa8; +} +.text-warning { + color: #f0ad4e; +} +a.text-warning:hover { + color: #ec971f; +} +.text-danger { + color: #d9534f; +} +a.text-danger:hover { + color: #c9302c; +} +.bg-primary { + color: #fff; + background-color: #0da58e; +} +a.bg-primary:hover { + background-color: #097665; +} +.bg-success { + background-color: #a3d7a3; +} +a.bg-success:hover { + background-color: #80c780; +} +.bg-info { + background-color: #86c5e3; +} +a.bg-info:hover { + background-color: #5db1d9; +} +.bg-warning { + background-color: #f8d9ac; +} +a.bg-warning:hover { + background-color: #f4c37d; +} +.bg-danger { + background-color: #eba5a3; +} +a.bg-danger:hover { + background-color: #e27c79; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eeeeee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + list-style: none; + margin-left: -5px; +} +.list-inline > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +.container { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; +} +.row { + margin-left: -15px; + margin-right: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-left: 15px; + padding-right: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0%; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0%; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0%; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0%; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after { + content: " "; + display: table; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after { + clear: both; +} +.center-block { + display: block; + margin-left: auto; + margin-right: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; +} +.affix { + position: fixed; +} +body { + font-weight: 300; + letter-spacing: .015em; +} +.hero h1 { + letter-spacing: -0.04em; +} +.hero h3 { + font-weight: 300; + letter-spacing: .075em; +} +h1 { + font-weight: 300; +} +.wf-active body { + font-family: 'Roboto'; +} +.wf-active h1 { + letter-spacing: -0.1em; +} +.wf-active .hero { + font-family: 'Quicksand', sans-serif; +} +.wf-active .hero h1, +.wf-active .hero h3 { + font-family: 'Quicksand', sans-serif; +} +.wf-active h1, +.wf-active h2, +.wf-active h3, +.wf-active h4, +.wf-active h5, +.wf-active h6, +.wf-active .h1, +.wf-active .h2, +.wf-active .h3, +.wf-active .h4, +.wf-active .h5, +.wf-active .h6 { + font-family: 'Roboto'; +} +h1, +h2, +h3 { + margin: 0 0 20px 0; +} +.s10, +.spacer { + height: 10px; +} +.s20 { + height: 20px; +} +.s30 { + height: 30px; +} +.s60 { + height: 60px; +} +/*** Grid ***/ +div[class*='col-'] { + margin-bottom: 20px; +} +div[class*='col-']:last-child { + margin-bottom: 0; +} +/*** Navbar ***/ +.navbar { + min-height: 74px; + margin-bottom: 20px; +} +.navbar-default { + background-color: #444444; + border-color: #333333; +} +.navbar .container { + display: none; +} +.navbar.navbar-overlap { + position: absolute; + top: 0; + width: 100%; +} +.navbar-inverse.navbar-transparent { + background-color: transparent; + border-color: transparent; +} +.navbar-inverse.navbar-transparent .navbar-nav > .open > a, +.navbar-inverse.navbar-transparent .navbar-nav > .open > a:hover, +.navbar-inverse.navbar-transparent .navbar-nav > .open > a:focus { + background-color: rgba(0, 0, 0, 0.12); + color: #ffffff; +} +.navbar + .section.section-hero { + padding-top: 105px; +} +/*** Sections ***/ +.section { + padding: 35px 0; +} +div[class*='section-'] { + padding: 70px 0; + color: #eee; + margin-bottom: 2px; +} +div[class*='section-'] h1, +div[class*='section-'] h2, +div[class*='section-'] h3, +div[class*='section-'] h4, +div[class*='section-'] h5, +div[class*='section-'] h6 { + color: #fff; +} +div[class*='section-']:first-child li:after { + content: "·"; + padding-left: 10px; +} +.section { + overflow: hidden; +} +div[class*='section-'] .hero { + padding-top: 50px; + padding-bottom: 50px; +} +div[class*='section-'] .hero h1, +div[class*='section-'] .hero h3 { + color: #fff; +} +.trianglify, +.pre-trianglify { + background-size: cover; + background-position: center center; + background-color: #179A99; +} +.section-hero { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQ0MCIgaGVpZ2h0PSIzOTQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGc+PHBhdGggZD0iTS03OSwtNjNMLTE0OSw5M0wxMTksNTVaIiBmaWxsPSIjMThiNDljIiBzdHJva2U9IiMxOGI0OWMiLz48cGF0aCBkPSJNLTc5LC02M0wxMTksNTVMMTQ4LC02NVoiIGZpbGw9IiMxOGI2OWMiIHN0cm9rZT0iIzE4YjY5YyIvPjxwYXRoIGQ9Ik0tNzksLTYzTDE0OCwtNjVMMzY4LC0xMzdaIiBmaWxsPSIjMThiOTljIiBzdHJva2U9IiMxOGI5OWMiLz48cGF0aCBkPSJNMTQ4LC02NUwxMTksNTVMMzQ3LDk1WiIgZmlsbD0iIzE4YjA5YiIgc3Ryb2tlPSIjMThiMDliIi8+PHBhdGggZD0iTTE0OCwtNjVMMzQ3LDk1TDM2OCwtMTM3WiIgZmlsbD0iIzE4YjM5YyIgc3Ryb2tlPSIjMThiMzljIi8+PHBhdGggZD0iTTM2OCwtMTM3TDM0Nyw5NUw0MjcsNjVaIiBmaWxsPSIjMThhZTliIiBzdHJva2U9IiMxOGFlOWIiLz48cGF0aCBkPSJNMzY4LC0xMzdMNDI3LDY1TDU1NSwtNTZaIiBmaWxsPSIjMThiMTliIiBzdHJva2U9IiMxOGIxOWIiLz48cGF0aCBkPSJNMzY4LC0xMzdMNTU1LC01Nkw2OTEsLTc3WiIgZmlsbD0iIzE4YjM5YiIgc3Ryb2tlPSIjMThiMzliIi8+PHBhdGggZD0iTTM2OCwtMTM3TDY5MSwtNzdMMTI3MSwtMTY0WiIgZmlsbD0iIzE3YjI5YiIgc3Ryb2tlPSIjMTdiMjliIi8+PHBhdGggZD0iTTU1NSwtNTZMNDI3LDY1TDYyMiwxMTBaIiBmaWxsPSIjMThhOTlhIiBzdHJva2U9IiMxOGE5OWEiLz48cGF0aCBkPSJNNTU1LC01Nkw2MjIsMTEwTDY5MSwtNzdaIiBmaWxsPSIjMTdhYjlhIiBzdHJva2U9IiMxN2FiOWEiLz48cGF0aCBkPSJNNjkxLC03N0w2MjIsMTEwTDkyMCw0N1oiIGZpbGw9IiMxN2E3OWEiIHN0cm9rZT0iIzE3YTc5YSIvPjxwYXRoIGQ9Ik02OTEsLTc3TDkyMCw0N0w5MDIsLTQ3WiIgZmlsbD0iIzE3YTk5YSIgc3Ryb2tlPSIjMTdhOTlhIi8+PHBhdGggZD0iTTY5MSwtNzdMOTAyLC00N0wxMjcxLC0xNjRaIiBmaWxsPSIjMTdhYzlhIiBzdHJva2U9IiMxN2FjOWEiLz48cGF0aCBkPSJNOTAyLC00N0w5MjAsNDdMMTAzMCw1OFoiIGZpbGw9IiMxN2E0OWEiIHN0cm9rZT0iIzE3YTQ5YSIvPjxwYXRoIGQ9Ik05MDIsLTQ3TDEwMzAsNThMMTEzMiwtNDlaIiBmaWxsPSIjMTdhNTlhIiBzdHJva2U9IiMxN2E1OWEiLz48cGF0aCBkPSJNOTAyLC00N0wxMTMyLC00OUwxMjcxLC0xNjRaIiBmaWxsPSIjMTdhOTlhIiBzdHJva2U9IiMxN2E5OWEiLz48cGF0aCBkPSJNMTEzMiwtNDlMMTAzMCw1OEwxMjczLDEzM1oiIGZpbGw9IiMxNzlmOTkiIHN0cm9rZT0iIzE3OWY5OSIvPjxwYXRoIGQ9Ik0xMTMyLC00OUwxMjczLDEzM0wxNDEyLC0zMloiIGZpbGw9IiMxNjlmOTkiIHN0cm9rZT0iIzE2OWY5OSIvPjxwYXRoIGQ9Ik0xMTMyLC00OUwxNDEyLC0zMkwxMjcxLC0xNjRaIiBmaWxsPSIjMTZhNjlhIiBzdHJva2U9IiMxNmE2OWEiLz48cGF0aCBkPSJNMTI3MSwtMTY0TDE0MTIsLTMyTDE1ODAsLTEyMFoiIGZpbGw9IiMxNmE2OWEiIHN0cm9rZT0iIzE2YTY5YSIvPjxwYXRoIGQ9Ik0xNDEyLC0zMkwxMjczLDEzM0wxNDgwLDg5WiIgZmlsbD0iIzE2OWE5OCIgc3Ryb2tlPSIjMTY5YTk4Ii8+PHBhdGggZD0iTTE0MTIsLTMyTDE0ODAsODlMMTU4MCwtMTIwWiIgZmlsbD0iIzE2OWU5OSIgc3Ryb2tlPSIjMTY5ZTk5Ii8+PHBhdGggZD0iTTE1ODAsLTEyMEwxNDgwLDg5TDE2NjUsMTQ3WiIgZmlsbD0iIzE2OTg5OCIgc3Ryb2tlPSIjMTY5ODk4Ii8+PHBhdGggZD0iTS0xNDksOTNMLTE3NCw0ODhMLTE0NCwzMjZaIiBmaWxsPSIjMThhMTlhIiBzdHJva2U9IiMxOGExOWEiLz48cGF0aCBkPSJNLTE0OSw5M0wtMTQ0LDMyNkw1MSwyOTdaIiBmaWxsPSIjMThhNTlhIiBzdHJva2U9IiMxOGE1OWEiLz48cGF0aCBkPSJNLTE0OSw5M0w1MSwyOTdMMTE5LDU1WiIgZmlsbD0iIzE4YWE5YiIgc3Ryb2tlPSIjMThhYTliIi8+PHBhdGggZD0iTTExOSw1NUw1MSwyOTdMMzExLDMwNloiIGZpbGw9IiMxOGEzOWEiIHN0cm9rZT0iIzE4YTM5YSIvPjxwYXRoIGQ9Ik0xMTksNTVMMzExLDMwNkwzNDcsOTVaIiBmaWxsPSIjMThhNjlhIiBzdHJva2U9IiMxOGE2OWEiLz48cGF0aCBkPSJNMzQ3LDk1TDMxMSwzMDZMNTQxLDMyN1oiIGZpbGw9IiMxODlkOTkiIHN0cm9rZT0iIzE4OWQ5OSIvPjxwYXRoIGQ9Ik0zNDcsOTVMNTQxLDMyN0w0MjcsNjVaIiBmaWxsPSIjMThhMjlhIiBzdHJva2U9IiMxOGEyOWEiLz48cGF0aCBkPSJNNDI3LDY1TDU0MSwzMjdMNjIyLDExMFoiIGZpbGw9IiMxOGEwOTkiIHN0cm9rZT0iIzE4YTA5OSIvPjxwYXRoIGQ9Ik02MjIsMTEwTDU0MSwzMjdMNjE1LDMwM1oiIGZpbGw9IiMxNzk5OTkiIHN0cm9rZT0iIzE3OTk5OSIvPjxwYXRoIGQ9Ik02MjIsMTEwTDYxNSwzMDNMODIwLDIxNloiIGZpbGw9IiMxNzlhOTkiIHN0cm9rZT0iIzE3OWE5OSIvPjxwYXRoIGQ9Ik02MjIsMTEwTDgyMCwyMTZMOTIwLDQ3WiIgZmlsbD0iIzE3OWY5OSIgc3Ryb2tlPSIjMTc5Zjk5Ii8+PHBhdGggZD0iTTkyMCw0N0w4MjAsMjE2TDEwMzAsNThaIiBmaWxsPSIjMTc5ZTk5IiBzdHJva2U9IiMxNzllOTkiLz48cGF0aCBkPSJNMTAzMCw1OEw4MjAsMjE2TDEwMjksMzU5WiIgZmlsbD0iIzE3OTY5OCIgc3Ryb2tlPSIjMTc5Njk4Ii8+PHBhdGggZD0iTTEwMzAsNThMMTAyOSwzNTlMMTI3MywxMzNaIiBmaWxsPSIjMTc5NTk4IiBzdHJva2U9IiMxNzk1OTgiLz48cGF0aCBkPSJNMTI3MywxMzNMMTAyOSwzNTlMMTE4Niw0MjBaIiBmaWxsPSIjMTc4Yjk3IiBzdHJva2U9IiMxNzhiOTciLz48cGF0aCBkPSJNMTI3MywxMzNMMTE4Niw0MjBMMTMxOSwzMTdaIiBmaWxsPSIjMTc4Yjk3IiBzdHJva2U9IiMxNzhiOTciLz48cGF0aCBkPSJNMTI3MywxMzNMMTMxOSwzMTdMMTQyMiwyMTlaIiBmaWxsPSIjMTY4ZTk3IiBzdHJva2U9IiMxNjhlOTciLz48cGF0aCBkPSJNMTI3MywxMzNMMTQyMiwyMTlMMTQ4MCw4OVoiIGZpbGw9IiMxNjkzOTgiIHN0cm9rZT0iIzE2OTM5OCIvPjxwYXRoIGQ9Ik0xNDgwLDg5TDE0MjIsMjE5TDE2NjUsMTQ3WiIgZmlsbD0iIzE2OTE5OCIgc3Ryb2tlPSIjMTY5MTk4Ii8+PHBhdGggZD0iTTE2NjUsMTQ3TDE0MjIsMjE5TDE2MzUsMzQ5WiIgZmlsbD0iIzE2OGE5NiIgc3Ryb2tlPSIjMTY4YTk2Ii8+PHBhdGggZD0iTTE2NjUsMTQ3TDE2MzUsMzQ5TDE3MTUsNDk2WiIgZmlsbD0iIzE2ODE5NiIgc3Ryb2tlPSIjMTY4MTk2Ii8+PHBhdGggZD0iTS0xNDQsMzI2TC0xNzQsNDg4TDUxLDI5N1oiIGZpbGw9IiMxODliOTkiIHN0cm9rZT0iIzE4OWI5OSIvPjxwYXRoIGQ9Ik01MSwyOTdMLTE3NCw0ODhMMTM4LDU1MFoiIGZpbGw9IiMxODk0OTgiIHN0cm9rZT0iIzE4OTQ5OCIvPjxwYXRoIGQ9Ik01MSwyOTdMMTM4LDU1MEwzMTEsMzA2WiIgZmlsbD0iIzE4OTY5OCIgc3Ryb2tlPSIjMTg5Njk4Ii8+PHBhdGggZD0iTTMxMSwzMDZMMTM4LDU1MEwyNjUsNTYyWiIgZmlsbD0iIzE4OGU5OCIgc3Ryb2tlPSIjMTg4ZTk4Ii8+PHBhdGggZD0iTTMxMSwzMDZMMjY1LDU2Mkw0MzcsNDU2WiIgZmlsbD0iIzE4OGY5OCIgc3Ryb2tlPSIjMTg4Zjk4Ii8+PHBhdGggZD0iTTMxMSwzMDZMNDM3LDQ1Nkw1NDEsMzI3WiIgZmlsbD0iIzE4OTQ5OCIgc3Ryb2tlPSIjMTg5NDk4Ii8+PHBhdGggZD0iTTU0MSwzMjdMNDM3LDQ1Nkw2MzMsNTAxWiIgZmlsbD0iIzE4OGQ5NyIgc3Ryb2tlPSIjMTg4ZDk3Ii8+PHBhdGggZD0iTTU0MSwzMjdMNjMzLDUwMUw2MTUsMzAzWiIgZmlsbD0iIzE3OTA5OCIgc3Ryb2tlPSIjMTc5MDk4Ii8+PHBhdGggZD0iTTYxNSwzMDNMNjMzLDUwMUw4MjIsNDI0WiIgZmlsbD0iIzE3OGM5NyIgc3Ryb2tlPSIjMTc4Yzk3Ii8+PHBhdGggZD0iTTYxNSwzMDNMODIyLDQyNEw4MjAsMjE2WiIgZmlsbD0iIzE3OTI5OCIgc3Ryb2tlPSIjMTc5Mjk4Ii8+PHBhdGggZD0iTTgyMCwyMTZMODIyLDQyNEwxMDI5LDM1OVoiIGZpbGw9IiMxNzhlOTciIHN0cm9rZT0iIzE3OGU5NyIvPjxwYXRoIGQ9Ik0xMDI5LDM1OUw4MjIsNDI0TDEwNzYsNDU4WiIgZmlsbD0iIzE3ODY5NyIgc3Ryb2tlPSIjMTc4Njk3Ii8+PHBhdGggZD0iTTEwMjksMzU5TDEwNzYsNDU4TDExODYsNDIwWiIgZmlsbD0iIzE3ODQ5NiIgc3Ryb2tlPSIjMTc4NDk2Ii8+PHBhdGggZD0iTTEzMTksMzE3TDExODYsNDIwTDE0NjksNTI3WiIgZmlsbD0iIzE2ODA5NiIgc3Ryb2tlPSIjMTY4MDk2Ii8+PHBhdGggZD0iTTEzMTksMzE3TDE0NjksNTI3TDE0MjIsMjE5WiIgZmlsbD0iIzE2ODQ5NiIgc3Ryb2tlPSIjMTY4NDk2Ii8+PHBhdGggZD0iTTE0MjIsMjE5TDE0NjksNTI3TDE2MzUsMzQ5WiIgZmlsbD0iIzE2ODE5NiIgc3Ryb2tlPSIjMTY4MTk2Ii8+PHBhdGggZD0iTTE2MzUsMzQ5TDE0NjksNTI3TDE3MTUsNDk2WiIgZmlsbD0iIzE2Nzk5NSIgc3Ryb2tlPSIjMTY3OTk1Ii8+PHBhdGggZD0iTS0xNzQsNDg4TC0xMDQsNjkwTDEzOCw1NTBaIiBmaWxsPSIjMTg4Yzk3IiBzdHJva2U9IiMxODhjOTciLz48cGF0aCBkPSJNMTM4LDU1MEwtMTA0LDY5MEwxMDUsNzIxWiIgZmlsbD0iIzE4ODQ5NyIgc3Ryb2tlPSIjMTg4NDk3Ii8+PHBhdGggZD0iTTEzOCw1NTBMMTA1LDcyMUwyNjUsNTYyWiIgZmlsbD0iIzE4ODU5NyIgc3Ryb2tlPSIjMTg4NTk3Ii8+PHBhdGggZD0iTTI2NSw1NjJMMTA1LDcyMUwzMTMsNjgzWiIgZmlsbD0iIzE4ODE5NiIgc3Ryb2tlPSIjMTg4MTk2Ii8+PHBhdGggZD0iTTI2NSw1NjJMMzEzLDY4M0w0MzcsNjQ5WiIgZmlsbD0iIzE4ODE5NiIgc3Ryb2tlPSIjMTg4MTk2Ii8+PHBhdGggZD0iTTI2NSw1NjJMNDM3LDY0OUw0MzcsNDU2WiIgZmlsbD0iIzE4ODY5NyIgc3Ryb2tlPSIjMTg4Njk3Ii8+PHBhdGggZD0iTTQzNyw0NTZMNDM3LDY0OUw2MzMsNTAxWiIgZmlsbD0iIzE4ODU5NyIgc3Ryb2tlPSIjMTg4NTk3Ii8+PHBhdGggZD0iTTYzMyw1MDFMNDM3LDY0OUw3MzEsNjQzWiIgZmlsbD0iIzE3N2Y5NiIgc3Ryb2tlPSIjMTc3Zjk2Ii8+PHBhdGggZD0iTTYzMyw1MDFMNzMxLDY0M0w4MjIsNDI0WiIgZmlsbD0iIzE3ODM5NiIgc3Ryb2tlPSIjMTc4Mzk2Ii8+PHBhdGggZD0iTTgyMiw0MjRMNzMxLDY0M0w4MzksNjA5WiIgZmlsbD0iIzE3N2Y5NiIgc3Ryb2tlPSIjMTc3Zjk2Ii8+PHBhdGggZD0iTTgyMiw0MjRMODM5LDYwOUwxMDc2LDQ1OFoiIGZpbGw9IiMxNzgyOTYiIHN0cm9rZT0iIzE3ODI5NiIvPjxwYXRoIGQ9Ik0xMDc2LDQ1OEw4MzksNjA5TDEwMDIsNjUyWiIgZmlsbD0iIzE3N2I5NSIgc3Ryb2tlPSIjMTc3Yjk1Ii8+PHBhdGggZD0iTTEwNzYsNDU4TDEwMDIsNjUyTDEzMzQsNzMyWiIgZmlsbD0iIzE3NzU5NSIgc3Ryb2tlPSIjMTc3NTk1Ii8+PHBhdGggZD0iTTEwNzYsNDU4TDEzMzQsNzMyTDExODYsNDIwWiIgZmlsbD0iIzE3N2E5NSIgc3Ryb2tlPSIjMTc3YTk1Ii8+PHBhdGggZD0iTTExODYsNDIwTDEzMzQsNzMyTDEzNzksNjQ4WiIgZmlsbD0iIzE2NzQ5NSIgc3Ryb2tlPSIjMTY3NDk1Ii8+PHBhdGggZD0iTTExODYsNDIwTDEzNzksNjQ4TDE0NjksNTI3WiIgZmlsbD0iIzE2Nzg5NSIgc3Ryb2tlPSIjMTY3ODk1Ii8+PHBhdGggZD0iTTE0NjksNTI3TDEzNzksNjQ4TDE2NDEsNjI2WiIgZmlsbD0iIzE2NzA5NCIgc3Ryb2tlPSIjMTY3MDk0Ii8+PHBhdGggZD0iTTE0NjksNTI3TDE2NDEsNjI2TDE3MTUsNDk2WiIgZmlsbD0iIzE2NzI5NCIgc3Ryb2tlPSIjMTY3Mjk0Ii8+PHBhdGggZD0iTTEwNSw3MjFMMTMzNCw3MzJMMzEzLDY4M1oiIGZpbGw9IiMxNzc3OTUiIHN0cm9rZT0iIzE3Nzc5NSIvPjxwYXRoIGQ9Ik0zMTMsNjgzTDEzMzQsNzMyTDczMSw2NDNaIiBmaWxsPSIjMTc3Njk1IiBzdHJva2U9IiMxNzc2OTUiLz48cGF0aCBkPSJNMzEzLDY4M0w3MzEsNjQzTDQzNyw2NDlaIiBmaWxsPSIjMTg3ZDk2IiBzdHJva2U9IiMxODdkOTYiLz48cGF0aCBkPSJNNzMxLDY0M0wxMzM0LDczMkwxMDAyLDY1MloiIGZpbGw9IiMxNzczOTUiIHN0cm9rZT0iIzE3NzM5NSIvPjxwYXRoIGQ9Ik03MzEsNjQzTDEwMDIsNjUyTDgzOSw2MDlaIiBmaWxsPSIjMTc3ODk1IiBzdHJva2U9IiMxNzc4OTUiLz48cGF0aCBkPSJNMTMzNCw3MzJMMTY0MSw2MjZMMTM3OSw2NDhaIiBmaWxsPSIjMTY2Yzk0IiBzdHJva2U9IiMxNjZjOTQiLz48L2c+PC9zdmc+); +} +svg { + width: 100%; + height: 100%; +} +.icon { + margin-bottom: 30px; +} +.icon svg { + height: 80px; +} +.icon svg g { + stroke-width: 5; +} +/*** Block ***/ +blockquote { + text-align: center; + border-left: 0; + padding: 2em 10px; + quotes: "\201C" "\201D" "\2018" "\2019"; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #eeeeee; +} +blockquote a, +blockquote a:visited { + color: inherit; +} +@media (min-width: 768px) { + blockquote.with-button footer, + blockquote.with-button p { + padding-right: 200px; + } +} +@media (min-width: 768px) { + blockquote button { + position: absolute; + right: 25px; + top: 36px; + } +} +@media (max-width: 767px) { + blockquote button { + margin-top: 20px; + } +} + + + + + + + +.body-docs { + padding-top: 0; +} +.body-docs .bs-sidebar.affix { + position: fixed; + top: 20px; +} +.body-docs .navbar { + margin-bottom: 0; +} +.body-docs .section-hero { + padding: 0; + height: 10px; + margin: 0; + background-position: 0% 69%; + background-size: cover; +} +.body-docs .section-color-gray { + padding: 30px 0; + margin: 0; +} +.body-docs .section-color-gray h2 { + color: #D0D0D0; + margin: 0; +} +.body-docs .section-color-gray div[class*='col-'] { + margin-bottom: 0; +} +.docs-box { + background-color: #fff; + padding: 40px 50px; + margin-bottom: 40px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24), 0 0px 3px rgba(0, 0, 0, 0.05); + /*** Fonts ***/ +} +.docs-box h1, +.docs-box h2, +.docs-box h3, +.docs-box h4 { + font-family: 'Quicksand', sans-serif; + font-weight: 600; +} +.docs-box h1 { + font-size: 30px; + letter-spacing: inherit; +} +.docs-box h1:after { + content: " "; + display: block; + border-bottom: 1px solid #dddddd; + margin-top: 30px; +} +.docs-box .underline:after { + content: " "; + display: block; + border-bottom: 1px solid #dddddd; + margin-top: 30px; +} +.docs-box h1 + p, +.docs-box h2 + p, +.docs-box h1 + h5 + p, +.docs-box h2 + h5 + p, +.docs-box h1 + blockquote, +.docs-box h2 + blockquote, +.docs-box h1 + h5 + blockquote, +.docs-box h2 + h5 + blockquote { + margin-top: 40px; +} +.docs-box h3 { + margin-top: 20px; +} +.docs-box h5 { + font-style: italic; + margin-top: 10px; + font-weight: 300; + font-size: 11px; + text-align: right; +} +.docs-box h5:before { + content: "Written by "; +} +.docs-box hr { + margin: 50px -50px; + border-color: #dddddd; +} +.docs-box .hook { + font-size: 14px; +} +.docs-box .hook .label-info { + border-radius: .25em 0 0 .25em; + border: 1px solid #0da58e; + background-color: #0da58e; +} +.docs-box .hook .label-url { + border: 1px solid #0da58e; + border-left-width: 0; + border-radius: 0 .25em .25em 0; + color: #0da58e; + font-weight: 500; +} +/*** Images ***/ +img { + max-width: 100%; + box-shadow: 1px 5px 12px rgba(0, 0, 0, 0.2); +} +.bs-sidebar .nav { + list-style: none; + padding-left: 0; +} +.bs-sidebar h4 { + font-size: 12px; + text-transform: uppercase; + color: #333333; + margin-top: 20px; +} +.bs-sidebar .nav > li > a { + display: block; + z-index: 1; + color: #333333; + font-size: 12px; + padding: 7px 0; + font-weight: 300; +} +.bs-sidebar .nav > li > a:hover, +.bs-sidebar .nav > li > a:focus { + background-color: transparent; + font-weight: 500; +} +.bs-sidebar .nav > .active > a, +.bs-sidebar .nav > .active:hover > a, +.bs-sidebar .nav > .active:focus > a { + color: #0da58e; + font-weight: 500; +} +@media screen and (min-width: 768px) { + .affix-3 { + width: 154px; + } +} +@media screen and (min-width: 992px) { + .affix-3 { + width: 213px; + } +} +@media screen and (min-width: 1200px) { + .affix-3 { + width: 263px; + } +} +/*** PRE ***/ +pre { + padding: 0; + margin: 20px 0 20px 0; + word-break: break-all; + word-wrap: break-word; + background-color: transparent; + border: 0; + position: relative; +} +pre .label { + position: absolute; + top: 0; + left: 0; +} +pre code { + border: 1px solid #CFCFCF; + background-color: #fff; +} +.hljs { + padding: 20px; + border-radius: 4px; +} +.hljs-keyword, +.css .rule .hljs-keyword, +.hljs-winutils, +.nginx .hljs-title, +.hljs-subst, +.hljs-request, +.hljs-status { + color: #034482; + font-weight: bold; +} +.hljs-title, +.hljs-id, +.scss .hljs-preprocessor { + color: #0da58e; +} +code.powershell.hljs { + background-color: #333; + color: #fff; + border-color: #333; +} +/*** TOCs ***/ +.toc-index h3 { + margin: 0; + margin-bottom: 16px; +} +/*** Form ***/ +#searchResultHeader { + display: none; +} +#searchResult { + display: none; +} +.icon-in-input { + position: relative; + margin-top: 0 !important; +} +.icon-in-input input { + opacity: .5; + padding-left: 30px; +} +.icon-in-input input:focus { + opacity: 1; +} +.icon-in-input .fa { + position: absolute; + left: 10px; + top: 8px; + z-index: 1; + border: 0; + background-color: transparent; + padding: 0; + margin: 0; +} + + .table-api { + width: 100%; +} +.table-api tr { + border: 1px solid #eee; +} +.table-api td { + padding: 20px; + vertical-align: text-top; +} +.table-api td:first-child { + width: 150px; + font-weight: bold; +} +.table-api td:nth-child(2) { + width: 100px; +} +.table-api .text-muted { + font-weight: normal; + font-size: 12px; + padding-top: 5px; +} +.tab-content pre { + border-radius: 0px 0px 4px 4px; + border-top: 0; +} diff --git a/tips-and-tricks-to-stay-below-your-message-limit/index.html b/tips-and-tricks-to-stay-below-your-message-limit/index.html new file mode 100644 index 0000000000..e2848bee3d --- /dev/null +++ b/tips-and-tricks-to-stay-below-your-message-limit/index.html @@ -0,0 +1,729 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Tips and tricks to stay below your message limit + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Tips and tricks to stay below your message limit

    + +

    Each plan on elmah.io includes a maximum number of messages per month. The number of messages is calculated from how many times your applications have called our API and successfully stored a message (in most cases messages equal errors). Deleting messages either one by one or in batches is fully supported, but does not result in a decrease in the current message count. Our costs are primarily around receiving, indexing, and notifying about messages, why we cannot allow someone on a lower plan like the Small Business, to log millions and yet millions of messages and then just clean up regularly. We're sure that everyone understands the challenge here.

    +

    With that said, we want to help you stay within your message limits. Luckily, there are a lot of ways to limit messages. This article contains a list of the most common tactics to stay below your message limit.

    +

    Ignore Rules

    +
    +

    Be aware that Ignore rules and filters are only meant as a temporary way of ignoring messages. In case you want to permanently ignore one or more log messages, use client-side filtering.

    +
    +

    The easiest way to limit logged messages is by ignoring some of them. Ignored messages do not count toward the message limit. Message rules can be configured through the Rules tab on the Log Settings view.

    +

    Rules consist of a query and an action. The query can either be a full-text query or written using Lucene Query Syntax. To create a new ignore rule, input a query on the Rules tab:

    +

    Create Ignore Rule

    +

    All new rules are created with an ignore action as default, why you don't need to click the Then link for this type of rule. In the example above, ignore all messages with a status code of 404.

    +

    For more information about the possibilities with rules, check out Creating Rules to Perform Actions on Messages.

    +

    Filters

    +

    Filters are Ignore Rules in disguise. With Filters, we have collected the most common ignore rules and made them available as a set of checkboxes. To ignore all messages matching a specific filter, enable one of the checkboxes on the Filters tab on Log Settings:

    +

    Filters

    +

    If your website is available for everyone to access, ignoring known crawlers, bots, and spiders is a good idea in most cases. Filtering below warning can also be a good idea unless you are using elmah.io to log all log severities from a logging framework like NLog or Serilog.

    +

    If staying within the message limit is more important than getting the full picture of the errors generated by your website, there are a couple of filters that will help you with that. The Filter Known filter will make sure that only one instance of each error is logged. If you still want to log multiple instances but stop at some point, the Filter Burst filter will stop logging after 50 instances are logged. Finally, you can set a limit on how many errors you want logged to a specific log each month, by inputting a number in the Filter Limit filter. Please notice that using any of these last three filters will cause inconsistent results in different graphs and features (like spike detection). They can solve the problem of logging too much, but it is a sign that you are logging more data than is included in your plan. A perfectly valid solution is to purchase a larger plan, get your logging under control (maybe even fix some errors?), and then downgrade when you are ready.

    +

    Ignore future messages like this

    +

    Sometimes you may find yourself on the Search tab with a search result thinking: "I don't care about these messages". By clicking the caret next to the query filters, an Ignore future messages like this option is revealed:

    +

    Ignore Like This

    +

    Clicking this option automatically ignores any future messages matching your current search result.

    +

    Disable logs

    +

    Each log can be disabled from Log Settings:

    +

    Enabled/Disable Log

    +

    Disabled logs are shown as semi-transparent on the dashboard, to help you remember that you disabled a log.

    +

    Client-side message filtering

    +

    Most of our clients support client filtering. All of the filtering options described above filter messages server-side. This means that your application still communicates with elmah.io's API and needs to wait for that to answer (even for ignored messages).

    +

    Filtering client-side from ASP.NET, MVC, Web API, and other frameworks built on top of ASP.NET, can be done using ELMAH's (the open-source project) filtering feature. To filter messages, create a method named ErrorLog_Filtering in the Global.asax.cs file:

    +

    void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs args)
    +{
    +    var httpContext = args.Context as HttpContext;
    +    if (httpContext.Response.StatusCode == 404)
    +    {
    +        args.Dismiss();
    +    }
    +}
    +

    +

    If you're using ASP.NET Core, our client supports the OnFilter action:

    +

    builder.Services.AddElmahIo(o =>
    +{
    +    // ...
    +    o.OnFilter = message =>
    +    {
    +        return message.StatusCode == 404;
    +    };
    +});
    +

    +

    To help you set up client-side filtering in commonly used logging frameworks, click the button on the log message details toolbar. This will show a dialog containing extensive help on setting up client-side filtering.

    +

    Monitor current usage

    +

    We send you an email when you have used 90% of your limit and again when reaching the limit. Monitoring your usage is a good supplement to the emails since you can react early on (by upgrading, ignoring errors, or something else). There's a usage graph on the Organisation Settings view:

    +

    Usage Graph

    +

    By clicking the information icon above the counter, you will be able to see which logs that are taking up space:

    +

    Detailed Usage Report

    +

    Fix bugs

    +

    Seeing the same error over and over again? Maybe the best idea is to fix it :) I mean, that's the whole purpose of elmah.io: to help you fix bugs. And remember, the fewer bugs you have, the cheaper elmah.io gets. The ultimate motivation!

    +

    Purchase a top-up

    +

    Sometimes, a spike in errors can be caused by unexpected events like a black hat bot deciding to bombard your site with requests or a junior dev on your team accidentally enabling verbose logging. In these cases, purchasing a top-up may be a better solution than permanently upgrading your plan. Top-ups can be purchased from your subscription when you reach 90% of your included messages. Top-ups are purchased in bundles of 25,000 messages, valid for the rest of the calendar month.

    +

    Upgrading to the next plan

    +

    If you constantly go over the limit, you have probably reached a point where you will need to upgrade to a larger plan. You can upgrade and downgrade at any time, so upgrading for a few months (until you get errors under control) and then downgrading again, is perfectly fine.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/upgrade-elmah-io-from-v2-to-v3/index.html b/upgrade-elmah-io-from-v2-to-v3/index.html new file mode 100644 index 0000000000..cea594f109 --- /dev/null +++ b/upgrade-elmah-io-from-v2-to-v3/index.html @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Upgrade elmah.io from v2 to v3 + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Upgrade elmah.io from v2 to v3

    +

    When we launched the new version of our API (v3), we used the jump in major version to fix some issues that would require major changes in our client. One of them is a move to netstandard, which makes the new client usable from .NET Core. With interface changes in the client, upgrading from 2.x to 3.x requires more than simply upgrading the NuGet package. This is a guide to upgrading the Elmah.Io package.

    +
    +

    If you are logging to elmah.io from ASP.NET Core, you are already using the 3.x client.

    +
    +

    Updating the NuGet package

    +

    First, you need to upgrade the Elmah.Io NuGet package:

    +

    Update-Package Elmah.Io
    +

    +

    This installs the latest 3.x client. After doing so, we recommend updating to the latest Elmah.Io.Client package as well (updating Elmah.Io already updated Elmah.Io.Client but to the lowest possible version):

    +

    Update-Package Elmah.Io.Client
    +

    +

    The elmah.io.core package is no longer needed and can be uninstalled:

    +

    Uninstall-Package elmah.io.core
    +

    +

    Next, you will need to add your API key to your web.config. Where the 2.x client only required a log ID to log messages to elmah.io, the new API improves security by introducing API keys (Where is my API key?). Copy your API key and extend the errorLog-element in web.config:

    +

    <elmah>
    +  <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="API_KEY" logId="LOG_ID" />
    +</elmah>
    +

    +

    If you didn't utilize elmah.io's code from C#, you are ready to rock and roll.

    +

    Code changes

    +

    The 3.x client is auto-generated from our new Swagger endpoint. This means that the code doesn't work like previously. We have tried to create extension methods to make some of the API work like previously, but since the client now supports both Messages, Logs, and Deployments, code changes are needed.

    +

    If you are using the ErrorSignal class from ELMAH (the open-source project) to log exceptions manually, everything works as previously. If you are using methods from the Elmah.Io.Client package, there's a new API documented here: Logging from Console.

    +

    Elmah.Io.Mvc and Elmah.Io.WebApi

    +

    When launching the packages for 3.x, we also decided to create two new proxy packages: Elmah.Io.Mvc and Elmah.Io.WebApi. The reason I call them proxy packages is, that they do nothing more than simply install the dependencies needed to log from each framework. The packages are intended for new installs only, so if your code already logs exceptions to elmah.io, there is no need to install any of these packages.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/upgrade-elmah-io-from-v3-to-v4/index.html b/upgrade-elmah-io-from-v3-to-v4/index.html new file mode 100644 index 0000000000..0bc8a7eb55 --- /dev/null +++ b/upgrade-elmah-io-from-v3-to-v4/index.html @@ -0,0 +1,706 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Upgrade elmah.io from v3 to v4 + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Upgrade elmah.io from v3 to v4

    + +

    There is a new major version update named 4.x on all client packages. All integrations are based on the Elmah.Io.Client package which has switched from using AutoRest to NSwag. We have tried supporting most of the existing code from the 3.x packages, but some minor code changes may be needed after upgrading. This guide will go through the changes needed and potential problems when upgrading.

    +

    You may experience problems where NuGet refuses to upgrade an Elmah.Io.* package. We experienced this when referencing more Elmah.Io.* packages from the same project. Like if you would reference both Elmah.Io.AspNetCore and Elmah.Io.Extensions.Logging from the same project. Upgrading in this scenario can be achieved in one of the following ways:

    +
      +
    1. Uninstall all Elmah.Io.* packages and install them one by one.
    2. +
    3. Install the 4.x version of the Elmah.Io.Client NuGet package. Then upgrade the remaining Elmah.Io.* NuGet packages one by one. Finally, uninstall the Elmah.Io.Client package.
    4. +
    +

    Upgrading Elmah.Io.Client

    +

    If you are using the Elmah.Io.Client package directly, you can simply upgrade that package through NuGet:

    +

    Update-Package Elmah.Io.Client
    +

    +

    This installs the latest 4.x client. Most people are using one of the integrations with ASP.NET Core, Serilog, NLog, or similar. There are new major versions of these packages available as well.

    +

    Code changes

    +

    As mentioned, some code changes may be required on your part after upgrading. Most of the API stayed the same, but NSwag generates output differently from AutoRest in some cases. Here's a list of changes needed:

    +
      +
    • Remove any references to Elmah.Io.Client.Models. All generated classes are now in the Elmah.Io.Client namespace.
    • +
    • Replace any reference to IMessages, IHeartbeats, and similar to IMessagesClient, IHeartbeatsClient, and similar.
    • +
    • Replace any instances of new ElmahioAPI(new ApiKeyCredentials(apiKey)); with ElmahioAPI.Create(apiKey);. The ApiKeyCredentials class was specific to AutoRest and has therefore been removed.
    • +
    • Collection types on messages like Data, ServerVariables, etc. are now of type ICollection and not IList. This means you can no longer use indexers on these properties. To use indexer you can call ToList() on each collection.
    • +
    • Properties of type DateTime are replaced with DateTimeOffset. This shouldn't require any changes on your part since you can assign a value of type DateTime to a property of type DateTimeOffset.
    • +
    • You no longer need to make the following cast: (ElmahioAPI)ElmahioAPI.Create(apiKey). The IElmahioAPI interface will have all the properties you need from the client.
    • +
    • If you for some reason manually installed the Microsoft.Rest.ClientRuntime package you can remove that after upgrading Elmah.Io.Client to v4. Unless you have other dependencies on Microsoft.Rest.ClientRuntime.
    • +
    • The OnMessageFail event, available on the IMessagesClient interface (previously just IMessages), does now trigger more often. On v3, this event would only trigger on timeouts connecting with the server, when the server returned 500, and similar. In v4, the event will trigger on every failing request, meaning a request where the response contains a status code >= 400. This will make it a lot easier to make smart choices on the client when something fails. An example of this could be to implement a circuit breaker pattern or a local processing queue on the client when the elmah.io API starts returning 429 (too many requests).
    • +
    +

    Upgrading Elmah.Io.AspNetCore

    +

    Upgrading the Elmah.Io.AspNetCore package can be done through NuGet:

    +

    Update-Package Elmah.Io.AspNetCore
    +

    +

    Code changes

    +

    Besides the changes mentioned in the section about the Elmah.Io.Client package, there's a single change in the Elmah.Io.AspNetCore v4 package as well:

    +
      +
    • The ElmahIoExtensions class has been moved from the Elmah.Io.AspNetCore namespace to the Microsoft.Extensions.DependencyInjection namespace. The ElmahIoExtensions class contains the UseElmahIo and AddElmahIo methods that you call in your Startup.cs file. This change should not cause any compile errors. For simple installations without custom options, you can most likely remove the using Elmah.Io.AspNetCore; line from the Startup.cs file. Visual Studio will tell if that line is no longer needed.
    • +
    +

    Troubleshooting

    +

    Could not load file or assembly 'Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'

    +

    Elmah.Io.Client requires Newtonsoft.Json version 10.0.3 or newer. Please upgrade to fix this error.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/upgrade-elmah-io-from-v4-to-v5/index.html b/upgrade-elmah-io-from-v4-to-v5/index.html new file mode 100644 index 0000000000..df4afad31e --- /dev/null +++ b/upgrade-elmah-io-from-v4-to-v5/index.html @@ -0,0 +1,759 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Upgrade elmah.io from v4 to v5 + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Upgrade elmah.io from v4 to v5

    + +

    There is a new major version update named 5.x on all client packages. We have tried supporting most of the existing code from the 4.x packages, but some minor code changes may be needed after upgrading. This guide will go through the changes needed and potential problems when upgrading.

    +

    You may experience problems where NuGet refuses to upgrade an Elmah.Io.* package. We experienced this when referencing more Elmah.Io.* packages from the same project. Like if you would reference both Elmah.Io.AspNetCore and Elmah.Io.Extensions.Logging from the same project. Upgrading in this scenario can be achieved in one of the following ways:

    +
      +
    1. Uninstall all Elmah.Io.* packages and install them one by one.
    2. +
    3. Install the 5.x version of the Elmah.Io.Client NuGet package. Then upgrade the remaining Elmah.Io.* NuGet packages one by one. Finally, uninstall the Elmah.Io.Client package.
    4. +
    +

    Upgrading Elmah.Io.Client

    +

    If you are using the Elmah.Io.Client package directly, you can simply upgrade that package through NuGet:

    +

    Update-Package Elmah.Io.Client
    +

    +

    This installs the latest 5.x client. Most people are using one of the integrations with ASP.NET Core, Serilog, NLog, or similar. There are new major versions of these packages available as well.

    +

    Code changes

    +

    We used the major version number upgrade to clean up the double methods problem introduced back when adding overloads with CancellationToken on all async methods. This means that async methods now only have a single version looking similar to this:

    +

    Task<Message> CreateAndNotifyAsync(
    +    Guid logId, CreateMessage message, CancellationToken cancellationToken = default)
    +

    +

    This shouldn't cause any compile problems since the v4 version had the following two overloads:

    +

    Task<Message> CreateAndNotifyAsync(
    +    Guid logId, CreateMessage message)
    +Task<Message> CreateAndNotifyAsync(
    +    Guid logId, CreateMessage message, CancellationToken cancellationToken = default)
    +

    +

    In addition, we removed all code marked as Obsolete. Each section below will describe what has been removed for each package. For the Elmah.Io.Client package, the HttpClient property on the IElmahioAPI interface has been removed. If you need to provide a custom HttpClient for the client, you can use the following overload of the Create method on the ElmahioAPI class:

    +

    public static IElmahioAPI Create(string apiKey, ElmahIoOptions options, HttpClient httpClient)
    +

    +

    Upgrading Elmah.Io.AspNetCore

    +

    Upgrading the Elmah.Io.AspNetCore package can be done through NuGet:

    +

    Update-Package Elmah.Io.AspNetCore
    +

    +

    Code changes

    +

    Besides the changes mentioned in the section about the Elmah.Io.Client package, the following obsolete method has been removed from the IHealthChecksBuilder interface:

    +

    public static IHealthChecksBuilder AddElmahIoPublisher(
    +    this IHealthChecksBuilder builder, string apiKey, Guid logId, string application = null)
    +

    +

    Instead, use the AddElmahIoPublisher method accepting options:

    +

    public static IHealthChecksBuilder AddElmahIoPublisher(
    +    this IHealthChecksBuilder builder, Action<ElmahIoPublisherOptions> options)
    +

    +

    Upgrading Serilog.Sinks.ElmahIo

    +

    Upgrading the Serilog.Sinks.ElmahIo package can be done through NuGet:

    +

    Update-Package Serilog.Sinks.ElmahIo
    +

    +

    Code changes

    +

    v5 of the Serilog.Sinks.ElmahIo package moves to Serilog 3. Serilog 3 contains a lot of changes which are fully documented here. Your code may need updates caused by changes in the Serilog package.

    +

    Besides the changes mentioned in the section about the Elmah.Io.Client package, the following obsolete methods have been removed from the LoggerSinkConfiguration class:

    +

    public static LoggerConfiguration ElmahIo(
    +    this LoggerSinkConfiguration loggerConfiguration,
    +    string apiKey,
    +    string logId,
    +    LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
    +    IFormatProvider formatProvider = null)
    +
    +public static LoggerConfiguration ElmahIo(
    +    this LoggerSinkConfiguration loggerConfiguration,
    +    string apiKey,
    +    Guid logId,
    +    LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
    +    IFormatProvider formatProvider = null)
    +

    +

    Instead, use the ElmahIo method accepting options:

    +

    public static LoggerConfiguration ElmahIo(
    +    this LoggerSinkConfiguration loggerConfiguration,
    +    ElmahIoSinkOptions options)
    +

    +

    Upgrading Elmah.Io.NLog

    +

    Upgrading the Elmah.Io.NLog package can be done through NuGet:

    +

    Update-Package Elmah.Io.NLog
    +

    +

    Unlike other packages, Elmah.Io.NLog had a version 5.0.38 that was built on top of Elmah.Io.Client v4 and NLog v5. Going forward we recommend using Elmah.Io.NLog version 5.1.x which is built on top of the v5 version of Elmah.Io.Client.

    +

    Code changes

    +

    v5 of the Elmah.Io.NLog package moves to NLog 5. NLog 5 contains a lot of changes that are fully documented here. Your code may need updates caused by changes in the NLog package.

    +

    Upgrading Elmah.Io.Umbraco

    +

    Upgrading the Elmah.Io.Umbraco package can be done through NuGet:

    +

    Update-Package Elmah.Io.Umbraco
    +

    +

    Code changes

    +

    v5 of the Elmah.Io.Umbraco package moves to Umbraco 10. For earlier versions of Umbraco, check out our documentation here. Since Umbraco 10 is built on top of ASP.NET Core, pretty much everything in the v5 version of the Elmah.Io.Umbraco package is a breaking change. The package now relies on the Elmah.Io.AspNetCore package documented above. We recommend completely uninstalling any Elmah.Io.* NuGet packages and install the Elmah.Io.Umbraco package from scratch.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/uptime-monitoring-troubleshooting/index.html b/uptime-monitoring-troubleshooting/index.html new file mode 100644 index 0000000000..dc0ff93e28 --- /dev/null +++ b/uptime-monitoring-troubleshooting/index.html @@ -0,0 +1,659 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Uptime Monitoring Troubleshooting + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Uptime Monitoring Troubleshooting

    +

    Cloudflare Super Bot Fight Mode disallows the elmah.io uptime user-agent

    +

    We have tried to get Cloudflare to adopt the elmah.io Uptime user-agent as an allowed bot. This is not possible since bots need to come from a fixed set of IPs which is not possible when hosting on Microsoft Azure.

    +

    To allow the elmah.io Uptime user-agent you can create a new firewall rule. To do so go to Security | WAF and select the Custom rules tab. Click the Create rule button and input the following values:

    +

    Create firewall rule

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/use-extended-user-details-without-email-as-id/index.html b/use-extended-user-details-without-email-as-id/index.html new file mode 100644 index 0000000000..35ebaf2c02 --- /dev/null +++ b/use-extended-user-details-without-email-as-id/index.html @@ -0,0 +1,684 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Use extended user details without email as ID + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Use extended user details without email as ID

    +

    Most of our integrations automatically log the user identity as part of the error. To make that happen, packages typically use the identity object on the current thread, which gets set by most authentication frameworks for .NET (like ASP.NET Membership Provider and ASP.NET Core Identity). You may use the user's email as the key or a database identifier. If you are using an email, you are already covered and able to see Extended User Details. If not, you need to provide elmah.io with a little help.

    +

    To tell elmah.io about the user's email and still keep the identifier in the user field, you can enrich the message with a piece of custom data, before sending it off to elmah.io. By putting the user's email in a Data item named X-ELMAHIO-USEREMAIL Extended User Details will pick this up and show the correct user. How you set the Data item is dependent on the elmah.io NuGet package you are using.

    +

    For ASP.NET, MVC, or Web API, the code could look like this:

    +

    Elmah.ErrorLog.GetDefault(null); // Forces creation of logger client
    +var logger = ErrorLog.Client;
    +logger.OnMessage += (sender, args) =>
    +{
    +    if (string.IsNullOrWhiteSpace(args.Message.User)) return;
    +    var db = /*...*/;
    +    var user = db.GetById<User>(args.Message.User);
    +    args.Message.Data.Add(new Item {Key = "X-ELMAHIO-USEREMAIL", Value = user.Email});
    +}
    +

    +

    For ASP.NET Core the code could look like this:

    +

    builder.Services.AddElmahIo(o =>
    +{
    +    // ...
    +    o.OnMessage = message =>
    +    {
    +        if (string.IsNullOrWhiteSpace(message.User)) return;
    +        var db = /*...*/;
    +        var user = db.GetById<User>(message.User);
    +        message.Data.Add(new Item {Key = "X-ELMAHIO-USEREMAIL", Value = user.Email});
    +    };
    +});
    +

    +

    OnMessage event handlers are executed just before a message is sent to elmah.io. In the body of the event handler, the user's email is fetched from the database by calling the GetById method. How you will be able to convert the user ID to an email depends on your tech stack, but you get the picture.

    +

    That's it! A few lines of code and you can watch every little detail about the users experiencing problems on your website:

    +

    Extended User Details

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/use-multiple-logs-for-different-environments/index.html b/use-multiple-logs-for-different-environments/index.html new file mode 100644 index 0000000000..876c2e8e22 --- /dev/null +++ b/use-multiple-logs-for-different-environments/index.html @@ -0,0 +1,698 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Use multiple logs for different environments + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Use multiple logs for different environments

    +

    We bet that you use at least two environments for hosting your website: localhost and a production environment. You probably need to log website errors on all your environments, but you don’t want to mix errors from different environments in the same error log. Lucky for you, Microsoft provides a great way of differentiating configuration for different environments called Web Config transformation.

    +
    +

    To avoid spending numerous hours of debugging, remember that Web Config transformations are only run on deploy and not on build. In other words, deploy your website using Visual Studio, MSBuild, or third for the transformations to replace the right ELMAH config.

    +
    +

    Whether or not you want errors from localhost logged on elmah.io, start by installing the Elmah.Io NuGet package:

    +
    Install-Package Elmah.Io
    +
    dotnet add package Elmah.Io
    +
    <PackageReference Include="Elmah.Io" Version="5.*" />
    +
    paket add Elmah.Io
    +
    +

    Then choose one of the two paths below.

    +
    +
    +
    +
    +
    +
    Our Web.config transformations - The definitive syntax guide contains general information about how transformations work and you can use the Web.config Transformation Tester to validate transformation files.
    +
    +
    + +

    Logging to elmah.io from both localhost and production

    +

    Create two new logs at the elmah.io website called something like “My website” and “My website development”. The naming isn’t really important, so pick something telling.

    +

    During installation of the elmah.io package, NuGet will ask you for your elmah.io log id. In this dialog input the log id from the log named “My website development”. The default configuration is used when running your website locally. When installed open the web.release.config file and add the following code:

    +

    <elmah xdt:Transform="Replace">
    +  <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="API_KEY" logId="LOG_ID" />
    +  <security allowRemoteAccess="false" />
    +</elmah>
    +

    +

    Replace the API_KEY with your API key (Where is my API key?) and LOG_ID with your log ID (Where is my log ID?). For more information about Web.config transformations, check out the blog post Web.config transformations - The definitive syntax guide. For help debugging problems, we have created the Web.config Transformation Tester.

    +

    That’s it! You can now build and deploy your website using different configurations. When nothing is changed, Visual Studio will build your website using the Debug configuration. This configuration looks for the ELMAH code in the web.debug.config file. We didn’t add any ELMAH configuration to this file, why the default values from web.config are used. When selecting the Release configuration, Web. Config transformations will replace the default values in web.config with the new ELMAH configuration from web.release.config.

    +

    Logging to elmah.io from production only

    +

    During the installation, NuGet will ask you for your elmah.io log id. You don't need to write anything in this dialog since we will remove the default elmah.io config in a moment. When installed open the web.config file and locate the <elmah> element. Remove the <errorLog> element and set the allowRemoveAccess attribute to true. Your configuration should look like this now:

    +

    <elmah>
    +  <security allowRemoteAccess="true" />
    +</elmah>
    +

    +

    Open the web.release.config file and insert the following code:

    +

    <elmah xdt:Transform="Replace">
    +  <errorLog type="Elmah.Io.ErrorLog, Elmah.Io" apiKey="API_KEY" logId="LOG_ID" />
    +  <security allowRemoteAccess="false" />
    +</elmah>
    +

    +

    Like above, replace API_KEY and LOG_ID with the correct values. Errors happening on your local machine will be logged using ELMAH's default error logger (in-memory) and errors happening in production will be logged to elmah.io.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/using-different-logs-per-environment-in-aspnet-core/index.html b/using-different-logs-per-environment-in-aspnet-core/index.html new file mode 100644 index 0000000000..b5c21ee05a --- /dev/null +++ b/using-different-logs-per-environment-in-aspnet-core/index.html @@ -0,0 +1,683 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Using different logs per environment in ASP.NET Core + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Using different logs per environment in ASP.NET Core

    +

    We are often asked the question: Should I create a log per environment and how do I set it up with ASP.NET Core?

    +

    Creating a log per environment (staging, production, etc.) is a good idea since you probably want different notification rules and/or user access depending on the environment. This document provides a range of possibilities for setting up a log per environment.

    +
    +

    This document is intended for the Elmah.Io.AspNetCore package only. You can use the same approach for Elmah.Io.Extensions.Logging but we recommend using log filtering in the appsettings.json file or through C# instead.

    +
    +

    Using appsettings.{Environment}.json

    +

    All ASP.NET Core websites read an environment variable named ASPNETCORE_ENVIRONMENT. The value can be used to set config variables depending on the current environment. The feature works a bit like Web.config transformations that you may remember from the good old ASP.NET days. The value of ASPNETCORE_ENVIRONMENT can be tailored to your need but the following three values are provided out of the box: Development, Staging, and Production.

    +

    To only add elmah.io when on staging or production, you can add the following code when setting up the elmah.io middleware:

    +

    if (builder.Environment.IsProduction() || builder.Environment.IsStaging())
    +{
    +    app.UseElmahIo();
    +}
    +

    +

    To create errors in different logs depending on the current environment, create two new files: appsettings.staging.json and appsettings.production.json. Add the ElmahIo config section to both files:

    +

    {
    +  "ElmahIo": {
    +    "ApiKey": "API_KEY",
    +    "LogId": "LOG_ID"
    +  }
    +}
    +

    +

    Inside each file, replace API_KEY with your API key and LOG_ID with the different log IDs.

    +

    The downside of this approach is that you have your production API key and log ID in source control.

    +

    Using Azure Application settings

    +

    If you are hosting on Azure (other cloud hosting platforms have a similar feature), you can utilize the built-in Application settings feature to avoid adding API keys and log IDs to source control. Using application settings requires you to specify your elmah.io configuration in the appsettings.json file or one of the environment-specific files as shown above.

    +

    To replace the values inside the ApiKey and LogId properties, use the colon syntax as shown here:

    +

    Azure Application settings

    +

    (replace API_KEY and LOG_ID with your staging or production values depending on which environment you are configuring)

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/using-the-elmah-io-extension-for-visual-studio/index.html b/using-the-elmah-io-extension-for-visual-studio/index.html new file mode 100644 index 0000000000..dcdcb427f7 --- /dev/null +++ b/using-the-elmah-io-extension-for-visual-studio/index.html @@ -0,0 +1,676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Using the elmah.io extension for Visual Studio + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Using the elmah.io extension for Visual Studio

    +

    Being able to focus on .NET developers makes it possible to do all kinds of cool things. Like building an elmah.io extension for Visual Studio. That's exactly what we've done and here's how to use it.

    +

    Installation

    +

    Download the elmah.io extension for Visual Studio ≤ 2019 or Visual Studio 2022 from the Visual Studio Marketplace.

    +
    +

    Don't use the Extensions and Updates feature inside Visual Studio, since Visual Studio causes a problem with installing extensions in previous versions.

    +
    +

    Double/click the downloaded VSIX and enable elmah.io in the versions of Visual Studio of your choice. The extension supports Visual Studio 2015, 2017, 2019, and 2022.

    +

    Usage

    +

    Inside Visual Studio navigate to View | Other Windows | elmah.io or simply search for elmah.io in the Quick Launcher (Ctrl + Q).

    +

    You'll see the elmah.io window somewhere. Click the Sign In button and sign in with a username/password or one of the social providers:

    +

    Sign in

    +

    If you are part of multiple elmah.io organizations, select the one you want to access:

    +

    Select organization

    +

    If this is the first time someone is browsing the chosen organization from Visual Studio, authorize the creation of a new API key:

    +

    Authorize API key

    +

    Once logged in, the list of logs is populated with all of your logs defined at elmah.io. Select a log and click the search icon:

    +

    Browse a log inside Visual Studio

    +

    Log messages can be filtered by date range, full-text search, and using Lucene query language as already known from the elmah.io UI.

    +

    To inspect a single message, double-click it and the message details window will open:

    +

    Message details

    +

    The toolbar at the top provides a couple of options: View the message on elmah.io, hide the message and delete the message.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/using-the-rest-api/index.html b/using-the-rest-api/index.html new file mode 100644 index 0000000000..b197a56048 --- /dev/null +++ b/using-the-rest-api/index.html @@ -0,0 +1,789 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Using the REST API + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Using the REST API

    + +

    Under the hood, everything related to communicating with elmah.io happens through our REST API. In this article, we will present the possibilities of using the API in a use case-driven approach. For a detailed reference of the various endpoints, visit the API V3 documentation.

    +

    Security

    +

    Security is implemented using API keys (Where is my API key?). When creating a new organization, a default API key is automatically created.

    +

    You can create new keys and revoke an existing key if you suspect that the key has been compromised. The API key acts as a secret and should not be available to people outside your team/organization.

    +

    All requests to the elmah.io API needs the API key as either an HTTP header or query string parameter named api_key like this:

    +
    GEThttps://api.elmah.io/v3/messages/LOG_ID?api_key=MY_API_KEY
    + +

    Messages

    +

    Creating messages

    +

    Before doing anything, we will need some messages to play with. The Create Message endpoint does just that. To create a simple message, POST to:

    +
    POSThttps://api.elmah.io/v3/messages/LOG_ID
    + +

    with a JSON body:

    +

    {
    +    "title": "This is a test message"
    +}
    +

    +

    (replace LOG_ID with your log ID):

    +

    The title field is the only required field on a message, but fields for specifying severity, timestamp, etc. are there. For more information, check out the documentation.

    +

    If everything where successful, the API returns an HTTP status code of 201 and a location to where to fetch the new message. If the endpoint fails, the response will contain a description of what went wrong. Forgetting to set Content-Length, Content-Type and similar, will result in an invalid request.

    +

    Getting a message

    +

    In the example above, the API returned the URL for getting the newly created message:

    +
    GEThttps://api.elmah.io/v3/messages/LOG_ID/81C7C282C9FDAEA3
    + +

    By making a GET request to this URL, we get back the message details:

    +

    {
    +  "id": "99CDEA3D6A631F09",
    +  "title": "This is a test message",
    +  "dateTime": "2016-07-03T14:25:46.087857Z",
    +  "severity": "Information"
    +}
    +

    +

    As shown in the returned body, elmah.io automatically inserted some missing fields like a timestamp and a severity. If no severity is specified during creating, a message is treated as information.

    +

    Searching messages

    +

    For the demo, we have inserted a couple of additional messages, which leads us to the next endpoint: searching messages. The search endpoint shares the root path with the get message endpoint but only takes a log ID. The simplest possible configuration queries the API for a list of the 15 most recent messages by calling:

    +
    GEThttps://api.elmah.io/v3/messages/LOG_ID
    + +

    The response body looks like this:

    +

    {
    +  "messages": [
    +    {
    +      "id": "81C7C282C9FDAEA3",
    +      "title": "This is another test message",
    +      "dateTime": "2016-07-03T14:31:45.053851Z",
    +      "severity": "Information"
    +    },
    +    {
    +      "id": "99CDEA3D6A631F09",
    +      "title": "This is a test message",
    +      "dateTime": "2016-07-03T14:25:46.087857Z",
    +      "severity": "Information"
    +    },
    +    // ...
    +  ],
    +  "total": 42
    +}
    +

    +

    For simplicity, the response has been simplified by not showing all of the results. The important thing to notice here is the list of messages and the total count. messages contain 15 messages, which is the default page size in the search endpoint. To increase the number of returned messages, set the pagesize parameter in the URL (max 100 messages per request). The total count tells you if more messages are matching your search. To select messages from the next page, use the pageindex parameter (or use the searchAfter property as shown later).

    +

    Returning all messages may be fine, but being able to search by terms is even more fun. To search, use the query, from, and to parameters as shown here:

    +
    GEThttps://api.elmah.io/v3/messages/LOG_ID?query=another
    + +

    Searching for another will return the following response:

    +

    {
    +  "messages": [
    +    {
    +      "id": "81C7C282C9FDAEA3",
    +      "title": "This is another test message",
    +      "dateTime": "2016-07-03T14:25:46.087857Z",
    +      "severity": "Information"
    +    }
    +  ],
    +  "total": 1
    +}
    +

    +

    Now only 81C7C282C9FDAEA3 shows up since that message contains the text another in the title field. Like specifying the query parameter, you can limit the number of messages using the from, to, and pageSize parameters.

    +

    There is a limitation of using the pageSize and pageIndex parameters. The data is stored in Elasticsearch which doesn't allow pagination in more than 10,000 documents. If you need to fetch more than 10,000 documents from your log, we recommend breaking this up into weekly, daily, or hourly jobs instead of changing your job to fetch more than 10,000 messages. In case this is not possible, you can switch from using the pageIndex parameter to searchAfter. Each search result will return a value in the searchAfter property:

    +

    {
    +  "messages": [
    +    // ...
    +  ],
    +  "searchAfter": "1694180633270",
    +  "total": 42
    +}
    +

    +

    To fetch the next list of messages you can provide the search endpoint with the value of searchAfter:

    +
    GEThttps://api.elmah.io/v3/messages/LOG_ID?searchAfter=1694180633270
    + +

    You will need to use the same set of parameters in query, from, and to as in the previous request for this to work.

    +

    Deleting a message

    +

    When fixing the bug causing an error logged at elmah.io, you may want to delete the error. Deleting a single error is as easy as fetching it. Create a DELETE request to the errors unique URL:

    +
    DELETEhttps://api.elmah.io/v3/messages/LOG_ID/81C7C282C9FDAEA3
    + +

    When successfully deleted, the delete endpoint returns an HTTP status code of 200.

    +

    Deleting messages

    +

    Deleting messages one by one can be tedious work. To delete multiple errors, you can utilize the Delete Messages endpoint by creating a DELETE request to:

    +
    DELETEhttps://api.elmah.io/v3/messages/LOG_ID
    + +

    The request must contain a body with at least a query:

    +

    {
    +    "query": "test"
    +}
    +

    +

    An option for deleting messages by date range is available as well. Check out the API documentation for details.

    +

    Hiding a message

    +

    Depending on your use case, you may want to hide a message, rather than delete it. Hidden messages are shown as default through neither the UI nor the REST API. But you will be able to search for them by enabling the Hidden checkbox on the UI.

    +

    To hide a message, use the _hide endpoint like this:

    +
    POSThttps://api.elmah.io/v3/messages/LOG_ID/99CDEA3D6A631F09/_hide
    + +

    If successful, the endpoint returns an HTTP status code of 200.

    +

    Fixing a message

    +

    When you have fixed a bug in your code, it's a good idea to mark any instances of this error in elmah.io as fixed. This gives a better overview of what to fix and ensures that you are notified if the error happens again.

    +

    To mark a message as fixed, use the _fix endpoint like this:

    +
    POSThttps://api.elmah.io/v3/messages/LOG_ID/99CDEA3D6A631F09/_fix
    + +

    If successful, the endpoint returns an HTTP status code of 200. This will mark a single message as fixed. In case you want to mark all instances of this message as fixe, include the markAllAsFixed parameter:

    +
    POSThttps://api.elmah.io/v3/messages/LOG_ID/99CDEA3D6A631F09/_fix?markAllAsFixed=true
    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/where-is-my-api-key/index.html b/where-is-my-api-key/index.html new file mode 100644 index 0000000000..56e9f4b0a2 --- /dev/null +++ b/where-is-my-api-key/index.html @@ -0,0 +1,661 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Where is my API key? + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Where is my API key?

    +

    API keys are a new concept introduced with version 3.x of our API. API keys are located on the organization settings page.

    +

    To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard:

    +

    Organization settings

    +

    When on the organization settings page, click the API Keys tab and copy your API key:

    +

    API keys on organization settings

    +

    This view also lets you generate new API keys and revoke an existing key if you believe that the key is compromised.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/where-is-my-invoice-receipt/index.html b/where-is-my-invoice-receipt/index.html new file mode 100644 index 0000000000..740272aab3 --- /dev/null +++ b/where-is-my-invoice-receipt/index.html @@ -0,0 +1,662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Where is my invoice/receipt? + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Where is my invoice/receipt?

    +

    Invoices are located on the organization settings page.

    +

    To open organization settings, click the gears icon next to your organization name on either the left menu or through the dashboard:

    +

    Organization settings

    +

    When on the organization settings page, click the Invoices tab:

    +

    Invoices on organization settings

    +

    There's a row per invoice in the table on this page. You can open or download the invoice using the links to the right.

    +

    If you want invoices emailed to you (or your accountant) every time a payment is made, click the Email invoices button and input an email address.

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/where-is-my-log-id/index.html b/where-is-my-log-id/index.html new file mode 100644 index 0000000000..f362c765ca --- /dev/null +++ b/where-is-my-log-id/index.html @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Where is my log ID? + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Where is my log ID?

    +

    A log ID represents a log on elmah.io. A log is a container for log messages. How you choose to split up your logs is totally up to you, but creating a log per application/service is what most users do.

    +

    The log ID is located on the log settings page. To open log settings, click the gears icon next to the log name in the menu or in the log box on the dashboard:

    +

    Log settings

    +

    When clicking on the icon, you are taken directly to the Install tab:

    +

    Log ID on log settings

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/where-is-the-permalink-button/index.html b/where-is-the-permalink-button/index.html new file mode 100644 index 0000000000..a7043f45ed --- /dev/null +++ b/where-is-the-permalink-button/index.html @@ -0,0 +1,659 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Where is the permalink button + + + + + + + + + + + + + + + + + + +
    +
    + + Documentation menu +
    +
    + +

    Where is the permalink button

    +

    A permalink is a link to a specific log message on elmah.io. When generating a permalink, the log message will be shown in expanded form on the log search page. To get the permalink, go to the log search page and click a log message to expand the details. Beneath the log message, there's a permalink button that will open the message in a new tab:

    +

    Permalink on log message details

    +

    You can also get a permalink directly from the search result:

    +

    Permalink on search result

    + +
    +

    This article was brought to you by the elmah.io team. elmah.io is the best error management system for .NET web applications. We monitor your website, alert you when errors start happening, and help you fix errors fast.

    +

    + See how we can help you monitor your website for crashes + Monitor your website +

    +
    +
    +
    +
    + + + + + + + + + + + + + \ No newline at end of file