|
| 1 | +// Copyright (c) .NET Foundation. All rights reserved. |
| 2 | +// Licensed under the MIT License. See License.txt in the project root for license information. |
| 3 | + |
| 4 | +using System; |
| 5 | +using System.Collections.Generic; |
| 6 | +using System.IO; |
| 7 | +using System.Linq; |
| 8 | +using System.Net.Http; |
| 9 | +using System.Threading; |
| 10 | +using System.Threading.Tasks; |
| 11 | +using Microsoft.ApplicationInsights.Channel; |
| 12 | +using Microsoft.ApplicationInsights.DataContracts; |
| 13 | +using Microsoft.AspNetCore.Hosting; |
| 14 | +using Microsoft.AspNetCore.TestHost; |
| 15 | +using Microsoft.Azure.WebJobs.Logging; |
| 16 | +using Microsoft.Azure.WebJobs.Logging.ApplicationInsights; |
| 17 | +using Microsoft.Azure.WebJobs.Script.Configuration; |
| 18 | +using Microsoft.Azure.WebJobs.Script.Rpc; |
| 19 | +using Microsoft.Azure.WebJobs.Script.WebHost; |
| 20 | +using Microsoft.Extensions.Configuration; |
| 21 | +using Microsoft.Extensions.DependencyInjection; |
| 22 | +using Microsoft.Extensions.Logging; |
| 23 | +using Microsoft.Extensions.Options; |
| 24 | +using Microsoft.WebJobs.Script.Tests; |
| 25 | +using Xunit; |
| 26 | + |
| 27 | +namespace Microsoft.Azure.WebJobs.Script.Tests.ApplicationInsights |
| 28 | +{ |
| 29 | + public class ApplicationInsightsSpecializationTests |
| 30 | + { |
| 31 | + [Fact] |
| 32 | + public async Task InvocationsContainDifferentOperationIds() |
| 33 | + { |
| 34 | + // Verify that when a request specializes the host we don't capture the context |
| 35 | + // of that request. Application Insights uses this context to correlate telemetry |
| 36 | + // so it had a confusing effect. Previously all TimerTrigger traces would have the |
| 37 | + // operation id of this request and all host logs would as well. |
| 38 | + |
| 39 | + // Start a host in standby mode. |
| 40 | + StandbyManager.ResetChangeToken(); |
| 41 | + |
| 42 | + string standbyPath = Path.Combine(Path.GetTempPath(), "functions", "standby", "wwwroot"); |
| 43 | + string specializedScriptRoot = @"TestScripts\CSharp"; |
| 44 | + string scriptRootConfigPath = ConfigurationPath.Combine(ConfigurationSectionNames.WebHost, nameof(ScriptApplicationHostOptions.ScriptPath)); |
| 45 | + |
| 46 | + var settings = new Dictionary<string, string>() |
| 47 | + { |
| 48 | + { EnvironmentSettingNames.AzureWebsitePlaceholderMode, "1" }, |
| 49 | + { EnvironmentSettingNames.AzureWebsiteContainerReady, null }, |
| 50 | + }; |
| 51 | + |
| 52 | + var environment = new TestEnvironment(settings); |
| 53 | + var loggerProvider = new TestLoggerProvider(); |
| 54 | + var channel = new TestTelemetryChannel(); |
| 55 | + |
| 56 | + var builder = Program.CreateWebHostBuilder() |
| 57 | + .ConfigureLogging(b => |
| 58 | + { |
| 59 | + b.AddProvider(loggerProvider); |
| 60 | + }) |
| 61 | + .ConfigureAppConfiguration(c => |
| 62 | + { |
| 63 | + c.AddInMemoryCollection(new Dictionary<string, string> |
| 64 | + { |
| 65 | + { scriptRootConfigPath, specializedScriptRoot } |
| 66 | + }); |
| 67 | + }) |
| 68 | + .ConfigureServices((bc, s) => |
| 69 | + { |
| 70 | + s.AddSingleton<IEnvironment>(environment); |
| 71 | + |
| 72 | + // Ensure that we don't have a race between the timer and the |
| 73 | + // request for triggering specialization. |
| 74 | + s.AddSingleton<IStandbyManager, InfiniteTimerStandbyManager>(); |
| 75 | + }) |
| 76 | + .AddScriptHostBuilder(webJobsBuilder => |
| 77 | + { |
| 78 | + webJobsBuilder.Services.AddSingleton<ITelemetryChannel>(_ => channel); |
| 79 | + |
| 80 | + webJobsBuilder.Services.Configure<FunctionResultAggregatorOptions>(o => |
| 81 | + { |
| 82 | + o.IsEnabled = false; |
| 83 | + }); |
| 84 | + |
| 85 | + webJobsBuilder.Services.PostConfigure<ApplicationInsightsLoggerOptions>(o => |
| 86 | + { |
| 87 | + o.SamplingSettings = null; |
| 88 | + }); |
| 89 | + |
| 90 | + webJobsBuilder.Services.PostConfigure<ScriptJobHostOptions>(o => |
| 91 | + { |
| 92 | + // Only load the function we care about, but not during standby |
| 93 | + if (o.RootScriptPath != standbyPath) |
| 94 | + { |
| 95 | + o.Functions = new[] |
| 96 | + { |
| 97 | + "OneSecondTimer", |
| 98 | + "FunctionExecutionContext" |
| 99 | + }; |
| 100 | + } |
| 101 | + }); |
| 102 | + }) |
| 103 | + .ConfigureScriptHostAppConfiguration(c => |
| 104 | + { |
| 105 | + c.AddInMemoryCollection(new Dictionary<string, string> |
| 106 | + { |
| 107 | + [EnvironmentSettingNames.AppInsightsInstrumentationKey] = "some_key" |
| 108 | + }); |
| 109 | + }); |
| 110 | + |
| 111 | + using (var testServer = new TestServer(builder)) |
| 112 | + { |
| 113 | + var client = testServer.CreateClient(); |
| 114 | + |
| 115 | + HttpResponseMessage response = await client.GetAsync("api/warmup"); |
| 116 | + Assert.True(response.IsSuccessStatusCode, loggerProvider.GetLog()); |
| 117 | + |
| 118 | + // Now that standby mode is warmed up, set the specialization properties... |
| 119 | + environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1"); |
| 120 | + environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0"); |
| 121 | + |
| 122 | + // ...and issue a request which will force specialization. |
| 123 | + response = await client.GetAsync("api/functionexecutioncontext"); |
| 124 | + Assert.True(response.IsSuccessStatusCode, loggerProvider.GetLog()); |
| 125 | + |
| 126 | + // Wait until we have a few logs from the timer trigger. |
| 127 | + IEnumerable<TraceTelemetry> timerLogs = null; |
| 128 | + await TestHelpers.Await(() => |
| 129 | + { |
| 130 | + timerLogs = channel.Telemetries |
| 131 | + .OfType<TraceTelemetry>() |
| 132 | + .Where(p => p.Message == "OneSecondTimer fired!"); |
| 133 | + |
| 134 | + return timerLogs.Count() >= 3; |
| 135 | + }); |
| 136 | + |
| 137 | + var startupRequest = channel.Telemetries |
| 138 | + .OfType<RequestTelemetry>() |
| 139 | + .Where(p => p.Name == "FunctionExecutionContext") |
| 140 | + .Single(); |
| 141 | + |
| 142 | + // Make sure that auto-Http tracking worked with this request. |
| 143 | + Assert.Equal("200", startupRequest.ResponseCode); |
| 144 | + |
| 145 | + // The host logs should not be associated with this request. |
| 146 | + var logsWithRequestId = channel.Telemetries |
| 147 | + .OfType<TraceTelemetry>() |
| 148 | + .Select(p => p.Context.Operation.Id) |
| 149 | + .Where(p => p == startupRequest.Context.Operation.Id); |
| 150 | + |
| 151 | + // Just expect the "Executing" and "Executed" logs from the actual request. |
| 152 | + Assert.Equal(2, logsWithRequestId.Count()); |
| 153 | + |
| 154 | + // And each of the timer invocations should have a different operation id, and none |
| 155 | + // should match the request id. |
| 156 | + var distinctOpIds = timerLogs.Select(p => p.Context.Operation.Id).Distinct(); |
| 157 | + Assert.Equal(timerLogs.Count(), distinctOpIds.Count()); |
| 158 | + Assert.Empty(timerLogs.Where(p => p.Context.Operation.Id == startupRequest.Context.Operation.Id)); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + private class InfiniteTimerStandbyManager : StandbyManager |
| 163 | + { |
| 164 | + public InfiniteTimerStandbyManager(IScriptHostManager scriptHostManager, ILanguageWorkerChannelManager languageWorkerChannelManager, |
| 165 | + IConfiguration configuration, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment, |
| 166 | + IOptionsMonitor<ScriptApplicationHostOptions> options, ILogger<StandbyManager> logger, HostNameProvider hostNameProvider) |
| 167 | + : base(scriptHostManager, languageWorkerChannelManager, configuration, webHostEnvironment, environment, options, |
| 168 | + logger, hostNameProvider, TimeSpan.FromMilliseconds(-1)) |
| 169 | + { |
| 170 | + } |
| 171 | + } |
| 172 | + } |
| 173 | +} |
0 commit comments