Dotnet diagnostics / open telemetry on CI in 6 simple steps
Prerequisites : Having Jaeger / Zipkin for visualization . The easy way for installing Jaeger is to have Jaeger all in one ( either via Docker, either run on PC)
Step 1: Modify the csproj to add this
<ItemGroup> <PackageReference Include="OpenTelemetry" Version="1.1.0-beta1" /> <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc3" /> <PackageReference Include="OpenTelemetry.Exporter.Jaeger" Version="1.1.0-beta1" /> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.0.0-rc3" /> <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc3" /> <PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.0.0-rc3" /> <AdditionalFiles Include="../AutoMethod.txt" /> <PackageReference Include="AOPMethodsCommon" Version="2021.5.17.2230" /> <PackageReference Include="AOPMethodsGenerator" Version="2021.5.17.2230" /> </ItemGroup>
Optional : To see the RSCG files, add also
<PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath> </PropertyGroup>
Step 2:
2.1 If ASP.NET Core app, add the following to Startup.cs
services.AddOpenTelemetryTracing(b => { string nameAssemblyEntry = Assembly.GetEntryAssembly().GetName().Name; var rb = ResourceBuilder.CreateDefault(); rb.AddService(nameAssemblyEntry); var data = new Dictionary<string, object>() { { "PC", Environment.MachineName } }; data.Add("Exe", nameAssemblyEntry); rb.AddAttributes(data); _ = b .AddAspNetCoreInstrumentation(opt=> { opt.RecordException = true; }) .AddHttpClientInstrumentation(opt=> { opt.SetHttpFlavor = true; }) .AddSqlClientInstrumentation(opt=> { opt.SetDbStatementForStoredProcedure = true; opt.SetDbStatementForText = true; opt.RecordException = true; opt.EnableConnectionLevelAttributes = true; }) .AddSource("MySource") .SetResourceBuilder(rb) .AddJaegerExporter(c => { //var s = Configuration.GetSection("Jaeger"); //s.Bind(c); c.AgentHost = "localhost";//TODO: put the name of the PC with Jaeger }); }) ;
2.2 If .NET Core executable , add the following code to main.cs
string nameAssemblyEntry = Assembly.GetEntryAssembly().GetName().Name; var rb = ResourceBuilder.CreateDefault(); rb.AddService(nameAssemblyEntry); var data = new Dictionary<string, object>() { { "PC", Environment.MachineName } }; data.Add("Exe", nameAssemblyEntry); rb.AddAttributes(data); openTelemetry = Sdk.CreateTracerProviderBuilder() .AddSource("MySource") .AddHttpClientInstrumentation() .AddSqlClientInstrumentation() .SetResourceBuilder(rb) .AddJaegerExporter(o => { //var s = Configuration.GetSection("Jaeger"); //s.Bind(c); c.AgentHost = "localhost";//TODO: put the name of the PC with Jaeger }) .Build();
Step 3: Create near the .sln the file AutoMetods.ps1 with the following contents
function ReplaceWithAuto([string]$m )
{
$m=$m.Trim();
$firstpart= $method
$secondPart =""
#Write-Host $method
# do it fast now. Find first ( , name method is before that
#( could be done better: find from the last : add 1when found ( , add -1 when found ), stop at 0 )
# find first space before
$nr = ([regex]::Matches($m, [regex]::Escape(")") )).count
if($nr -gt 1){$characters = [char[]]$method;
$nrOpen=0;
$iterator= $characters.Length;
do{
$iterator--;
if($characters[$iterator] -eq '('){
$nrOpen++;
}if($characters[$iterator] -eq ')'){
$nrOpen--;
}}while(($iterator -gt 0) -and ($nrOpen -ne 0))
$firstpart = $m.Substring(0,$iterator).Trim()
#return $firstpart
$spaceArr= $firstpart.Split(' ');
$firstpart =$spaceArr[$spaceArr.Length-1]
#return $firstpart}
$arr = $firstpart.Split("(");
#Write-Host $arr[0]
$splitSpace = $arr[0].Split(" ")
$nameMethod = $splitSpace[$splitSpace.Length-1]
#Write-Host $nameMethod
[regex]$pattern = $nameMethod
#$method= ($method -replace [regex]::Escape($nameMethod) ,("auto"+$nameMethod))
$method= $pattern.Replace($method,("auto"+$nameMethod),1)
#Write-Host $method
return $method}
function VerifyFolder()
{
Write-Host "starting"
$fileName = Get-ChildItem "*.cs" -Recurse
#$varItem = [regex]::Escape( "AOPMarkerMethod")
$varItem = "AOPMarkerMethod"
$filename | %{$fileContent = gc $_
$x= ($fileContent -imatch "AOPMarkerMethod" )
#Write-Host $x.Length
if($x.Length -gt 0){
Write-Host $_.fullname
$LineNumbers =@( Select-String $_ -Pattern $varItem| Select-Object -ExpandProperty 'LineNumber')
Write-Host "found " $LineNumbers
Foreach ($LineNumber in $LineNumbers){
$method = $fileContent[$LineNumber]Write-Host "Line where string is found: "$LineNumber
Write-Host $method#Write-Host ReplaceWithAuto $method
$fileContent[$LineNumber] = ReplaceWithAuto($method)Write-Host $fileContent[$LineNumber]
}$fileContent |Set-Content $_.fullname
}
#(gc $_) -replace $varItem,"auto" |Set-Content $_.fullname}
}VerifyFolder
Step 4 : Create near the .sln Automethod.txt with the following content:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.CodeDom.Compiler;
using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Linq;
using OpenTelemetry.Trace;
namespace {{NamespaceName}} {[GeneratedCode("AOPMethods", "{{version}}")]
[CompilerGenerated]
public partial class {{ClassName}}{public static readonly ActivitySource MyActivitySource = new ActivitySource("MySource");
{{~ for mi in Methods ~}}
//{{mi.Name}}
{{strAwait = ""
strAsync =""
if mi.CouldUseAsync == true
strAwait = " await "
strAsync = " async "
end
separator = ""
if(mi.NrParameters > 0)
separator = ","
end
returnString = ""
if mi.CouldReturnVoidFromAsync == false
returnString = " return "
end
}}
public {{strAsync}} {{mi.ReturnType}} {{mi.NewName}} ({{mi.parametersDefinitionCSharp }}
{{separator}}
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0){
var sw=Stopwatch.StartNew();
using var span = MyActivitySource.StartActivity("{{mi.NewName}}", ActivityKind.Producer);span.Start();
//parameters
{{~ for miArg in mi.Parameters ~}}
var val{{ miArg.Key }} = System.Text.Json.JsonSerializer.Serialize({{ miArg.Key }});
span.SetTag("Argument_{{ miArg.Key }}",val{{ miArg.Key }});
{{ end}}span.SetTag("method","{{mi.NewName}}");
span.SetTag("called from ",memberName);
span.SetTag("called from file",sourceFilePath);
span.SetTag("called from line",sourceLineNumber);
var utcTime =System.DateTime.UtcNow;
span.SetTag("UTCDate",utcTime.ToString("yyyyMMddHHmmss"));
var userEmail = ((System.Security.Claims.ClaimsPrincipal)System.Threading.Thread.CurrentPrincipal)?.Claims?.Where(c => c.Type == "email")?.SingleOrDefault()?.Value;
userEmail ??= "NoUserConfigured";
span.SetTag("user",userEmail);
span.SetStatus(Status.Ok);
try{
Console.WriteLine("--{{mi.Name}} start ");
{{returnString}} {{ strAwait }} {{mi.Name}}({{ mi.parametersCallCSharp }});
}
catch(Exception ex){
Console.WriteLine("--{{mi.Name}} exception ");
span?.RecordException(ex);
span.SetStatus(Status.Error);
throw;
}
finally{
Console.WriteLine("--{{mi.Name}} finally ");
span.SetTag("ElapsedMilliseconds",sw.Elapsed.TotalMilliseconds);
span.Stop();
//span.Duration= sw.Elapsed.TotalMilliseconds;}
}//end {{mi.NewName}}{{ end}}
}
}
Step 5: Decorate your method with [AOPMarkerMethod] ( namespace AOPMethodsCommon )
Something like this
[AOPMarkerMethod] private NewCountry[] ArrangeCountriesSearch(NewCountry[] data)
And in the class definition
[AutoMethods(template = TemplateMethod.CustomTemplateFile,CustomTemplateFileName ="Automethod.txt", MethodSuffix = "bup")]
Step 6: Modify your CI to run
pwsh AutoMetods.ps1
or
powershell AutoMetods.ps1
Enjoy, that will be all !