diff --git a/docs/data-shippers/benchmark-dotnet.asciidoc b/docs/data-shippers/benchmark-dotnet.asciidoc new file mode 100644 index 00000000..db131603 --- /dev/null +++ b/docs/data-shippers/benchmark-dotnet.asciidoc @@ -0,0 +1,221 @@ +[[benchmark-dotnet-data-shipper]] +=== BenchmarkDotnet Exporter + +An exporter for https://github.com/dotnet/BenchmarkDotNet[BenchmarkDotnet] that will index benchmarking results directly into Elasticsearch. + +==== Installation + +Add a reference to the http://nuget.org/packages/Elastic.CommonSchema.BenchmarkDotNetExporter[Elastic.CommonSchema.BenchmarkDotNetExporter] package: + +[source,xml] +[subs="attributes"] +---- + +---- + +==== Usage + +[source,csharp] +---- +var options = new ElasticsearchBenchmarkExporterOptions(url) +{ + GitBranch = "externally-provided-branch", + GitCommitMessage = "externally provided git commit message", + GitRepositoryIdentifier = "repository" +}; +var exporter = new ElasticsearchBenchmarkExporter(options); + +var config = CreateDefaultConfig().With(exporter); +BenchmarkRunner.Run(typeof(Md5VsSha256), config); + +---- + +The code snippet above configures the `ElasticsearchBenchmarkExporter` with the supplied `ElasticsearchBenchmarkExporterOptions`. It is possible to configure the exporter to use https://www.elastic.co/cloud/[Elastic Cloud] as follows: + +[source,csharp] +---- +var options = new ElasticsearchBenchmarkExporterOptions(url) +{ + CloudId = "CLOUD_ID_HERE" +}; + +---- + +Example _source from a search in Elasticsearch after a benchmark run: + +[source,json] +---- +{ + "_index":"benchmark-dotnet-2020-01-01", + "_type":"_doc", + "_id":"pfFAh28B14pBZI_VO098", + "_score":1.0, + "_source":{ + "agent":{ + "git":{ + "branch_name":"externally-provided-branch", + "commit_message":"externally provided git commit message", + "repository":"repository" + }, + "language":{ + "jit_info":"RyuJIT", + "dot_net_sdk_version":"3.0.101", + "benchmark_dot_net_caption":"BenchmarkDotNet", + "has_ryu_jit":true, + "build_configuration":"RELEASE", + "benchmark_dot_net_version":"0.12.0", + "version":".NET Core 3.0.1 (CoreCLR 4.700.19.47502, CoreFX 4.700.19.51008)" + }, + "type":"Elastic.CommonSchema.BenchmarkDotNetExporter", + "version":"1.0.0+7cedae2aaa06092ea253155279b835cee6160b3a" + }, + "os":{ + "name":"Linux", + "version":"ubuntu 18.10", + "platform":"unix" + }, + "message":null, + "benchmark":{ + "q1":3632.625, + "lower_outliers":[], + "q3":5047.625, + "confidence_interval":{ + "margin":14613.282591693971, + "level":12, + "mean":4123.291666666667, + "lower":-10489.990925027305, + "n":3, + "standard_error":462.4594877151704 + }, + "percentiles":{ + "p0":3632.625, + "p67":4151.345, + "p25":3661.125, + "p100":5047.625, + "p90":4776.025000000001, + "p80":4504.425, + "p50":3689.625, + "p85":4640.225, + "p95":4911.825 + }, + "memory":{ + "bytes_allocated_per_operation":112, + "total_operations":4, + "gen2_collections":0, + "gen1_collections":0, + "gen0_collections":0 + }, + "max":5047.625, + "interquartile_range":1415, + "all_outliers":[], + "upper_fence":7170.125, + "standard_deviation":801.0033291649501, + "kurtosis":0.6666666666666661, + "n":3, + "standard_error":462.4594877151704, + "min":3632.625, + "median":3689.625, + "upper_outliers":[], + "variance":641606.3333333333, + "mean":4123.291666666667, + "lower_fence":1510.125, + "skewness":0.3827086238595402 + }, + "@timestamp":"2020-01-08T22:22:10.7917398+00:00", + "host":{ + "hardware_timer_kind":"Unknown", + "physical_processor_count":1, + "logical_core_count":12, + "in_docker":false, + "processor_name":"Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz", + "chronometer_frequency_hertz":1000000000, + "has_attached_debugger":false, + "physical_core_count":6, + "architecture":"X64" + }, + "log.level":null, + "event":{ + "duration":1385324200, + "measurement_stages":[ + { + "operations":2, + "iteration_mode":"Overhead", + "iteration_stage":"Jitting" + }, + { + "operations":2, + "iteration_mode":"Workload", + "iteration_stage":"Jitting" + }, + { + "operations":4, + "iteration_mode":"Overhead", + "iteration_stage":"Warmup" + }, + { + "operations":4, + "iteration_mode":"Overhead", + "iteration_stage":"Actual" + }, + { + "operations":4, + "iteration_mode":"Workload", + "iteration_stage":"Warmup" + }, + { + "operations":4, + "iteration_mode":"Workload", + "iteration_stage":"Actual" + }, + { + "operations":4, + "iteration_mode":"Workload", + "iteration_stage":"Result" + } + ], + "job_config":{ + "run_time":".NET Core 3.0", + "jit":"Default", + "launch":{ + "unroll_factor":2, + "max_iteration_count":0, + "launch_count":1, + "iteration_count":3, + "run_strategy":"Throughput", + "iteration_time_in_milliseconds":0, + "warm_count":3, + "max_warmup_iteration_count":0, + "invocation_count":4, + "min_warmup_iteration_count":0, + "min_iteration_count":0 + }, + "id":"ShortRun", + "gc":{ + "heap_affinitize_mask":0, + "server":false, + "no_affinitize":false, + "allow_very_large_objects":false, + "retain_vm":false, + "cpu_groups":false, + "concurrent":false, + "heap_count":0, + "force":false + }, + "platform":"AnyCpu" + }, + "original":"Md5VsSha256.Sha256: ShortRun(Runtime=.NET Core 3.0, InvocationCount=4, IterationCount=3, LaunchCount=1, UnrollFactor=2, WarmupCount=3) [N=1000]", + "method":"Elastic.CommonSchema.BenchmarkDotNetExporter.IntegrationTests.Md5VsSha256.Sha256(N: 1000)", + "module":"Elastic.CommonSchema.BenchmarkDotNetExporter.IntegrationTests", + "description":"Sha256", + "action":"Sha256", + "category":"Elastic.CommonSchema.BenchmarkDotNetExporter.IntegrationTests.Md5VsSha256-20200108-232208", + "type":"Md5VsSha256", + "parameters":"N=1000", + "repetitions":{ + "measured":4, + "warmup":4 + } + } + } +} +---- \ No newline at end of file diff --git a/docs/data-shippers/extensions-logging.asciidoc b/docs/data-shippers/extensions-logging.asciidoc new file mode 100644 index 00000000..23dcbdad --- /dev/null +++ b/docs/data-shippers/extensions-logging.asciidoc @@ -0,0 +1,444 @@ +[[extensions-logging-data-shipper]] +=== Elastic.Extensions.Logging + +Elastic logger provider for Microsoft.Extensions.Logging. + +Writes direct to Elasticsearch using the https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[Elastic Common Schema (ECS)], +with semantic logging of structured data from message and scope values. The results can be viewed and queried in the Kibana console. + +==== Installation + +Add a reference to the `Elastic.Extensions.Logging` package: + +[source,xml] +[subs="attributes"] +---- + +---- + +==== Usage + +Then, add the provider to the loggingBuilder during host construction, using the provided extension method. + +[source,c#] +---- +using Elasticsearch.Extensions.Logging; + +// ... + + .ConfigureLogging((hostContext, loggingBuilder) => + { + loggingBuilder.AddElasticsearch(); + }) + +---- + +The default configuration will write to a local Elasticsearch running at http://localhost:9200/. + +Once you have sent some log event, open Kibana (e.g. http://localhost:5601/) and define an +index pattern for "dotnet-*" with the time filter "@timestamp". + +You can then discover the log events for the index. Some useful columns to add +are `log.level`, `log.logger`, `event.code`, `message`, `tags`, and `process.thread.id`. + +If you are running multiple applications or on multiple servers, you might want to +include `service.type`, `service.version`, and `host.hostname`. + +Additional fields are defined below, and all individual message and scope values are logged +as `labels.*` custom key/value pairs, e.g. `labels.CustomerId`. + +==== Basic configuration + +For deployment you will usually want to override the configuration with your actual server location(s). +The other useful value to configure is a tag for the environment, e.g. Development/Staging/Production. + +[source,json] +---- +{ + "Logging": { + "Elasticsearch": { + "NodeUris": [ "https://elastic-staging.example.com:9200" ], + "Tags": [ "Staging" ] + } + } +} +---- + +*NOTE:* You don't need any configuration to just use a local Elasticsearch instance, as it defaults to http://localhost:9200/. + +==== Configuration settings + +The logger provider will be automatically configured with any logging settings under the alias `Elasticsearch`. + +The following default settings are used. + +[source,json] +---- +{ + "Logging": { + "Elasticsearch": { + "IncludeHost": true, + "IncludeProcess": true, + "IncludeScopes": true, + "IncludeUser": true, + "Index": "dotnet-{0:yyyy.MM.dd}", + "IndexOffset": null, + "IsEnabled": true, + "ListSeparator": ", ", + "MapCorrelationValues": true, + "Tags": [], + "ShipTo": { + "NodePoolType": "SingleNode", + "NodeUris": [ "http://localhost:9200" ] + } + } + } +} +---- + +|=== +|Setting |Type |Description + +|IncludeHost |boolean |Default `true`; set to `false` to disable logging host values. +|IncludeProcess |boolean |Default `true`; set to `false` to disable logging process values. +|IncludeScopes |boolean |Default `true`; set to `false` to disable logging scope values. +|IncludeUser |boolean |Default `true`; set to `false` to disable logging user details. +|Index |format |Format string used to generate the Elasticsearch `index`, using the current timestamp. Default is `dotnet-{0:yyyy.MM.dd}`. +|IndexOffset |timespan |Override to set the offset used to generate the `index`. Default value is `null`, which uses the system local offset; use `"00:00&"` for UTC. +|IsEnabled |boolean |Default `true`; set to `false` to disable the logger. +|ListSeparator |string |Separator to use for `IEnumerable` in `labels.*` values. Default is `", "`. +|Tags |array |Additional tags to include in the message. Useful to specify the environment or other details, e.g. `[ "Staging", "Priority"]` +|=== + +The ShipTo settings can have the following properties, depending on the type of connection pool. + +|=== +|Setting |Type |Description + +|ApiKey |string |API Key, where connection pool type is Cloud, and authenticating via API Key. +|CloudId |string |Cloud ID, where connection pool type is Cloud. +|NodePoolType |enum |Default is `Singlenode`, or `Sniffing` for multiple nodes, or `Cloud` if `CloudId` is provided. Other supported values are `Static` or `Sticky`. +|NodeUris |array |URI(s) of the Elasticsearch nodes to connect to. Default is a single node `[ "http://localhost:9200" ]` +|Password |string |Password, where connection pool type is Cloud, and authenticating via username/password. +|Username |string |Username, where connection pool type is Cloud, and authenticating via username/password. +|=== + +If you want to configure from a different section, it can be configured manually: + +[source,c#] +---- + .ConfigureLogging((hostContext, loggingBuilder) => + { + loggingBuilder.AddElasticsearch(options => + hostContext.Configuration.Bind("Logging:CustomElasticsearch", options)); + }) +---- + +Configuration can, of course, also be done in code, e.g. to add the environment as a tag. + +==== Elastic Cloud configuration + +If `CloudId` is provided, the `ConnectionPoolType` defaults to `Cloud`: + +[source,json] +---- +{ + "Logging": { + "Elasticsearch": { + "ShipTo": { + "CloudId": "12345", + "ApiKey": "abcdef" + } + } + } +} +---- + +==== Output - Elastic Common Schema (ECS) + +Log messages sent to Elasticsearch follow the https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[Elastic Common Schema (ECS)]. + +==== Example document + +The `_source` field is the message sent from the LoggerProvider, along with the `_index` and `_id` (a GUID). + +[source,json] +---- +{ + "_index": "dotnet-2020.04.12", + "_type": "_doc", + "_id": "563503a8-9d10-46ff-a09f-c6ccbf124db9", + "_version": 1, + "_score": null, + "_source": { + "MessageTemplate": "Unexpected error processing customer {CustomerId}.", + "Scopes": [ + "IP address 2001:db8:85a3::8a2e:370:7334", + "PlainScope" + ], + "agent": { + "version": "1.0.0+bd3ad6", + "type": "Elasticsearch.Extensions.Logging.LoggerProvider" + }, + "ecs": { + "version": "1.5.0" + }, + "error": { + "message": "Calculation error", + "type": "System.Exception", + "stack_trace": "System.Exception: Calculation error\n ---> System.DivideByZeroException: Attempted to divide by zero.\n at HelloElasticsearch.Worker.ExecuteAsync(CancellationToken stoppingToken) in /home/sly/Code/essential-logging/examples/HelloElasticsearch/Worker.cs:line 80\n --- End of inner exception stack trace ---\n at HelloElasticsearch.Worker.ExecuteAsync(CancellationToken stoppingToken) in /home/sly/Code/essential-logging/examples/HelloElasticsearch/Worker.cs:line 84" + }, + "event": { + "code": "5000", + "action": "ErrorProcessingCustomer", + "severity": 3 + }, + "host": { + "os": { + "platform": "Unix", + "full": "Linux 4.15.0-91-generic #92-Ubuntu SMP Fri Feb 28 11:09:48 UTC 2020", + "version": "4.15.0.91" + }, + "hostname": "VUB1804", + "architecture": "X64" + }, + "log": { + "level": "Error", + "logger": "HelloElasticsearch.Worker" + }, + "process": { + "thread": { + "id": 10 + }, + "pid": 25982, + "name": "HelloElasticsearch" + }, + "service": { + "type": "HelloElasticsearch", + "version": "1.0.0" + }, + "user": { + "id": "sgryphon+es@live.com", + "name": "sly", + "domain": "VUB1804" + }, + "@timestamp": "2020-04-13T21:25:22.3352989+10:00", + "tags": [ + "Development" + ], + "labels": { + "ip": "2001:db8:85a3::8a2e:370:7334", + "CustomerId": "12345" + }, + "message": "Unexpected error processing customer 12345.", + "trace": { + "id": "c20bde1071f7cf4e9a6f368c824e05f7" + }, + "transaction": { + "id": "92ba5ee64d963746" + } + }, + "fields": { + "@timestamp": [ + "2020-04-13T11:25:22.335Z" + ] + }, + "sort": [ + 1586777122335 + ] +} +---- + +==== Standard Fields + +|=== +|Field |Type |Description + +|@timestamp |date |`DateTimeOffset` when the message was logged, including local offset. +|message |string |The formatted log message and arguments. +|tags |array |Custom tags from configuration, e.g. `[ "Staging", "Priority" ]`. Can have multiple values. +|event.action |string |The name of the logged EventId, e.g. `ErrorProcessingCustomer`. +|event.code |string |The numeric value (as a string) of the EventId, e.g. `5000`. +|event.severity |long |The syslog severity corresponding to the log level, 2 = critical, 3 = error, 4 = warning, 6 = information, 7 = debug and trace. (Also used in the Systemd format of ConsoleLoggerProvider) +|log.level |string |The log level: `Critical`, `Error`, `Warning`, `Information`, `Debug`, or `Trace`. +|log.logger |string |The category name (namespace and class) of the logger, e.g. `HelloElasticsearch.Worker`. +|=== + +The `event.severity` field is numeric and can be used to order events by level, e.g. Kibana query `event.severity <= 4` will get all messages that have log level `Warning` or worse. + +==== Error fields + +If the log message includes an exception, the details are reported in the error fields. + +|=== +|Field |Type |Description + +|error.message |string |The `Message` property of any exception. +|error.stack_trace |string |Full details of the exception, `Exception.ToString()`, including the stack trace and the stack trace of any inner exceptions. +|error.type |string |The type of the error message, e.g. `System.DivideByZeroException` +|=== + +==== Custom fields + +Following the ECS conventions, these use alternative Title casing, to identify them as non-standard fields. + +|=== +|Field |Type |Description + +|MessageTemplate |string |The original message template, e.g. "Unexpected error processing customer {CustomerId}." +|Scopes |array |Array of string formatted scope values, in the order added. +|=== + +==== Label values + +|=== +|Field |Type |Description + +|labels.* |string |Custom key/value pairs of all named parameter values and named scope values. All values are strings (no nested objects). +|=== + +Label values can be accessed by their key, for example if the message, or scope, includes a parameter CustomerId, then the value will be logged as `labels.CustomerId` can be searched in Kibana using "labels.CustomerId: 12345". + +*Example:* + +The following will generate two labels, `labels.EndTime` from the message and `labels.CustomerId` from the scope: + +[source,c#] +---- +using (_logger.BeginScope("{CustomerId}", customerId)) +{ + _logger.LogWarning("End of processing reached at {EndTime}.", end); +} +---- + +Labels are taken from the message (state) and any scope values (may be disabled via the configuration options). In Microsoft.Extensions.Logging an +internal FormattedLogValues is used in the ILogger overloads for log levels and scopes; it implements the `IEnumerable<KeyValuePair<string,object>>` +interface that is used to extract the individual parameter values. + +The `labels` property in ECS should not contain nested objects, so values are converted to keyword strings. For most objects this is just calling ToString(), +with specific formats for some types, e.g. calling string on a list is usually not very useful, so the contents of the list is logged instead. + +*Labels value formatting* + +|=== +|Type |Formatting + +|byte |Hex, e.g. "9A" +|byte[] |Prefixed hex, e.g. "0x12789AF0" +|DateTimeOffset |ISO format, e.g. "2020-01-02T03:04:05.000000+06:00" +|DateTime |In most cases `DateTimeOffset` should be used instead (1). Where `DateTime` is used for date only (with no time component), it is formatted as a date, e.g. "2020-01-02". If it has a time component, the roundtrip ("o") format is used. +|IEnumerable |Values separated by ", " (configurable) +|IDictionary<string, object> |A string containing key value pairs, e.g. `token="0x12789AF0" count="5"` +|_other values_ |The result of `ToString()`, including scalar values, e.g. the number `5.3` is logged as the string "5.3" +|=== + +*(1) See https://docs.microsoft.com/en-us/dotnet/standard/datetime/choosing-between-datetime* + +==== Agent fields + +These identify the version of the logger provider being used. + +|=== +|Field |Type |Description + +|agent.type |string |Name of the logger provider assembly, `Elasticsearch.Extensions.Logging.LoggerProvider`. +|agent.version |string |Informational version number of the logger assembly, e.g. `1.1.1+bd3ad63`. +|ecs.version |string |Version of ECS standard used, currently `1.5`. +|=== + +==== Service fields + +This identifies the application/service that is running and generating the logs. + +The values are pulled from the entry assemb, `Assembly.GetEntryAssembly()`, using the `Name` +and `AssemblyInformationalVersionAttribute` values (if informational version is not set +it falls back to assembly `Version`). + +|=== +|Field |Type |Description + +|service.type |string |Name of the entry assembly, `HelloElasticsearch`. +|service.version |string |Informational version number of the entry assembly, e.g. `1.2.0-beta.1+79d095a`. +|=== + +*Note:* You should be using a build process that sets the assembly informational version correctly. +e.g. If you have a dotnet project using git you can install the local tool `GitVersion.Tool`, +and use it to automatically generate semantic version numbers from the git branch information. + +To install the tool: + +[source,powershell] +---- +dotnet new tool-manifest +dotnet tool install GitVersion.Tool +---- + +Then use the tool to create a semantic version number that can be used in your build process: + +[source,powershell] +---- +dotnet tool restore +dotnet gitversion +---- + +You are welcome to use the link:../../build.ps1[`build.ps1`] script in this repository as an example. + +==== Tracing fields + +|=== +|Field |Type |Description + +|trace.id |string |Cross-service trace correlation identifier. From `Activity.Current.RootId` from `System.Diagnostics`, with a fallback to `CorrelationManager.ActivityId`. Can be overridden by a message or scope value `trace.id`. +|transaction.id |string |Transaction for this service, e.g. individual request identifier. If in W3C format, parse out the SpanId from `Activity.Current.Id` from `System.Diagnostics`, otherwise just use the full `Activity.Current.Id` (e.g. if hierarchical). Can be overridden by message or scope value `transaction.id`. +|=== + +ASP.NET will automatically pass correlation identifiers between tiers; from 3.0 it also supports the W3C Trace Context standard (https://www.w3.org/TR/trace-context/). + +The value of `Activity.Current.RootId` is used as the cross-service identifier (in W3C format this is the Trace ID), +if in W3C format the Span ID portion of `Activity.Current.Id` is used for the transaction, otherwise the full value is used (this is consistent with the way ASP.NET works). + +It is recommended to turn on W3C format, for compatibility with other systems: + +[source,c#] +---- +Activity.DefaultIdFormat = ActivityIdFormat.W3C; +---- + +==== Host fields + +NOTE: Can be disabled via configuration. + +|=== +|Field |Type |Description + +|host.architecture |string |The processor architecture, e.g. X64. Value of `RuntimeInformation.OSArchitecture`. +|host.hostname |string |The computer name. Value of `Environment.MachineName`. +|host.os.full |string |Full description of the operation system. Value of `RuntimeInformation.OSDescription`. +|host.os.platform |string |Operating system platform. Value of `Environment.OSVersion.Platform`. +|host.os.version |string |Operating system version. Value of `Environment.OSVersion.Version`. +|=== + +==== Process fields + +NOTE: Can be disabled via configuration. + +|=== +|Field |Type |Description + +|process.name |string |The current process name. From `Process.GetCurrentProcess()`. +|process.pid |long |The current process ID. From `Process.GetCurrentProcess()`. +|process.thread.id |long |Current thread ID. Value of `Thread.CurrentThread.ManagedThreadId`. +|process.thread.name |string |Name of the thread. From `Thread.CurrentThread.Name`. +|=== + +==== User fields + +NOTE: Can be disabled via configuration. + +|=== +|Field |Type |Description + +|user.domain |string |The current domain, either the machine name or a Windows domain. Value of `Environment.UserDomainName`. +|user.id |string |Current user principal name, if set. Value of `Thread.CurrentPrincipal.Identity.Name`. +|user.name |string |The current user. Value of `Environment.UserName`. +|=== \ No newline at end of file diff --git a/docs/data-shippers/index.asciidoc b/docs/data-shippers/index.asciidoc new file mode 100644 index 00000000..da72082e --- /dev/null +++ b/docs/data-shippers/index.asciidoc @@ -0,0 +1,18 @@ +ifdef::env-github[] +NOTE: For the best reading experience, +please view this documentation at https://www.elastic.co/guide/en/ecs-logging/dotnet[elastic.co] +endif::[] + +[[data-shippers]] +== Data Shippers + +Our datashippers integrate with logging frameworks to facilitate +sending events (such as logs) to various outputs. + +Currently these shippers support Elastic Cloud & Elasticsearch but +other outputs are in the works. + +include::./ingest-commonschema.asciidoc[Elastic.Ingest.Elasticsearch.CommonSchema] +include::./serilog.asciidoc[Serilog] +include::./extensions-logging.asciidoc[Microsoft.Extensions.Logging] +include::./benchmark-dotnet.asciidoc[BenchmarkDotNet] diff --git a/docs/data-shippers/ingest-commonschema.asciidoc b/docs/data-shippers/ingest-commonschema.asciidoc new file mode 100644 index 00000000..58e5b9de --- /dev/null +++ b/docs/data-shippers/ingest-commonschema.asciidoc @@ -0,0 +1,107 @@ +[[ecs-ingest-channels]] +=== ECS Ingest Channels + +A specialization of https://www.nuget.org/packages/Elastic.Ingest.Elasticsearch#readme-body-tab[`Elastic.Ingest.Elasticsearch`] that offers two channel implementations that make it easy to write ECS formatted data and bootstrap the target datastreams/indices with ECS mappings and settings. + +==== Installation + +Add a reference to the `Elastic.Ingest.Elasticsearch.CommonSchema` package: + +[source,xml] +[subs="attributes"] +---- + +---- + +==== Usage + +===== EcsDataStreamChannel + +A channel that specializes to writing data with a timestamp to Elasticsearch data streams. + +A channel can be created to push data to the `logs-dotnet-default` data stream. + +[source,csharp] +---- +var dataStream = new DataStreamName("logs", "dotnet"); +var bufferOptions = new BufferOptions { } +var options = new DataStreamChannelOptions(transport) +{ + DataStream = dataStream, + BufferOptions = bufferOptions +}; +var channel = new EcsDataStreamChannel(options); + +---- + +TIP: Learn more about Elastic's data stream naming convention in https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme[this blog post]. + +We can now push data to Elasticsearch using the `EcsDataStreamChannel` + +[source,csharp] +---- +var doc = new EcsDocument +{ + Timestamp = DateTimeOffset.Now, + Message = "Hello World!", +} +channel.TryWrite(doc); +---- + +===== EcsIndexChannel + +A channel that specializes in writing catalog data to Elastic indices. + +We can create an `EcsIndexChannel<>` to push `EcsDocument` (or subclassed) instances. + +[source,csharp] +---- +var options = new IndexChannelOptions(transport) +{ + IndexFormat = "catalog-data-{0:yyyy.MM.dd}", + // BulkOperationIdLookup = c => null, + TimestampLookup = c => c.Timestamp, +}; +var channel = new EcsIndexChannel(options); +---- + +Now we can push data using: + +[source,csharp] +---- +var doc = new CatalogDocument +{ + Created = date, + Title = "Hello World!", + Id = "hello-world" +} +channel.TryWrite(doc); +---- + +This will push data to `catalog-data-2023.01.1` because `TimestampLookup` yields `Timestamp` to `IndexFormat`. + +`IndexFormat` can also simply be a fixed string to write to an Elasticsearch alias/index. + +`BulkOperationIdLookup` determines if the document should be pushed to Elasticsearch using a `create` or `index` operation. + +[float] +=== Bootstrapping target Datastream or Index + +Optionally the target data stream or index can be bootstrapped using the following. + +[source,csharp] +---- +await channel.BootstrapElasticsearchAsync(BootstrapMethod.Failure, "7-days-default"); +---- + +This will bootstrap: + +* Set up component templates for all ECS fieldsets +* reference: {ecs-ref}/ecs-field-reference.html +* templates: https://github.com/elastic/ecs/tree/main/generated/elasticsearch/composable/component +* Create a special `*-settings` component template for the datastream/indices that sets up ILM. +* Set up an {ref}/index-templates.html[index template] for the target data streams or indices. + +If the index template already exists no further bootstrapping will occur. + +Just like `Elastic.Ingest.Elasticsearch` the channel is aware that `logs` and `metrics` have default component templates and ensures the new index tempate references them. \ No newline at end of file diff --git a/docs/data-shippers/serilog.asciidoc b/docs/data-shippers/serilog.asciidoc new file mode 100644 index 00000000..0b02cb95 --- /dev/null +++ b/docs/data-shippers/serilog.asciidoc @@ -0,0 +1,112 @@ +[[serilog-data-shipper]] +=== Elastic.Serilog.Sinks + +A https://serilog.net/[Serilog] sink that writes logs directly to https://www.elastic.co/elasticsearch/[Elasticsearch] or {ess-cloud}[Elastic Cloud] + +==== Installation + +Add a reference to the `Elastic.Serilog.Sinks` package: + +[source,xml] +[subs="attributes"] +---- + +---- + +==== Usage + +There's a few ways that you can extend a `Serilog` `LoggerConfiguration`: + +[source,csharp] +---- +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .Enrich.FromLogContext() + +---- + +*NOTE:* Don't forget we also publish an https://github.com/elastic/ecs-dotnet/blob/main/src/Elastic.Apm.SerilogEnricher/readme.md[`Elastic.Apm.SerilogEnricher`] for the Elastic APM Agent! + +Writing to `Elasticsearch` + +[source,csharp] +---- +.WriteTo.Elasticsearch(new [] { new Uri("http://localhost:9200" )}, opts => +{ + opts.DataStream = new DataStreamName("logs", "console-example", "demo"); + opts.BootstrapMethod = BootstrapMethod.Failure; + opts.ConfigureChannel = channelOpts => + { + channelOpts.BufferOptions = new BufferOptions + { + ConcurrentConsumers = 10 + }; + }; +}) + +---- + +Writing to `Elastic Cloud`: + +[source,csharp] +---- +.WriteTo.ElasticCloud("cloudId", "cloudUser", "cloudPass", opts => +---- + +`opts` is an instance of `ElasticsearchSinkOptions` with the following options + + +==== Configuration + +|=== +|Option |Description + +|`Transport` |An instance of `Elastic.Transport` that dictates where and how wer are communicating to. Defaults to `http://localhost:9200` +|`DataStream` |Where to write data, defaults to the `logs-dotnet-default` datastream. +|`BootstrapMethod` |Wheter the sink should attempt to install component and index templates to ensure the datastream has ECS mappings. Can be be either `None` (the default), `Silent` (attempt but fail silently), `Failure` (attempt and fail with exceptions if bootstrapping fails). +|`TextFormatting` |Allows explicit control of over the `EcsTextFormatterConfiguration` used to emit ECS json documents. See https://github.com/elastic/ecs-dotnet/tree/main/src/Elastic.CommonSchema.Serilog[`Elastic.CommonSchema.Serilog`] for available options. +|`ConfigureChannel` |A callback receiving the `DatastreamChannelOptions` which allows you to control sizing, backpressure etc. See https://github.com/elastic/elastic-ingest-dotnet/blob/main/src/Elastic.Ingest.Elasticsearch/README.md#elasticingestelasticsearch[`Elastic.Ingest.Elasticsearch`] for more information. +|=== + +Note that you can also pass `ElasticsearchSinkOptions` directly + +[source,csharp] +---- +.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(client.Transport)) +---- + +This allows you to reuse the `Transport` used by the Elasticsearch Client for instance. + +==== ECS Aware Message Templates + +This sink by proxy of its formatter allows you to set ECS fields directly from the message template using properties that adhere to the +https://messagetemplates.org/ format. + +The available ECS message template properties are listed under `LogTemplateProperties.*` e.g `LogTemplateProperties.TraceId` + +[source,chsarp] +---- +Log.Information("The time is {TraceId}", "my-trace-id"); +---- + +Will override `trace.id` on the resulting ECS json document. + +==== Comparison with https://github.com/serilog-contrib/serilog-sinks-elasticsearch[`Serilog.Sinks.Elasticsearch`] + +* `Serilog.Sinks.Elasticsearch` is an amazing community led sink that has a ton of options and works against older Elasticsearch versions `< 8.0`. +* `Serilog.Sinks.Elasticsearch` is unofficially supported by Elastic with some of the .NET team helping to maintain it. +* `Elastic.CommonSchema.Serilog.Sink` is *officially* supported by Elastic and was purposely build to adhere to newer best practices around logging, datastreams and ILM. +* `Elastic.CommonSchema.Serilog.Sink` is purposely build to have fewer configuration options and be more prescriptive than `Serilog.Sinks.Elasticsearch`. +* That is not to say there aren't plenty of configuration hooks in `Elastic.CommonSchema.Serilog.Sink` + +===== Notable absent features: + +* `Elastic.CommonSchema.Serilog.Sink` only works with `Elasticsearch 8.x` and up. +* This is because the bootrapping (`BootstrapMethod`) attempts to load templates build for Elasticsearch 8.0 and up. +* `Elastic.CommonSchema.Serilog.Sink` has only one way it emits data to Elasticsearch confirming to the https://github.com/elastic/ecs-logging[ecs-logging specification] +* That doesn't mean you can not introduce your own additional properties though. +* `Elastic.CommonSchema.Serilog.Sink` has no durable mode. +* If you need higher guarantees on log delivery use https://github.com/serilog/serilog-sinks-file[`Serilog.Sinks.File`] with our https://www.nuget.org/packages/Elastic.CommonSchema.Serilog/[ECS log formatter] for Serilog and use https://www.elastic.co/beats/filebeat[filebeat] to ship these logs. +* Check out {fleet-ref}/fleet-overview.html[Elastic Agent and Fleet] to simplify collecting logs and metrics on the edge. + +If you miss a particular feature from `Serilog.Sinks.Elasticsearch` in `Elastic.CommonSchema.Serilog.Sink` please open a https://github.com/elastic/ecs-dotnet/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=%5BFEATURE%5D[feature request]! We'd love to grow this sink organically moving forward. \ No newline at end of file diff --git a/docs/enrichers/apm-nlog.asciidoc b/docs/enrichers/apm-nlog.asciidoc new file mode 100644 index 00000000..07203af7 --- /dev/null +++ b/docs/enrichers/apm-nlog.asciidoc @@ -0,0 +1,63 @@ +[[apm-nlog-enricher]] +=== APM NLog Layout + +Allows you to add the following place holders in your NLog templates: + +* `ElasticApmTraceId` +* `ElasticApmTransactionId` +* `ElasticApmSpanId` +* `ElasticApmServiceName` +* `ElasticApmServiceNodeName` +* `ElasticApmServiceVersion` + +Which will be replaced with the appropriate Elastic APM variables if available + +==== Installation + +Add a reference to the http://nuget.org/packages/Elastic.Apm.NLog[Elastic.Apm.NLog] package: + +[source,xml] +[subs="attributes"] +---- + +---- + +==== Usage + +===== How to use from API + +[source,csharp] +---- +// Logged message will be in format of `trace-id|transation-id|span-id|InTransaction` +// or `|||InTransaction` if the place holders are not available +var consoleTarget = new ConsoleTarget("console"); +consoleTarget.Layout = + "${ElasticApmServiceName}|${ElasticApmTraceId}|${ElasticApmTransactionId}|${ElasticApmSpanId}|${message}"; +config.AddRule(LogLevel.Debug, LogLevel.Fatal, consoleTarget); +LogManager.Configuration = config; +var logger = LogManager.GetCurrentClassLogger(); + +---- + +===== How to use from NLog.config + +[source,xml] +---- + + + + + + + + + + + +---- + +==== Prerequisite + +The prerequisite for this to work is a configured https://github.com/elastic/apm-agent-dotnet[Elastic APM Agent]. If the agent is not configured the APM place holders will be empty. \ No newline at end of file diff --git a/docs/enrichers/apm-serilog.asciidoc b/docs/enrichers/apm-serilog.asciidoc new file mode 100644 index 00000000..a6a38238 --- /dev/null +++ b/docs/enrichers/apm-serilog.asciidoc @@ -0,0 +1,44 @@ +[[apm-serilog-enricher]] +=== APM Serilog Enricher + +This enricher adds the transaction id and trace id to every Serilog log message that is created during a transaction. + +==== Installation + +Add a reference to the http://nuget.org/packages/Elastic.Apm.SerilogEnricher[Elastic.Apm.SerilogEnricher] package: + +[source,xml] +[subs="attributes"] +---- + +---- + +==== Usage + +[source,csharp] +---- +var logger = new LoggerConfiguration() + .Enrich.WithElasticApmCorrelationInfo() + .WriteTo.Console(outputTemplate: "[{ElasticApmTraceId} {ElasticApmTransactionId} {ElasticApmSpanId} {Message:lj} {NewLine}{Exception}") + .CreateLogger(); +---- + +==== Properties + +In the code snippet above `Enrich.WithElasticApmCorrelationInfo()` enables the enricher from this project, +which will set 3 properties for log lines that are created during a transaction: + +* `ElasticApmTraceId` +* `ElasticApmTransactionId` +* `ElasticApmSpanId` + +These two properties are printed to the Console using the `outputTemplate` parameter, of course they can +be used with any sink, you could consider using a filesystem sink and +https://www.elastic.co/downloads/beats/filebeat[Elastic Filebeat] for durable and reliable ingestion. +This enricher is also compatible with the +https://www.nuget.org/packages/Elastic.CommonSchema.Serilog[Elastic.CommonSchema.Serilog] package. + +==== Prerequisite + +The prerequisite for this to work is a configured https://github.com/elastic/apm-agent-dotnet[Elastic APM Agent]. +If the agent is not configured the enricher won't add anything to the logs. \ No newline at end of file diff --git a/docs/enrichers/index.asciidoc b/docs/enrichers/index.asciidoc new file mode 100644 index 00000000..daf0a1fd --- /dev/null +++ b/docs/enrichers/index.asciidoc @@ -0,0 +1,13 @@ +ifdef::env-github[] +NOTE: For the best reading experience, +please view this documentation at https://www.elastic.co/guide/en/ecs-logging/dotnet[elastic.co] +endif::[] + +== Enrichers + +Enrichers can be installed next to `Log Formatters` and `Data Shippers` to automatically +enrich the ECS json that gets produced. + +include::./apm-serilog.asciidoc[Serilog] +include::./apm-nlog.asciidoc[NLog] + diff --git a/docs/formatters/index.asciidoc b/docs/formatters/index.asciidoc new file mode 100644 index 00000000..73f8598e --- /dev/null +++ b/docs/formatters/index.asciidoc @@ -0,0 +1,42 @@ +ifdef::env-github[] +NOTE: For the best reading experience, +please view this documentation at https://www.elastic.co/guide/en/ecs-logging/dotnet[elastic.co] +endif::[] + +== Formatters + +Our log formatters allow you to change the way various logging frameworks +log to IO (file/console) to use ECS json. + +[float] +=== ECS Aware Message Templates + +Our log formatters allow you to set ECS fields directly from the message template using properties that adhere to the +https://messagetemplates.org/ format. + +[source,chsarp] +---- +Log.Information("The time is {TraceId}", "my-trace-id"); +---- + +Will directly override `trace.id` on the resulting ECS json document. + +All supported ECS message template properties are available as constants under the `LogTemplateProperties` static class. +For example `LogTemplateProperties.TraceId` will return `"TraceId"`. + + + +[float] +=== Ingest ECS log files + +If you are using one of our formatter libraries to log to file or stdout/stderr you can use the following options +to get these logs into Elasticsearch or Elastic Cloud: + +NOTE: We also support writing logs directly to Elasticsearch or Elastic Cloud. See our <> + +include::{ecs-repo-dir}/setup.asciidoc[tag=configure-filebeat] + +include::./serilog.asciidoc[Serilog] +include::./nlog.asciidoc[NLog] +include::./log4net.asciidoc[log4net] + diff --git a/docs/formatters/log4net.asciidoc b/docs/formatters/log4net.asciidoc new file mode 100644 index 00000000..28b6a01c --- /dev/null +++ b/docs/formatters/log4net.asciidoc @@ -0,0 +1,120 @@ +[[log4net-formatter]] +=== log4net + +This Layout implementation formats a log4net event into a JSON representation that adheres to the Elastic Common Schema specification. + +==== Installation + +Add a reference to the http://nuget.org/packages/Elastic.CommonSchema.Log4net[Elastic.CommonSchema.Log4net] package: + +[source,xml] +[subs="attributes"] +---- + +---- + +==== Usage + +===== Setup using configuration + +Specify layout type in appender's configuration: + +[source,xml] +---- + + + + + + + + + + +---- + +===== Setup programatically + +[source,csharp] +---- +var hierarchy = (Hierarchy)LogManager.CreateRepository(Guid.NewGuid().ToString()); +var appender = new ConsoleAppender { Layout = new EcsLayout() }; // Use the ECS layout. +hierarchy.Root.AddAppender(appender); +hierarchy.Root.Level = Level.All; +hierarchy.Configured = true; + +---- + +The `Layout = new EcsLayout()` line then instructs log4net to use ECS layout. +The sample above uses the console appender, but you are free to use any appender of your choice, perhaps consider using a +filesystem target and https://www.elastic.co/downloads/beats/filebeat[Elastic Filebeat] for durable and reliable ingestion. + +==== ECS Aware Properties + +Any valid ECS log template properties that is available under `LogTemplateProperties.*` e.g `LogTemplateProperties.TraceId` +is supported and will directly set the appropriate ECS field. + +==== Output + +Apart from {ecs-ref}/ecs-guidelines.html#_general_guidelines[mandatory fields], the output contains additional data: + +* `log.origin.file.name` is taken from `LocationInformation` +* `log.origin.file.line` is taken from `LocationInformation` +* `log.origin.function` is taken from `LocationInformation` +* `event.created` is taken from timestamp +* `event.timezone` is equal to local timezone +* `host.hostname` is taken from `HostName` property +* `process.thread.id` is taken from `ThreadName` if it has numeric value +* `process.thread.name` is taken from `ThreadName` if it doesn't have numeric value +* `service.name` is taken from entry or calling assembly +* `service.version` is taken from entry or calling assembly +* `error.message` is taken from `ExceptionObject` +* `error.type` is taken from `ExceptionObject` +* `error.stacktrace` is taken from `ExceptionObject` +* `metadata` is taken from properties. It also contains message template and arguments in case a formatted message was logged + +Sample log event output (formatted for readability): + +[source,json] +---- +{ + "@timestamp": "2022-08-28T14:06:28.5121651+02:00", + "log.level": "INFO", + "message": "Hi! Welcome to example!", + "metadata": { + "global_property": "Example", + "message_template": "{0}! Welcome to example!" + "0": "Hi" + }, + "ecs": { + "version": "8.3.1" + }, + "event": { + "timezone": "Central European Time", + "created": "2022-08-28T14:06:28.5121651+02:00" + }, + "host": { + "hostname": "HGU780D3" + }, + "log": { + "logger": "Elastic.CommonSchema.Log4net.Example.Program", + "original": null, + "origin": { + "file": { + "name": "C:\\Development\\Elastic.CommonSchema.Log4net.Example\\Program.cs", + "line": 17 + }, + "function": "Main" + } + }, + "process": { + "thread": { + "id": 1 + } + }, + "service": { + "name": "Elastic.CommonSchema.Log4net.Example", + "version": "1.0.0.0" + } +} +---- \ No newline at end of file diff --git a/docs/formatters/nlog.asciidoc b/docs/formatters/nlog.asciidoc new file mode 100644 index 00000000..aefbdf61 --- /dev/null +++ b/docs/formatters/nlog.asciidoc @@ -0,0 +1,174 @@ +[[nlog-formatter]] +=== NLog Layout + +This Layout implementation formats an NLog event into a JSON representation that adheres to the Elastic Common Schema specification. + +==== Installation + +Add a reference to the Elastic.CommonSchema.NLog package: + +[source,xml] +[subs="attributes"] +---- + +---- + +==== Usage + +===== Setup Programatically + +[source,csharp] +---- +Layout.Register("EcsLayout"); // Register the ECS layout. +var config = new LoggingConfiguration(); +var consoleTarget = new ConsoleTarget("console") { Layout = new EcsLayout() }; // Use the ECS layout. +config.AddRule(LogLevel.Debug, LogLevel.Fatal, consoleTarget); +LogManager.Configuration = config; +var logger = LogManager.GetCurrentClassLogger(); + +---- + +In the code snippet above `Layout.Register("EcsLayout")` registers the `EcsLayout` with NLog. +The `Layout = new EcsLayout()` line then instructs NLog to use the registered layout. +The sample above uses the console target, but you are free to use any target of your choice; perhaps consider using a +filesystem target and https://www.elastic.co/downloads/beats/filebeat[Elastic Filebeat] for durable and reliable ingestion. + +===== Setup using NLog.config + +[source,xml] +---- + + + + + + + + + + + + + + + + +---- + +==== EcsLayout Parameter Options + +* *Metadata Options* +* _IncludeEventProperties_ - Include LogEvent properties as metadata. Default: `true` +* _IncludeScopeProperties_ - Include NLog Scope Context Properties as metadata. Default: `false` +* _ExcludeProperties_ - Comma separated string with names which properties to exclude. +* *Event Options* + +* _EventAction_ - +* _EventCategory_ - +* _EventId_ - +* _EventKind_ - +* _EventSeverity_ - +* *Agent Options* + +* _AgentId_ - +* _AgentName_ - +* _AgentType_ - +* _AgentVersion_ - +* *Process Options* + +* _ProcessExecutable_ - Default: `${processname:FullName=true}` +* _ProcessId_ - Default: `${processid}` +* _ProcessName_ - Default: `${processname:FullName=false}` +* _ProcessThreadId_ - Default: `${threadid}` +* _ProcessTitle_ - Default: `${processinfo:MainWindowTitle}` +* *Server Options* + +* _ServerAddress_ - +* _ServerIp_ - +* _ServerUser_ - Default: `${environment-user}` +* *Host Options* + +* _HostId_ - +* _HostIp_ - Default: `${local-ip:cachedSeconds=60}` +* _HostName_ - Default: `${machinename}` +* *Log Origin Options* + +* _LogOriginCallSiteMethod_ - Default: `${exception:format=method}` +* _LogOriginCallSiteFile_ - Default: `${exception:format=source}` +* _LogOriginCallSiteLine_ - +* *Http Options* + +* _HttpRequestId_ - Default: `${aspnet-trace-identifier}` +* _HttpRequestMethod_ - Default: `${aspnet-request-method}` +* _HttpRequestBytes_ - Default: `${aspnet-request-contentlength}` +* _HttpRequestReferrer_ - Default: `${aspnet-request-referrer}` +* _HttpResponseStatusCode_ - Default: `${aspnet-response-statuscode}` +* *Url Options* + +* _UrlScheme_ - Default: `${aspnet-request-url:IncludeScheme=true:IncludeHost=false:IncludePath=false}` +* _UrlDomain_ - Default: `${aspnet-request-url:IncludeScheme=false:IncludeHost=true:IncludePath=false}` +* _UrlPath_ - Default: `${aspnet-request-url:IncludeScheme=false:IncludeHost=false:IncludePath=true}` +* _UrlPort_ - Default: `${aspnet-request-url:IncludeScheme=false:IncludeHost=false:IncludePath=false:IncludePort=true}` +* _UrlQuery_ - Default: `${aspnet-request-url:IncludeScheme=false:IncludeHost=false:IncludePath=false:IncludeQueryString=true}` +* _UrlUserName_ - Default: `${aspnet-user-identity}` +* *Trace Options* + +* _ApmTraceId_ - Default: `${ElasticApmTraceId}` +* *Transaction Options* + +* _ApmTransactionId_ - Default: `${ElasticApmTransactionId}` +* + +==== ECS Aware Message Templates + +Additionally any valid ECS log template properties that is available under `LogTemplateProperties.*` e.g `LogTemplateProperties.TraceId` +is supported and will directly set the appropriate ECS fields. + +[source,chsarp] +---- +logger.Info("The time is {TraceId}", "my-trace-id"); +---- + +Will override `trace.id` on the resulting ECS json document. + +==== Example output from EcsLayout + +An example of the output is given below: + +[source,json] +---- +{ + "@timestamp":"2020-02-20T16:07:06.7109766+11:00", + "log.level":"Info", + "message":"Info \"X\" 2.2", + "metadata":{ + "value_x":"X", + "some_y":2.2 + }, + "ecs":{ + "version":"1.4.0" + }, + "event":{ + "severity":6, + "timezone":"AUS Eastern Standard Time", + "created":"2020-02-20T16:07:06.7109766+11:00" + }, + "host":{ + "name":"LAPTOP" + }, + "log":{ + "logger":"Elastic.CommonSchema.NLog", + "original":"Info {ValueX} {SomeY}" + }, + "process":{ + "thread":{ + "id":17592 + }, + "pid":17592, + "name":"dotnet", + "executable":"C:\\Program Files\\dotnet\\dotnet.exe" + } +} +---- diff --git a/docs/formatters/serilog.asciidoc b/docs/formatters/serilog.asciidoc new file mode 100644 index 00000000..46ea6bf5 --- /dev/null +++ b/docs/formatters/serilog.asciidoc @@ -0,0 +1,99 @@ +[[serilog-formatter]] +=== Serilog formatter + +This `ITextFormatter` implementation formats a Serilog event into a JSON representation that adheres to the Elastic Common Schema specification. + +==== Installation + +Add a reference to the http://nuget.org/packages/Elastic.CommonSchema.Serilog[Elastic.CommonSchema.Serilog] package: + +[source,xml] +[subs="attributes"] +---- + +---- + +==== Usage + +[source,csharp] +---- +var logger = new LoggerConfiguration() + .WriteTo.Console(new EcsTextFormatter()) + .CreateLogger(); + +---- + +In the code snippet above `new EcsTextFormatter()` enables the text formatter and instructs Serilog to format the event as JSON. The sample above uses the Console sink, but you are free to use any sink of your choice, perhaps consider using a filesystem sink and https://www.elastic.co/downloads/beats/filebeat[Elastic Filebeat] for durable and reliable ingestion. + +In ASP.NET (core) applications + +[source,csharp] +---- +.UseSerilog((ctx, config) => +{ + // Ensure HttpContextAccessor is accessible + var httpAccessor = ctx.Configuration.Get(); + + config + .ReadFrom.Configuration(ctx.Configuration) + .Enrich.WithEcsHttpContext(httpAccessor) + .WriteTo.Async(a => a.Console(new EcsTextFormatter())); +}) + +---- + +The `WithEcsHttpContext` ensures logs will be enriched with `HttpContext` data. + +An example of the output is given below: + +[source,json] +---- +{ + "@timestamp": "2019-11-22T14:59:02.5903135+11:00", + "log.level": "Information", + "message": "Log message", + "ecs": { + "version": "1.4.0" + }, + "event": { + "severity": 0, + "timezone": "AUS Eastern Standard Time", + "created": "2019-11-22T14:59:02.5903135+11:00" + }, + "log": { + "logger": "Elastic.CommonSchema.Serilog" + }, + "process": { + "thread": { + "id": 1 + }, + "executable": "System.Threading.ExecutionContext" + } +} + +---- + +==== Configuration + +|=== +|Option |Description + +|`MapCurrentThead` | `true` map `ecs.process` by looking up the `Process` from the current thread +|`MapHttpAdapter` | `null` a way to map `HttpContextAccessor` to ECS fields. +|`LogEventsPropertiesToFilter` | A `Set` of properties that should not be emitted as `labels.*` or `metadata.*` +|`MapCustom` | A Func that allows you to mutate the EcsDocument before its fully converted. +|=== + +==== ECS Aware Message Templates + +This formatter also allows you to set ECS fields directly from the message template using properties that adhere to the +https://messagetemplates.org/ format. + +The available ECS message template properties are listed under `LogTemplateProperties.*` e.g `LogTemplateProperties.TraceId` + +[source,chsarp] +---- +Log.Information("The time is {TraceId}", "my-trace-id"); +---- + +Will override `trace.id` on the resulting ECS json document. diff --git a/docs/images/dotnet-overview.png b/docs/images/dotnet-overview.png new file mode 100644 index 00000000..d2d1c189 Binary files /dev/null and b/docs/images/dotnet-overview.png differ diff --git a/docs/images/ecs-dotnet-overview.png b/docs/images/ecs-dotnet-overview.png new file mode 100644 index 00000000..db9389ed Binary files /dev/null and b/docs/images/ecs-dotnet-overview.png differ diff --git a/docs/index.asciidoc b/docs/index.asciidoc index fa8ff34a..b6b96f5d 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -1,5 +1,5 @@ :ecs-repo-dir: {ecs-logging-root}/docs/ -:ecs-logging-dotnet.version: 8.4.0-alpha1 +:ecs-logging-dotnet-version: 8.6.0 include::{asciidoc-dir}/../../shared/versions/stack/current.asciidoc[] include::{asciidoc-dir}/../../shared/attributes.asciidoc[] @@ -14,4 +14,8 @@ endif::[] ifndef::env-github[] include::./intro.asciidoc[Introduction] include::./setup.asciidoc[Set up] +include::./types/types.asciidoc[Types] +include::./formatters/index.asciidoc[Formatters] +include::./data-shippers/index.asciidoc[Data Shippers] +include::./enrichers/index.asciidoc[Enrichers] endif::[] \ No newline at end of file diff --git a/docs/intro.asciidoc b/docs/intro.asciidoc index 5662eae7..65baaf34 100644 --- a/docs/intro.asciidoc +++ b/docs/intro.asciidoc @@ -7,4 +7,73 @@ They make it easy to format your logs into ECS-compatible JSON. TIP: Want to learn more about ECS, ECS logging, and other available language plugins? See the {ecs-logging-ref}/intro.html[ECS logging overview]. -Ready to jump into `ecs-logging-dotnet`? <>. +The .NET ECS libraries fall into several categories: + +[float] +=== Model + +At its core all .NET ECS libraries are powered by `Elastic.CommonSchema` +which strongly types the ECS specification to .NET classes. + +This library can be used to map your events to ECS in a typesafe fashion. + +[source,csharp] +---- +var doc = EcsDocument.CreateNewWithDefaults(); +doc = new EcsDocument(); +---- + +<>. + +[float] +=== Log Formatters + +Our log formatters allow you to change the way various logging frameworks +log to IO (file/console) to use ECS json. + +* <> +* <> +* <> + +[float] +=== Data Shippers + +Our datashippers integrate with logging frameworks to facilitate +sending events (logs) to various off Elastic receivers. + +Currently these shippers support Elastic Cloud & Elasticsearch but +other outputs are in the works. + +* <> +* <> +* <> + +All the data shippers utilize <> to send events to Elasticsearch. + +[float] +=== Enrichers + +Enrichers can be installed next to `Log Formatters` and `Data Shippers` to automatically +enrich the ECS json that gets produced. + +* <> +* <> + +[float] +=== Architecture + +The libraries reuse the same components that power other .NET libraries from Elastic--ensuring a common way to both configure and monitor Elastic's .NET libraries. + +image:images/ecs-dotnet-overview.png["ECS.NET Artichtecture overview"] + +At its core all data shippers depend on https://github.com/elastic/elastic-transport-net[Elastic.Transport] to +coordinate HttpRequests. This library is also the heart of Elastic's clients and ensures +we share best practices and configuration options. + +The data shipper libraries all depend upon the push based ingestion +abstractions from https://github.com/elastic/elastic-ingest-dotnet[Elastic.Ingest.*] that +presents an easy to use `System.Threading.Channels` backed method to push events as batches at variable rates to external datasources. + +<>. + + diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc index ea734c8b..2d0d058f 100644 --- a/docs/setup.asciidoc +++ b/docs/setup.asciidoc @@ -5,20 +5,20 @@ [[setup-step-1]] === Step 1: Configure application logging -The following logging frameworks are supported: +If you want to integrate with an existing logger emitting ECS json to a file or stdout/stderr. -* Serilog -* NLog +Choose one of our formatters: -[float] -==== Add the dependency +* <> +* <> +* <> -include::./tab-widgets/add-dependency-widget.asciidoc[] +If you want to write the logs directly to one of Elastic's endpoints (e.g Elastic Cloud / Elasticsearch) -[float] -==== Use the ECS integration +Choose one of our data shipping loggers: -include::./tab-widgets/ecs-integration-widget.asciidoc[] +* <> +* <> [float] [[setup-step-2]] @@ -27,8 +27,15 @@ If you are using the Elastic APM .NET agent, {apm-dotnet-ref}/log-correlation.html[log correlation can be configured] to inject trace, transaction and span id fields into log events. +By default the ECS logging integrations will read tracing information from +https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activity?view=net-7.0[System.Diagnostics.Activity] +if the APM logging corrolation libraries are not installed. + [float] [[setup-step-3]] -=== Step 3: Configure Filebeat +=== Step 3: Configure Filebeat (optional) + +If you are using one of our log formatters you can use the following methods +to ship these logs to Elastic. include::{ecs-repo-dir}/setup.asciidoc[tag=configure-filebeat] diff --git a/docs/tab-widgets/add-dependency-widget.asciidoc b/docs/tab-widgets/add-dependency-widget.asciidoc deleted file mode 100644 index 20df2d7c..00000000 --- a/docs/tab-widgets/add-dependency-widget.asciidoc +++ /dev/null @@ -1,40 +0,0 @@ -++++ -
-
- - -
-
-++++ - -include::add-dependency.asciidoc[tag=serilog] - -++++ -
- -
-++++ \ No newline at end of file diff --git a/docs/tab-widgets/add-dependency.asciidoc b/docs/tab-widgets/add-dependency.asciidoc deleted file mode 100644 index f2655770..00000000 --- a/docs/tab-widgets/add-dependency.asciidoc +++ /dev/null @@ -1,135 +0,0 @@ -// tag::serilog[] -The following target frameworks are supported - -|=== -| Name | Target Framework Moniker (TFM) - -| .NET Framework 4.6.1 -| `net461` - -| .NET Standard 2.0 -| `netstandard2.0` - -| .NET Standard 2.1 -| `netstandard2.1` -|=== - -The minimum required Serilog version is 2.9.0. - -Add a dependency to your application project file: -[source,xml] -[subs="attributes"] ----- - - - - - - ----- - -or install with the .NET CLI -[source,sh] -[subs="attributes"] ----- -dotnet add package Elastic.CommonSchema.Serilog --version ${ecs-logging-dotnet.version} ----- - -or nuget CLI -[source,sh] -[subs="attributes"] ----- -Install-Package Elastic.CommonSchema.Serilog -Version ${ecs-logging-dotnet.version} ----- - -// end::serilog[] - -// tag::nlog[] -The following target frameworks are supported - -|=== -| Name | Target Framework Moniker (TFM) - -| .NET Framework 4.6.1 -| `net461` - -| .NET Standard 2.0 -| `netstandard2.0` - -| .NET Standard 2.1 -| `netstandard2.1` -|=== - -The minimum required NLog version is 4.5.4. - -Add a dependency to your application project file: -[source,xml] -[subs="attributes"] ----- - - - - - - ----- - -or install with the .NET CLI -[source,sh] -[subs="attributes"] ----- -dotnet add package Elastic.CommonSchema.NLog --version ${ecs-logging-dotnet.version} ----- - -or nuget CLI -[source,sh] -[subs="attributes"] ----- -Install-Package Elastic.CommonSchema.NLog -Version ${ecs-logging-dotnet.version} ----- -// end::nlog[] - -// tag::log4net[] -The following target frameworks are supported - -|=== -| Name | Target Framework Moniker (TFM) - -| .NET Framework 4.6.1 -| `net461` - -| .NET Standard 2.0 -| `netstandard2.0` - -| .NET Standard 2.1 -| `netstandard2.1` -|=== - -The minimum required log4net version is 2.0.15. - -Add a dependency to your application project file: -[source,xml] -[subs="attributes"] ----- - - - - - - ----- - -or install with the .NET CLI -[source,sh] -[subs="attributes"] ----- -dotnet add package Elastic.CommonSchema.Log4net --version ${ecs-logging-dotnet.version} ----- - -or nuget CLI -[source,sh] -[subs="attributes"] ----- -Install-Package Elastic.CommonSchema.Log4net -Version ${ecs-logging-dotnet.version} ----- -// end::log4net[] diff --git a/docs/tab-widgets/ecs-integration-widget.asciidoc b/docs/tab-widgets/ecs-integration-widget.asciidoc deleted file mode 100644 index fc63d675..00000000 --- a/docs/tab-widgets/ecs-integration-widget.asciidoc +++ /dev/null @@ -1,40 +0,0 @@ -++++ -
-
- - -
-
-++++ - -include::ecs-integration.asciidoc[tag=serilog] - -++++ -
- -
-++++ \ No newline at end of file diff --git a/docs/tab-widgets/ecs-integration.asciidoc b/docs/tab-widgets/ecs-integration.asciidoc deleted file mode 100644 index d7824b76..00000000 --- a/docs/tab-widgets/ecs-integration.asciidoc +++ /dev/null @@ -1,309 +0,0 @@ -// tag::serilog[] -**Serilog Text Formatter** - -`EcsTextFormatter` is an `ITextFormatter` implementation in -Elastic.CommonSchema.Serilog that formats Serilog events into -a JSON representation that adheres to the Elastic Common Schema specification. - -It can be configured in conjunction with a Serilog Sink. To configure -with the file sink for example, first install -https://www.nuget.org/packages/Serilog.Sinks.File/[Serilog.Sinks.File] nuget -package - -[source,sh] ----- -Install-Package Serilog.Sinks.File ----- - -Then configure the file sink to use `EcsTextFormatter`: - -[source,csharp] ----- -var logger = new LoggerConfiguration() - .WriteTo.File(new EcsTextFormatter(), "/path/to/log.txt") - .CreateLogger(); ----- - -Now, when logging events - -[source,csharp] ----- -logger.Information("Checkout contains {ItemCount} items", 12); <1> ----- -<1> log a message with the information level - -Each Serilog log event will be formatted as single line JSON in the file -`/path/to/log.txt`. - -// end::serilog[] - -// tag::nlog[] -**NLog Layout** - -`EcsLayout` is a `Layout` implementation in Elastic.CommonSchema.NLog that -formats an NLog event into a JSON representation that adheres to the -Elastic Common Schema specification. - -It can be configured as the layout of an NLog Target. To configure -with the file target for example, using code configuration: - -[source, csharp] ----- -var config = new LoggingConfiguration(); -var fileTarget = new FileTarget() -{ - Layout = new EcsLayout(), <1> - FileName = "/path/to/log.txt" -}; -config.AddRule(LogLevel.Info, LogLevel.Fatal, fileTarget); -LogManager.Configuration = config; -var logger = LogManager.GetCurrentClassLogger(); ----- -<1> Create a new instance of ECS layout - -In addition to code configuration, `EcsLayout` can be configured using -XML configuration in https://github.com/nlog/NLog/wiki/Configuration-file[`NLog.config`]: - -[source,xml] ----- - - - - - - - - - - - - - - ----- - -Now, when logging events: - -[source,csharp] ----- -logger.Info("Checkout contains {ItemCount} items", 12); <1> ----- -<1> log a message with the information level - -Each NLog log event will be formatted as single line JSON in the file -`/path/to/log.txt`. - -**EcsLayout Parameter Options** - -The following properties determine which properties should be included -or excluded as metadata of an event - -|==== -| Parameter name | Type | Default | Description - -| `IncludeAllProperties` -| bool -| `true` -| Include LogEvent properties as metadata - -| `IncludeMdlc` -| bool -| `false` -| Include NLog Scope Context Properties as metadata - -| `ExcludeProperties` -| string -| -| Comma separated string of properties to exclude from metadata -|==== - -The following are NLog `Layout` properties used to capture -information for an event - -|==== -| Parameter name | Type | Default | Description - -| `EventAction` -| string -| -| The action captured by the event - -| `EventCategory` -| string -| -| The category of the event. - -| `EventId` -| string -| -| The unique identifier for the event - -| `EventKind` -| string -| -| The kind of event. High level information about what kind of information the event contains - -| `EventSeverity` -| long -| _NLog level_ - -* Trace, Debug:: 7 -* Info:: 6 -* Warn:: 4 -* Error:: 3 -* Fatal:: 2 - -| The severity of the event - -| `AgentId` -| string -| -| The id of the agent collecting events - -| `AgentName` -| string -| -| The name of the agent collecting events - -| `AgentType` -| string -| -| The type of the agent collecting events - -| `AgentVersion` -| string -| -| The version of the agent collecting events - -| `ProcessExecutable` -| string -| `${processname:FullName=true}` -| Absolute path to the process executable - -| `ProcessId` -| long -| `${processid}` -| The process id (pid) - -| `ProcessName` -| string -| `${processname:FullName=false}` -| The name of the process - -| `ProcessThreadId` -| long -| `${threadid}` -| The id of the process thread - -| `ProcessTitle` -| string -| `${processinfo:MainWindowTitle}` -| The title of the process - -| `ServerAddress` -| string -| -| The address of the server - -| `ServerIp` -| string -| -| The IP address of the server - -| `ServerUser` -| string -| `${environment-user}` -| Information about the user that is relevant to the event - -| `HostId` -| string -| -| Unique host id. Hostnames are not always unique, so use an id that is meaningful in your environment - -| `HostIp` -| -| `${local-ip:cachedSeconds=60}` _(requires NLog 4.6.8+)_ -| The IP address of the host - -| `HostName` -| string -| `${machinename}` -| The name of the host - -| `LogOriginCallSiteMethod` -| string -| `${exception:format=method}` -| The name of the function or method which originated the event - -| `LogOriginCallSiteFile` -| string -| `${exception:format=source}` -| The file containing the source code which originated the event - -| `LogOriginCallSiteLine` -| string -| -| The line number of the file containing the source code which originated the event - -|==== - -// end::nlog[] - -// tag::log4net[] -// These docs are currently not included in the documentation. -// See this comment for details: https://github.com/elastic/sdh-apm/issues/759#issuecomment-1299789675. -// To readd these docs, update the widget to include them. - -**log4net Layout** - -`EcsLayout` is a `ILayout` implementation in Elastic.CommonSchema.Log4net that -formats a log4net event into a JSON representation that adheres to the -Elastic Common Schema specification. - -It can be configured as the layout of a log4net appender. To configure -with the file appender for example, using code configuration: - -[source, csharp] ----- -var repositoryId = Guid.NewGuid().ToString(); -var hierarchy = (Hierarchy)LogManager.CreateRepository(repositoryId); -var appender = new FileAppender -{ - Layout = new EcsLayout(), <1> - File = "/path/to/log.txt" -}; -hierarchy.Root.AddAppender(appender); -hierarchy.Root.Level = Level.All; -hierarchy.Configured = true; -var logger = LogManager.GetLogger(repositoryId, GetType().Name); ----- -<1> Create a new instance of ECS layout - -In addition to code configuration, `EcsLayout` can be configured using -https://logging.apache.org/log4net/release/manual/configuration.html[`XML configuration`]: - -[source,xml] ----- - - - - - - - - - ----- - -Now, when logging events: - -[source,csharp] ----- -logger.InfoFormat("Checkout contains {0} items", 12); <1> ----- -<1> log a message with the information level - -Each log4net log event will be formatted as single line JSON in the file -`/path/to/log.txt`. - -// end::log4net[] \ No newline at end of file diff --git a/docs/types/types.asciidoc b/docs/types/types.asciidoc new file mode 100644 index 00000000..ae5e75fc --- /dev/null +++ b/docs/types/types.asciidoc @@ -0,0 +1,183 @@ +[[ecs-dotnet]] +== .NET Model of ECS + +The `Elastic.CommonSchema` project contains a full C# representation of the https://github.com/elastic/ecs[Elastic Common Schema (ECS)]. +The intention of this library is to form a reliable and correct basis for integrating into Elasticsearch, using both +Microsoft .NET and ECS. + +These types can be used either as-is or in conjunction with the https://github.com/elastic/elasticsearch-net[Official .NET clients for Elasticsearch]. The types are annotated with the corresponding `DataMember` attributes, enabling out-of-the-box serialization support with the Elasticsearch.net clients. + +[float] +=== Installation + +Add a reference to the Elastic.CommonSchema package: + +[source,xml] +[subs="attributes"] +---- + +---- + +TIP: Use https://github.com/elastic/ecs-dotnet/tree/main/src/Elastic.Ingest.Elasticsearch.CommonSchema[Elastic.Ingest.Elasticsearch.CommonSchema] to easily persist ECS documents to Elasticsearch or Elastic Cloud. + + +[float] +==== Versioning + +The version of the Elastic.CommonSchema package matches the published ECS version, with the same corresponding branch names: + +* Nested Schema (The C# types are generated from this YAML file): https://github.com/elastic/ecs/blob/v1.4.0/generated/ecs/ecs_nested.yml +* .NET types: https://github.com/elastic/ecs-dotnet/tree/v1.4.0 + +The version numbers of the NuGet package must match the exact version of ECS used within Elasticsearch. Attempting to use mismatched versions, for example a NuGet package with version 1.2.0 against an Elasticsearch index configured to use an ECS template with version 1.1.0, will result in indexing and data problems. + +=== Usage + +==== Creating an ECS event + +The recommended way to create instances of `EcsDocument` is through: + +[source,csharp] +---- +var doc = EcsDocument.CreateNewWithDefaults(); +---- + +This will automatically assign most common ECS fields that can be inferred from the running process. + +However there is no requirement to do so, simply creating a new `EcsDocument` instance directly +is completely valid and supported. + +[source,csharp] +---- +var ecsDocument = new EcsDocument +{ + Timestamp = DateTimeOffset.Parse("2019-10-23T19:44:38.485Z"), + Dns = new Dns + { + Id = "23666", + OpCode = "QUERY", + Type = "answer", + QuestionName = "www.example.com", + QuestionType = "A", + QuestionClass = "IN", + QuestionRegisteredDomain = "example.com", + HeaderFlags = new[] { "RD", "RA" }, + ResponseCode = "NOERROR", + ResolvedIp = new[] { "10.0.190.47", "10.0.190.117" }, + Answers = new[] + { + new DnsAnswers + { + Data = "10.0.190.47", + Name = "www.example.com", + Type = "A", + Class = "IN", + Ttl = 59 + }, + new DnsAnswers + { + Data = "10.0.190.117", + Name = "www.example.com", + Type = "A", + Class = "IN", + Ttl = 59 + } + } + }, + Network = new Network + { + Type = "ipv4", + Transport = "udp", + Protocol = "dns", + Direction = "outbound", + CommunityId = "1:19beef+RWVW9+BEEF/Q45VFU+2Y=", + Bytes = 126 + }, + Source = new Source { Ip = "192.168.86.26", Port = 5785, Bytes = 31 }, + Destination = new Destination { Ip = "8.8.4.4", Port = 53, Bytes = 95 }, + Client = new Client { Ip = "192.168.86.26", Port = 5785, Bytes = 31 }, + Server = new Server { Ip = "8.8.4.4", Port = 53, Bytes = 95 }, + Event = new Event + { + Duration = 122433000, + Start = DateTimeOffset.Parse("2019-10-23T19:44:38.485Z"), + End = DateTimeOffset.Parse("2019-10-23T19:44:38.607Z"), + Kind = "event", + Category = new[] { "network_traffic" } + }, + Ecs = new Ecs { Version = "1.2.0" }, + Metadata = new Dictionary { { "client", "ecs-dotnet" } } +}; + +---- + +[float] +=== Dynamically assign ECS fields + +Additionally, ECS fields can be dynamically assigned through + +[source,csharp] +---- +ecsDocument.AssignProperty("orchestrator.cluster.id", "id"); + +---- + +This will assign `ecsDocument.Orchestrator.ClusterId` to `"id"` and automatically create a new `Orchestrator` instance if needed. + +Any `string` or `boolean` value that is not a known `ecs` field will be assigned to `labels.*` and everything else to `metatadata.*` + +[id=intro_to_xyz,titleabbrev=" XYZ Intro"] +=== A note on the `Metadata` property + +The C# `EcsDocument` type includes a property called `Metadata` with the signature: + +[source,csharp] +---- +/// +/// Container for additional metadata against this event. +/// +[JsonPropertyName("metadata"), DataMember(Name = "metadata")] +public IDictionary Metadata { get; set; } +---- + +This property is not part of the ECS specification, but is included as a means to index supplementary information. + +=== Extending EcsDocument + +In instances where using the `IDictionary Metadata` property is not sufficient, or there is a clearer definition of the structure of the ECS-compatible document you would like to index, it is possible to subclass the `EcsDocument` object and provide your own property definitions. + +Through `TryRead`/`ReceiveProperty`/`WriteAdditionalProperties` you can hook into the `EcsDocumentJsonConverter` and read/write additional properties. + +[source,csharp] +---- +/// +/// An extended ECS document with an additional property +/// +[JsonConverter(typeof(EcsDocumentJsonConverterFactory))] +public class MyEcsDocument : EcsDocument +{ + [JsonPropertyName("my_root_property"), DataMember(Name = "my_root_property")] + public MyCustomType MyRootProperty { get; set; } + + protected override bool TryRead(string propertyName, out Type type) + { + type = propertyName switch + { + "my_root_property" => typeof(MyCustomType), + _ => null + }; + return type != null; + } + + protected override bool ReceiveProperty(string propertyName, object value) => + propertyName switch + { + "my_root_property" => null != (MyRootProperty = value as MyCustomType), + _ => false + }; + + protected override void WriteAdditionalProperties(Action write) => write("my_root_property", MyCustomType); +} +---- + +The Elastic.CommonSchema.BenchmarkDotNetExporter project takes this approach in the https://github.com/elastic/ecs-dotnet/tree/main/src/Elastic.CommonSchema.BenchmarkDotNetExporter[Domain source directory], where the BenchmarkDocument subclasses EcsDocument. diff --git a/src/Elastic.CommonSchema.Serilog/Elastic.CommonSchema.Serilog.csproj b/src/Elastic.CommonSchema.Serilog/Elastic.CommonSchema.Serilog.csproj index 80396092..0706c585 100644 --- a/src/Elastic.CommonSchema.Serilog/Elastic.CommonSchema.Serilog.csproj +++ b/src/Elastic.CommonSchema.Serilog/Elastic.CommonSchema.Serilog.csproj @@ -1,26 +1,29 @@ + - - netstandard2.0;netstandard2.1;net461 - Elastic Common Schema (ECS) Serilog Formatter - Serilog TextFormatter that formats log events in accordance with Elastic Common Schema (ECS). - True - - - - - - + + netstandard2.0;netstandard2.1;net461 + Elastic Common Schema (ECS) Serilog Formatter + Serilog TextFormatter that formats log events in accordance with Elastic Common Schema (ECS). + True + + + + + + UAParser.regexes.yaml - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Elastic.CommonSchema/EcsDocument.cs b/src/Elastic.CommonSchema/EcsDocument.cs index 44881e57..57ae1d63 100644 --- a/src/Elastic.CommonSchema/EcsDocument.cs +++ b/src/Elastic.CommonSchema/EcsDocument.cs @@ -51,7 +51,10 @@ public static TEcsDocument CreateNewWithDefaults( { var doc = new TEcsDocument { - Timestamp = timestamp ?? DateTimeOffset.UtcNow, Ecs = EcsFieldDefault, Error = GetError(exception), Service = GetService() + Timestamp = timestamp ?? DateTimeOffset.UtcNow, + Ecs = EcsFieldDefault, + Error = GetError(exception), + Service = GetService() }; SetActivityData(doc);