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 !