Add Heartbeat properties for App Services#869
Conversation
…s) that will add some infrastructure-identifying properties. - Implements part of Option 1 in issue #868
…into WindowsServer. Update logic to ensure we keep heartbeat values updated. - Update logic in AzureWebAppRoleEnvironment to also signal AppServicesHeartbeat to update itself when changes are detected - Update AppServicesHeartbeat to expose an Instance variable to make it easier for update signals to be sent to it - Update CHANGELOG - Issue #868 discusses the need for the change to the AzureWebAppRoleEnvrionmeentInitializer
|
@SergeyKanzhelev can you please have a look at this and let me know if this satisfies the topics we covered in issue #868? I believe I captured all the items discussed there, but I do still need to find a way to allow for updates in non-internal scenarios (AspNetCore). |
| /// Provides default values for the heartbeat feature of Application Insights that | ||
| /// are specific to Azure App Services (Web Apps, Functions, etc...). | ||
| /// </summary> | ||
| internal class AppServicesHeartbeatTelemetryModule : ITelemetryModule, IDisposable |
There was a problem hiding this comment.
must be public so it can be instantiated from code
| /// <summary> | ||
| /// Initializes a new instance of the<see cref="AppServicesHeartbeatTelemetryModule" /> class. | ||
| /// </summary> | ||
| internal AppServicesHeartbeatTelemetryModule() |
There was a problem hiding this comment.
must be public so it can be instantiated from code
There was a problem hiding this comment.
Done. Totally missed this one. Thanks.
|
|
||
| private static AppServicesHeartbeatTelemetryModule instance; | ||
| private object lockObject = new object(); | ||
| private bool isInitialized = false; |
There was a problem hiding this comment.
does this double initialization control matter for this module? There is no harm in initializing twice. Furthermore - this protection in module creates a false perception that nobody will call Initialize twice. In reality it may easily happen with re-creating of entire TelemetryConfiguration
There was a problem hiding this comment.
I don't need to protect it, no. I do need to know when it gets re-initialized but that's all.
| new KeyValuePair<string, string>("appSrv_wsHost", "WEBSITE_HOSTNAME") | ||
| }; | ||
|
|
||
| private static AppServicesHeartbeatTelemetryModule instance; |
There was a problem hiding this comment.
To allow access to the 'Update' method. I don't have a better mechanism at this moment, but I think we can define a better one given some time.
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets a value to provide internal access to the only instance of this class that *should* be available. |
There was a problem hiding this comment.
I don't understand why it is a singleton. Can you provide more details
There was a problem hiding this comment.
We need a way to allow for the update of the heartbeat values associated to AppServices. I made this a singleton to allow other modules to signal this module to update the values. Perhaps there is a better mechanism for this, but I am not aware of one at this time. (I have thoughts on how to improve it in the future though, but it might take some time).
|
|
||
| [Event(34, | ||
| Message = "Could not gain access to the Heartbeat Manager instance during initialization. Exception raised: {0}", | ||
| Level = EventLevel.Verbose)] |
There was a problem hiding this comment.
Make it warning at least. This error is very unlikely to happen. It indicates that something is broken in SDK. So we want to have it at least at warning level.
There was a problem hiding this comment.
Agreed. I've gone over the other calls and altered their levels (including some I had set previously for the Azure IMS data as well).
| string hbeatKey = kvp.Key.ToString(); | ||
| if (isUpdateOperation) | ||
| { | ||
| hbeatManager.SetHeartbeatProperty(hbeatKey, hbeatValue); |
There was a problem hiding this comment.
Would you expect hbeatManager to be null ever?
There was a problem hiding this comment.
Yes, I've added an early exit to counter this possibility.
| WindowsServerEventSource.Log.HeartbeatManagerAccessFailure(hearbeatManagerAccessException.Message); | ||
| } | ||
|
|
||
| return hbeatManager; |
There was a problem hiding this comment.
You do not check for null here. It may be a good idea though
| try | ||
| { | ||
| // get the variable, then expand it (otherwise we get the name we queried for in the value) | ||
| string hbeatValue = Environment.GetEnvironmentVariable(kvp.Value); |
There was a problem hiding this comment.
this may throw security exception when it's not full trust appdomain. It is good idea to have a global try{}catch in the Initialize
|
I don't see how you are addressing synchronization with telemetry initializer and slot swap issue |
| else if (!nodeName.Equals(this.lastNodeValue, StringComparison.Ordinal)) | ||
| { | ||
| // if the AppServices heartbeat telemetry module exists, signal it to update the values in heartbeat | ||
| AppServicesHeartbeatTelemetryModule.Instance?.UpdateHeartbeatWithAppServiceEnvVarValues(); |
There was a problem hiding this comment.
@SergeyKanzhelev, here is where I signal the AppServicesHeartbeatTelemetryModule to update the values in the heartbeat properties. This is the reason I chose to go with the Singleton pattern. Perhaps there is a better way to go about this?
- make new AppServicesHeartbeat module public! - add try/catch scenarios around initialize - add further diagnostic messages where appropriate
|
@SergeyKanzhelev ping? |
- Use to keep the value of WEBSITE_SITE_NAME and other App Services values current during runime
…, update code for testability.
|
@SergeyKanzhelev @cijothomas updated PR with tests, can be merged without any need for follow up PR.
|
cijothomas
left a comment
There was a problem hiding this comment.
Please address comments. Also, it'd be great if you can provide info about why we need to send these values as HeartBeats again, since they are already sent with every telemetry item.
| { | ||
| this.lastNodeValue = nodeName; | ||
| } | ||
| else if (!nodeName.Equals(this.lastNodeValue, StringComparison.Ordinal)) |
There was a problem hiding this comment.
How does this work? nodename is cached and won't change after 1st initialization.
There was a problem hiding this comment.
Agreed. Please see PR #870 where I fix this issue you call out here, and where I am adding some protection for performance as well. (My intention is to merge that PR into this one, as I prefer that solution).
| envVars.Add(kvp.Value, Environment.GetEnvironmentVariable(kvp.Value)); | ||
| if (string.IsNullOrEmpty(envVars[kvp.Value])) | ||
| { | ||
| Environment.SetEnvironmentVariable(kvp.Value, kvp.Key); |
There was a problem hiding this comment.
The effect of setting the ENV variable persists even after the test? This may have unintended consequences in the next test.
If modifying any ENV, make sure to set it back to original after the test
There was a problem hiding this comment.
Agreed. Also, in PR #870 see that I've added a ClassInitializer/ClassCleanup method to handle environment changes.
There was a problem hiding this comment.
Merged #870 here where I fixed this issue by using unique env vars in place of the standard during testing. Note that I left the original testing for the TelemetryInitializer intact (although it really should be updated to do the same).
| <Import_RootNamespace>Microsoft.ApplicationInsights.WindowsServer</Import_RootNamespace> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <Compile Include="$(MSBuildThisFileDirectory)AppServicesHeartbeatTests.cs" /> |
There was a problem hiding this comment.
AppServicesHeartbeatTelemetryModuleTests might be better name.
| { | ||
| try | ||
| { | ||
| // get the variable, then expand it (otherwise we get the name we queried for in the value) |
There was a problem hiding this comment.
I am not sure i understood this. Why not just read ENV variable like done everywhere else?
https://github.com/Microsoft/ApplicationInsights-dotnet-server/blob/develop/Src/WindowsServer/WindowsServer.Shared/AzureWebAppRoleEnvironmentTelemetryInitializer.cs#L65
There was a problem hiding this comment.
Agreed. There doesn't ever seem to be any need to handle environment variable expansion for this value. PR #870 does not attempt expansion.
There was a problem hiding this comment.
ExpandEnvironmentVariable will make this method slower, let's skip if unnecessary.
| /// </summary> | ||
| internal static readonly KeyValuePair<string, string>[] WebHeartbeatPropertyNameEnvVarMap = new KeyValuePair<string, string>[] | ||
| { | ||
| new KeyValuePair<string, string>("appSrv_SiteName", "WEBSITE_SITE_NAME"), |
There was a problem hiding this comment.
Aren't these values appended to all telemetry items by the corresponding TelemetryInitializers? Why do we send these values (duplicated) into HeartBeat again?
There was a problem hiding this comment.
Heartbeat and telemetry initializers are two separate solutions. I am not 100% aware of all the downstream consumers of the TelemetryInitializers, but I am keenly aware of the heartbeat ones (app 2 infra). I want the app 2 infra feature to only look in 1 single place for the values it requires. I believe the duplication caused by heartbeats (default 15-minute cadence) is minimal enough to support the end purpose.
| /// Environment variables we use in our heartbeat payload. | ||
| /// </summary> | ||
| /// <returns>A value indicating whether or not an update to the environment variables occurred.</returns> | ||
| public bool UpdateHeartbeatWithAppServiceEnvVarValues() |
There was a problem hiding this comment.
Instead of a TelemetryInitializers telling this module to update, can this module update itself (re-read ENV vars) every 'n' minute? And maybe inform the TelemetryInitializer that ENV values changed so that it'll also pick new values.
There was a problem hiding this comment.
Agreed. I was thinking that a subscriber model would work really nicely actually - that way the heartbeat module and the initializer module could simply 'subscribe' to the environment variable monitor (see PR #870, that's the terminology I use there) and get updated only as necessary.
For this PR, I wanted to keep it as concise as possible to get the job done. I would certainly be open to creating such a subscriber in our next sprint.
There was a problem hiding this comment.
After discussions with Dmitry I will create an alternate PR to this one that contains a subscriber model solution. Although due to time constraints we still may opt for this solution.
| /// <param name="configuration">Unused parameter.</param> | ||
| public void Initialize(TelemetryConfiguration configuration) | ||
| { | ||
| this.isInitialized = this.UpdateHeartbeatWithAppServiceEnvVarValues(); |
There was a problem hiding this comment.
this.isInitialized -- Where is this used?
There was a problem hiding this comment.
Line 94 - in the call to AddAppServiceEnvironmentVariablesToHeartbeat. The first time (isInitialized=false) we use AddHeartbeatProperty, each subsequent time we use SetHeartbeatProperty.
|
Need to ensure the tests are run in CI builds. I searched for newly added tests but couldn't find them in test results for this CI build. |
| { | ||
| try | ||
| { | ||
| // get the variable, then expand it (otherwise we get the name we queried for in the value) |
There was a problem hiding this comment.
ExpandEnvironmentVariable will make this method slower, let's skip if unnecessary.
- Add heartbeat provider for Web Apps/App Services - Add monitor for App Services environment variables (which can change during the runtime of an AppServices app) - Ensure adequate testing is added, update current Env Var tests to be testable in a parallel environment
- Use env var monitor to check for udpates to environment in controlled/minimal fashion - default to 30 second interval for checking the changes to environment - corrections for stylecop
Updates cover all prior issues raised. Please re-review (or allow Dmitry to review)
- PR suggestion
There was some iteration on builds as I added the tests, perhaps some of them got added as 'New Tests' in prior builds from the one you checked (I looked, and in the .1 build for that day there was 1 newly added test). HOWEVER Tests in the |
Add heartbeat provider for Azure App Services (in particular, Web Apps) that will add some infrastructure-identifying properties. - Solves issue #648, #868, and #872. - Both `AzureWebAppRoleEnvironment` and `AppServiceHeartbeatTelemetryModule` need to update their telemetry cache whenever the environment chances (ie, slot swap). `AppServiceEnvironmentVariableMonitor` solves this. - Keep the value of WEBSITE_SITE_NAME and other App Services values current during runtime - Merge PR #870 and #869, regarding heartbeat providers for App Services - Ensure adequate testing is added, update current Env Var tests to be testable in a parallel environment - `EnvironmentVariableMonitor` only updates the environment variables at set intervals, its subscribers only update when they need to. - Disable monitor if we don't detect that we are in Azure App Service environment, and on SecurityExceptions, disable the monitor altogether
|
This PR is replaced by PR #873. |
Fix Issue #868.
Add heartbeat properties when App Services environment is detected. Keep App Services environment in sync using existing telemetry initializer used for same purpose.