TelemetryService.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. /*
  2. * Password Management Servlets (PWM)
  3. * http://www.pwm-project.org
  4. *
  5. * Copyright (c) 2006-2009 Novell, Inc.
  6. * Copyright (c) 2009-2017 The PWM Project
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation; either version 2 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program; if not, write to the Free Software
  20. * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  21. */
  22. package password.pwm.svc.telemetry;
  23. import com.novell.ldapchai.provider.DirectoryVendor;
  24. import lombok.Builder;
  25. import lombok.Getter;
  26. import password.pwm.AppProperty;
  27. import password.pwm.PwmAboutProperty;
  28. import password.pwm.PwmApplication;
  29. import password.pwm.PwmApplicationMode;
  30. import password.pwm.PwmConstants;
  31. import password.pwm.bean.SessionLabel;
  32. import password.pwm.bean.TelemetryPublishBean;
  33. import password.pwm.config.Configuration;
  34. import password.pwm.config.PwmSetting;
  35. import password.pwm.config.profile.LdapProfile;
  36. import password.pwm.error.ErrorInformation;
  37. import password.pwm.error.PwmError;
  38. import password.pwm.error.PwmException;
  39. import password.pwm.error.PwmUnrecoverableException;
  40. import password.pwm.health.HealthRecord;
  41. import password.pwm.ldap.PwmLdapVendor;
  42. import password.pwm.svc.PwmService;
  43. import password.pwm.svc.stats.Statistic;
  44. import password.pwm.svc.stats.StatisticsBundle;
  45. import password.pwm.svc.stats.StatisticsManager;
  46. import password.pwm.util.java.JavaHelper;
  47. import password.pwm.util.java.JsonUtil;
  48. import password.pwm.util.java.StringUtil;
  49. import password.pwm.util.java.TimeDuration;
  50. import password.pwm.util.localdb.LocalDB;
  51. import password.pwm.util.logging.PwmLogger;
  52. import password.pwm.util.macro.MacroMachine;
  53. import password.pwm.util.secure.PwmRandom;
  54. import java.io.IOException;
  55. import java.net.URISyntaxException;
  56. import java.time.Instant;
  57. import java.time.ZoneId;
  58. import java.time.ZonedDateTime;
  59. import java.time.format.DateTimeFormatter;
  60. import java.util.ArrayList;
  61. import java.util.Collections;
  62. import java.util.LinkedHashMap;
  63. import java.util.LinkedHashSet;
  64. import java.util.List;
  65. import java.util.Map;
  66. import java.util.Set;
  67. import java.util.TreeMap;
  68. import java.util.concurrent.ScheduledExecutorService;
  69. import java.util.concurrent.TimeUnit;
  70. public class TelemetryService implements PwmService {
  71. private static final PwmLogger LOGGER = PwmLogger.forClass(TelemetryService.class);
  72. private ScheduledExecutorService executorService;
  73. private PwmApplication pwmApplication;
  74. private Settings settings;
  75. private Instant lastPublishTime;
  76. private ErrorInformation lastError;
  77. private TelemetrySender sender;
  78. private STATUS status = STATUS.NEW;
  79. @Override
  80. public STATUS status()
  81. {
  82. return status;
  83. }
  84. @Override
  85. public void init(final PwmApplication pwmApplication) throws PwmException
  86. {
  87. status = STATUS.OPENING;
  88. this.pwmApplication = pwmApplication;
  89. if (pwmApplication.getApplicationMode() != PwmApplicationMode.RUNNING) {
  90. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, app is not running");
  91. status = STATUS.CLOSED;
  92. return;
  93. }
  94. if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.PUBLISH_STATS_ENABLE)) {
  95. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, publish stats not enabled");
  96. status = STATUS.CLOSED;
  97. return;
  98. }
  99. if (pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN) {
  100. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, localdb not enabled");
  101. status = STATUS.CLOSED;
  102. return;
  103. }
  104. if (pwmApplication.getStatisticsManager().status() != STATUS.OPEN) {
  105. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, statistics manager is not enabled");
  106. status = STATUS.CLOSED;
  107. return;
  108. }
  109. settings = Settings.fromConfig(pwmApplication.getConfig());
  110. try {
  111. initSender();
  112. } catch (PwmUnrecoverableException e) {
  113. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, unable to init sender: " + e.getMessage());
  114. status = STATUS.CLOSED;
  115. return;
  116. }
  117. {
  118. final Instant storedLastPublishTimestamp = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.TELEMETRY_LAST_PUBLISH_TIMESTAMP, Instant.class);
  119. lastPublishTime = storedLastPublishTimestamp != null ?
  120. storedLastPublishTimestamp :
  121. pwmApplication.getInstallTime();
  122. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "last publish time was " + JavaHelper.toIsoDate(lastPublishTime));
  123. }
  124. executorService = JavaHelper.makeSingleThreadExecutorService(pwmApplication, TelemetryService.class);
  125. scheduleNextJob();
  126. status = STATUS.OPEN;
  127. }
  128. private void initSender() throws PwmUnrecoverableException
  129. {
  130. if (StringUtil.isEmpty(settings.getSenderImplementation())) {
  131. final String msg = "telemetry sender implementation not specified";
  132. throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
  133. }
  134. final TelemetrySender telemetrySender;
  135. try {
  136. final String senderClass = settings.getSenderImplementation();
  137. final Class theClass = Class.forName(senderClass);
  138. telemetrySender = (TelemetrySender) theClass.newInstance();
  139. } catch (Exception e) {
  140. final String msg = "unable to load implementation class: " + e.getMessage();
  141. throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, msg));
  142. }
  143. try {
  144. final String macrodSettings = MacroMachine.forNonUserSpecific(pwmApplication, null).expandMacros(settings.getSenderSettings());
  145. telemetrySender.init(pwmApplication, macrodSettings);
  146. } catch (Exception e) {
  147. final String msg = "unable to init implementation class: " + e.getMessage();
  148. throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, msg));
  149. }
  150. sender = telemetrySender;
  151. }
  152. private void executePublishJob() throws PwmUnrecoverableException, IOException, URISyntaxException
  153. {
  154. final String authValue = pwmApplication.getStatisticsManager().getStatBundleForKey(StatisticsManager.KEY_CUMULATIVE).getStatistic(Statistic.AUTHENTICATIONS);
  155. if (StringUtil.isEmpty(authValue) || Integer.parseInt(authValue) < settings.getMinimumAuthentications()) {
  156. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "skipping telemetry send, authentication count is too low");
  157. } else {
  158. try {
  159. final TelemetryPublishBean telemetryPublishBean = generatePublishableBean();
  160. sender.publish(telemetryPublishBean);
  161. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "sent telemetry data: " + JsonUtil.serialize(telemetryPublishBean));
  162. } catch (PwmException e) {
  163. lastError = e.getErrorInformation();
  164. LOGGER.error(SessionLabel.TELEMETRY_SESSION_LABEL, "error sending telemetry data: " + e.getMessage());
  165. }
  166. }
  167. lastPublishTime = Instant.now();
  168. pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.TELEMETRY_LAST_PUBLISH_TIMESTAMP, lastPublishTime);
  169. scheduleNextJob();
  170. }
  171. private void scheduleNextJob() {
  172. final TimeDuration durationUntilNextPublish = durationUntilNextPublish();
  173. executorService.schedule(
  174. new PublishJob(),
  175. durationUntilNextPublish.getTotalMilliseconds(),
  176. TimeUnit.MILLISECONDS);
  177. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "next publish time: " + durationUntilNextPublish().asCompactString());
  178. }
  179. private class PublishJob implements Runnable {
  180. @Override
  181. public void run()
  182. {
  183. try {
  184. executePublishJob();
  185. } catch (PwmException e) {
  186. LOGGER.error(e.getErrorInformation());
  187. } catch (Exception e) {
  188. LOGGER.error("unexpected error during telemetry publish job: " + e.getMessage());
  189. }
  190. }
  191. }
  192. @Override
  193. public void close()
  194. {
  195. }
  196. @Override
  197. public List<HealthRecord> healthCheck()
  198. {
  199. return null;
  200. }
  201. @Override
  202. public ServiceInfoBean serviceInfo()
  203. {
  204. final Map<String,String> debugMap = new LinkedHashMap<>();
  205. debugMap.put("lastPublishTime", JavaHelper.toIsoDate(lastPublishTime));
  206. if (lastError != null) {
  207. debugMap.put("lastError", lastError.toDebugStr());
  208. }
  209. return new ServiceInfoBean(null,Collections.unmodifiableMap(debugMap));
  210. }
  211. public TelemetryPublishBean generatePublishableBean()
  212. throws URISyntaxException, IOException, PwmUnrecoverableException
  213. {
  214. final StatisticsBundle bundle = pwmApplication.getStatisticsManager().getStatBundleForKey(StatisticsManager.KEY_CUMULATIVE);
  215. final Configuration config = pwmApplication.getConfig();
  216. final Map<PwmAboutProperty,String> aboutPropertyStringMap = PwmAboutProperty.makeInfoBean(pwmApplication);
  217. final Map<String,String> statData = new TreeMap<>();
  218. for (final Statistic loopStat : Statistic.values()) {
  219. statData.put(loopStat.getKey(),bundle.getStatistic(loopStat));
  220. }
  221. final List<String> configuredSettings = new ArrayList<>();
  222. for (final PwmSetting pwmSetting : config.nonDefaultSettings()) {
  223. if (!pwmSetting.getCategory().hasProfiles() && !config.isDefaultValue(pwmSetting)) {
  224. configuredSettings.add(pwmSetting.getKey());
  225. }
  226. }
  227. final Set<PwmLdapVendor> ldapVendors = new LinkedHashSet<>();
  228. for (final LdapProfile ldapProfile : config.getLdapProfiles().values()) {
  229. try {
  230. final DirectoryVendor directoryVendor = ldapProfile.getProxyChaiProvider(pwmApplication).getDirectoryVendor();
  231. final PwmLdapVendor pwmLdapVendor = PwmLdapVendor.fromChaiVendor(directoryVendor);
  232. ldapVendors.add(pwmLdapVendor);
  233. } catch (Exception e) {
  234. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "unable to read ldap vendor type for stats publication: " + e.getMessage());
  235. }
  236. }
  237. final Map<String, String> aboutStrings = new TreeMap<>();
  238. {
  239. for (final Map.Entry<PwmAboutProperty, String> entry : aboutPropertyStringMap.entrySet()) {
  240. final PwmAboutProperty pwmAboutProperty = entry.getKey();
  241. aboutStrings.put(pwmAboutProperty.name(), entry.getValue());
  242. }
  243. aboutStrings.remove(PwmAboutProperty.app_instanceID.name());
  244. aboutStrings.remove(PwmAboutProperty.app_siteUrl.name());
  245. }
  246. final TelemetryPublishBean.TelemetryPublishBeanBuilder builder = TelemetryPublishBean.builder();
  247. builder.timestamp(Instant.now());
  248. builder.id(makeId(pwmApplication));
  249. builder.instanceHash(pwmApplication.getSecureService().hash(pwmApplication.getInstanceID()));
  250. builder.installTime(pwmApplication.getInstallTime());
  251. builder.siteDescription(config.readSettingAsString(PwmSetting.PUBLISH_STATS_SITE_DESCRIPTION));
  252. builder.versionBuild(PwmConstants.BUILD_NUMBER);
  253. builder.versionVersion(PwmConstants.BUILD_VERSION);
  254. builder.ldapVendor(Collections.unmodifiableList(new ArrayList<>(ldapVendors)));
  255. builder.statistics(Collections.unmodifiableMap(statData));
  256. builder.configuredSettings(Collections.unmodifiableList(configuredSettings));
  257. builder.about(aboutStrings);
  258. return builder.build();
  259. }
  260. private static String makeId(final PwmApplication pwmApplication) throws PwmUnrecoverableException
  261. {
  262. final String SEPARATOR = "-";
  263. final String DATETIME_PATTERN = "yyyyMMdd-HHmmss'Z'";
  264. final String timestamp = DateTimeFormatter.ofPattern(DATETIME_PATTERN).format(ZonedDateTime.now(ZoneId.of("Zulu")));
  265. return PwmConstants.PWM_APP_NAME.toLowerCase()
  266. + SEPARATOR + instanceHash(pwmApplication)
  267. + SEPARATOR + timestamp;
  268. }
  269. private static String instanceHash(final PwmApplication pwmApplication) throws PwmUnrecoverableException
  270. {
  271. final int MAX_HASH_LENGTH = 64;
  272. final String instanceID = pwmApplication.getInstanceID();
  273. final String hash = pwmApplication.getSecureService().hash(instanceID);
  274. return hash.length() > 64
  275. ? hash.substring(0, MAX_HASH_LENGTH)
  276. : hash;
  277. }
  278. @Getter
  279. @Builder
  280. private static class Settings {
  281. private TimeDuration publishFrequency;
  282. private int minimumAuthentications;
  283. private String senderImplementation;
  284. private String senderSettings;
  285. static Settings fromConfig(final Configuration config) {
  286. return Settings.builder()
  287. .minimumAuthentications(Integer.parseInt(config.readAppProperty(AppProperty.TELEMETRY_MIN_AUTHENTICATIONS)))
  288. .publishFrequency(new TimeDuration(Integer.parseInt(config.readAppProperty(AppProperty.TELEMETRY_SEND_FREQUENCY_SECONDS)),TimeUnit.SECONDS))
  289. .senderImplementation(config.readAppProperty(AppProperty.TELEMETRY_SENDER_IMPLEMENTATION))
  290. .senderSettings(config.readAppProperty(AppProperty.TELEMETRY_SENDER_SETTINGS))
  291. .build();
  292. }
  293. }
  294. private TimeDuration durationUntilNextPublish() {
  295. final Instant nextPublishTime = settings.getPublishFrequency().incrementFromInstant(lastPublishTime);
  296. final Instant minuteFromNow = TimeDuration.MINUTE.incrementFromInstant(Instant.now());
  297. return nextPublishTime.isBefore(minuteFromNow)
  298. ? TimeDuration.fromCurrent(minuteFromNow)
  299. : TimeDuration.fromCurrent(nextPublishTime.toEpochMilli() + (PwmRandom.getInstance().nextInt(600) - 300));
  300. }
  301. }