Laden...

Logging in Azure Function

Erstellt von Hqrtz vor 2 Jahren Letzter Beitrag vor 2 Jahren 908 Views
H
Hqrtz Themenstarter:in
13 Beiträge seit 2021
vor 2 Jahren
Logging in Azure Function

Hallo,

Ich habe eine Frage zum ILogger in Azure Functions.
Mein Code sieht wie folgt aus:


private ILogger<Function> logger;
private MeinService service;

        public Konstruktor(MeinService service, ILogger<MeinService> logger)
        {
            this.service= service;
            this.logger = logger;
            
        }

        [FunctionName("Function")]
        public void Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req)
        {
            logger.LogInformation("C# HTTP trigger function processed a request.");
            service.Main();


Service:


public class MeinService()
{
private ILogger<MeinService> logger;

        public Konstruktor(ILogger<MeinService> logger)
        {
            this.logger = logger;
        }

        public void Main()
        {
            logger.LogInformation("Test");
            //tut irgendwas
        }

}

Dependency Injektion:


public override void Configure(IFunctionsHostBuilder builder)
        {
             //services
             builder.Services.AddLogging();
        }

host.json:


{
    "version": "2.0",
    "logging": {
      "applicationInsights": {
        "samplingExcludedTypes": "Request",
        "samplingSettings": {
          "isEnabled": true
        }
      },
      "logLevel": {
        "MeinNamespace.Function": "Information"
      }
    }
}

Nun zu meiner Frage: Es werden in der Function Konsole keine Lognachrichten ausgegeben. Habe ich etwas falsch gemacht?
Ich habe es genauso in einer normalen Konsolenanwendung ausprobiert und da lief es problemlos.
Hier habe ich gefunden, dass es wohl normal sei das bei einer lokalen Ausführung keine Logausgaben angezeigt werden.
https://jan-v.nl/post/first-steps-into-logging-of-your-azure-functions/
Da das aber das erste mal ist das ich sowas gefunden habe und auf den anderen seiten davon nichts stand, wollte ich sichergehen, dass ich keinen Fehler gemacht habe.
Wenn ich den standard ILogger ohne DI an die Methoden übergeben würde, würde ja etwas auf der Konsole ausgegeben werden...

Wäre super wenn mir da jemand eine Antwort zu geben kann.
Viele Grüße

16.827 Beiträge seit 2008
vor 2 Jahren

Hi,

das ist soweit alles korrekt und entspricht dem dokumentierten Stand.
Jan sagt hier auch absolut das richtige: ILogger is the way to go.

Dem Code nach verwendest Du noch die bereits (nach .NET 6) abgekündigte In Process Variante der Functions, die nur eine abgespeckte Variante der Dependency Injection unterstützt.
Beispiel von mir: Dependency Injection with Azure Functions and FunctionsStartup - SchwabenCode.com | Benjamin Abt
ILogger<MeinService>* kannst Du respektive nur verwenden, wenn Du auch ein Dependency Injection Setup verwendest. Ansonsten kannst Du auch einfach ILogger an der Methode annehmen, wie dokumentiert.
* wobei "MeinService" an dieser Stelle falsch wäre.

Das Standardverhalten der Functions ist immer, dass der Output die Konsole ist, da entsprechend ein Console Logging Provider registriert wird. Das übernimmt in Deinem Fall die bereits erwähnte In Process Umgebung.
Alle Logs, die auf Azure in die Konsole geschrieben werden, tauchen automatisch in Application Insights in den Logs auf, sofern die Umgebungsvariable "APPINSIGHTS_INSTRUMENTATIONKEY" gesetzt ist.
Hat leider den Nebeneffekt, dass das nich so meeega performant ist, weil Console Logging immer teuer ist.

Verwendest Du eine Dependency Umgebung (entweder mit dem Link von mir oder mit der moderneren Isolated Process Variante), dann kannst Du einfach Serilog registrieren und Application Insights als Sink registrieren.
Hier entfallen dann alle "Besonderheiten" der Function Runtime und Du kannst eine Architektur verwenden, wie sie hier mit .NET üblich ist.

H
Hqrtz Themenstarter:in
13 Beiträge seit 2021
vor 2 Jahren

Mit "ILogger an der Methode annehmen" meinst du als Parameter jeder Methode übergeben? dachte da nur das das vlt nicht soo schön ist und eher gegen Dependency Injektion spricht so wie ich es bisher gemacht habe.

Also mein Dependency Injektion sieht eig genauso aus wie bei dir nur das ich eben meine Services als Transiant registriere und AddLogging().
Wenn ich das richtig verstehe ist das dann so richtig wie ich das gemacht habe nur es wird nicht in der Konsole angezeigt aber in ApplicationInsights wenn ich die Variable dafür setze?

Würde ungern die Architektur jetzt ändern, nur um Logging anders / besser nutzen zu können. Serilog habe ich bisher nicht genommen, weil ich eig nur Loggen möchte welche Methode aufgerufen wird und das sollte ja auch mit dem ILogger gehen

16.827 Beiträge seit 2008
vor 2 Jahren

In meinen Augen machst Du Dir das Leben selbst schwer, aber musst selbst wissen.

Verwende einfach die Isolated Process Variante, registrier Dein Serilog, setz Application Insights direkt als Sink -> fertig.
Ich verwend das seit dem ersten Tag der Isolated Process Möglichkeit in hunderten von Functions, super einfach und endlich muss man die Besonderheiten der Function nicht mehr beachten.
Musst für Serilog 2 Pakete einbinden, die Config setzen und eine Zeile Code einfügen.

Mit ILogger hat das nichts zutun, denn ILogger ist nur das Logging Interface, das eigentlich alle Logging Implementierungen verwenden. Das Kernstück hierzu ist die LoggerFactory.
Willst Du Serilog nicht nutzen - warum auch immer - dann kannst entweder das AI SDK selbst implementieren oder eben direkt auf die Console schreiben, das dann wieder im AI Log landet, weil das die Runtime automatisch macht, wenn die Umgebungsvariable gesetzt ist - alles soweit in meinem ersten Beitrag erwähnt.

Wenn ich das richtig verstehe ist das dann so richtig wie ich das gemacht habe nur es wird nicht in der Konsole angezeigt aber in ApplicationInsights wenn ich die Variable dafür setze?

Ja und nein, verwendest halt noch den alten Krams. Da Du aber ohnehin ne Function mit DI und Co verwendest liegt es eigentliche nahe, dass man eben nicht mehr In Process sondern Isolated Process verwendet, weil Du dann nichts extra umbauen musst, was die Function Runtime so will. Musst halt selbst abwägen.
Wenn Du das so weiter machen willst, dann fehlt - soweit ich das sehe - nur die Umgebungsvariable.

Würde ungern die Architektur jetzt ändern, nur um Logging anders / besser nutzen zu können.

Langfristig wirst Du das so oder so müssen, weil - wie bereits gesagt - In Process abgekündigt ist.
.NET 6 wird die letzte Runtime-Version sein, die das unterstützt. Siehe Link.

Serilog habe ich bisher nicht genommen, weil ich eig nur Loggen möchte welche Methode aufgerufen wird und das sollte ja auch mit dem ILogger gehen

ILogger ist ein Interface, das Serilog genauso nutzt. ILogger allein loggt gar nichts.
Getauscht wird nur die Implementierung, die Standardmäßig der Default Output ist, was wiederum die nackte Console über den TraceWriter ist.
Wenn Du Serilog registrierst, dann kannst verschiedene Sinks angeben und genauso loggen. Keinerlei Unterschied zur Architektur, aber zum Logging.
Glaub Du hast das Konzept dahinter noch nicht 100% verstanden, oder?

H
Hqrtz Themenstarter:in
13 Beiträge seit 2021
vor 2 Jahren

Ok ich denke ich werde mit den Isolated Process nochmal angucken.

Vlt hängt ein weiteres Problem auch damit zusammen. Nach dem ich testweise meine Function gepublished habe, kann ich die auch nicht mehr lokal ausführen.
Ich bekomme immer so einen Dependency Injektion Fehler:

Fehlermeldung:
Executed 'Function' (Failed, Duration=62ms)
Microsoft.Extensions.DependencyInjection.Abstractions: Unable to resolve service for type 'Function.Services.IMeinService' while attempting to activate 'Function.Function'.
An unhandled host error has occurred.
Microsoft.Extensions.DependencyInjection.Abstractions: Unable to resolve service for type 'Function.Services.IMeinService' while attempting to activate 'Function.Function'.

Hiernochmal der wichtigste Code:

Function:


public class Function
    {
        private readonly IMeinService meinService;

        public Function(IMeinService meinService)

        {
            this.meinService= meinService;
        }

        [FunctionName("Function")]
        public void Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");
       
             meinService.Main();       
        }

Startup:



[assembly: FunctionsStartup(typeof(Function.Startup))]

    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var config = new ConfigurationBuilder()
             .SetBasePath(Directory.GetCurrentDirectory())
             .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
             .AddEnvironmentVariables()
             .Build();

            builder.Services.AddTransient<IMeinService, MeinService>();
            //weitere Services
        }
    }
}


