66using System . Collections . ObjectModel ;
77using System . IO ;
88using System . Linq ;
9+ using System . Reflection ;
910using System . Threading ;
1011using System . Threading . Tasks ;
12+ using Azure . Identity ;
13+ using Azure . Monitor . OpenTelemetry . Exporter ;
1114using Microsoft . ApplicationInsights . AspNetCore ;
1215using Microsoft . ApplicationInsights . AspNetCore . Extensions ;
1316using Microsoft . ApplicationInsights . DependencyCollector ;
3639using OpenTelemetry . Metrics ;
3740using OpenTelemetry . Trace ;
3841using static Microsoft . Azure . WebJobs . Script . EnvironmentSettingNames ;
42+ using AppInsightsCredentialOptions = Microsoft . Azure . WebJobs . Logging . ApplicationInsights . TokenCredentialOptions ;
3943using IApplicationLifetime = Microsoft . AspNetCore . Hosting . IApplicationLifetime ;
4044
4145namespace Microsoft . Azure . WebJobs . Script . WebHost
@@ -224,7 +228,7 @@ public async Task StartAsync(CancellationToken cancellationToken)
224228 }
225229 }
226230
227- private void CheckFileSystem ( )
231+ private async Task CheckFileSystemAsync ( )
228232 {
229233 if ( _environment . ZipDeploymentAppSettingsExist ( ) )
230234 {
@@ -247,7 +251,10 @@ private void CheckFileSystem()
247251 }
248252 finally
249253 {
250- _logger . LogError ( errorPrefix + errorSuffix ) ;
254+ var errorMessage = $ "{ errorPrefix } { errorSuffix } ";
255+
256+ _logger . LogError ( errorMessage ) ;
257+ await LogErrorWithTransientOtelLoggerAsync ( errorMessage ) ;
251258 }
252259 _applicationLifetime . StopApplication ( ) ;
253260 }
@@ -319,7 +326,7 @@ private async Task StartHostAsync(CancellationToken cancellationToken, int attem
319326 /// </summary>
320327 private async Task UnsynchronizedStartHostAsync ( ScriptHostStartupOperation activeOperation , int attemptCount = 0 , JobHostStartupMode startupMode = JobHostStartupMode . Normal )
321328 {
322- CheckFileSystem ( ) ;
329+ await CheckFileSystemAsync ( ) ;
323330 if ( ShutdownRequested )
324331 {
325332 return ;
@@ -736,7 +743,20 @@ private ILogger GetHostLogger(IHost host)
736743
737744 // Attempt to get the host logger with JobHost configuration applied
738745 // using the default logger as a fallback
739- return hostLoggerFactory ? . CreateLogger ( LogCategories . Startup ) ?? _logger ;
746+ if ( hostLoggerFactory is not null )
747+ {
748+ return hostLoggerFactory ? . CreateLogger ( LogCategories . Startup ) ;
749+ }
750+
751+ // An error occurred before the host was built; a minimal logger factory is being created to send telemetry to AppInsights/Otel.
752+ var otelLoggerFactory = BuildOtelLoggerFactory ( ) ;
753+
754+ // If the Otel logger factory is null, use the fallback logger instead. These logs will not be accessible in AppInsights/Otel.
755+ if ( otelLoggerFactory is null )
756+ {
757+ return _logger ;
758+ }
759+ return new CompositeLogger ( _logger , otelLoggerFactory . CreateLogger ( LogCategories . Startup ) ) ;
740760 }
741761
742762 private void LogInitialization ( IHost host , bool isOffline , int attemptCount , int startCount , Guid operationId )
@@ -1047,6 +1067,87 @@ private void EndStartupOperation(ScriptHostStartupOperation operation)
10471067 _logger . StartupOperationCompleted ( operation . Id ) ;
10481068 }
10491069
1070+ private async Task LogErrorWithTransientOtelLoggerAsync ( string log )
1071+ {
1072+ var loggerFactory = BuildOtelLoggerFactory ( ) ;
1073+
1074+ if ( loggerFactory is not null )
1075+ {
1076+ var logger = loggerFactory . CreateLogger ( ScriptConstants . LogCategoryHostGeneral ) ;
1077+ logger . LogError ( log ) ;
1078+
1079+ // Delay increases the chance that the log is sent to AppInsights/Otel before the logger factory is disposed
1080+ await Task . Delay ( 2000 ) ;
1081+ // Do a force flush as the host is shutting down and we want to ensure the logs are sent before disposing the logger factory
1082+ ForceFlush ( loggerFactory ) ;
1083+ // Give some time for the logger to flush
1084+ await Task . Delay ( 4000 ) ;
1085+ }
1086+ }
1087+
1088+ private ILoggerFactory BuildOtelLoggerFactory ( )
1089+ {
1090+ var appInsightsConnStr = GetConfigurationValue ( AppInsightsConnectionString , _config ) ;
1091+ var otlpEndpoint = GetConfigurationValue ( OtlpEndpoint , _config ) ;
1092+ if ( appInsightsConnStr is not { Length : > 0 } && otlpEndpoint is not { Length : > 0 } )
1093+ {
1094+ return null ; // Nothing configured
1095+ }
1096+
1097+ // Create a minimal logger factory with OpenTelemetry and Azure Monitor exporter
1098+ return LoggerFactory . Create ( builder =>
1099+ {
1100+ builder . AddOpenTelemetry ( logging =>
1101+ {
1102+ logging . IncludeScopes = true ;
1103+ logging . IncludeFormattedMessage = true ;
1104+
1105+ if ( appInsightsConnStr is { Length : > 0 } )
1106+ {
1107+ logging . AddAzureMonitorLogExporter ( options =>
1108+ {
1109+ options . ConnectionString = appInsightsConnStr ;
1110+
1111+ var appInsightsAuthStr = GetConfigurationValue ( AppInsightsAuthenticationString , _config ) ;
1112+ if ( appInsightsAuthStr is { Length : > 0 } )
1113+ {
1114+ var credOptions = AppInsightsCredentialOptions . ParseAuthenticationString ( appInsightsAuthStr ) ;
1115+ options . Credential = new ManagedIdentityCredential ( credOptions . ClientId ) ;
1116+ }
1117+ } ) ;
1118+ }
1119+
1120+ if ( otlpEndpoint is { Length : > 0 } )
1121+ {
1122+ logging . AddOtlpExporter ( ) ;
1123+ }
1124+ } ) ;
1125+ } ) ;
1126+ }
1127+
1128+ private void ForceFlush ( ILoggerFactory loggerFactory )
1129+ {
1130+ var serviceProvider = ( IServiceProvider ) loggerFactory . GetType ( )
1131+ . GetField ( "_serviceProvider" , BindingFlags . NonPublic | BindingFlags . Instance )
1132+ . GetValue ( loggerFactory ) ;
1133+
1134+ // Get all logger providers from the service provider
1135+ var providers = serviceProvider ? . GetServices < ILoggerProvider > ( ) ?? Enumerable . Empty < ILoggerProvider > ( ) ;
1136+
1137+ foreach ( var provider in providers )
1138+ {
1139+ if ( provider is OpenTelemetryLoggerProvider otelProvider )
1140+ {
1141+ otelProvider . Dispose ( ) ;
1142+ }
1143+ }
1144+ }
1145+
1146+ private static string GetConfigurationValue ( string key , IConfiguration configuration = null )
1147+ {
1148+ return configuration ? [ key ] ?? Environment . GetEnvironmentVariable ( key ) ;
1149+ }
1150+
10501151 protected virtual void Dispose ( bool disposing )
10511152 {
10521153 if ( ! _disposed )
0 commit comments