Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HeartBeat Feature Implementation #631

Merged
merged 26 commits into from
Apr 20, 2018
Merged

HeartBeat Feature Implementation #631

merged 26 commits into from
Apr 20, 2018

Conversation

dhaval24
Copy link
Contributor

@dhaval24 dhaval24 commented Mar 31, 2018

We would like to introduce HeartBeat feature.

What is Heartbeat?

Heartbeat in computer science is a periodic signal generated by software or hardware to indicate the normal operation or perform some synchronization. (https://en.wikipedia.org/wiki/Heartbeat_(computing))

HeartBeat in Application Insights

HeartBeat in Application Insights would send out some minimal amount of information on a scheduled interval to assist in high level monitoring of application health and would provide some meta data like OS type, Java version, Application Insights SDK version, Website Name and Host (If running on Azure WebApps) and is extensible to add more information should your application have heartbeat specific requirements.

HeartBeat Default Payload

Default Heartbeat payload will contain following information :
HeartBeat would be MetricTelemetry Item, a successful healthy heartbeat would have Metric Value = 0
It would contain following custom Dimensions -
OS Name - Indicates the name of operating system on which application is running
Java Version - Indicates the JDK version which is used by the application.
SDK Version - Indicates the version of ApplicationInsights Java SDK being used
processSessionId - a random guid which represents each running session of application

If Application is running on Azure WebApps it would also have following information -
Website Name - Name of the website
Host Stamp Name - Name of the host

Core Components

HeartBeatProviderInterface - Defines the implementation of concrete HeartBeat Provider. Should a user want to create a custom heartbeat provider they should implement the contract of this interface.

HearBeatProvider - Default Concrete Implementation of HeartBeatProviderInterface for transmitting HeartBeat Metric

HeartBeatDefaultPayloadProviderInterface - Interface which defines HeartBeatPayloadProvider and mechanism to add heartbeat property via separate callable. If a user would like to add custom properties which belong to specific group, implementing this interface is recommended.

BaseDefaultHeartbeatPropertyProvider - Concrete implementation of HeartBeatDefaultPayloadProviderInterface for default properties like SDK version, OS name, Java Version and processSessionId

WebAppsDefaultHeartbeatProvider - Concrete implementation of HeartBeatDefaultPayloadProviderInterface for default azure webapps properties.

HeartBeatModule - Concrete Implementation of TelemetryModule interface which composes HeartBeatProvider class to transmit heartbeat periodically. This module is registered by default even with bare minimum telemetry configuration.

HeartBeatPropertyPayload - A container for holding heartbeat property value and meta data about if it has updated and health.

Can you turn of HeartBeat ?

Yes, heartbeat can be turned off but it is highly not recommended to do so.

What is the default interval at which heatbeat is sent?

The, default interval at which heartbeat will be sent is 15 minutes.

What is the minimum interval which can be configured to send heartbeat?

The, minimum interval which can be configured is 5 seconds.

Can we override default heartbeat properties?

No, you cannot override default heartbeat properties. The are default and vital.

Can you exclude default heartbeat providers?

Yes, you can but it is not recommended

For significant contributions please make sure you have completed the following items:

  • Changes in public surface reviewed
  • CHANGELOG.md updated

@dhaval24 dhaval24 added this to the 2.1.0 milestone Mar 31, 2018
@dhaval24 dhaval24 self-assigned this Mar 31, 2018
@dhaval24
Copy link
Contributor Author

dhaval24 commented Apr 3, 2018

A general comment on the heartbeat interval. Currently there is no maximum limit on heartbeat interval. I believe C# implementation also does not have the same, but I would suggest having an upper limit to make sure that we always receive heartbeat in some period.

Copy link
Member

@Dmitry-Matveev Dmitry-Matveev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small comments below, I'd recommend adding Derek (@d3r3kk) into the review as well.

@@ -173,7 +176,13 @@ private void setTelemetryModules(ApplicationInsightsXmlConfiguration appConfigur
ReflectionUtils.loadComponents(TelemetryModule.class, modules, configurationModules.getAdds());
}

//if heartbeat module is not loaded, load heartbeat module
if (!isHeartBeatModuleAdded(modules)) {
addHeartBeatModule(configuration);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this behavior common across AI SDKs implementing heartbeat? I would not want customers remember in which SDK they would need to add HeartBeat explicitly if replacing modules vs. where it will be added by default even if they specifically didn't mention it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dmitry-Matveev in .net SDK DiagnosticModule is used to enable HeartBeat module and I believe DiagnosticModule is turned on by default. However, in Java SDK there is no diagnostic module and if we want to enable heartbeat module by default if not specified in the configuration file I think this would be the way. If you have better option I would like to hear that.
As far as behavior will go, both the SDKs would be consistent that means unless user has turned the HeartBeat module off explicitly in configuration it will be turned on by default with default settings.

for (String fieldName : enabledProperties) {
try {
switch (fieldName) {
case "website_site_name":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you read it for every heartbeat? In .NET SDK, Telemetry Initializers cached the value, and, therefore, were missing slot swap when the name changes. It looks like you read it every time here, so heartbeat will pick new name up.

Copy link
Contributor Author

@dhaval24 dhaval24 Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am not reading it everytime, its cached in final vairable. But this is a good point, if this is a scenario then we might have to read always or if not atleast every X minutes.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See microsoft/ApplicationInsights-dotnet-server#872 for the issue tracking the slot-swap problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@d3r3kk and @Dmitry-Matveev I have updated the code to remove the caching of environment variable to querying every-time. I believe this won't be a performance overhead per say because environment variable because in case of windows I believe environment variables are stored in Registry and in Linux they are stored in /etc/environment which is also a very fast access location.

@dhaval24
Copy link
Contributor Author

dhaval24 commented Apr 3, 2018

Somehow Github doesn't find @d3r3kk in reviewers list. Please feel free to add your points here Derek.

setResult = true;
}
else {
throw new Exception("heartbeat property cannot be set without adding it first, or heartbeat"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the chance to do this a second time, I would allow Add or Set to actually add new fields. Perhaps issue a logger warning if a user is Adding new values via Set, if there is any real danger of doing this.

Originally, I was concerned that allowing adds to occur at any time might encourage the abuse of the Heartbeat for what is more appropriate to do with TrackEvent calls. And with the heartbeats properties being around for the entire duration of a program's runtime, I was slightly concerned with memory fragmentation should any abuse occur. However, I doubt there is much of a real problem there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@d3r3kk Actually, I would like to keep the convention of add by default for adding and set by default for modifying. So keeping this in mind logically add should come before modifying (aka set in this case). I agree this is a bit opinionated and restricting the users but setting the convention I think will prove to be better in long run. Lot's of tunable knobs sometimes lead to spaghetti :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dhaval24, I'm inclined to agree with @d3r3kk. See the methods defined in Java's Map class. There could be places in the code where you won't know if a key has been added or not. I'm not a fan of the if containsKey set(x=y) else add(x=y) pattern when you can have a simple set(x=y).

Just as in Java's Map, putIfAbsent can be nice, if there's a precedent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@littleaj this is addressed

@d3r3kk
Copy link

d3r3kk commented Apr 4, 2018

Added some comments, and enjoyed reading through this - I haven't looked at Java for a very long time. I don't see any obvious problems with this change, and the API is aligned nicely with the .NET SDK for the same feature which is probably for the best.

@dhaval24
Copy link
Contributor Author

dhaval24 commented Apr 4, 2018

@d3r3kk thank you very much for reviewing and comments. I have addressed one of it and left my opinion on another. I am glad you enjoyed reading through this.

@dhaval24
Copy link
Contributor Author

I made some changes to accommodate auto configuration of HeartBeat in SpringBoot starter

@dhaval24
Copy link
Contributor Author

@littleaj I have tried to address maximum of your comments in my latest commit. Take a look hopefully those which are not addressed are not very critical at the moment.

synchronized (lock) {
if (!isInitialized) {
this.heartBeatProviderInterface.initialize(configuration);
InternalLogger.INSTANCE.info("heartbeat is enabled");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this change will work. isHeartbeatEnabled() uses the heartbeatProviderInterface, but the provider doesn't get the config values until it's initialized. Right?

@@ -133,10 +135,10 @@ public boolean addHeartBeatProperty(String propertyName, String propertyValue,
payload.setPayloadValue(propertyValue);
heartbeatProperties.put(propertyName, payload);
isAdded = true;
InternalLogger.INSTANCE.trace("added heartbeat property");
InternalLogger.INSTANCE.trace("added heartbeat property %s - %s", propertyName, propertyValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hyphen is an interesting choice. Usually you see %s:%s or %s=%s to indicate key=value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nit: I think should be fine :)

@@ -145,7 +152,7 @@ private String getSdkVersion() {
* Gets the OS version on which application is running.
* @return String representing OS version
*/
private String getOsType() {
private String getOsVersion() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want the OS version? If so, you should explore the difference between os.name and os.version (i.e. os.name does not contain version information).

I also found a few articles stating that os.name doesn't have the correct value when on Windows 10. Perhaps there is a different way to find this information.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can amend it, if there is a better way though I am not aware of one. This is meta data which is not very very vital though.

@@ -88,6 +90,8 @@ public HeartBeatProvider() {
this.heartbeatProperties = new ConcurrentHashMap<>();
this.isEnabled = true;
this.heartbeatsSent = new AtomicLong(0);
this.propertyUpdateService = Executors.newCachedThreadPool(ThreadPoolUtils.createDaemonThreadFactory(HeartBeatProvider.class));
Copy link
Contributor

@littleaj littleaj Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes these two thread pools indistinguishable. You may need another method in ThreadPool utils to specify a custom name. #Resolved

}

@Override
public boolean addHeartBeatProperty(String propertyName, String propertyValue,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must say, I have strong feelings against this add/set property pattern. I don't understand why we need to throw if someone is overwriting a property with add or setting a new property with set. Please explain why we are doing it this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@littleaj I believe it adds more readability and easier to distinguish operations. I know that it is not common in Java's map class however at the same time I also do feel that being some what opinionated here is not causing any blockage. Unless there is a mistake in documentation I don't think user will play around with this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One knob is simpler than two knobs; and there's no reason to be this strict. That's all I'm saying.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@littleaj fair enough. I was under impression to change the api to have only set() method instead of add() and set() both. But from the comment now I understand that the only ask here is to not throw the exception but log the message accordingly which is acceptable. I can make this changes too.

InternalLogger.INSTANCE.trace("added heartbeat property %s - %s", propertyName, propertyValue);
}
else {
throw new Exception("heartbeat property cannot be added twice. Please use setHeartBeatProperty instead to modify the value");
Copy link
Contributor

@littleaj littleaj Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a more specific Exception subclass. #Resolved

Copy link
Contributor Author

@dhaval24 dhaval24 Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nice to have but I don't think this should block the PR. #Resolved

@littleaj
Copy link
Contributor

@dhaval24, also address the failing unit test.

@@ -119,7 +120,9 @@ public final void initialize(TelemetryConfiguration configuration) {
private void setMinimumConfiguration(ApplicationInsightsXmlConfiguration userConfiguration, TelemetryConfiguration configuration) {
Copy link
Contributor

@littleaj littleaj Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the heartbeat module always be added? It didn't look like it the way this method is called. #Resolved

Copy link
Contributor Author

@dhaval24 dhaval24 Apr 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this module will always be added. #Resolved

Copy link
Contributor Author

@dhaval24 dhaval24 Apr 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@littleaj look into the method setTelemetryModules(). It adds HB module explicitly and also look atsetMininmumConfiguration(). This module would be present by default. #Resolved

Copy link
Contributor

@littleaj littleaj Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dhaval24 , Ok, I see.

I did notice that you can reduce some redundancy if you remove the call to addHeartbeatModule from setMinimumConfiguration. Since setTelemetryModules is always called and setMinimumConfiguration is only called when the XML config is missing, you don't need the add call in setMinimumConfiguration. #Resolved

Copy link
Contributor Author

@dhaval24 dhaval24 Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@littleaj I need to add this for SpringBoot scenarios. Here it is not called because SpringBoot autoconfigures it using beans. #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I see. I missed the return statements above.


In reply to: 181910995 [](ancestors = 181910995)

* @author Dhaval Doshi
* @since 03-30-2018
*/
public class HeartBeatModule implements TelemetryModule {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears this needs a no arg constructor as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have that ... but not needed. We can pass the null inside the parameterized constructor. There is a check there which handles it. I don't think we need to expose other constructor here if we can avoid.


//Submit task to set properties to dictionary using separate thread. we do not wait for the
//results to come out as some I/O bound properties may take time.
propertyUpdateService.submit(HeartbeatDefaultPayload.populateDefaultPayload(getExcludedHeartBeatProperties(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which properties are I/O bound? I wasn't able to find any. We may be able to remove the thread pool.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no properties which are IO bound today. But later on there will be when we will query ARM to get VM ID. I would rather keep it.

MetricTelemetry telemetry = (MetricTelemetry)gatherData();
telemetry.getContext().getOperation().setSyntheticSource(HEARTBEAT_SYNTHETIC_METRIC_NAME);
telemetryClient.trackMetric(telemetry);
heartbeatsSent.incrementAndGet();
Copy link
Contributor

@littleaj littleaj Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be in the log line below. It returns the value after incrementing. Also, since it's in a lock it doesn't need to be atomic #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other comment regarding the lock. If you remove the lock, you can keep the atomic int.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@dhaval24
Copy link
Contributor Author

@littleaj I have addressed your concerns on ThreadPool identification and fixed the broken tests.

@dhaval24
Copy link
Contributor Author

@littleaj all of your concerns must have been addressed with my recent changes. Please take a look and would be great if we can get this merged if no blockers are left.

InternalLogger.INSTANCE.trace("added heartbeat property %s - %s", propertyName, propertyValue);
}
else {
throw new IllegalStateException("heartbeat property cannot be added twice. Please use setHeartBeatProperty instead to modify the value");
Copy link
Contributor

@littleaj littleaj Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed this in my first review. If you are throwing and catching in the same method based on a conditional, you can refactor it so that the statements in the catch block are moved to inside the else block (in place of throw). Another option would be to throw the exception out of this method. #Resolved

Copy link
Contributor Author

@dhaval24 dhaval24 Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Statements and returns should just be fine. I will make this changes. #Resolved

* Creates and returns the heartbeat telemetry.
* @return Metric Telemetry which represent heartbeat.
*/
private Telemetry gatherData() {
Copy link
Contributor

@littleaj littleaj Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just return MetricTelemetry so you don't have to cast it. #Resolved

Copy link
Contributor Author

@dhaval24 dhaval24 Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, Okay this should be addressed. #Resolved

return new Runnable() {
@Override
public void run() {
if (isEnabled) {
Copy link
Contributor

@littleaj littleaj Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow this conditional. Will the thread start even when it's disabled? #Resolved

Copy link
Contributor Author

@dhaval24 dhaval24 Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well actually it is unnecessary. But it doesn't hurt either, so I would not mind just keeping it there. #Resolved

*/
private void send() {

synchronized (lock) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the lock guarding? The ConcurrentMap keeps the CRUD to it thread-safe. I think you can just remove the lock and keep the atomic int

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@littleaj my intention here is to send heartBeats synchronously. Telemetry client's track() method sends Telemetry asynchronously which is not desired here. I wanted to guard against an untoward situation like this. I have removed the Atomic version to primitive as you mentioned we are already locking and therefore avoid overhead.

MetricTelemetry telemetry = (MetricTelemetry)gatherData();
telemetry.getContext().getOperation().setSyntheticSource(HEARTBEAT_SYNTHETIC_METRIC_NAME);
telemetryClient.trackMetric(telemetry);
heartbeatsSent.incrementAndGet();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other comment regarding the lock. If you remove the lock, you can keep the atomic int.

@dhaval24
Copy link
Contributor Author

@littleaj I have done another round of edits for your comments. I think everything is now addressed and this should be good to go. Kindly please approve after checking.

@dhaval24
Copy link
Contributor Author

@littleaj fixed couple of remaining unaddressed feedback also. Let me know if there are anything else.

Copy link
Contributor Author

@dhaval24 dhaval24 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@littleaj I have carefully gone through all your comments, marked them with fixed in most of the cases. The only one which is left is about ScheduledThreadPool. Let me know which route is preferred for this case. See my comments on it.

@d3r3kk
Copy link

d3r3kk commented Apr 19, 2018

Please note that there is a relevant change in the dotnet repo here today:
Known sdk versions (added heartbeat): Microsoft/ApplicationInsights-Home#213
Update heartbeat properties to include internal sdk version: microsoft/ApplicationInsights-dotnet#782

This change will affect the 2.7.0-stable release of .Net in July, so you might want to track an issue separate from this PR.

@@ -1,4 +1,3 @@
#Fri Feb 09 18:53:31 PST 2018
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dhaval24 The gradle related files don't need to be updated. (gradle-wrapper.properties, gradle-wrapper.jar, gradlew)

Copy link
Contributor

@littleaj littleaj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make the few changes we discussed and ship it!

/**
* Flag to seek if module is initialized
*/
private static boolean isInitialized = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.heartbeatProperties = new ConcurrentHashMap<>();
this.isEnabled = true;
this.heartbeatsSent = 0;
SDKShutdownActivity.INSTANCE.register(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this as the last line of the constructor so if the shutdown is triggered before the constructor ends it won't NPE.

* @param inputList list of string containing reserved keywords
* @return true if input word is present in the list ignoring the case.
*/
static boolean containsIgnoreCase(String keyword, Set<String> inputList) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove unused method

* @param timeout Max time to wait
* @param timeUnit Timeunit for timeout
*/
public static void stop(ExecutorService executorService, long timeout, TimeUnit timeUnit) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete the older method since ThreadPoolExecutor is an ExecutorService

@dhaval24
Copy link
Contributor Author

All the issues are now addressed. Merging to master.

@dhaval24 dhaval24 merged commit 7e37691 into master Apr 20, 2018
@dhaval24 dhaval24 deleted the HeartBeat branch September 16, 2018 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants