OpenTelemetry is set up in a .net REST api project with background services to trace calls to both the endpoints and service activities.
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.3.2" />
<PackageReference Include="OpenTelemetry.Exporter.Jaeger" Version="1.3.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc9.11" />
services.AddOpenTelemetry()
.ConfigureResource(resourceBuilder =>
{
resourceBuilder.AddService(
serviceName: "main service name",
serviceVersion: "1",
serviceInstanceId: Environment.MachineName);
})
.WithTracing(tracerProviderBuilder =>
{
tracerProviderBuilder
.SetSampler(new AlwaysOnSampler())
.AddSource(CustomActivitySources.MessageServiceSource.Name)
.AddProcessor<TracerEndPointsFilterProcessor>()
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation();
Without .AddSource, the endpoints tracing is working fine. Adding that feature stops producing trace for any requests to any endpoint.
To my understanding, these are 2 separate concerns and shouldn't affect each other.
My main question is how to fix this, to work in both cases (adding sources and filtering them)?
Why is this happening in the first place? is something wrong with the implementation?
Any pointers are appreciated.
Relevant code snippets:
TracerEndPointsFilterProcessor is a cutom processor to filter routes out from tracing:
internal sealed class TracerEndPointsFilterProcessor : BaseProcessor<Activity>
{
public override void OnEnd(Activity activity)
{
if (IsHealthOrMetricsEndpoint(activity.DisplayName))
{
activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded;
}
}
private static bool IsHealthOrMetricsEndpoint(string displayName)
{
if (string.IsNullOrEmpty(displayName))
{
return false;
}
return
displayName.StartsWith("/version", StringComparison.CurrentCultureIgnoreCase) ||
displayName.StartsWith("/health", StringComparison.CurrentCultureIgnoreCase) ||
displayName.StartsWith("/swagger", StringComparison.CurrentCultureIgnoreCase);
}
Where CustomActivitySources is an extension used to dynamically hook up activities
internal static class CustomActivitySources
{
internal static readonly ActivitySource MessageServiceSource = new("MessageService");
static CustomActivitySources()
{
ActivitySource.AddActivityListener(new ActivityListener()
{
ShouldListenTo = _ => true,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
});
}
}
for example, any non-http api related service would use it like this (in case that matters)
var initialTags = new ActivityTagsCollection
{
//some tags
};
const string activityName = "ServiceName";
activity = CustomActivitySources.MessageServiceSource.StartActivity(activityName,
ActivityKind.Consumer,
"trace id", initialTags);
Using AllDataAndRecorded instead of AllData for the sampling worked to trace both services and endpoints
Related
I currently have this web app in which I need to use a custom authentication/authorization middleware. We're registering the routes using minimal APIs like so:
// snip
app.UseEndpoints(x => {
x.MapGet("/api/something", () => { ... returns something ... });
})
// snip
Our custom middleware requires us to configure a list of all the paths that need to be authorized to certain types of users and those that allow for anonymous access. But what I would really like to do is define extension methods that would work similarly to how AllowAnonymous() and RequiresAuthorization(...) work:
// snip
app.UseEndpoints(x => {
x.MapGet("/api/something", () => { ... returns something ... }).RequiresAuthorization("Admins");
})
// snip
This way I can avoid having to remember to update the configuration values in the middleware, while also having the auth information in the same place as the route definition. So far I can't seem to figure out what to set up in the extension method to be able to then inspect the context of each request inside the custom middleware to know if the route should have auth or not. I'm guessing I have to set some sort of metadata on the route, but I'm not sure what or where to set it up. I tried checking the source code for asp net core, but it's so meta and indirect that you really never know what anything actually does in that code. Any guidance or pointers would be very much appreciated :D
You can just add any metadata and analyze it in your custom middleware:
public class SomeCustomMeta
{
public string Meta { get; set; }
}
app.MapGet("/test", () => Results.Ok("Hello World!"))
.WithMetadata(new SomeCustomMeta{ Meta = "Test"});
And in the middleware:
public async Task InvokeAsync(HttpContext context)
{
if(context.GetEndpoint()?.Metadata.GetMetadata<SomeCustomMeta>() is { } meta)
{
Console.WriteLine($"SomeCustomMeta: {meta.Meta}");
}
await _next(context);
}
The WithMetadata... call can be moved to nice extension method:
public static class BuilderExts
{
public static TBuilder RequireCustomAuth<TBuilder>(this TBuilder builder, string meta) where TBuilder : IEndpointConventionBuilder
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return builder.WithMetadata(new SomeCustomMeta { Meta = meta });
}
}
I recently added authentication to my development database, authenticating against the "admin" database, and using a username/password combination in my connection string, e.g. mongodb://username:password#server:27017. Almost immediately I started seeing connections failing to open with an exception showing "Server sent an invalid nonce". To try and mitigate the problem I looked at the lifetime of my IMongoClient objects, and moved from instantiating many such objects to using a Singleton injected into my business services using the Microsoft.Extensions.DependencyInjection libraries. This hasn't alleviated the issue. I set up the MongoClient in my Startup.cs using .AddSingleton<IMongoClient>(factory => new MongoClient(Configuration["Storage:MongoConnectionString"])). I know that the connection string is correct as it works in MongoDB Compass, and also because the first couple of calls through the driver work successfully; the issue starts occuring when multiple concurrent calls are in progress.
I am using the MongoDB .NET driver, version 2.11.0, under .NET Core 3.1.2. The problem occurs in my local environment running Windows 10, and also my staging environment running inside Docker on VMware Photon.
There are two components of the app that make connnections to MongoDB, both of which are ASP.Net Core applications, one serving an API for interactive usage of my applicaiton, and one running a Quartz scheduler for background processing. I'm running MongoDB 4.4.0 Community inside a Docker container.
My references to include the driver are:
<PackageReference Include="MongoDB.Bson" Version="2.11.0" />
<PackageReference Include="MongoDB.Driver" Version="2.11.0" />
<PackageReference Include="MongoDB.Driver.Core" Version="2.11.0" />
According to this post on the MongoDB Jira site I'm not the first person to experience this issue. Mathias Lorenzen suggested in the issue on Jira that he had reduced the number of errors he encountered with various fixes including recreating the user, using SCRAM-SHA-1, and increasing the maximum number of connections permitted on the server. With these changes in place, the issue still occurs for me.
I'm guessing that the problem is related to threading when used in conjunction with database authentication. I can't, for obvious reasons, put this code into production use by disabling authentication to work around the problem, and equally the use of a synchronous model rather than async seems counter-productive. What steps can I take to try and resolve the authentication issues? Is this likely to be a bug in the Mongo C# driver, or am I simply using it wrong.
Insight on what I can try next, or alternative approaches, would be gratefully received.
Edit: Minimum reproducible example as requested:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
namespace MongoDbIssueExample
{
internal class Program
{
private static IServiceProvider services;
private static IServiceProvider BuildDependencyInjector()
{
services = new ServiceCollection()
.AddSingleton<TestThingsService>()
.AddSingleton<IMongoClient>(factory => new MongoClient("mongodb://username:password#server:27017"))
.BuildServiceProvider();
return services;
}
private static async Task DoSeed()
{
var service = services.GetService<TestThingsService>();
// Don't do these async as we'll never get any data in...
service.CreateTestThings().Wait();
service.CreateOtherTestThings().Wait();
}
private static async Task DoTest()
{
var service = services.GetService<TestThingsService>();
var things = service.GetTestThings();
var otherThings = service.GetOtherTestThings();
Task.WaitAll(things, otherThings);
}
private static async Task Main(string[] args)
{
BuildDependencyInjector();
await DoTest();
}
}
public class TestThingsService
{
private readonly IMongoClient _client;
private readonly IMongoDatabase _database;
private readonly IMongoCollection<OtherTestThing> _otherTestThingsCollection;
private readonly IMongoCollection<TestThing> _testThingsCollection;
public TestThingsService(IMongoClient client)
{
_client = client;
_database = _client.GetDatabase("Things");
_testThingsCollection = _database.GetCollection<TestThing>("TestThings");
_otherTestThingsCollection = _database.GetCollection<OtherTestThing>("OtherTestThings");
}
public async Task CreateOtherTestThings()
{
for (var item = 1; item <= 10000; item++)
{
var testThing = new OtherTestThing {Id = item, Name = $"Other thing no. {item}", WhenCreated = DateTime.UtcNow};
await _otherTestThingsCollection.ReplaceOneAsync(f => f.Id == item, testThing, new ReplaceOptions {IsUpsert = true});
}
}
public async Task CreateTestThings()
{
for (var item = 1; item <= 10000; item++)
{
var testThing = new TestThing {Id = item, Name = $"Thing no. {item}", WhenCreated = DateTime.UtcNow};
await _testThingsCollection.ReplaceOneAsync(f => f.Id == item, testThing, new ReplaceOptions {IsUpsert = true});
}
}
public async Task<List<OtherTestThing>> GetOtherTestThings()
{
return await _otherTestThingsCollection.Find(_ => true).ToListAsync();
}
public async Task<List<TestThing>> GetTestThings()
{
return await _testThingsCollection.Find(_ => true).ToListAsync();
}
}
public class OtherTestThing
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime WhenCreated { get; set; }
}
public class TestThing
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime WhenCreated { get; set; }
}
}
Requires references as follows:
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.6" />
<PackageReference Include="MongoDB.Bson" Version="2.11.0" />
<PackageReference Include="MongoDB.Driver" Version="2.11.0" />
<PackageReference Include="MongoDB.Driver.Core" Version="2.11.0" />
I also hit this issue recently, and was able to use your test code to reproduce the problem locally. I debugged the mongo c# driver and found the issue to be a race condition using a feature designed to piggyback part of the authentication protocol onto an initial server discovery (isMaster) request -- part of that authentication state ends up being stored in a global authenticator object and multiple concurrent authentication requests use the last seen state, which means that one request sees the nonce generated for the other request which is mismatched with the server response, leading to the exception being thrown.
There is a more recent bug report in the mongo bug tracker which contains the details. I have also fixed the issue in this fork of the mongo c# driver by associating the authentication state with the discovery request. I am using a build of the forked mongo driver to work around the issue (you can create one of these by running build --target=Package in the forked repository).
This issue has been resolved in a newer version of the MongoDB driver for C#, version 2.11.2.
Minimum code is:
var client = new MongoClient(_connection_string);;
var database = client.GetDatabase(Database_name);
var collection = database.GetCollection<T>(Collection_name);
var document_count = collection.EstimatedDocumentCount();
remove the last row, it works correctly.
environment: .Net Core SDK 3.1.401, Mongodb.Driver 2.11.0, using Sharding Mongodb 4.4.
I recently upgraded from NServiceBus 5x to 6.0.0-beta0004 to be able to host a ASP.NET Core application (whose main function is to listen to NServiceBus messages). I'm having problems with the startup of the host as the endpoint doesn't seem to subscribe to the publisher.
I am using the pubsub example to fix the problem. Apart from the projects that are in there by default, I added one extra project, a fresh ASP.NET Core project (based on the full .NET framework). I tried to use the exact same NServiceBus configuration, but instead of using the app/web.config, I am using the following configuration:
public class ConfigurationSource : IConfigurationSource
{
public T GetConfiguration<T>() where T : class, new()
{
UnicastBusConfig config = new UnicastBusConfig()
{
MessageEndpointMappings = new MessageEndpointMappingCollection()
};
var endpointMapping = new MessageEndpointMapping
{
AssemblyName = "Shared",
Endpoint = "Samples.PubSub.MyPublisher"
};
config.MessageEndpointMappings.Add(endpointMapping);
return config as T;
}
}
The Startup class was extended with the following code:
public IConfigurationRoot Configuration
{
get;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
services.AddMvc();
services.AddSingleton(this.GetBus());
}
private IEndpointInstance GetBus()
{
LogManager.Use<DefaultFactory> ().Level(NServiceBus.Logging.LogLevel.Info);
var endpointConfiguration = new EndpointConfiguration("Samples.PubSub.Subscriber3");
endpointConfiguration.UseSerialization<JsonSerializer>();
endpointConfiguration.DisableFeature<AutoSubscribe>();
endpointConfiguration.UsePersistence<InMemoryPersistence>();
endpointConfiguration.SendFailedMessagesTo("error");
endpointConfiguration.EnableInstallers();
// Skip web.config settings and use programmatic approach
endpointConfiguration.CustomConfigurationSource(new ConfigurationSource());
var endpointInstance = Endpoint.Start(endpointConfiguration).Result;
endpointInstance.Subscribe<IMyEvent>().Wait();
return endpointInstance;
}
The Message Handler is identical to the other Subscriber projects in the solution:
public class EventMessageHandler : IHandleMessages<IMyEvent>
{
static ILog log = LogManager.GetLogger<EventMessageHandler>();
public Task Handle(IMyEvent message, IMessageHandlerContext context)
{
log.Info($"Subscriber 2 received IEvent with Id {message.EventId}.");
log.Info($"Message time: {message.Time}.");
log.Info($"Message duration: {message.Duration}.");
return Task.FromResult(0);
}
}
If I run the sample, I notice Subscriber1 and Subscriber2 are subscribed perfectly and they receive messages if I execute some of the commands in the Publisher console application. However Subscriber3 doesn't appear to be doing anything. No exceptions are thrown and in this sample I don't seem to find any relevant log information that could lead to misconfiguration.
Has anyone tried a similar setup with ASP.NET Core & NServiceBus 6 and if so, what should I do next?
Update 04/04/2017
There are some updates that provide some interesting insights:
https://groups.google.com/forum/#!topic/particularsoftware/AVrA1E-VHtk
https://particular.net/blog/nservicebus-on-net-core-why-not
I'd like to use AutoFac in a way that both a State and Strategy pattern coexist. After researching how, I got familiar with the Keyed/Named registration of Autofac and used this for my states using the passive IIndex method. After this, I was looking at the Strategy pattern, which to me looked like a good way of using the same idea, with resolving IIndex for both State and Strategy. I've saved my Strategy options in the same way (enum) as State and Keyed them in the DependencyResolver:
builder.RegisterType<NewAanvraag>().Keyed<IAanvraagState>(AanvraagState.Nieuw).Keyed<IAanvraagState>(BusinessState.Default);
builder.RegisterType<RareNewAanvraag>().Keyed<IAanvraagState>(AanvraagState.Nieuw).Keyed<IAanvraagState>(BusinessState.Rare);
builder.RegisterType<OpvoerenInformatie>().Keyed<IAanvraagState>(AanvraagState.OpvoerenInformatie).Keyed<IAanvraagState>(BusinessState.Default);
This way, I would like to use both options to be created in dynamic order, whereas some implementations might be the same as the default, and some are not.
However, when trying to access both the state and the strategy, I got a notion of KeyedServiceIndex2 (DelegateActivator), but neither option could be resolved by itself
private readonly IIndex<AanvraagState, IAanvraagState> _states;
private readonly IIndex<BusinessState, IAanvraagState> _strategyState;
public IAanvraagDto AanvraagDto { get; set; }
private IAanvraagState CurrentState{ get { return _states[AanvraagDto.State];} }
private IAanvraagState CurrentStrategy { get { return _strategyState[AanvraagDto.BusinessState]; } }
public Aanvraag(IIndex<AanvraagState, IAanvraagState> states, IIndex<BusinessState, IAanvraagState> strategyState)
{
_states = states;
_strategyState = strategyState;
}
public void Start()
{
CurrentStrategy.Start(AanvraagDto);
SetState(AanvraagState.OpvoerenInformatie);
}
When I tried to use both it couldn't find the implementation (also tried IIndex<BusinessState, IIndex<AanvraagState, IAanvraagState>>):
private readonly IIndex<AanvraagState, IIndex<BusinessState, IAanvraagState>> _states;
public IAanvraagDto AanvraagDto { get; set; }
private IAanvraagState CurrentState { get { return _states[AanvraagDto.State][AanvraagDto.BusinessState]; } }
public Aanvraag(IIndex<AanvraagState, IIndex<BusinessState, IAanvraagState>> states)
{
_states = states;
}
public void Start()
{
CurrentState.Start(AanvraagDto);
SetState(AanvraagState.OpvoerenInformatie);
}
Does anyone know how to use 2 Keyed variables to retrieve a grid-like structure for resolving the concrete implementation?
PS: This is the first question I ask on StackOverflow, so any constructive feedback is highly appreciated.
The IIndex<K,V> relationship is really just for single-dimension keyed services. It won't work for multi-dimensional selection.
What you're more likely looking for is component metadata, the ability to associate any arbitrary data with a registration and select the registration based on that data.
The documentation has some great examples and details, but I'll show you a simple example that might fit closely with what you're doing.
First, you need to define a metadata class. This is the thing that will track the various "dimensions" of the "matrix" by which you want to select your component. I'll do something simple here - two Boolean fields so there are only four total combinations of metadata available:
public class ServiceMetadata
{
public bool ApplicationState { get; set; }
public bool BusinessState { get; set; }
}
I'll use some very simple empty services just for illustration. Yours will obviously do something more. Note I have four services - one for each combination of metadata.
// Simple interface defining the "service."
public interface IService { }
// Four different services - one for each
// combination of application and business state
// (e.g., ApplicationState=true, BusinessState=false).
public class FirstService : IService { }
public class SecondService : IService { }
public class ThirdService : IService { }
public class FourthService : IService { }
Here's where you consume the services. To more easily take advantage of the strongly-typed metadata, you'll need to reference System.ComponentModel.Composition so you have access to System.Lazy<T, TMetadata>.
public class Consumer
{
private IEnumerable<Lazy<IService, ServiceMetadata>> _services;
public Consumer(IEnumerable<Lazy<IService, ServiceMetadata>> services)
{
this._services = services;
}
public void DoWork(bool applicationState, bool businessState)
{
// Select the service using LINQ against the metadata.
var service =
this._services
.First(s =>
s.Metadata.ApplicationState == applicationState &&
s.Metadata.BusinessState == businessState)
.Value;
// Do whatever work you need with the selected service.
Console.WriteLine("Service = {0}", service.GetType());
}
}
When you do your registrations, you'll need to register the metadata along with the components so they know which combination of data they belong to.
var builder = new ContainerBuilder();
builder.RegisterType<Consumer>();
builder.RegisterType<FirstService>()
.As<IService>()
.WithMetadata<ServiceMetadata>(m => {
m.For(sm => sm.ApplicationState, false);
m.For(sm => sm.BusinessState, false);
});
builder.RegisterType<SecondService>()
.As<IService>()
.WithMetadata<ServiceMetadata>(m => {
m.For(sm => sm.ApplicationState, false);
m.For(sm => sm.BusinessState, true);
});
builder.RegisterType<ThirdService>()
.As<IService>()
.WithMetadata<ServiceMetadata>(m => {
m.For(sm => sm.ApplicationState, true);
m.For(sm => sm.BusinessState, false);
});
builder.RegisterType<FourthService>()
.As<IService>()
.WithMetadata<ServiceMetadata>(m => {
m.For(sm => sm.ApplicationState, true);
m.For(sm => sm.BusinessState, true);
});
var container = builder.Build();
Finally, you can then use your consumer class to get services by "matrix," as you say. This code:
using(var scope = container.BeginLifetimeScope())
{
var consumer = scope.Resolve<Consumer>();
consumer.DoWork(false, false);
consumer.DoWork(false, true);
consumer.DoWork(true, false);
consumer.DoWork(true, true);
}
Will yield this on the console:
Service = FirstService
Service = SecondService
Service = ThirdService
Service = FourthService
Again, you'll definitely want to check out the documentation for additional details and examples. It will add clarification and help you understand other options you have available to maybe make this easier or work better in your system.
For my SpecFlow tests, I want to setup an individual logging / tracing of the test progress during the execution of tests. E.g. i want to write
all passed / failed steps
started / ended scenarios
started / ended features
to the windows event log (in order to synchronize it with event log messages generated by other system components during the test).
I tried to use the [BeforeFeature], [BeforeScenario], [BeforeStep] Hooks for doing that, but it turned out that I do not have all the required information within this hooks. E.g. i do not know how to get the current text line of the current step executed (including line information, etc.) or the result (failed / passed) of the current step.
Is there a way to get this information within those hooks or in any other way during the execution of the test?
If not:
Is there a way to customize the trace output created by Specflow in any other way?
In order to provide a custom implementation of ITestTracer you should create a plugin for SpecFlow.
Create a class library project with a name CustomTracer.SpecflowPlugin. CustomTracer is your name of choice for plugin.
Then put the following code into your new assembly
[assembly: RuntimePlugin(typeof(CustomTracer.SpecflowPlugin.CustomTracerPlugin))]
namespace CustomTracer.SpecflowPlugin
{
public class CustomTracerPlugin : IRuntimePlugin
{
public void RegisterDependencies(ObjectContainer container)
{
}
public void RegisterCustomizations(ObjectContainer container, RuntimeConfiguration runtimeConfiguration)
{
container.RegisterTypeAs<CustomTracer, ITestTracer>();
}
public void RegisterConfigurationDefaults(RuntimeConfiguration runtimeConfiguration)
{
}
}
public class CustomTracer : ITestTracer
{
// Your implementation here
}
}
Compile assembly and put in the project folder where your specs are (where .csprog file is).
Edit app.config, specFlow section to include:
<plugins>
<add name="CustomTracer" path="." type="Runtime"/>
</plugins>
That is it. Your plugin should load and your custom ITracer implementation should be called during scenario execution. You can even debug it, if you run scenarios under debug.
Finally, after some investigation, i found out that you can replace the DefaultTraceListener by your own implementation by implementing the Interface ITraceListener:
public class foo: TechTalk.SpecFlow.Tracing.ITraceListener
{
public void WriteTestOutput(string message)
{
EventLog.WriteEntry("mysource", "output: " + message);
}
public void WriteToolOutput(string message)
{
EventLog.WriteEntry("mysource", "specflow: " + message);
}
}
And modifying your App.config configuration by adding this to the "specflow" section:
<trace traceSuccessfulSteps="true"
traceTimings="false"
minTracedDuration="0:0:0.1"
listener="MyNamespace.foo, MyAssemblyName"/>
However, this is more a "workaround" for me since I don't have typed information (e.g. of the StepInstance class) and I have to rely or modify the output formatting of SpecFlow.
I would prefer replacing the TestTracer (ITestTracer) implementation by my own one in some way, but i did not find a way to do this. Does anyone now how to do it?
In Specflow 2.1 its a little different.
Instead of:
public class CustomTracerPlugin : IRuntimePlugin
{
public void RegisterDependencies(ObjectContainer container)
{
}
public void RegisterCustomizations(ObjectContainer container, RuntimeConfiguration runtimeConfiguration)
{
container.RegisterTypeAs<CustomTracer, ITestTracer>();
}
public void RegisterConfigurationDefaults(RuntimeConfiguration runtimeConfiguration)
{
}
}
Do this:
public class CustomTracerPlugin : IRuntimePlugin
{
public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters)
{
runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { args.ObjectContainer.RegisterTypeAs<CustomTracer, ITestTracer>(); };
}
}
Every thing else is the same as Vladimir Perevalovs answer.
To register a custom ITestTracer in SpecFlow 3.x:
[assembly: RuntimePlugin(typeof(CustomTracerPlugin))]
public class CustomTracerPlugin : IRuntimePlugin
{
public void Initialize(
RuntimePluginEvents runtimePluginEvents,
RuntimePluginParameters runtimePluginParameters,
UnitTestProviderConfiguration unitTestProviderConfiguration)
{
runtimePluginEvents.CustomizeGlobalDependencies +=
(s, ea) => ea.ObjectContainer.RegisterTypeAs<CustomTracer, ITestTracer>();
}
}
public class CustomTracer : ITestTracer
{
...
}