From 651f72bf0ad167328adbe62b0340b69795084b43 Mon Sep 17 00:00:00 2001 From: Karol Zadora-Przylecki Date: Mon, 28 Dec 2020 18:46:59 -0800 Subject: [PATCH] Add ActivitySourceInput Fixes issue https://github.com/Azure/diagnostics-eventflow/issues/364 Includes re-organization of the README file to make navigation easier --- README.md | 264 +++++- Warsaw.sln | 15 +- .../DiagnosticPipelineFactory.cs | 1 + .../Implementations}/StringBuilderCache.cs | 4 +- ...icrosoft.Diagnostics.EventFlow.Core.csproj | 2 +- .../ActivityPathDecoder.cs | 2 + ....Diagnostics.EventFlow.EtwUtilities.csproj | 3 +- .../ActivitySourceInput.cs | 260 ++++++ .../ActivitySourceInputFactory.cs | 17 + .../ActivitySourceConfiguration.cs | 42 + .../ActivitySourceInputConfiguration.cs | 22 + ...ics.EventFlow.Inputs.ActivitySource.csproj | 31 + .../Properties/AssemblyInfo.cs | 27 + ...osoft.Diagnostics.EventFlow.Signing.csproj | 6 +- .../DiagnosticsPipelineFactoryTests.cs | 66 +- .../MetadataTests.cs | 4 +- ...ft.Diagnostics.EventFlow.Core.Tests.csproj | 6 +- .../ActivitySourceInputTests.cs | 851 ++++++++++++++++++ .../EtwInputTests.cs | 22 - .../EventSourceInputTests.cs | 2 +- ....Diagnostics.EventFlow.Inputs.Tests.csproj | 6 +- .../FirstChanceExceptionCounter.cs | 11 +- test/TestHelpers/TestObserver.cs | 32 + 23 files changed, 1607 insertions(+), 89 deletions(-) rename src/{Microsoft.Diagnostics.EventFlow.EtwUtilities => Microsoft.Diagnostics.EventFlow.Core/Implementations}/StringBuilderCache.cs (97%) create mode 100644 src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/ActivitySourceInput.cs create mode 100644 src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/ActivitySourceInputFactory.cs create mode 100644 src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Configuration/ActivitySourceConfiguration.cs create mode 100644 src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Configuration/ActivitySourceInputConfiguration.cs create mode 100644 src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource.csproj create mode 100644 src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/ActivitySourceInputTests.cs create mode 100644 test/TestHelpers/TestObserver.cs diff --git a/README.md b/README.md index 0ff7c8e9..1176cd1b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,22 @@ ## Introduction The EventFlow library suite allows applications to define what diagnostics data to collect, and where they should be outputted to. Diagnostics data can be anything from performance counters to application traces. -It runs in the same process as the application, so communication overhead is minimized. It also has an extensibility mechanism so additional inputs and outputs can be created and plugged into the framework. It comes with the following inputs and outputs: +It runs in the same process as the application, so communication overhead is minimized. It also has an extensibility mechanism so additional inputs and outputs can be created and plugged into the framework. -*Inputs* +The EventFlow suite supports .NET applications and .NET Core applications. It allows diagnostic data to be collected and transferred for applications running in these Azure environments: + +- Azure Web Apps +- Service Fabric +- Azure Cloud Service +- Azure Virtual Machines + +The core of the library, as well as inputs and outputs listed above [are available as NuGet packages](https://www.nuget.org/packages?q=Microsoft.Diagnostics.EventFlow). + +## Topics + +[**Getting started**](#getting-started) + +[**Inputs**](#inputs) - [Trace (a.k.a. System.Diagnostics.Trace)](#trace) - [EventSource](#eventsource) - [PerformanceCounter](#performancecounter) @@ -15,8 +28,9 @@ It runs in the same process as the application, so communication overhead is min - [Log4net](#log4net) - [NLog](#nlog) - [DiagnosticSource](#diagnosticsource) +- [ActivitySource](#activitysource) -*Outputs* +[**Outputs**](#outputs) - [StdOutput (console output)](#stdoutput) - [HTTP (json via http)](#http) - [Application Insights](#application-insights) @@ -30,16 +44,31 @@ There are several EventFlow extensions available from non-Microsoft authors and - [ReflectInsight output](https://github.com/reflectsoftware/Microsoft.Diagnostics.EventFlow.Outputs.ReflectInsight) - [Splunk output](https://github.com/hortha/diagnostics-eventflow-splunk) -The EventFlow suite supports .NET applications and .NET Core applications. It allows diagnostic data to be collected and transferred for applications running in these Azure environments: +[**Filters**](#filters) -- Azure Web Apps -- Service Fabric -- Azure Cloud Service -- Azure Virtual Machines +[**Standard metatadata types**](#standard-metadata-types) -The core of the library, as well as inputs and outputs listed above [are available as NuGet packages](https://www.nuget.org/packages?q=Microsoft.Diagnostics.EventFlow). +[**Health reporter**](#health-reporter) + +[**Pipeline settings**](#pipeline-settings) + +[**Service Fabric support**](#service-fabric-support) + +[**Filter expressions**](#filter-expressions) + +[**Secret storage**](#store-secrets-securely) + +[**Extensibility**](#extensibility) + +[**Troubleshooting**](#troubleshooting) + +[**Platform support**](#platform-support) + +[**Contributing to EventFlow**](#contributions) ## Getting Started +The EventFlow pipeline is built around three core concepts: [inputs](#inputs), [outputs](#outputs), and [filters](#filters). The number of inputs, outputs, and filters depend on the need of diagnostics. The configuration also has a healthReporter and settings section for configuring settings fundamental to the pipeline operation. Finally, the extensions section allows declaration of custom developed plugins. These extension declarations act like references. On pipeline initialization, EventFlow will search extensions to instantiate custom inputs, outputs, or filters. + 1. To quickly get started, you can create a simple console application in VisualStudio and install the following Nuget packages: * Microsoft.Diagnostics.EventFlow.Inputs.Trace * Microsoft.Diagnostics.EventFlow.Outputs.ApplicationInsights @@ -94,15 +123,12 @@ Note: if you are using VisualStudio for Mac, you might need to edit the project ``` It usually takes a couple of minutes for the traces to show in Application Insights Azure portal. -## Configuration Details -The EventFlow pipeline is built around three core concepts: [inputs](#inputs), [outputs](#outputs), and [filters](#filters). The number of inputs, outputs, and filters depend on the need of diagnostics. The configuration -also has a healthReporter and settings section for configuring settings fundamental to the pipeline operation. Finally, the extensions section allows declaration of custom developed -plugins. These extension declarations act like references. On pipeline initialization, EventFlow will search extensions to instantiate custom inputs, outputs, or filters. +[**Back to Topics**](#topics) -### Inputs +## Inputs These define what data will flow into the engine. At least one input is required. Each input type has its own set of parameters. -#### Trace +### Trace *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Inputs.Trace**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.Trace/) This input listens to traces written with System.Diagnostics.Trace API. Here is an example showing all possible settings: @@ -117,7 +143,9 @@ This input listens to traces written with System.Diagnostics.Trace API. Here is | `type` | "Trace" | Yes | Specifies the input type. For this input, it must be "Trace". | | `traceLevel` | Critical, Error, Warning, Information, Verbose, All | No | Specifies the collection trace level. Traces with equal or higher severity than specified are collected. For example, if Warning is specified, then Critial, Error, and Warning traces are collected. Default is Error. | -#### EventSource +[**Back to Topics**](#topics) + +### EventSource *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Inputs.EventSource**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.EventSource/) This input listens to EventSource traces. EventSource classes can be created in the application by deriving from the [System.Diagnostics.Tracing.EventSource](https://msdn.microsoft.com/en-us/library/system.diagnostics.tracing.eventsource(v=vs.110).aspx) class. Here is an example showing all possible settings: @@ -161,7 +189,9 @@ This input listens to EventSource traces. EventSource classes can be created in (***) There is an issue with .NET frameworks 4.6 and 4.7, and .NET Core framework 1.1 and 2.0 where dynamically created EventSource events are dispatched to all listeners, regardless whether listeners subscribe to events from these EventSources; for more information see https://github.com/dotnet/coreclr/issues/14434 `disabledProviderNamePrefix` property can be usesd to suppress these events.
Disabling EventSources is not recommended under normal circumstances, as it introduces a slight performance penalty. Instead, selectively enable necessary events through combination of EventSource names, event levels, and keywords. -#### DiagnosticSource +[**Back to Topics**](#topics) + +### DiagnosticSource *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Inputs.DiagnosticSource**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.DiagnosticSource/) This input listens to System.Diagnostics.DiagnosticSource sources. See the [DiagnosticSource User's Guide](https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/DiagnosticSourceUsersGuide.md) for details of usage. Here is an example showing all possible settings: @@ -187,7 +217,67 @@ This input listens to System.Diagnostics.DiagnosticSource sources. See the [Diag | :---- | :----------- | :------: | :---------- | | `providerName` | DiagnosticSource name | Yes | Specifies the name of the DiagnosticSource to track. | -#### PerformanceCounter +[**Back to Topics**](#topics) + +### ActivitySource +*Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/) + +This input listens to `System.Diagnostics.ActivitySource` sources (introduced in .NET 5.0). ActivitySource is designed to emit telemetry in a way that is compatible with [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/). Here is an example showing all possible settings: +```jsonc +{ + "type": "ActivitySource", + "sources": [ + { + "ActivitySourceName": "JobSchedulingActivitySource", + "ActivityName": "JobStatusRequest", + "CapturedData": "PropagationData" + }, + { + "ActivitySourceName": "JobSchedulingActivitySource", + "ActivityName": "NewJobRequest", + "CapturedData": "AllDataAndRecorded", + "CapturedEvents": "Both" + } + ] +} +``` + +*Top object* + +| Field | Values/Types | Required | Description | +| :---- | :----------- | :------: | :---------- | +| `type` | "ActivitySource" | Yes | Specifies the input type. For this input, it must be "ActivitySource". | +| `sources` | JSON array | Yes | Specifies the activity data to collect. | + +*Source object (element of the sources array)* + +| Field | Values/Types | Required | Description | +| :---- | :----------- | :------: | :---------- | +| `ActivitySourceName` | string | No | The name of the `ActivitySource` to track. If left empty or omitted, all `ActivitySources` available in the process will be tracked. | +| `ActivityName` | string | No | The name of the activity to track. If left empty or omitted, all activities from a given source will be captured. | +| `CapturedData` | `AllData`, `AllDataAndRecorded`, `PropagationData`, or `None` | No | Specifies what data will be captured for the activity. For more information about what each option means see [ActivitySamplingResult documentation](https://docs.microsoft.com/dotnet/api/system.diagnostics.activitysamplingresult)

The default value for this setting is is `AllData`. | +| `CapturedEvents` | `Start`, `Stop`, `Both`, or `None` | No | Specifies when activity data gets captured. `Start` means activity data will be captured just after the activity is started. `Stop` means the activity data will be captured just after the activity is completed. `Both` means the activity data will be captured twice: at the beginning, and at the end of the activity.