Wie gesagt für mich ergibt das keinen Sinn und kann nur was mit dem Publishen zutun haben?
Vlt kannst du mir ja dabei helfen. Das sollte doch so auch ohne Isolated Process gehen oder?

16.827 Beiträge seit 2008
vor 2 Jahren

Du verfremdelst Deinen Code ein bisschen zu sehr, wenn ich Dir das sagen darf.
Dein Code kann niemals funktionieren, weil offenbar laut dem Beispielcode hier Dein Namespace und Deine Function beides Function heissen - das kann nicht sein.
Kannst bitte Dein Code nicht ganz so sehr verfremdeln, oder zumindest sinnvoller?

Rein von der Registrierung hier sehe ich keinen Fehler. Das Transient vs. Scoped macht hier keinen Unterschied.
Sind Deine NuGet Pakete auch wirklich aktuell und bist auch auf der aktuellen Function Runtime?

Hat Dein MainService noch irgendwelche Abhängigkeiten, die nicht aufgelöst werden können?

Ok ich denke ich werde mit den Isolated Process nochmal angucken.

Solltest tun, macht vieles einfacher.

H
Hqrtz Themenstarter:in
13 Beiträge seit 2021
vor 2 Jahren

Also ich habe extra nochmal nachgeguckt. Der Namespace in der Function.cs heißt definition 1:1 wie die Function Klasse sowie in [FunctionName("Function")]

