6
6
using System . Collections . ObjectModel ;
7
7
using System . IO ;
8
8
using System . Linq ;
9
+ using System . Reflection ;
9
10
using System . Threading ;
10
11
using System . Threading . Tasks ;
12
+ using Azure . Identity ;
13
+ using Azure . Monitor . OpenTelemetry . Exporter ;
11
14
using Microsoft . ApplicationInsights . AspNetCore ;
12
15
using Microsoft . ApplicationInsights . AspNetCore . Extensions ;
13
16
using Microsoft . ApplicationInsights . DependencyCollector ;
36
39
using OpenTelemetry . Metrics ;
37
40
using OpenTelemetry . Trace ;
38
41
using static Microsoft . Azure . WebJobs . Script . EnvironmentSettingNames ;
42
+ using AppInsightsCredentialOptions = Microsoft . Azure . WebJobs . Logging . ApplicationInsights . TokenCredentialOptions ;
39
43
using IApplicationLifetime = Microsoft . AspNetCore . Hosting . IApplicationLifetime ;
40
44
41
45
namespace Microsoft . Azure . WebJobs . Script . WebHost
@@ -224,7 +228,7 @@ public async Task StartAsync(CancellationToken cancellationToken)
224
228
}
225
229
}
226
230
227
- private void CheckFileSystem ( )
231
+ private async Task CheckFileSystemAsync ( )
228
232
{
229
233
if ( _environment . ZipDeploymentAppSettingsExist ( ) )
230
234
{
@@ -247,7 +251,10 @@ private void CheckFileSystem()
247
251
}
248
252
finally
249
253
{
250
- _logger . LogError ( errorPrefix + errorSuffix ) ;
254
+ var errorMessage = $ "{ errorPrefix } { errorSuffix } ";
255
+
256
+ _logger . LogError ( errorMessage ) ;
257
+ await LogErrorWithTransientOtelLoggerAsync ( errorMessage ) ;
251
258
}
252
259
_applicationLifetime . StopApplication ( ) ;
253
260
}
@@ -319,7 +326,7 @@ private async Task StartHostAsync(CancellationToken cancellationToken, int attem
319
326
/// </summary>
320
327
private async Task UnsynchronizedStartHostAsync ( ScriptHostStartupOperation activeOperation , int attemptCount = 0 , JobHostStartupMode startupMode = JobHostStartupMode . Normal )
321
328
{
322
- CheckFileSystem ( ) ;
329
+ await CheckFileSystemAsync ( ) ;
323
330
if ( ShutdownRequested )
324
331
{
325
332
return ;
@@ -736,7 +743,20 @@ private ILogger GetHostLogger(IHost host)
736
743
737
744
// Attempt to get the host logger with JobHost configuration applied
738
745
// 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 ) ) ;
740
760
}
741
761
742
762
private void LogInitialization ( IHost host , bool isOffline , int attemptCount , int startCount , Guid operationId )
@@ -1047,6 +1067,87 @@ private void EndStartupOperation(ScriptHostStartupOperation operation)
1047
1067
_logger . StartupOperationCompleted ( operation . Id ) ;
1048
1068
}
1049
1069
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
+
1050
1151
protected virtual void Dispose ( bool disposing )
1051
1152
{
1052
1153
if ( ! _disposed )
0 commit comments