The default value for this setting is is `Stop`. | + +*Notes* + +Be careful about leaving `ActivitySourceName` and `ActivityName` blank. It might be tempting to capture all activity data for the process, but it might have a significant, negative impact on performance, and result in a lot of data that will be expensive to process and store. + +It is possible to come up with configuration that will "match" a given activity multiple times. Records in the `sources` array are matched to activities *in the order in which they appear in configuration*; the first record "wins" and determines what settings will be used. For example, if the configuration is + +```jsonc +{ + "Type": "ActivitySource", + "Sources": [ + { "ActivitySourceName": "S", "CapturedData": "PropagationData" }, + { "ActivitySourceName": "S", "ActivityName": "A", "CapturedData": "AllData" } + ] +} +``` +and activity `A` from source `S` occurs, the first `sources` record will match the activity, and only propagation data will be captured for activity `A`. That is why more specific sources should generally precede less specific sources (ones that omit `ActivitySourceName` or `ActivityName`). A common scenario when this rule comes handy is when it is necessary to capture all data from specific subset of activities from a given source, and then capture propagation data for all other activities from the same source. + +[**Back to Topics**](#topics) + +### PerformanceCounter *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Inputs.PerformanceCounter**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.PerformanceCounter/) This input enables gathering data from Windows performance counters. Only *process-specific* counters are supported, that is, the counter must have an instance that is associated with the current process. For machine-wide counters use an external agent such as [Azure Diagnostics Agent](https://azure.microsoft.com/en-us/documentation/articles/azure-diagnostics/) or create a custom input. @@ -247,7 +337,9 @@ This can manifest itself by health reporter reporting "category does not exist" despite the fact that the category and counter are properly configured and clearly visible in Windows Performance Monitor. If you need to consume such counters, make sure the account your process runs under belongs to Performance Monitor Users group. -#### Serilog +[**Back to Topics**](#topics) + +### Serilog *Nuget package:* [**Microsoft.Diagnostics.EventFlow.Inputs.Serilog**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.Serilog/) @@ -298,7 +390,9 @@ namespace SerilogEventFlow } ``` -#### Microsoft.Extensions.Logging +[**Back to Topics**](#topics) + +### Microsoft.Extensions.Logging *Nuget package:* [**Microsoft.Diagnostics.EventFlow.Inputs.MicrosoftLogging**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.MicrosoftLogging/) @@ -452,9 +546,9 @@ The following example shows how to enable EventFlow ILogger inside a Service Fab // (rest of controller code is irrelevant) ``` +[**Back to Topics**](#topics) - -#### ETW (Event Tracing for Windows) +### ETW (Event Tracing for Windows) *Nuget package:* [**Microsoft.Diagnostics.EventFlow.Inputs.Etw**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.Etw/) @@ -502,7 +596,9 @@ To capture data from EventSources running in the same process as EventFlow, the (*) Either providerName, or providerGuid must be specified. When both are specified, provider GUID takes precedence. -#### Application Insights input +[**Back to Topics**](#topics) + +### Application Insights input *Nuget package:* [**Microsoft.Diagnostics.EventFlow.Inputs.ApplicationInsights**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.ApplicationInsights/) @@ -616,10 +712,11 @@ namespace AspNetCoreEventFlow } } } - ``` -#### Log4net +[**Back to Topics**](#topics) + +### Log4net *Nuget package:* [**Microsoft.Diagnostics.EventFlow.Inputs.Log4net**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.Log4net/) @@ -663,7 +760,9 @@ namespace ConsoleApp2 } ``` -#### NLog +[**Back to Topics**](#topics) + +### NLog *Nuget package:* [**Microsoft.Diagnostics.EventFlow.Inputs.NLog**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Inputs.NLog/) @@ -703,11 +802,12 @@ namespace NLogEventFlow } ``` +[**Back to Topics**](#topics) -### Outputs +## Outputs Outputs define where data will be published from the engine. It's an error if there are no outputs defined. Each output type has its own set of parameters. -#### StdOutput +### StdOutput *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Outputs.StdOutput**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Outputs.StdOutput/) This output writes data to the console window. Here is an example showing all possible settings: @@ -720,7 +820,9 @@ This output writes data to the console window. Here is an example showing all po | :---- | :-------------- | :------: | :---------- | | `type` | "StdOutput" | Yes | Specifies the output type. For this output, it must be "StdOutput". | -#### Http +[**Back to Topics**](#topics) + +### Http *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Outputs.HttpOutput**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Outputs.HttpOutput/) This output writes data to a webserver using diffent encoding methods (Json or JsonLines, eg. for logstash). Here is an example showing all possible settings: @@ -744,7 +846,9 @@ This output writes data to a webserver using diffent encoding methods (Json or J | `httpContentType` | string | No | Defines the HTTP Content-Type header | | `headers` | object | No | Specifies custom headers that will be added to event upload request. Each property of the object becomes a separate header. | -#### Event Hub +[**Back to Topics**](#topics) + +### Event Hub *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Outputs.EventHub**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Outputs.EventHub/) This output writes data to the [Azure Event Hub](https://azure.microsoft.com/en-us/documentation/articles/event-hubs-overview/). Here is an example showing all possible settings: @@ -762,7 +866,9 @@ This output writes data to the [Azure Event Hub](https://azure.microsoft.com/en- | `connectionString` | connection string | Yes | Specifies the connection string for the event hub. The corresponding shared access policy must have send permission. If the event hub name does not appear in the connection string, then it must be specified in the eventHubName field. | | `partitionKeyProperty` | string | No | The name of the event property that will be used as the PartitionKey for the Event Hub events. | -#### Application Insights +[**Back to Topics**](#topics) + +### Application Insights *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Outputs.ApplicationInsights**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Outputs.ApplicationInsights/) This output writes data to the [Azure Application Insights service](https://azure.microsoft.com/en-us/documentation/articles/app-insights-overview/). Here is an example showing all possible settings: @@ -799,7 +905,9 @@ Remarks: All other events will be reported as Application Insights *traces* (telemetry of type Trace). -#### Elasticsearch +[**Back to Topics**](#topics) + +### Elasticsearch *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Outputs.ElasticSearch**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Outputs.ElasticSearch/) This output writes data to the [Elasticsearch](https://www.elastic.co/products/elasticsearch). Here is an example showing all possible settings: @@ -869,7 +977,9 @@ Fields injected byt the `request` metadata are: | 2.6.x | 6.x | | 2.7.x | 7.x | -#### Azure Monitor Logs +[**Back to Topics**](#topics) + +### Azure Monitor Logs *Nuget package*: [**Microsoft.Diagnostics.EventFlow.Outputs.AzureMonitorLogs**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Outputs.AzureMonitorLogs/) @@ -893,7 +1003,9 @@ Supported configuration settings are: | `logTypeName` | string | No | Specifies the log entry type created by the output. Default value for this setting is "Event", which results in "Event_CL" entries being created in Log Analytics (the "_CL" suffix is appended automatically by Log Analytics Data Collector). | | `serviceDomain` | string | No | Specifies the domain for your Log Analytics workspace. Default value is "ods.opinsights.azure.com", for Azure Commercial. -### Filters +[**Back to Topics**](#topics) + +## Filters As data comes through the EventFlow pipeline, the application can add extra processing or tagging to them. These optional operations are accomplished with filters. Filters can transform, drop, or tag data with extra metadata, with rules based on custom expressions. With metadata tags, filters and outputs operating further down the pipeline can apply different processing for different data. For example, an output component can choose to send only data with a certain tag. Each filter type has its own set of parameters. @@ -933,7 +1045,7 @@ Filters can appear in two places in the EventFlow configuration: on the same lev EventFlow comes with two standard filter types: `drop` and `metadata`. -#### drop +### Drop *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Core**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Core/) This filter discards all data that satisfies the include expression. Here is an example showing all possible settings: @@ -948,7 +1060,9 @@ This filter discards all data that satisfies the include expression. Here is an | `type` | "drop" | Yes | Specifies the filter type. For this filter, it must be "drop". | | `include` | logical expression | Yes | Specifies the logical expression that determines if the action should apply to the event data or not. For information about the logical expression, please see section [Logical Expressions](#logical-expressions). | -#### metadata +[**Back to Topics**](#topics) + +### Metadata *Nuget Package*: [**Microsoft.Diagnostics.EventFlow.Core**](https://www.nuget.org/packages/Microsoft.Diagnostics.EventFlow.Core/) This filter adds additional metadata to all event data that satisfies the include expression. The filter recognizes a few standard properties (`type`, `metadata` and `include`); the rest are custom properties, specific for the given metadata type: @@ -1010,11 +1124,13 @@ Here are a few examples of using the metadata filter: } ``` -### Standard metadata types +[**Back to Topics**](#topics) + +## Standard metadata types EventFlow core library defines several standard metadata types. They have pre-defined set of fields and are recognized by [Application Insights](#application-insights) and [Elasticsearch](#elasticsearch) outputs (see documentation for each output, respectively, to learn how they handle standard metadata). -**Metric metadata type** +### Metric metadata type Metrics are named time series of floating-point values. Metric metadata defines how metrics are derived from ordinary events. Following fields are supported: @@ -1030,7 +1146,9 @@ Remarks: 1. Either `metricName` or `metricNameProperty` must be specified. 2. Either `metricValue` or `metricValueProperty` must be specified. -**Request metadata type** +[**Back to Topics**](#topics) + +### Request metadata type Requests are special events that represent invocations of a network service by its clients. Request metadata defines how requests are derived from ordinary events. Following fields are supported: @@ -1043,7 +1161,9 @@ Requests are special events that represent invocations of a network service by i | `durationUnit` | "TimeSpan", "milliseconds", "seconds", "minutes" or "hours" | No | Specifies the type of data used by request duration property. If not set, it is assumed that request duration is expressed as a double value, representing milliseconds. | | `responseCodeProperty` | string | No | The name of the event property that specifies response code associated with the request. A response code describes in more detail the outcome of the request. It is expected that the event property is, or can be converted to a string. | -**Dependency metadata type** +[**Back to Topics**](#topics) + +### Dependency metadata type Dependency event represents the act of calling a service that your service depends on. It has the following properties: @@ -1057,7 +1177,9 @@ Dependency event represents the act of calling a service that your service depen | `targetProperty` | string | Yes | The name of the event property that specifies the target of the call, i.e. the identifier of the service that your service depends on. | | `dependencyType` | string | No | An optional, user-defined designation of the dependency type. For example, it could be "SQL", "cache", "customer_data_service" or similar. | -**Exception metadata type** +[**Back to Topics**](#topics) + +### Exception metadata type Exception event corresponds to an occurrence of an unexpected exception. Usually a small amount of exceptions is continuously being thrown, caught and handled by a .NET process, this is normal and should not raise a concern. On the other hand, if an exception is unhandled, or unexpected, it needs to be logged and examined. This metadata is meant to cover the second case. It has the following properties: @@ -1068,7 +1190,9 @@ Exception event corresponds to an occurrence of an unexpected exception. Usually Also see [Application Insight Exception metadata type with EvenSource input issue](https://github.com/Azure/diagnostics-eventflow/issues/92) -### Health Reporter +[**Back to Topics**](#topics) + +## Health Reporter Every software component can generate errors or warnings the developer should be aware of. The EventFlow library is no exception. An EventFlow health reporter reports errors and warnings generated by any components in the EventFlow pipeline. In what format the report is presented depends on the implementation of the health reporter. The EventFlow library suite includes two health reporters: CsvHealthReporter and ServiceFabricHealthReporter. @@ -1076,7 +1200,7 @@ The CsvHealthReporter is the default health reporter for EventFlow library and i ServiceFabricHealthReporter is described in the [Service Fabric support paragraph](#service-fabric-support). It is designed to be used in the context of Service Fabric applications and does not need any configuration. -#### CsvHealthReporter +### CsvHealthReporter *Nuget Package*: **Microsoft.Diagnostics.EventFlow.Core** This health reporter writes all errors, warnings, and informational traces generated from the pipeline into a CSV file. Here is an example showing all possible settings: @@ -1107,7 +1231,9 @@ This health reporter writes all errors, warnings, and informational traces gener CsvHealthReporter will try to open the log file for writing during initialization. If it can't, by default, a debug message will be output to the debugger viewer like Visual Studio Output window, etc. This can happen especially if a value for the log file path is not provided (default is used, which is application executables folder) and the application executables are residing on a read-only file system. Docker tools for Visual Studio use this configuration during debugging, so for containerized services the recommended practice is to specify the log file path explicitly. -### Pipeline Settings +[**Back to Topics**](#topics) + +## Pipeline Settings The EventFlow configuration has settings allowing the application to adjust certain behaviors of the pipeline. These range from how many events the pipeline buffer, to the timeout the pipeline should use when waiting for an operation. If this section is omitted, the pipeline will use default settings. Here is an example of all the possible settings: ```jsonc @@ -1127,6 +1253,8 @@ Here is an example of all the possible settings: | `maxConcurrency` | number | No | Specifies the maximum number of threads that events can be processed. Each event will be processed by a single thread, by multiple threads can process different events simultaneously. | | `pipelineCompletionTimeoutMsec` | number of milliseconds | No | Specifies the timeout to wait for the pipeline to shutdown and clean up. The shutdown process starts when the DiagnosePipeline object is disposed, which usually happens on application exit. | +[**Back to Topics**](#topics) + ## Service Fabric Support *Nuget Package*: **Microsoft.Diagnostics.EventFlow.ServiceFabric** @@ -1200,6 +1328,7 @@ The UnhandledException event method is a very simple addition to the standard Se Depending on the type of inputs and outputs used, additional startup code may be necessary. For example [Microsoft.Extensions.Logging](#microsoftextensionslogging) input requires a call to `LoggerFactory.AddEventFlow()` method to register EventFlow logger provider. +[**Back to Topics**](#topics) ### Support for Service Fabric settings and application parameters Version 1.0.1 of the EventFlow Service Fabric NuGet package introduced the ability to refer to Service Fabric settings from EventFlow configuration using special syntax for values: @@ -1218,6 +1347,8 @@ Version 1.1.2 added support for resolving paths to other configuration files tha At run time this value will be substituted with a full path to the configuration file with the given name. This is especially useful if an EventFlow pipeline element wraps an existing library that has its own configuration file format (as is the case with Application Insights, for example). +[**Back to Topics**](#topics) + ### Using Application Insights ServerTelemetryChannel For Service Fabric applications running in production, we recommend to use Application Insights `ServerTelemetryChannel`. This channel [has the capability to store data on disk during periods of intermittent connectivity](https://docs.microsoft.com/en-us/azure/azure-monitor/app/telemetry-channels) and is a better choice for long-running server processes than the default in-memory channel. @@ -1267,8 +1398,10 @@ To use the `ServerMemoryChannel` you need to create an EventFlow configuration f Ensure that your service consumes `Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel` NuGet package. -## Logical Expressions -The logical expression allows you to filter events based on the event properties. For example, you can have an expression like `ProviderName == MyEventProvider && EventId == 3`, where you specify the event property name on the left side and the value to compare on the right side. If the value on the right side contains special characters, you can enclose it in double quotes. +[**Back to Topics**](#topics) + +## Filter Expressions +Filter expressions allows you to filter events based on the event properties. For example, you can have an expression like `ProviderName == MyEventProvider && EventId == 3`, where you specify the event property name on the left side and the value to compare on the right side. If the value on the right side contains special characters, you can enclose it in double quotes. Standard and custom (payload) properties are treated equally; there is no need to prefix custom properties in any way. For example, expression `TenantId != Unknown && ProviderName == MyEventProvider` will evaluate to true for events that - were created by `MyEventProvider` provider (ProviderName is a standard property available for all events), and @@ -1285,7 +1418,9 @@ The following table lists operators supported by logical expressions | Logical | `&&`, `||`, `!` | Enables building complex logical expressions out of simpler ones.
The precedence is `!` > `&&` > `||` (highest to lowest). | Grouping | `(expression)` | Grouping can be used to change the evaluation order of expressions with logical operators. | -## Store Secret Securely +[**Back to Topics**](#topics) + +## Store Secrets Securely If you don't want to put sensitive information in the EventFlow configuration file, you can store the information at a secured place and pass it to the configuration at run time. Here is the sample code: ```csharp string configFilePath = @".\eventFlowConfig.json"; @@ -1304,6 +1439,8 @@ using (DiagnosticPipeline pipeline = DiagnosticPipelineFactory.CreatePipeline(co } ``` +[**Back to Topics**](#topics) + ## Extensibility Every pipeline element type (input, filter, output and health reporter) is a point of extensibility, that is, custom elements of these types can be used inside the pipeline. Contracts for all EventFlow element types are provided by [EventFlow.Core assembly](https://github.com/Azure/diagnostics-eventflow/tree/master/src/Microsoft.Diagnostics.EventFlow.Core/Interfaces) (except from the input type, see below). @@ -1325,6 +1462,8 @@ EventFlow pipelines operate on [EventData objects](https://github.com/Azure/diag Note that EventData type is not thread-safe. Don't try to use it concurrently from multiple threads. +[**Back to Topics**](#topics) + ### EventFlow pipeline element types #### Inputs Inputs are producing new events (`EventData` instances). Anything that implements `IObservable` can be used as an input for EventFlow. `IObservable` is a standard .NET interface in the System namespace. @@ -1351,6 +1490,8 @@ public interface IOutput ``` The output receives a batch of events, along with transmission sequence number and a cancellation token. The transmission sequence number can be treated as an identifier for the `SendEventsAsync()` method invocation; it is guaranteed to be unique for each invocation, but there is no guarantee that there will be no "gaps", nor that it will be strictly increasing. The cancellation token should be used to cancel long-running operations if necessary; typically it is passed as a parameter to asynchronous i/o operations. +[**Back to Topics**](#topics) + ### Pipeline structure and threading considerations Every EventFlow pipeline can be created imperatively, i.e. in the program code. The structure of every pipeline is reflected in the constructor of the `DiagnosticPipeline` class and is as follows: @@ -1364,6 +1505,8 @@ EventFlow employs the following policies with regards to concurrency: 2. EventFlow will ensure that only one filter at a time is evaluating any given EventData object. That said, the same filter can be invoked concurrently for different events. 3. Outputs will invoked concurrently for different batches of data. +[**Back to Topics**](#topics) + ### Using custom pipeline items imperatively The simplest way to use custom EventFlow elements is to create a pipeline imperatively, in code. You just need to create a read-only collections of the inputs, global filters and sinks and pass them to `DiagnosticPipeline` constructor. Custom and standard elements can be combined freely; each of the standard pipeline elements has a public constructor and associated public configuration class and can be created imperatively. @@ -1445,6 +1588,27 @@ namespace EventFlowCustomOutput } ``` +[**Back to Topics**](#topics) + +## Troubleshooting + +### Events are getting dropped + +Under high load you might emit a health warning that says + +`An event was dropped from the diagnostic pipeline because there was not enough capacity` + +The “capacity” referred to in the error message is the size of the event buffer that EventFlow has. By default the buffer can hold 1000 events, although that can be changed by [configuration settings](#pipeline-settings). The “not enough capacity” error indicates EventFlow output(s) cannot consume incoming events fast enough. The buffer is exhausted and events are starting to get dropped. + +If the event inflow is quite bursty, increasing the buffer size might help. + +For `ApplicationInsightsOutput` make you are using [the ServerTelemetryChannel](https://docs.microsoft.com/en-us/azure/azure-monitor/app/telemetry-channels) for sending data to Application Insights. This channel has the ability to buffer data on disk if the connectivity to Application Insights is intermittent. It can be [enabled via output configuration](#application-insights), using a separate [ApplicationInsights configuration file](https://docs.microsoft.com/en-us/azure/azure-monitor/app/configuration-with-applicationinsights-config#telemetry-channel). + +Ultimately it might be necessary just to reduce the number of events produced by the service + + +[**Back to Topics**](#topics) + ## Platform Support EventFlow supports full .NET Framework (.NET 4.5 series and 4.6 series) and .NET Core, but not all inputs and outputs are supported on all platforms. The following table lists platform support for standard inputs and outputs. @@ -1467,10 +1631,14 @@ The following table lists platform support for standard inputs and outputs. | [Azure EventHub](#event-hub) | Yes | Yes | Yes | | [Elasticsearch](#elasticsearch) | Yes | Yes | Yes | | [Azure Monitor Logs](#azure-monitor-logs) | Yes | Yes | Yes | -[HTTP (json via http)](#http) | Yes | Yes | Yes | +| [HTTP (json via http)](#http) | Yes | Yes | Yes | + +[**Back to Topics**](#topics) ## Contributions Refer to [contribution guide](contributing.md). ## Code of Conduct Refer to [Code of Conduct guide](CODE_OF_CONDUCT.md). + +[**Back to Topics**](#topics) diff --git a/Warsaw.sln b/Warsaw.sln index f5cb6d7e..bf7128f0 100644 --- a/Warsaw.sln +++ b/Warsaw.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30804.86 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{FC86C736-6428-431B-B83F-940BF7182757}" EndProject @@ -90,6 +90,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Event EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Diagnostics.EventFlow.Signing", "src\Microsoft.Diagnostics.EventFlow.Signing\Microsoft.Diagnostics.EventFlow.Signing.csproj", "{69AA6421-82A5-43BF-AD27-8E2C172FF8CE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource", "src\Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource\Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource.csproj", "{4B9EF551-2476-4494-9A5E-C52A97505CD4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -330,6 +332,14 @@ Global {69AA6421-82A5-43BF-AD27-8E2C172FF8CE}.Release|Any CPU.Build.0 = Release|Any CPU {69AA6421-82A5-43BF-AD27-8E2C172FF8CE}.Release|x64.ActiveCfg = Release|Any CPU {69AA6421-82A5-43BF-AD27-8E2C172FF8CE}.Release|x64.Build.0 = Release|Any CPU + {4B9EF551-2476-4494-9A5E-C52A97505CD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B9EF551-2476-4494-9A5E-C52A97505CD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B9EF551-2476-4494-9A5E-C52A97505CD4}.Debug|x64.ActiveCfg = Debug|Any CPU + {4B9EF551-2476-4494-9A5E-C52A97505CD4}.Debug|x64.Build.0 = Debug|Any CPU + {4B9EF551-2476-4494-9A5E-C52A97505CD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B9EF551-2476-4494-9A5E-C52A97505CD4}.Release|Any CPU.Build.0 = Release|Any CPU + {4B9EF551-2476-4494-9A5E-C52A97505CD4}.Release|x64.ActiveCfg = Release|Any CPU + {4B9EF551-2476-4494-9A5E-C52A97505CD4}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -364,6 +374,7 @@ Global {C1CB9BDC-2F26-46AA-9F2C-F9C12E9A3418} = {FC86C736-6428-431B-B83F-940BF7182757} {A53665AF-C4B4-4DD8-B002-F8065C1A3D10} = {47200F40-43E1-4B09-B803-A921FED2BF05} {69AA6421-82A5-43BF-AD27-8E2C172FF8CE} = {FC86C736-6428-431B-B83F-940BF7182757} + {4B9EF551-2476-4494-9A5E-C52A97505CD4} = {47200F40-43E1-4B09-B803-A921FED2BF05} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8F1BC23F-956D-4D83-B675-710B743F08EF} diff --git a/src/Microsoft.Diagnostics.EventFlow.Core/Implementations/DiagnosticPipelineFactory.cs b/src/Microsoft.Diagnostics.EventFlow.Core/Implementations/DiagnosticPipelineFactory.cs index 8039a1cf..76ea01a8 100644 --- a/src/Microsoft.Diagnostics.EventFlow.Core/Implementations/DiagnosticPipelineFactory.cs +++ b/src/Microsoft.Diagnostics.EventFlow.Core/Implementations/DiagnosticPipelineFactory.cs @@ -326,6 +326,7 @@ private static void CreateItemFactories( inputFactories["Log4net"] = "Microsoft.Diagnostics.EventFlow.Inputs.Log4netFactory, Microsoft.Diagnostics.EventFlow.Inputs.Log4net"; inputFactories["NLog"] = "Microsoft.Diagnostics.EventFlow.Inputs.NLogInputFactory, Microsoft.Diagnostics.EventFlow.Inputs.NLog"; inputFactories["DiagnosticSource"] = "Microsoft.Diagnostics.EventFlow.Inputs.DiagnosticSource.DiagnosticSourceInputFactory, Microsoft.Diagnostics.EventFlow.Inputs.DiagnosticSource"; + inputFactories["ActivitySource"] = "Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource.ActivitySourceInputFactory, Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource"; outputFactories = new Dictionary(StringComparer.OrdinalIgnoreCase); outputFactories["ApplicationInsights"] = "Microsoft.Diagnostics.EventFlow.Outputs.ApplicationInsightsOutputFactory, Microsoft.Diagnostics.EventFlow.Outputs.ApplicationInsights"; diff --git a/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/StringBuilderCache.cs b/src/Microsoft.Diagnostics.EventFlow.Core/Implementations/StringBuilderCache.cs similarity index 97% rename from src/Microsoft.Diagnostics.EventFlow.EtwUtilities/StringBuilderCache.cs rename to src/Microsoft.Diagnostics.EventFlow.Core/Implementations/StringBuilderCache.cs index 32547107..9951ea95 100644 --- a/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/StringBuilderCache.cs +++ b/src/Microsoft.Diagnostics.EventFlow.Core/Implementations/StringBuilderCache.cs @@ -6,12 +6,12 @@ using System; using System.Text; -namespace Microsoft.Diagnostics.EventFlow.Utilities.Etw +namespace Microsoft.Diagnostics.EventFlow { /// /// Provides a cached reusable instance of a StringBuilder per thread. It is an optimization that reduces the number of instances constructed and collected. /// - internal static class StringBuilderCache + public static class StringBuilderCache { // The value 360 was chosen in discussion with performance experts as a compromise between using // as litle memory (per thread) as possible and still covering a large part of short-lived diff --git a/src/Microsoft.Diagnostics.EventFlow.Core/Microsoft.Diagnostics.EventFlow.Core.csproj b/src/Microsoft.Diagnostics.EventFlow.Core/Microsoft.Diagnostics.EventFlow.Core.csproj index e814e83b..6ed5e5a2 100644 --- a/src/Microsoft.Diagnostics.EventFlow.Core/Microsoft.Diagnostics.EventFlow.Core.csproj +++ b/src/Microsoft.Diagnostics.EventFlow.Core/Microsoft.Diagnostics.EventFlow.Core.csproj @@ -3,7 +3,7 @@ Defines core interfaces and types that comprise Microsoft.Diagnostics.EventFlow library. © Microsoft Corporation. All rights reserved. - 1.10.0 + 1.10.1 Microsoft netstandard1.6;netstandard2.0;net452;net471 Microsoft.Diagnostics.EventFlow.Core diff --git a/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/ActivityPathDecoder.cs b/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/ActivityPathDecoder.cs index 529e5207..e5b7418c 100644 --- a/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/ActivityPathDecoder.cs +++ b/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/ActivityPathDecoder.cs @@ -7,6 +7,8 @@ using System.Diagnostics; using System.Text; +using Microsoft.Diagnostics.EventFlow; + namespace Microsoft.Diagnostics.EventFlow.Utilities.Etw { /// diff --git a/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/Microsoft.Diagnostics.EventFlow.EtwUtilities.csproj b/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/Microsoft.Diagnostics.EventFlow.EtwUtilities.csproj index 2e3d3cce..e64f402e 100644 --- a/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/Microsoft.Diagnostics.EventFlow.EtwUtilities.csproj +++ b/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/Microsoft.Diagnostics.EventFlow.EtwUtilities.csproj @@ -7,7 +7,7 @@ netstandard1.6;net452;netstandard2.0;net471 true Microsoft.Diagnostics.EventFlow.EtwUtilities - 1.8.0 + 1.8.1 true Microsoft.Diagnostics.EventFlow.EtwUtilities Microsoft;Diagnostics;EventFlow;Utilities;Event Tracing for Windows @@ -45,6 +45,7 @@ + diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/ActivitySourceInput.cs b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/ActivitySourceInput.cs new file mode 100644 index 00000000..94fc7562 --- /dev/null +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/ActivitySourceInput.cs @@ -0,0 +1,260 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using Validation; + +using Microsoft.Diagnostics.EventFlow.Configuration; + +namespace Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource +{ + public class ActivitySourceInput : IObservable, IDisposable + { + private const int SamplingDecisionCacheFlushThreshold = 1024; + + // Using a static dictionary like this is faster than doing Enum.GetName() + private static readonly IDictionary ActivityKindNames = + Enum.GetValues(typeof(ActivityKind)).Cast().ToDictionary(k => k, k => k.ToString()); + + private EventFlowSubject subject_; + private ActivityListener activityListener_; + private ActivitySourceInputConfiguration configuration_; + private ConcurrentDictionary activitySampling_; + private bool hasUnrestrictedSources_; + private IHealthReporter healthReporter_; + + public ActivitySourceInput(IConfiguration configuration, IHealthReporter healthReporter) + { + Requires.NotNull(configuration, nameof(configuration)); + Requires.NotNull(healthReporter, nameof(healthReporter)); + + var inputConfiguration = new ActivitySourceInputConfiguration(); + try + { + configuration.Bind(inputConfiguration); + } + catch + { + healthReporter.ReportProblem( + $"Invalid {nameof(ActivitySourceInput)} configuration encountered: '{configuration.ToString()}'", + EventFlowContextIdentifiers.Configuration); + throw; + } + + Initialize(inputConfiguration, healthReporter); + } + + public ActivitySourceInput(ActivitySourceInputConfiguration configuration, IHealthReporter healthReporter) + { + Requires.NotNull(configuration, nameof(configuration)); + Requires.NotNull(healthReporter, nameof(healthReporter)); + + Initialize(configuration, healthReporter); + } + + public JsonSerializerSettings SerializerSettings { get; set; } + public ActivitySourceInputConfiguration Configuration => configuration_; + + public void Dispose() + { + activityListener_.Dispose(); + subject_.Dispose(); + } + + public IDisposable Subscribe(IObserver observer) + { + return subject_.Subscribe(observer); + } + + private void Initialize(ActivitySourceInputConfiguration configuration, IHealthReporter healthReporter) + { + healthReporter_ = healthReporter; + configuration_ = configuration.DeepClone(); + subject_ = new EventFlowSubject(); + activitySampling_ = new ConcurrentDictionary(); + activityListener_ = new ActivityListener(); + SerializerSettings = EventFlowJsonUtilities.GetDefaultSerializerSettings(); + + if (configuration_.Sources.Count == 0) + { + healthReporter.ReportWarning( + $"{nameof(ActivitySourceInput)}: configuration has no data sources. No activity data will be captured.", + EventFlowContextIdentifiers.Configuration); + } + + var removed = configuration_.Sources.RemoveAll(s => s.CapturedData == ActivitySamplingResult.None); + if (removed > 0) + { + healthReporter.ReportWarning( + $"{nameof(ActivitySourceInput)}: configuration has sources with CapturedData = None. These sources will be ignored.", + EventFlowContextIdentifiers.Configuration); + } + + hasUnrestrictedSources_ = configuration_.Sources.Any(s => string.IsNullOrWhiteSpace(s.ActivitySourceName)); + + activityListener_.Sample = (ref ActivityCreationOptions activityOptions) + => DetermineActivitySampling(activityOptions.Source.Name, activityOptions.Name).CapturedData; + activityListener_.SampleUsingParentId = (ref ActivityCreationOptions activityOptions) + => DetermineActivitySampling(activityOptions.Source.Name, activityOptions.Name).CapturedData; + activityListener_.ShouldListenTo = ShouldListenTo; + activityListener_.ActivityStarted = OnActivityStarted; + activityListener_.ActivityStopped = OnActivityStopped; + + System.Diagnostics.ActivitySource.AddActivityListener(activityListener_); + } + + private (ActivitySamplingResult CapturedData, CapturedActivityEvents CapturedEvents) DetermineActivitySampling(string activitySourceName, string activityName) + { + string activityKey = activitySourceName + ":" + activityName; + + if (activitySampling_.TryGetValue(activityKey, out var samplingSpec)) + { + return samplingSpec; + } + + foreach(var sc in configuration_.Sources) + { + bool sourceMatches = string.IsNullOrWhiteSpace(sc.ActivitySourceName) || StringComparer.OrdinalIgnoreCase.Equals(activitySourceName, sc.ActivitySourceName); + bool nameMatches = string.IsNullOrWhiteSpace(sc.ActivityName) || StringComparer.OrdinalIgnoreCase.Equals(activityName, sc.ActivityName); + + if (sourceMatches && nameMatches) + { + FlushSamplingInfoCacheIfNeeded(); + + activitySampling_.AddOrUpdate(activityKey, (sc.CapturedData, sc.CapturedEvents), (_, _) => (sc.CapturedData, sc.CapturedEvents)); + + return (sc.CapturedData, sc.CapturedEvents); + } + } + + return (ActivitySamplingResult.None, CapturedActivityEvents.None); + } + + private bool ShouldListenTo(System.Diagnostics.ActivitySource activitySource) + { + if (hasUnrestrictedSources_) + { + return true; + } + + bool found = configuration_.Sources.Any(s => + StringComparer.OrdinalIgnoreCase.Equals(activitySource.Name, s.ActivitySourceName) && + s.CapturedEvents != CapturedActivityEvents.None); + return found; + } + + private void OnActivityStarted(Activity activity) + { + (var capturedData, var capturedEvents) = DetermineActivitySampling(activity.Source.Name, activity.DisplayName); + if ( + capturedData == ActivitySamplingResult.None || + capturedEvents == CapturedActivityEvents.None || + (capturedEvents & CapturedActivityEvents.Start) == 0) + { + return; + } + + EventData e = ToEventData(activity, capturedData); + subject_.OnNext(e); + } + + private void OnActivityStopped(Activity activity) + { + (var capturedData, var capturedEvents) = DetermineActivitySampling(activity.Source.Name, activity.DisplayName); + if ( + capturedData == ActivitySamplingResult.None || + capturedEvents == CapturedActivityEvents.None || + (capturedEvents & CapturedActivityEvents.Stop) == 0) + { + return; + } + + EventData e = ToEventData(activity, capturedData); + subject_.OnNext(e); + } + + private EventData ToEventData(Activity activity, ActivitySamplingResult capturedData) + { + EventData e = new EventData + { + ProviderName = nameof(ActivitySourceInput), + Timestamp = activity.StartTimeUtc, + Level = LogLevel.Informational, + Keywords = (long) activity.ActivityTraceFlags + }; + + // Property names following OpenTelemetry conventions https://github.com/open-telemetry/opentelemetry-specification + e.Payload["Name"] = activity.DisplayName; + e.Payload["SpanId"] = activity.Id; + e.Payload["ParentSpanId"] = activity.ParentSpanId.ToHexString(); + e.Payload["StartTime"] = e.Timestamp; + if (activity.Duration != TimeSpan.Zero) + { + e.Payload["EndTime"] = activity.StartTimeUtc + activity.Duration; + } + e.Payload["TraceId"] = activity.TraceId.ToHexString(); + if (ActivityKindNames.TryGetValue(activity.Kind, out string activityKindName)) + { + e.Payload["SpanKind"] = activityKindName; + } + e.Payload["IsRecording"] = activity.Recorded; + + // The following property additions may cause name conflicts, so using AddPayloadProperty() to handle them. + foreach(var el in activity.Baggage) + { + AddPayloadProperty(e, el.Key, el.Value); + } + + AddPayloadProperty(e, "ActivitySourceName", activity.Source.Name); + + if (capturedData == ActivitySamplingResult.AllData || capturedData == ActivitySamplingResult.AllDataAndRecorded) + { + // Activity.Tags is a subset of Activity.TagObjects that have string value, + // so it is sufficient to just iterate over Activity.TagObjects to capture all activity tags. + foreach(var tagObject in activity.TagObjects) + { + AddPayloadProperty(e, tagObject.Key, tagObject.Value); + } + + if (activity.Events.Any()) + { + AddPayloadProperty(e, "Events", activity.Events); + } + + if (activity.Links.Any()) + { + AddPayloadProperty(e, "Links", activity.Links); + } + } + + return e; + } + + private void AddPayloadProperty(EventData e, string propertyName, object propertyValue) + { + e.AddPayloadProperty(propertyName, propertyValue, healthReporter_, nameof(ActivitySourceInput)); + } + + private void FlushSamplingInfoCacheIfNeeded() + { + if (activitySampling_.Count > SamplingDecisionCacheFlushThreshold) + { + lock(activitySampling_) + { + if (activitySampling_.Count > SamplingDecisionCacheFlushThreshold) + { + activitySampling_.Clear(); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/ActivitySourceInputFactory.cs b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/ActivitySourceInputFactory.cs new file mode 100644 index 00000000..7bbcb8e9 --- /dev/null +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/ActivitySourceInputFactory.cs @@ -0,0 +1,17 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource +{ + public class ActivitySourceInputFactory : IPipelineItemFactory + { + public ActivitySourceInput CreateItem(IConfiguration configuration, IHealthReporter healthReporter) + { + return new ActivitySourceInput(configuration, healthReporter); + } + } +} diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Configuration/ActivitySourceConfiguration.cs b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Configuration/ActivitySourceConfiguration.cs new file mode 100644 index 00000000..50b48883 --- /dev/null +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Configuration/ActivitySourceConfiguration.cs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.EventFlow.Configuration +{ + [Flags] + public enum CapturedActivityEvents + { + None = 0, + Start = 1, + Stop = 2, + Both = 3 + } + + public class ActivitySourceConfiguration + { + public string ActivityName { get; set; } + public string ActivitySourceName { get; set; } + public ActivitySamplingResult CapturedData { get; set; } + public CapturedActivityEvents CapturedEvents { get; set; } + + public ActivitySourceConfiguration() + { + this.ActivityName = this.ActivitySourceName = null; + this.CapturedData = ActivitySamplingResult.AllData; + this.CapturedEvents = CapturedActivityEvents.Stop; + } + + public ActivitySourceConfiguration(ActivitySourceConfiguration other) + { + this.ActivityName = other.ActivityName; + this.ActivitySourceName = other.ActivitySourceName; + this.CapturedData = other.CapturedData; + this.CapturedEvents = other.CapturedEvents; + } + } +} diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Configuration/ActivitySourceInputConfiguration.cs b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Configuration/ActivitySourceInputConfiguration.cs new file mode 100644 index 00000000..4da9b4ff --- /dev/null +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Configuration/ActivitySourceInputConfiguration.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Diagnostics.EventFlow.Configuration +{ + public class ActivitySourceInputConfiguration: ItemConfiguration + { + public List Sources { get; set; } = new List(); + + public ActivitySourceInputConfiguration DeepClone() + { + ActivitySourceInputConfiguration clone = new ActivitySourceInputConfiguration(); + clone.Sources.AddRange(Sources.Select(s => new ActivitySourceConfiguration(s))); + return clone; + } + } +} diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource.csproj b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource.csproj new file mode 100644 index 00000000..61d47abe --- /dev/null +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource.csproj @@ -0,0 +1,31 @@ + + + + Provides an input implementation for capturing diagnostics data sourced through System.Diagnostics.ActivitySource. + © Microsoft Corporation. All rights reserved. + Microsoft + net5.0 + Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource + 1.0.0 + true + Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource + Microsoft;Diagnostics;EventFlow;Inputs;ActivitySource + https://github.com/Azure/diagnostics-eventflow + MIT + true + false + false + false + + + + + + + + + + + + + diff --git a/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Properties/AssemblyInfo.cs b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..f195e922 --- /dev/null +++ b/src/Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d49d53b4-597d-4cd1-9962-100609d3f7a6")] + +[assembly: InternalsVisibleTo("Microsoft.Diagnostics.EventFlow.Inputs.Tests")] + diff --git a/src/Microsoft.Diagnostics.EventFlow.Signing/Microsoft.Diagnostics.EventFlow.Signing.csproj b/src/Microsoft.Diagnostics.EventFlow.Signing/Microsoft.Diagnostics.EventFlow.Signing.csproj index d0aec43e..ebda180d 100644 --- a/src/Microsoft.Diagnostics.EventFlow.Signing/Microsoft.Diagnostics.EventFlow.Signing.csproj +++ b/src/Microsoft.Diagnostics.EventFlow.Signing/Microsoft.Diagnostics.EventFlow.Signing.csproj @@ -122,15 +122,17 @@ ..\Microsoft.Diagnostics.EventFlow.Outputs.Oms\bin\$(Configuration)\netstandard2.0\Microsoft.Diagnostics.EventFlow.Outputs.Oms.dll; ..\Microsoft.Diagnostics.EventFlow.ServiceFabric\bin\$(Configuration)\netstandard2.0\Microsoft.Diagnostics.EventFlow.ServiceFabric.dll; ..\Microsoft.Diagnostics.EventFlow.Inputs.PerformanceCounter\bin\$(Configuration)\netstandard2.0\Microsoft.Diagnostics.EventFlow.Inputs.PerformanceCounter.dll;" /> + + - + Microsoft400 @@ -199,6 +201,8 @@ + + diff --git a/test/Microsoft.Diagnostics.EventFlow.Core.Tests/DiagnosticsPipelineFactoryTests.cs b/test/Microsoft.Diagnostics.EventFlow.Core.Tests/DiagnosticsPipelineFactoryTests.cs index ab303cb3..b14156e1 100644 --- a/test/Microsoft.Diagnostics.EventFlow.Core.Tests/DiagnosticsPipelineFactoryTests.cs +++ b/test/Microsoft.Diagnostics.EventFlow.Core.Tests/DiagnosticsPipelineFactoryTests.cs @@ -11,6 +11,9 @@ using Microsoft.Diagnostics.EventFlow.Filters; using Microsoft.Diagnostics.EventFlow.HealthReporters; using Microsoft.Diagnostics.EventFlow.Inputs; +#if NET5_0 +using Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource; +#endif using Microsoft.Diagnostics.EventFlow.Metadata; using Microsoft.Diagnostics.EventFlow.Outputs; using Microsoft.Diagnostics.EventFlow.TestHelpers; @@ -387,7 +390,7 @@ public void CanCreateAllStandardPipelineItems() ""schemaVersion"": ""2016-08-11"" }"; -#else +#elif NET471 || NETCOREAPP2_1 || NETCOREAPP3_1 string pipelineConfiguration = @" { ""inputs"": [ @@ -439,6 +442,62 @@ public void CanCreateAllStandardPipelineItems() } ], + ""schemaVersion"": ""2016-08-11"" + }"; +#else + string pipelineConfiguration = @" + { + ""inputs"": [ + { + ""type"": ""EventSource"", + ""sources"": [ + { ""providerName"": ""Microsoft-ServiceFabric-Services"" }, + ] + }, + { + ""type"": ""Microsoft.Extensions.Logging"" + }, + { + ""type"": ""Trace"" + }, + { + ""type"": ""Serilog"" + }, + { + ""type"": ""NLog"" + }, + { + ""type"": ""Log4net"", + ""LogLevel"": ""Verbose"" + }, + { + ""type"": ""ActivitySource"", + ""sources"": [ + { ""ActivitySourceName"": ""EventFlowTestSource"", ""ActivityName"": ""EventFlowTestActivity"" } + ] + } + ], + + ""outputs"": [ + { + ""type"": ""StdOutput"", + }, + { + ""type"": ""OmsOutput"", + ""workspaceId"": ""00000000-0000-0000-0000-000000000000"", + ""workspaceKey"": ""Tm90IGEgd29ya3NwYWNlIGtleQ=="" + }, + { + ""type"": ""AzureMonitorLogs"", + ""workspaceId"": ""00000000-0000-0000-0000-000000000000"", + ""workspaceKey"": ""Tm90IGEgd29ya3NwYWNlIGtleQ=="" + }, + { + ""type"": ""Http"", + ""serviceUri"": ""https://example.com/"" + } + ], + ""schemaVersion"": ""2016-08-11"" }"; #endif @@ -466,12 +525,15 @@ public void CanCreateAllStandardPipelineItems() #if NET461 , i => Assert.IsType(i) , i => Assert.IsType(i) +#endif +#if NET5_0 + , i => Assert.IsType(i) #endif ); Assert.Collection(pipeline.Sinks, s => Assert.IsType(s.Output), -#if (!NET461) +#if (!NET461 && !NET5_0) s => Assert.IsType(s.Output), #endif s => Assert.IsType(s.Output), diff --git a/test/Microsoft.Diagnostics.EventFlow.Core.Tests/MetadataTests.cs b/test/Microsoft.Diagnostics.EventFlow.Core.Tests/MetadataTests.cs index 361221e7..c03ddb4d 100644 --- a/test/Microsoft.Diagnostics.EventFlow.Core.Tests/MetadataTests.cs +++ b/test/Microsoft.Diagnostics.EventFlow.Core.Tests/MetadataTests.cs @@ -124,7 +124,7 @@ public void RequestDataReadSuccessfully() Assert.Equal("200 OK", rd.ResponseCode); // Note the TimeSpan.FromMilliseconds will round to the nearest millisecond on everything older than .NET Core 3.0 -#if NETCOREAPP3_1 +#if NETCOREAPP3_1 || NET5_0 Assert.Equal(34.7, rd.Duration.Value.TotalMilliseconds, DoublePrecisionTolerance); #else Assert.Equal(35, rd.Duration.Value.TotalMilliseconds, DoublePrecisionTolerance); @@ -155,7 +155,7 @@ public void RequestDataReadSuccessfully() eventData.Payload["duration"] = 65.7; result = RequestData.TryGetData(eventData, requestMetadata, out rd); Assert.Equal(DataRetrievalStatus.Success, result.Status); -#if NETCOREAPP3_1 +#if NETCOREAPP3_1 || NET5_0 Assert.Equal(65.7, rd.Duration.Value.TotalMilliseconds, DoublePrecisionTolerance); #else Assert.Equal(66, rd.Duration.Value.TotalMilliseconds, DoublePrecisionTolerance); diff --git a/test/Microsoft.Diagnostics.EventFlow.Core.Tests/Microsoft.Diagnostics.EventFlow.Core.Tests.csproj b/test/Microsoft.Diagnostics.EventFlow.Core.Tests/Microsoft.Diagnostics.EventFlow.Core.Tests.csproj index 719ffe66..ce4b9a65 100644 --- a/test/Microsoft.Diagnostics.EventFlow.Core.Tests/Microsoft.Diagnostics.EventFlow.Core.Tests.csproj +++ b/test/Microsoft.Diagnostics.EventFlow.Core.Tests/Microsoft.Diagnostics.EventFlow.Core.Tests.csproj @@ -1,7 +1,7 @@  - net461;netcoreapp2.1;net471;netcoreapp3.1 + net461;netcoreapp2.1;net471;netcoreapp3.1;net5.0 Microsoft.Diagnostics.EventFlow.Core.Tests true true @@ -39,6 +39,10 @@ + + + + diff --git a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/ActivitySourceInputTests.cs b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/ActivitySourceInputTests.cs new file mode 100644 index 00000000..5667e6b2 --- /dev/null +++ b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/ActivitySourceInputTests.cs @@ -0,0 +1,851 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +#if NET5_0 +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.Extensions.Configuration; +using Moq; +using Xunit; +using Validation; + +using Microsoft.Diagnostics.EventFlow.Configuration; +using Microsoft.Diagnostics.EventFlow.Inputs.ActivitySource; +using Microsoft.Diagnostics.EventFlow.TestHelpers; +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; + +namespace Microsoft.Diagnostics.EventFlow.Inputs.Tests +{ + public class ActivitySourceInputTests + { + private static readonly string SourceOneName = "EventFlow-ActivitySource-One"; + private static readonly string SourceTwoName = "EventFlow-ActivitySource-Two"; + private static readonly System.Diagnostics.ActivitySource SourceOne = new System.Diagnostics.ActivitySource(SourceOneName); + private static readonly System.Diagnostics.ActivitySource SourceTwo = new System.Diagnostics.ActivitySource(SourceTwoName); + + private static readonly string WellKnownTraceId = "0af7651916cd43dd8448eb211c80319c"; + private static readonly string WellKnownTraceIdTwo = "ce0d159c1755814f16fba28033db9940"; + private static readonly string SpanIdOne = "b7ad6b7169203331"; + private static readonly string SpanIdTwo = "00f067aa0ba902b7"; + private static readonly string SpanIdThree = "7fe76cfa20e81932"; + + [Fact] + public void BasicActivityTracking() + { + var healthReporter = new Mock(); + var observer = new TestObserver(); + var ActivityName = GetRandomName(); + + var sources = new List(new[] + { + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = ActivityName, + CapturedData = ActivitySamplingResult.AllData, + CapturedEvents = CapturedActivityEvents.Both + } + }); + + using (var input = new ActivitySourceInput(new ActivitySourceInputConfiguration { Sources = sources }, healthReporter.Object)) + { + input.Subscribe(observer); + + var ctx = new ActivityContext( + ActivityTraceId.CreateFromString(WellKnownTraceId), + ActivitySpanId.CreateFromString(SpanIdOne), + ActivityTraceFlags.None); + var activity = SourceOne.StartActivity(ActivityName, ActivityKind.Internal, ctx); + activity.Stop(); + } + + healthReporter.VerifyNoOtherCalls(); + + Assert.Equal(2, observer.Data.Count); + Assert.True(observer.Completed); + Assert.Null(observer.Error); + + Assert.True(observer.Data.TryDequeue(out EventData e)); + VerifyActivityEvent(e, ActivityName, SourceOneName, CapturedActivityEvents.Start, WellKnownTraceId, SpanIdOne); + Assert.True(observer.Data.TryDequeue(out e)); + VerifyActivityEvent(e, ActivityName, SourceOneName, CapturedActivityEvents.Stop, WellKnownTraceId, SpanIdOne); + } + + [Fact] + public void CapturedEventsSettingIsEffective() + { + var healthReporter = new Mock(); + var observer = new TestObserver(); + var ActivityNameSuffix = GetRandomName(); + + var sources = new List(new[] + { + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedEventsNone" + ActivityNameSuffix, + CapturedData = ActivitySamplingResult.AllData, + CapturedEvents = CapturedActivityEvents.None + }, + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedDataNone" + ActivityNameSuffix, + CapturedData = ActivitySamplingResult.None, + CapturedEvents = CapturedActivityEvents.Both + }, + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedEventsStart" + ActivityNameSuffix, + CapturedData = ActivitySamplingResult.AllData, + CapturedEvents = CapturedActivityEvents.Start + }, + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedEventsStop" + ActivityNameSuffix, + CapturedData = ActivitySamplingResult.AllData, + CapturedEvents = CapturedActivityEvents.Stop + }, + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedEventsBoth" + ActivityNameSuffix, + CapturedData = ActivitySamplingResult.AllData, + CapturedEvents = CapturedActivityEvents.Both + }, + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedEventsDefault" + ActivityNameSuffix, + CapturedData = ActivitySamplingResult.AllData + } + }); + + using (var input = new ActivitySourceInput(new ActivitySourceInputConfiguration { Sources = sources }, healthReporter.Object)) + { + input.Subscribe(observer); + var ctx = new ActivityContext( + ActivityTraceId.CreateFromString(WellKnownTraceId), + ActivitySpanId.CreateFromString(SpanIdOne), + ActivityTraceFlags.None); + + foreach (var s in sources) + { + var activity = SourceOne.StartActivity(s.ActivityName, ActivityKind.Internal, ctx); + activity?.Stop(); + } + } + + // We expect a warning about an ActivitySource configured with CapturedData = None, since that has no effect + healthReporter.Verify(hr => hr.ReportWarning( + It.Is(s => s.Contains("CapturedData = None", StringComparison.OrdinalIgnoreCase)), + It.Is(ctx => OrdinalEquals(ctx, EventFlowContextIdentifiers.Configuration)) + ), Times.Exactly(1)); + healthReporter.VerifyNoOtherCalls(); + + Assert.True(observer.Completed); + Assert.Null(observer.Error); + + var observed = observer.Data.ToArray(); + + // No events from CapturedEventsNone and CapturedDataNone activities + Assert.DoesNotContain(observed, o => OrdinalEquals(o.Payload["Name"], "CapturedEventsNone" + ActivityNameSuffix)); + Assert.DoesNotContain(observed, o => OrdinalEquals(o.Payload["Name"], "CapturedDataNone" + ActivityNameSuffix)); + + // Only activity start event captured for CapturedEventsStart activity + Assert.Equal(1, observed.Count(o => + OrdinalEquals(o.Payload["Name"], "CapturedEventsStart" + ActivityNameSuffix) && + !o.Payload.ContainsKey("EndTime") + )); + Assert.DoesNotContain(observed, o => + OrdinalEquals(o.Payload["Name"], "CapturedEventsStart" + ActivityNameSuffix) && + o.Payload.ContainsKey("EndTime") + ); + + // Only activity stop event captured for CapturedEventStop activity + Assert.Equal(1, observed.Count(o => + OrdinalEquals((string)o.Payload["Name"], "CapturedEventsStop" + ActivityNameSuffix) && + o.Payload.ContainsKey("EndTime") + )); + Assert.DoesNotContain(observed, o => + OrdinalEquals((string)o.Payload["Name"], "CapturedEventsStop" + ActivityNameSuffix) && + !o.Payload.ContainsKey("EndTime") + ); + + // Two events (start and stop) are captured for both CapturedEventsBoth and one for CapturedEventDefault activity + Assert.Equal(2, observed.Count(o => OrdinalEquals((string)o.Payload["Name"], "CapturedEventsBoth" + ActivityNameSuffix))); + Assert.Equal(1, observed.Count(o => OrdinalEquals((string)o.Payload["Name"], "CapturedEventsDefault" + ActivityNameSuffix))); + } + + [Fact] + public void CapturedDataSettingIsEffective() + { + var healthReporter = new Mock(); + var observer = new TestObserver(); + var ActivityNameSuffix = GetRandomName(); + + var sources = new List(new[] + { + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedDataPropagation" + ActivityNameSuffix, + CapturedData = ActivitySamplingResult.PropagationData + }, + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedDataAll" + ActivityNameSuffix, + CapturedData = ActivitySamplingResult.AllData + }, + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedDataAllRecorded" + ActivityNameSuffix, + CapturedData = ActivitySamplingResult.AllDataAndRecorded + }, + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "CapturedDataDefault" + ActivityNameSuffix + }, + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + ActivityName = "Outer" + ActivityNameSuffix + }, + }); + + string outerActivityId = null; + + using (var input = new ActivitySourceInput(new ActivitySourceInputConfiguration { Sources = sources }, healthReporter.Object)) + { + input.Subscribe(observer); + + var ctx = new ActivityContext( + ActivityTraceId.CreateFromString(WellKnownTraceId), + ActivitySpanId.CreateFromString(SpanIdOne), + ActivityTraceFlags.None); + var outer = SourceOne.StartActivity("Outer" + ActivityNameSuffix, ActivityKind.Server, ctx); + outer.AddBaggage("Outer.Baggage1", "Outer.Baggage1.Value"); + outerActivityId = outer.SpanId.ToHexString(); + + var activity = SourceOne.StartActivity("CapturedDataPropagation" + ActivityNameSuffix, ActivityKind.Internal, null, + new Dictionary + { + ["CapturedDataPropagation.Tag1"] = "CapturedDataPropagation.Tag1Value", + ["CapturedDataPropagation.Tag2"] = new NamedObject("CapturedDataPropagation.Tag2Value") + }, + new[] { + new ActivityLink(new ActivityContext( + ActivityTraceId.CreateFromString(WellKnownTraceIdTwo), + ActivitySpanId.CreateFromString(SpanIdTwo), + ActivityTraceFlags.None + )) + }); + Thread.Sleep(30); + activity.AddEvent(new ActivityEvent("CapturedDataPropagation.Event1")); + activity.Stop(); + + // Add extra baggage to outer activity and make sure it is captured by subsequent activities + outer.AddBaggage("Outer.Baggage2", "Outer.Baggage2.Value"); + + activity = SourceOne.StartActivity("CapturedDataAll" + ActivityNameSuffix, ActivityKind.Internal, null, + new Dictionary + { + ["CapturedDataAll.Tag1"] = "CapturedDataAll.Tag1Value", + ["CapturedDataAll.Tag2"] = new NamedObject("CapturedDataAll.Tag2Value") + }, + new[] { + new ActivityLink(new ActivityContext( + ActivityTraceId.CreateFromString(WellKnownTraceIdTwo), + ActivitySpanId.CreateFromString(SpanIdThree), + ActivityTraceFlags.None + )) + }); + Thread.Sleep(30); + activity.AddEvent(new ActivityEvent("CapturedDataAll.Event1")); + activity.Stop(); + + activity = SourceOne.StartActivity("CapturedDataAllRecorded" + ActivityNameSuffix, ActivityKind.Internal, null, + new Dictionary + { + ["CapturedDataAllRecorded.Tag1"] = "CapturedDataAllRecorded.Tag1Value", + ["CapturedDataAllRecorded.Tag2"] = new NamedObject("CapturedDataAllRecorded.Tag2Value") + }, + new[] { + new ActivityLink(new ActivityContext( + ActivityTraceId.CreateFromString(WellKnownTraceIdTwo), + ActivitySpanId.CreateFromString(SpanIdThree), + ActivityTraceFlags.None + )) + }); + Thread.Sleep(30); + activity.AddEvent(new ActivityEvent("CapturedDataAllRecorded.Event1")); + activity.Stop(); + + ctx = new ActivityContext( + ActivityTraceId.CreateFromString(WellKnownTraceId), + ActivitySpanId.CreateFromString(outerActivityId), + ActivityTraceFlags.None); + activity = SourceOne.StartActivity("CapturedDataDefault" + ActivityNameSuffix, ActivityKind.Internal, null, + new Dictionary + { + ["CapturedDataDefault.Tag1"] = "CapturedDataDefault.Tag1Value", + ["CapturedDataDefault.Tag2"] = new NamedObject("CapturedDataDefault.Tag2Value") + }, + new[] { + new ActivityLink(new ActivityContext( + ActivityTraceId.CreateFromString(WellKnownTraceIdTwo), + ActivitySpanId.CreateFromString(SpanIdTwo), + ActivityTraceFlags.None + )) + }); + Thread.Sleep(30); + activity.AddEvent(new ActivityEvent("CapturedDataDefault.Event1")); + activity.Stop(); + + outer.Stop(); + } + + healthReporter.VerifyNoOtherCalls(); + + // We expect one event the outer activity and 4 (stop) events for the inner ones + Assert.Equal(5, observer.Data.Count); + Assert.True(observer.Completed); + Assert.Null(observer.Error); + + var observed = observer.Data.ToArray(); + + // CapturedData == Propaation should capture baggage, but not links, tags, or events + var ae = observed.First(e => + OrdinalEquals(e.Payload["Name"], "CapturedDataPropagation" + ActivityNameSuffix) + && e.Payload.ContainsKey("EndTime")); + VerifyActivityEvent(ae, + "CapturedDataPropagation" + ActivityNameSuffix, SourceOneName, CapturedActivityEvents.Stop, + WellKnownTraceId, outerActivityId, false, ActivityKind.Internal, + new Dictionary> { + ["Outer.Baggage1"] = (val) => OrdinalEquals(val, "Outer.Baggage1.Value") + }, + new[] { "Links", "Tags", "Events" }); + + // CaptureDataAll and CaptureDataAllRecorded should have baggage, links, tags, and events + // They only differ by IsRecorded flag value + ae = observed.First(e => + OrdinalEquals(e.Payload["Name"], "CapturedDataAll" + ActivityNameSuffix) + && e.Payload.ContainsKey("EndTime")); + VerifyActivityEvent(ae, + "CapturedDataAll" + ActivityNameSuffix, SourceOneName, CapturedActivityEvents.Stop, + WellKnownTraceId, outerActivityId, false, ActivityKind.Internal, + new Dictionary> + { + ["Outer.Baggage1"] = (val) => OrdinalEquals(val, "Outer.Baggage1.Value"), + ["Outer.Baggage2"] = (val) => OrdinalEquals(val, "Outer.Baggage2.Value"), + ["Events"] = (evnts) => + { + Assert.Collection((IEnumerable) evnts, e => Assert.Equal("CapturedDataAll.Event1", e.Name, StringComparer.Ordinal)); + return true; + }, + ["CapturedDataAll.Tag1"] = (val) => OrdinalEquals(val, "CapturedDataAll.Tag1Value"), + ["CapturedDataAll.Tag2"] = (val) => OrdinalEquals(((NamedObject) val).Name, "CapturedDataAll.Tag2Value"), + ["Links"] = (links) => + { + Assert.Collection((IEnumerable)links, + l => Assert.True( + OrdinalEquals(l.Context.TraceId.ToHexString(), WellKnownTraceIdTwo) && + OrdinalEquals(l.Context.SpanId.ToHexString(), SpanIdThree)) + ); + return true; + } + } + ); + + ae = observed.First(e => + OrdinalEquals(e.Payload["Name"], "CapturedDataAllRecorded" + ActivityNameSuffix) + && e.Payload.ContainsKey("EndTime")); + VerifyActivityEvent(ae, + "CapturedDataAllRecorded" + ActivityNameSuffix, SourceOneName, CapturedActivityEvents.Stop, + WellKnownTraceId, outerActivityId, true, ActivityKind.Internal, + new Dictionary> + { + ["Outer.Baggage1"] = (val) => OrdinalEquals(val, "Outer.Baggage1.Value"), + ["Outer.Baggage2"] = (val) => OrdinalEquals(val, "Outer.Baggage2.Value"), + ["Events"] = (evnts) => + { + Assert.Collection((IEnumerable)evnts, e => Assert.Equal("CapturedDataAllRecorded.Event1", e.Name, StringComparer.Ordinal)); + return true; + }, + ["CapturedDataAllRecorded.Tag1"] = (val) => OrdinalEquals(val, "CapturedDataAllRecorded.Tag1Value"), + ["CapturedDataAllRecorded.Tag2"] = (val) => OrdinalEquals(((NamedObject)val).Name, "CapturedDataAllRecorded.Tag2Value"), + ["Links"] = (links) => + { + Assert.Collection((IEnumerable)links, + l => Assert.True( + OrdinalEquals(l.Context.TraceId.ToHexString(), WellKnownTraceIdTwo) && + OrdinalEquals(l.Context.SpanId.ToHexString(), SpanIdThree)) + ); + return true; + } + } + ); + + // The default for data capturing is AllData, so all data should be captured, but the recording flag should not be set. + ae = observed.First(e => + OrdinalEquals(e.Payload["Name"], "CapturedDataDefault" + ActivityNameSuffix) + && e.Payload.ContainsKey("EndTime")); + VerifyActivityEvent(ae, + "CapturedDataDefault" + ActivityNameSuffix, SourceOneName, CapturedActivityEvents.Stop, + WellKnownTraceId, outerActivityId, false, ActivityKind.Internal, + new Dictionary> + { + ["Outer.Baggage1"] = (val) => OrdinalEquals(val, "Outer.Baggage1.Value"), + ["Outer.Baggage2"] = (val) => OrdinalEquals(val, "Outer.Baggage2.Value"), + ["Events"] = (evnts) => + { + Assert.Collection((IEnumerable)evnts, e => Assert.Equal("CapturedDataDefault.Event1", e.Name, StringComparer.Ordinal)); + return true; + }, + ["CapturedDataDefault.Tag1"] = (val) => OrdinalEquals(val, "CapturedDataDefault.Tag1Value"), + ["CapturedDataDefault.Tag2"] = (val) => OrdinalEquals(((NamedObject)val).Name, "CapturedDataDefault.Tag2Value"), + ["Links"] = (links) => + { + Assert.Collection((IEnumerable)links, + l => Assert.True( + OrdinalEquals(l.Context.TraceId.ToHexString(), WellKnownTraceIdTwo) && + OrdinalEquals(l.Context.SpanId.ToHexString(), SpanIdTwo)) + ); + return true; + } + } + ); + + // Phew! That was a lot of checking :-) + } + + [Fact] + public void CaptureEverythingConfigurationWorks() + { + var healthReporter = new Mock(); + var observer = new TestObserver(); + var ActivityNameA = GetRandomName(); + var ActivityNameB = GetRandomName(); + + var sources = new List(new[] + { + // Empty configuration means capture all activities from all sources, + // with all data, and both start and stop events + new ActivitySourceConfiguration {} + }); + + using (var input = new ActivitySourceInput(new ActivitySourceInputConfiguration { Sources = sources }, healthReporter.Object)) + { + input.Subscribe(observer); + + var activity = SourceOne.StartActivity(ActivityNameA); + activity.Stop(); + + activity = SourceTwo.StartActivity(ActivityNameB); + activity.AddEvent(new ActivityEvent("ActivityB.Event1")); + activity.Stop(); + } + + healthReporter.VerifyNoOtherCalls(); + + Assert.True(observer.Completed); + Assert.Null(observer.Error); + + var observed = observer.Data.ToArray(); + + // We expect one stop evente for both activities, total 2. + // There might be more because we are capturing, well, everyting in the system %-) + Assert.Equal(2, observed.Count(e => + (OrdinalEquals(e.Payload["Name"], ActivityNameA) && OrdinalEquals(e.Payload["ActivitySourceName"], SourceOneName)) || + (OrdinalEquals(e.Payload["Name"], ActivityNameB) && OrdinalEquals(e.Payload["ActivitySourceName"], SourceTwoName)) + )); + + // Verify that all data was captured by checking for activity event associated with activity B + var activityB_Stop = observed.First(e => + OrdinalEquals(e.Payload["Name"], ActivityNameB) && + OrdinalEquals(e.Payload["ActivitySourceName"], SourceTwoName) && + e.Payload.ContainsKey("EndTime") + ); + Assert.Equal(1, ((IEnumerable)activityB_Stop.Payload["Events"]).Count(ae => OrdinalEquals(ae.Name, "ActivityB.Event1"))); + } + + [Fact] + public void AllActivitiesCapturedWhenActivityNameIsOmitted() + { + var healthReporter = new Mock(); + var observer = new TestObserver(); + var ActivityNameA = GetRandomName(); + var ActivityNameB = GetRandomName(); + var ActivityNameC = GetRandomName(); + + var sources = new List(new[] + { + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + CapturedEvents = CapturedActivityEvents.Stop + } + }); + + using (var input = new ActivitySourceInput(new ActivitySourceInputConfiguration { Sources = sources }, healthReporter.Object)) + { + input.Subscribe(observer); + + var activity = SourceOne.StartActivity(ActivityNameA); + activity.Stop(); + + activity = SourceOne.StartActivity(ActivityNameB); + activity.Stop(); + + activity = SourceTwo.StartActivity(ActivityNameC); + Assert.Null(activity); // Nobody is listening to source 2 + } + + healthReporter.VerifyNoOtherCalls(); + + Assert.True(observer.Completed); + Assert.Null(observer.Error); + + var observed = observer.Data.ToArray(); + + Assert.Equal(2, observed.Count()); + Assert.Equal(1, observed.Count(e => OrdinalEquals(e.Payload["Name"], ActivityNameA))); + Assert.Equal(1, observed.Count(e => OrdinalEquals(e.Payload["Name"], ActivityNameB))); + } + + [Fact] + public void AllSourcesCapturedWhenActivitySourceNameIsOmitted() + { + var healthReporter = new Mock(); + var observer = new TestObserver(); + var ActivityNameA = GetRandomName(); + var ActivityNameB = GetRandomName(); + var ActivityNameC = GetRandomName(); + + var sources = new List(new[] + { + new ActivitySourceConfiguration + { + ActivityName = ActivityNameA, + CapturedEvents = CapturedActivityEvents.Stop + } + }); + + using (var input = new ActivitySourceInput(new ActivitySourceInputConfiguration { Sources = sources }, healthReporter.Object)) + { + input.Subscribe(observer); + + var activity = SourceOne.StartActivity(ActivityNameA); + activity.Stop(); + + activity = SourceOne.StartActivity(ActivityNameB); + Assert.Null(activity); // Nobody is listening + + activity = SourceTwo.StartActivity(ActivityNameA); + activity.Stop(); + + activity = SourceTwo.StartActivity(ActivityNameC); + Assert.Null(activity); + } + + healthReporter.VerifyNoOtherCalls(); + + Assert.True(observer.Completed); + Assert.Null(observer.Error); + + var observed = observer.Data.ToArray(); + + Assert.Equal(2, observed.Count()); + Assert.All(observed, e => Assert.True(OrdinalEquals(e.Payload["Name"], ActivityNameA))); + } + + [Fact] + public void WarningIssuedIfNoSources() + { + var healthReporter = new Mock(); + var input = new ActivitySourceInput( + new ActivitySourceInputConfiguration { Sources = new List() }, + healthReporter.Object); + input.Dispose(); + + healthReporter.Verify(hr => hr.ReportWarning( + It.Is(s => s.Contains("no data sources", StringComparison.OrdinalIgnoreCase)), + It.Is(ctx => OrdinalEquals(ctx, EventFlowContextIdentifiers.Configuration)) + ), Times.Exactly(1)); + healthReporter.VerifyNoOtherCalls(); + } + + [Fact] + public void CanReadJsonConfiguration() + { + string inputConfiguration = @" + { + ""type"": ""ActivitySource"", + ""sources"": [ + { + ""ActivitySourceName"": ""Alpha"" + }, + { + ""ActivityName"": ""SuperImportant"" + }, + { + ""ActivitySourceName"": ""Bravo"", + ""ActivityName"": ""BravoOne"" + }, + { + ""ActivitySourceName"": ""Bravo"", + ""ActivityName"": ""BravoTwo"", + ""CapturedData"": ""PropagationData"", + ""CapturedEvents"": ""Start"" + }, + ] + } + "; + + using (var configFile = new TemporaryFile()) + { + configFile.Write(inputConfiguration); + var cb = new ConfigurationBuilder(); + cb.AddJsonFile(configFile.FilePath); + var configuration = cb.Build(); + + var healthReporter = new Mock(); + var input = new ActivitySourceInput(configuration, healthReporter.Object); + + healthReporter.VerifyNoOtherCalls(); + Assert.Collection(input.Configuration.Sources, + sc => { + Assert.Equal("Alpha", sc.ActivitySourceName, StringComparer.Ordinal); + Assert.True(string.IsNullOrEmpty(sc.ActivityName)); + Assert.Equal(ActivitySamplingResult.AllData, sc.CapturedData); + Assert.Equal(CapturedActivityEvents.Stop, sc.CapturedEvents); + }, + sc => { + Assert.True(string.IsNullOrEmpty(sc.ActivitySourceName)); + Assert.Equal("SuperImportant", sc.ActivityName, StringComparer.Ordinal); + Assert.Equal(ActivitySamplingResult.AllData, sc.CapturedData); + Assert.Equal(CapturedActivityEvents.Stop, sc.CapturedEvents); + }, + sc => { + Assert.Equal("Bravo", sc.ActivitySourceName, StringComparer.Ordinal); + Assert.Equal("BravoOne", sc.ActivityName, StringComparer.Ordinal); + Assert.Equal(ActivitySamplingResult.AllData, sc.CapturedData); + Assert.Equal(CapturedActivityEvents.Stop, sc.CapturedEvents); + }, + sc => { + Assert.Equal("Bravo", sc.ActivitySourceName, StringComparer.OrdinalIgnoreCase); + Assert.Equal("BravoTwo", sc.ActivityName, StringComparer.Ordinal); + Assert.Equal(ActivitySamplingResult.PropagationData, sc.CapturedData); + Assert.Equal(CapturedActivityEvents.Start, sc.CapturedEvents); + } + ); + } + } + + [Fact] + public void BaggageTakesPrecedenceOverTags() + { + var healthReporter = new Mock(); + var observer = new TestObserver(); + var ActivityName = GetRandomName(); + + var sources = new List(new[] + { + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + CapturedEvents = CapturedActivityEvents.Stop + } + }); + + using (var input = new ActivitySourceInput(new ActivitySourceInputConfiguration { Sources = sources }, healthReporter.Object)) + { + input.Subscribe(observer); + + var activity = SourceOne.StartActivity(ActivityName); + activity.AddTag("Alpha", "AlphaTag"); + activity.AddBaggage("Alpha", "AlphaBaggage"); + activity.Stop(); + } + + // Complaining about Alpha property name conflict + healthReporter.Verify(hr => hr.ReportWarning( + It.Is(s => s.Contains("Alpha", StringComparison.OrdinalIgnoreCase)), + It.Is(ctx => OrdinalEquals(ctx, nameof(ActivitySourceInput))) + ), Times.Exactly(1)); + healthReporter.VerifyNoOtherCalls(); + + Assert.True(observer.Completed); + Assert.Null(observer.Error); + + var observed = observer.Data.ToArray(); + var e = Assert.Single(observed); + Assert.True(OrdinalEquals(e.Payload["Alpha"], "AlphaBaggage") && OrdinalEquals(e.Payload["Alpha_1"], "AlphaTag")); + } + + [Fact] + public void SourceCanBeCreatedAfterInput() + { + var healthReporter = new Mock(); + var observer = new TestObserver(); + var ActivityName = GetRandomName(); + + const string TestSourceName = "EventFlowTestActivitySource"; + var sources = new List(new[] + { + new ActivitySourceConfiguration + { + ActivitySourceName = TestSourceName, + CapturedEvents = CapturedActivityEvents.Stop + } + }); + + using (var input = new ActivitySourceInput(new ActivitySourceInputConfiguration { Sources = sources }, healthReporter.Object)) + { + input.Subscribe(observer); + + using (var activitySouce = new System.Diagnostics.ActivitySource("EventFlowTestActivitySource")) + { + var activity = activitySouce.StartActivity(ActivityName); + activity.Stop(); + } + } + + healthReporter.VerifyNoOtherCalls(); + + Assert.True(observer.Completed); + Assert.Null(observer.Error); + + var observed = observer.Data.ToArray(); + var e = Assert.Single(observed); + Assert.Equal(ActivityName, e.Payload["Name"]); + } + + [Fact] + public void SubscriptionsAreDisposedUponInputDisposal() + { + var healthReporter = new Mock(); + var observer = new TestObserver(); + var ActivityName = GetRandomName(); + + var sources = new List(new[] + { + new ActivitySourceConfiguration + { + ActivitySourceName = SourceOneName, + CapturedEvents = CapturedActivityEvents.Stop + } + }); + + using (var input = new ActivitySourceInput(new ActivitySourceInputConfiguration { Sources = sources }, healthReporter.Object)) + { + input.Subscribe(observer); + } + + // The input is configured to listen for all activities, but it has been disposed, so no one should be listening anymore + // and no activity should be created. + Assert.Null(SourceOne.StartActivity(ActivityName)); + + Assert.True(observer.Completed); + Assert.Null(observer.Error); + Assert.Empty(observer.Data); + } + + private void VerifyActivityEvent( + EventData e, + string activityName, + string activitySourceName, + CapturedActivityEvents activityEventType, + string traceId, + string parentSpanId, + bool recorded = false, + ActivityKind activityKind = ActivityKind.Internal, + IDictionary> requiredProps = null, + IEnumerable nonexistentProps = null + ) { + Assert.Equal(activityName, e.Payload["Name"]); + Assert.Equal(activitySourceName, e.Payload["ActivitySourceName"]); + Assert.True(e.Payload.ContainsKey("StartTime")); + if (activityEventType == CapturedActivityEvents.Stop) + { + Assert.True(e.Payload.ContainsKey("EndTime")); + } + Assert.Equal(traceId, e.Payload["TraceId"]); + Assert.Equal(parentSpanId, e.Payload["ParentSpanId"]); + Assert.Equal(recorded, e.Payload["IsRecording"]); + Assert.Equal(activityKind, Enum.Parse(typeof(ActivityKind), (string) e.Payload["SpanKind"])); + + if (requiredProps != null) + { + foreach(var p in requiredProps) + { + Assert.True(p.Value(e.Payload[p.Key]), $"Assertion failed for activity '{activityName}': property '{p.Key}' has unexpected value: '{e.Payload[p.Key]}'"); + } + } + + if (nonexistentProps != null) + { + foreach(var np in nonexistentProps) + { + Assert.DoesNotContain(e.Payload, p => p.Key.Equals(np, StringComparison.Ordinal)); + } + } + } + + private bool OrdinalEquals(object o1, object o2) => StringComparer.Ordinal.Equals(o1, o2); + + private class NamedObject + { + public string Name { get; private set; } + + public NamedObject(string name) + { + Requires.NotNull(name, nameof(name)); + Name = name; + } + + public override bool Equals(object obj) + { + NamedObject other = obj as NamedObject; + if (other == null) return false; + return StringComparer.Ordinal.Equals(Name, other.Name); + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + } + + // To ensure that tests can be run in parallel we might use random activity source and activity names for each execution + private readonly Random random_ = new Random(); + private string GetRandomName() + { + const int Len = 8; + const char Offset = 'a'; + const int Range = 26; // a..z: length = 26 + + var builder = new StringBuilder(Len); + for (var i = 0; i < Len; i++) + { + var c = (char)random_.Next(Offset, Offset + Range); + builder.Append(c); + } + + return builder.ToString(); + } + } +} + +#endif \ No newline at end of file diff --git a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EtwInputTests.cs b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EtwInputTests.cs index 23602177..004a41d0 100644 --- a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EtwInputTests.cs +++ b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EtwInputTests.cs @@ -248,27 +248,5 @@ private void VerifyNoErrorsOrWarnings(Mock healthReporterMock) healthReporterMock.Verify(o => o.ReportProblem(It.IsAny(), It.IsAny()), Times.Exactly(0)); } - - private class TestObserver : IObserver - { - public bool Completed { get; private set; } = false; - public Exception Error { get; private set; } - public ConcurrentQueue Data { get; } = new ConcurrentQueue(); - - public void OnCompleted() - { - Completed = true; - } - - public void OnError(Exception error) - { - Error = error; - } - - public void OnNext(EventData value) - { - Data.Enqueue(value); - } - } } } diff --git a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EventSourceInputTests.cs b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EventSourceInputTests.cs index f7f54797..8f6665ae 100644 --- a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EventSourceInputTests.cs +++ b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/EventSourceInputTests.cs @@ -229,7 +229,7 @@ public async Task CapturesEventCountersFromEventSource() Assert.Equal("EventCounters", data.Payload["EventName"]); Assert.Equal("testCounter", data.Payload["Name"]); Assert.Equal(2, data.Payload["Count"]); -#if NETCOREAPP3_1 +#if NETCOREAPP3_1 || NET5_0 Assert.Equal((double)5, data.Payload["Max"]); Assert.Equal((double)1, data.Payload["Min"]); #else diff --git a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/Microsoft.Diagnostics.EventFlow.Inputs.Tests.csproj b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/Microsoft.Diagnostics.EventFlow.Inputs.Tests.csproj index 6963c4a7..e9e82f91 100644 --- a/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/Microsoft.Diagnostics.EventFlow.Inputs.Tests.csproj +++ b/test/Microsoft.Diagnostics.EventFlow.Inputs.Tests/Microsoft.Diagnostics.EventFlow.Inputs.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;net471;netcoreapp3.1 + netcoreapp2.1;net471;netcoreapp3.1;net5.0 portable Microsoft.Diagnostics.EventFlow.Inputs.Tests true @@ -45,6 +45,10 @@ + + + + diff --git a/test/TestHelpers/FirstChanceExceptionCounter.cs b/test/TestHelpers/FirstChanceExceptionCounter.cs index 482a5a65..09b88d8d 100644 --- a/test/TestHelpers/FirstChanceExceptionCounter.cs +++ b/test/TestHelpers/FirstChanceExceptionCounter.cs @@ -12,16 +12,16 @@ namespace Microsoft.Diagnostics.EventFlow.TestHelpers public class FirstChanceExceptionCounter : IDisposable { private Type exceptionType; - private int count; - private bool disposed; + private int count = 0; +#if !NETSTANDARD1_6 + private bool disposed = false; +#endif private int threadId; public FirstChanceExceptionCounter(Type exceptionType) { this.exceptionType = exceptionType; - this.count = 0; - this.disposed = false; this.threadId = Thread.CurrentThread.ManagedThreadId; // First-chance exception tracking is not supported with NETSTANDARD1.6, @@ -56,8 +56,9 @@ public void Dispose() { AppDomain.CurrentDomain.FirstChanceException -= OnException; } -#endif + this.disposed = true; +#endif } } } diff --git a/test/TestHelpers/TestObserver.cs b/test/TestHelpers/TestObserver.cs new file mode 100644 index 00000000..e297fe88 --- /dev/null +++ b/test/TestHelpers/TestObserver.cs @@ -0,0 +1,32 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Collections.Concurrent; + +namespace Microsoft.Diagnostics.EventFlow.TestHelpers +{ + public class TestObserver : IObserver + { + public bool Completed { get; private set; } = false; + public Exception Error { get; private set; } + public ConcurrentQueue Data { get; } = new ConcurrentQueue(); + + public void OnCompleted() + { + Completed = true; + } + + public void OnError(Exception error) + { + Error = error; + } + + public void OnNext(EventData value) + { + Data.Enqueue(value); + } + } +}