Ich habe jetzt nochmal eine Function erstellt und meinen Code rüberkopiert und die läuft jetzt. Also muss beim publishen irgendwas kaputt gegangen sein aber kp was.
Ich habe einen Storage Acc erstellt, dann die function app und auf Publish gedrückt. Funktioniert das DI nicht so wie ich das gemacht habe beim publishen?
Werde das DI dann morgen mal mit Isolated Prozess probieren. Vlt klappt es dann ja...

16.827 Beiträge seit 2008
vor 2 Jahren

Dann kommt das entweder vom verfremdeln Deines Codes, was mein Missverständnis auslöst, oder Du hast ein ganz kurioses Klassen/Namespace Design...


// Dieser Zeile nach befinde sich die Startup Class im Function Namespace
[assembly: FunctionsStartup(typeof(Function.Startup))]

=> Function.Startup
public class Startup : FunctionsStartup

// Die Function hat aber gleichzeitig den Klassenname Function
public class Function

H
Hqrtz Themenstarter:in
13 Beiträge seit 2021
vor 2 Jahren

Ja das kann gut sein das mein Klassen/namespace Design komisch ist. Da habe ich nicht soo drauf geachtet 😁

Ich habe das Projekt. Darin den Ordner Services.
Außerdem habe ich direkt im Projekt die Funktion Klasse und die Startup. Und halt local settings etc.

Und die Funktion heißt halt genauso wie das Projekt und damit auch wie der Namespace. Glaube das passiert automatisch beim Anlegen eines Funktion Projekts. Habe da eig nur den Projektnamen eingegeben und die Funktion heißt dann genauso wenn ich mich nicht vertue.

