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
01 02 03 04 05 06 07 08 09 10 11 12 13 | < 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
1 2 3 4 | < PropertyGroup > < EmitCompilerGeneratedFiles >true</ EmitCompilerGeneratedFiles > < CompilerGeneratedFilesOutputPath >$(BaseIntermediateOutputPath)Generated</ CompilerGeneratedFilesOutputPath > </ PropertyGroup > |
Step 2:
2.1 If ASP.NET Core app, add the following to Startup.cs
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | 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
1 2 | [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 !
Leave a Reply