Aber eig sollte das DI so wie ich es habe auch beim publishen klappen? Also wie gesagt local funktioniert es aufjedenfall. Nur nach dem publishen nicht mehr lokal. In einem 1:1 neu erstellten Funktion Projekt geht es aber wieder...

16.827 Beiträge seit 2008
vor 2 Jahren

Publishen hat mit Dependency Injection nichts zutun.
Dass nen neues Projekt funktioniert, da spricht es dafür, dass Deine Pakete nicht aktuell sind oder was anderes krum ist, zB. Runtime Version.

H
Hqrtz Themenstarter:in
13 Beiträge seit 2021
vor 2 Jahren

Also Runtime version ist laut der Overview seite im Portal: 3.0.15584.0 bzw in den App settings im Portal steht ~3
function version v3 und Framework .net core 3.1

Hier ist genau das selbe Problem das ich auch habe: https://github.com/Azure/azure-functions-host/issues/6861 und hier https://github.com/Azure/Azure-Functions/issues/972#issuecomment-426708865
extensionsbundle in host.json habe ich garnicht und soweit ich getestet habe ist es egal welchen Service ich in der Starup nicht auskommentiere.

Gibt es eine andere runtime version die ich probieren kann?

16.827 Beiträge seit 2008
vor 2 Jahren

Gibt es eine andere runtime version die ich probieren kann?

Jo, an der Stelle das vierte Mal der Hinweis auf Isolated Process.
Link weiterhin im ersten Beitrag 😉

Hier ist genau das selbe Problem

Offenbar nicht, denn der Issue-Ersteller schreibt selbst, dass es an seiner Auth-Implementierung lag und nicht am DI.

Dazu der Hinweis im zweiten Issue

Some of those issues are known limitations of the product at the moment. We are working on first class support for customer facing DI capabilities in Azure Functions, which will come with a full set of documentation, including services you can depend on and official guidance.

.. und mein Hinweis im ersten Beitrag:

Dem Code nach verwendest Du noch die bereits (nach .NET 6) abgekündigte In Process Variante der Functions, die nur eine abgespeckte Variante der Dependency Injection unterstützt.

16.827 Beiträge seit 2008
vor 2 Jahren

Ich hab mir kurz 2 Minuten Zeit genommen, hab das Standard Visual Studio Template genommen und ein Function Projekt angelegt.
Hab es konfiguriert, wie es in Verwenden der Abhängigkeitsinjektion in Azure Functions (.NET) beschrieben ist; hab Dein Code reinkopiert (und die Syntaxfehler beheben müssen...).


using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MyNamespace;

[assembly: FunctionsStartup(typeof(MyNamespace.Startup))]
namespace MyNamespace
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddTransient<IMyService, MeinService>();
        }
    }

    public interface IMyService
    {
        Task Do();
    }

    public class MeinService : IMyService
    {
        private readonly ILogger<MeinService> _logger;

        public MeinService(ILogger<MeinService> logger)
        {
            this._logger = logger;
        }

        public Task Do()
        {
            _logger.LogInformation("Test");
            return Task.CompletedTask;
        }

    }
}

namespace FunctionApp1
{
    public class Function1
    {
        private readonly IMyService _myService;
        private readonly ILogger<Function1> _log;

        public Function1(IMyService myService, ILogger<Function1> log)
        {
            _myService = myService;
            _log = log;
        }

        [FunctionName("Function1")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req)
        {
            _log.LogInformation("C# HTTP trigger function processed a request.");

            await _myService.Do();

            return new OkObjectResult("Works");
        }
    }
}


Anschließend Rechtsklick -> Publish -> Azure Function Linux -> Application Insights aktiviert.
Application Insights dann noch im Portal aktiviert. Fertig, funktioniert.

Das ist In Process, wie Du es hast - exakt nach Anleitung sowie das, was ich Dir hier schon in den Beiträgen gesagt hab.
Daher keine Ahnung, was Du machst.

Vermutlich hast irgendwas im Code, das Du uns hier nicht zeigen möchtest oder machst was, was Du hier nicht gesagt hast.

H
Hqrtz Themenstarter:in
13 Beiträge seit 2021
vor 2 Jahren

Ok dabnke dafür. Also ich konnte den Fehler identifizieren. Ich nutze RazorLight. Das scheint nicht mit Azure Functions kompatibel zu sein. Wenn ich den teil raus nehme, funktioniert es problemlos. Mit geht es leider nicht.

Kennst du oder jemand anderes eine alternative zu RazorLight zum Templating?

16.827 Beiträge seit 2008
vor 2 Jahren

RazorLight funktioniert einwandfrei in Functions; verwende ich auch.

PS: interessant, wie wir von Logging über Dependency Injection mittlerweile bei RazorLight angekommen sind, nur weil Du Deinen Code zu sehr verfremdelt hast.
Das kostet nicht nur Dich Zeit, sondern auch Helfern 😉

H
Hqrtz Themenstarter:in
13 Beiträge seit 2021
vor 2 Jahren

Ja das tut mir leid das ich so vom Thema abweiche 😁
Ok ich probiere nochnmal das wichtigste hier einzustellen.

Also ich hatte alles mit Razorlight rausgenommen und es lief aufeinmal. Ich habe keine Ahnung wieso


<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
    <_FunctionsSkipCleanOutput>true</_FunctionsSkipCleanOutput>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.2" />
    <PackageReference Include="RazorLight" Version="1.1.0" />
    <PackageReference Include="RazorLight.NetCore3" Version="3.0.2" />
    <PackageReference Include="RestSharp" Version="106.11.7" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

Startup:


[assembly: FunctionsStartup(typeof(FunctionApp1.Startup))]

namespace FunctionApp1
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var config = new ConfigurationBuilder()
             .SetBasePath(Directory.GetCurrentDirectory())
             .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
             .AddEnvironmentVariables()
             .Build();

            builder.Services.AddTransient<IApiService, ApiService>();
            builder.Services.AddTransient<IMainService, MainService>();
            builder.Services.AddTransient<ITemplateService, TemplateService>();
            builder.Services.AddTransient<IEmailService, EmailService>();
        }
    }
}

Funktion:



namespace FunctionApp1
{
    public class Function1
    {
        private readonly IMainService mainService;

        public Function1(IMainService mainService)
        {
            this.mainService= mainService;
        }

        [FunctionName("Function1")]
        public void Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            mainService.Run();
         
        }
    }
}

MainService:


namespace FunctionApp1.Services
{
    public class MainService: IMainService
    {
        private readonly IApiService apiService;
        private readonly ITemplateService templateService;
        private readonly IEmailService emailService;

        public MainService(IApiService apiService, ITemplateService templateService, IEmailService emailService)
        {
            this.apiService= apiService;
            this.templateService= templateService;
            this.emailService = emailService;
        }

        public void Run()
        {
            
            var result= GetDataFromApi
            
            var template= templateService.RenderTemplate(result).Result;

            emailService.SendEmail(template);
           
        }

TemplateService:


namespace FunctionApp1.Services
{
    public class TemplateService: ITemplateService
    {
        public async Task<string> RenderTemplate(List<Items> itemListe)
        {
            var engine = ConfigEngine();
            var template = File.ReadAllText(@"./RazorTemplate.cshtml", System.Text.Encoding.UTF8);

            var model = new Model
            {
                itemListe= itemListe
            };

            return await engine.CompileRenderStringAsync("Key9", template, model);
        }

        private RazorLightEngine ConfigEngine()
        {
            var engine = new RazorLightEngineBuilder()
                .UseEmbeddedResourcesProject(typeof(Model))
                .SetOperatingAssembly(typeof(Model).Assembly)
                .UseMemoryCachingProvider()
                .Build();

            return engine;
        }
    }
}

RazorTemplate:


@model Function1.Model.Model
<!DOCTYPE html>
<html>
<head>
</head>
<body>
    @foreach (var liste in @Model.itemListe)
    {
         <p>liste.text</p>
    }   
</body>
</html>

Also ich führe die Funktion aus. Die ruft über eine API Daten ab. Mit Razor erstelle ich damit ein Dokument und schicke mir das per email.
Funktioniert auch lokal alles soweit nur eben wenn ich es publishe nicht