TelemetryService.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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.ChaiProvider;
  24. import lombok.Builder;
  25. import lombok.Getter;
  26. import password.pwm.AppProperty;
  27. import password.pwm.PwmApplication;
  28. import password.pwm.PwmApplicationMode;
  29. import password.pwm.PwmConstants;
  30. import password.pwm.PwmEnvironment;
  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.svc.PwmService;
  42. import password.pwm.svc.stats.Statistic;
  43. import password.pwm.svc.stats.StatisticsBundle;
  44. import password.pwm.svc.stats.StatisticsManager;
  45. import password.pwm.util.java.JavaHelper;
  46. import password.pwm.util.java.JsonUtil;
  47. import password.pwm.util.java.StringUtil;
  48. import password.pwm.util.java.TimeDuration;
  49. import password.pwm.util.localdb.LocalDB;
  50. import password.pwm.util.logging.PwmLogger;
  51. import password.pwm.util.macro.MacroMachine;
  52. import password.pwm.util.secure.PwmRandom;
  53. import java.io.IOException;
  54. import java.net.URISyntaxException;
  55. import java.time.Instant;
  56. import java.time.ZoneId;
  57. import java.time.ZonedDateTime;
  58. import java.time.format.DateTimeFormatter;
  59. import java.util.ArrayList;
  60. import java.util.Collections;
  61. import java.util.LinkedHashMap;
  62. import java.util.LinkedHashSet;
  63. import java.util.List;
  64. import java.util.Map;
  65. import java.util.Set;
  66. import java.util.TreeMap;
  67. import java.util.concurrent.ScheduledExecutorService;
  68. import java.util.concurrent.TimeUnit;
  69. public class TelemetryService implements PwmService {
  70. private static final PwmLogger LOGGER = PwmLogger.forClass(TelemetryService.class);
  71. private ScheduledExecutorService executorService;
  72. private PwmApplication pwmApplication;
  73. private Settings settings;
  74. private Instant lastPublishTime;
  75. private ErrorInformation lastError;
  76. private TelemetrySender sender;
  77. private STATUS status = STATUS.NEW;
  78. @Override
  79. public STATUS status()
  80. {
  81. return null;
  82. }
  83. @Override
  84. public void init(final PwmApplication pwmApplication) throws PwmException
  85. {
  86. status = STATUS.OPENING;
  87. this.pwmApplication = pwmApplication;
  88. if (pwmApplication.getApplicationMode() != PwmApplicationMode.RUNNING) {
  89. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, app is not running");
  90. status = STATUS.CLOSED;
  91. return;
  92. }
  93. if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.PUBLISH_STATS_ENABLE)) {
  94. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, publish stats not enabled");
  95. status = STATUS.CLOSED;
  96. return;
  97. }
  98. if (pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN) {
  99. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, localdb not enabled");
  100. status = STATUS.CLOSED;
  101. return;
  102. }
  103. if (pwmApplication.getStatisticsManager().status() != STATUS.OPEN) {
  104. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, statistics manager is not enabled");
  105. status = STATUS.CLOSED;
  106. return;
  107. }
  108. settings = Settings.fromConfig(pwmApplication.getConfig());
  109. try {
  110. initSender();
  111. } catch (PwmUnrecoverableException e) {
  112. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "will remain closed, unable to init sender: " + e.getMessage());
  113. status = STATUS.CLOSED;
  114. return;
  115. }
  116. {
  117. final Instant storedLastPublishTimestamp = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.TELEMETRY_LAST_PUBLISH_TIMESTAMP, Instant.class);
  118. lastPublishTime = storedLastPublishTimestamp != null ?
  119. storedLastPublishTimestamp :
  120. pwmApplication.getInstallTime();
  121. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "last publish time was " + JavaHelper.toIsoDate(lastPublishTime));
  122. }
  123. executorService = JavaHelper.makeSingleThreadExecutorService(pwmApplication, TelemetryService.class);
  124. scheduleNextJob();
  125. }
  126. private void initSender() throws PwmUnrecoverableException
  127. {
  128. if (StringUtil.isEmpty(settings.getSenderImplementation())) {
  129. final String msg = "telemetry sender implementation not specified";
  130. throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TELEMETRY_SEND_ERROR, msg));
  131. }
  132. final TelemetrySender telemetrySender;
  133. try {
  134. final String senderClass = settings.getSenderImplementation();
  135. final Class theClass = Class.forName(senderClass);
  136. telemetrySender = (TelemetrySender) theClass.newInstance();
  137. } catch (Exception e) {
  138. final String msg = "unable to load implementation class: " + e.getMessage();
  139. throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, msg));
  140. }
  141. try {
  142. final String macrodSettings = MacroMachine.forNonUserSpecific(pwmApplication, null).expandMacros(settings.getSenderSettings());
  143. telemetrySender.init(pwmApplication, macrodSettings);
  144. } catch (Exception e) {
  145. final String msg = "unable to init implementation class: " + e.getMessage();
  146. throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, msg));
  147. }
  148. sender = telemetrySender;
  149. }
  150. private void executePublishJob() throws PwmUnrecoverableException, IOException, URISyntaxException
  151. {
  152. final String authValue = pwmApplication.getStatisticsManager().getStatBundleForKey(StatisticsManager.KEY_CUMULATIVE).getStatistic(Statistic.AUTHENTICATIONS);
  153. if (StringUtil.isEmpty(authValue) || Integer.parseInt(authValue) < settings.getMinimumAuthentications()) {
  154. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "skipping telemetry send, authentication count is too low");
  155. } else {
  156. try {
  157. final TelemetryPublishBean telemetryPublishBean = generatePublishableBean();
  158. sender.publish(telemetryPublishBean);
  159. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "sent telemetry data: " + JsonUtil.serialize(telemetryPublishBean));
  160. } catch (PwmException e) {
  161. lastError = e.getErrorInformation();
  162. LOGGER.error(SessionLabel.TELEMETRY_SESSION_LABEL, "error sending telemetry data: " + e.getMessage());
  163. }
  164. }
  165. lastPublishTime = Instant.now();
  166. pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.TELEMETRY_LAST_PUBLISH_TIMESTAMP, lastPublishTime);
  167. scheduleNextJob();
  168. }
  169. private void scheduleNextJob() {
  170. final TimeDuration durationUntilNextPublish = durationUntilNextPublish();
  171. executorService.schedule(
  172. new PublishJob(),
  173. durationUntilNextPublish.getTotalMilliseconds(),
  174. TimeUnit.MILLISECONDS);
  175. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "next publish time: " + durationUntilNextPublish().asCompactString());
  176. }
  177. private class PublishJob implements Runnable {
  178. @Override
  179. public void run()
  180. {
  181. try {
  182. executePublishJob();
  183. } catch (PwmException e) {
  184. LOGGER.error(e.getErrorInformation());
  185. } catch (Exception e) {
  186. LOGGER.error("unexpected error during telemetry publish job: " + e.getMessage());
  187. }
  188. }
  189. }
  190. @Override
  191. public void close()
  192. {
  193. }
  194. @Override
  195. public List<HealthRecord> healthCheck()
  196. {
  197. return null;
  198. }
  199. @Override
  200. public ServiceInfoBean serviceInfo()
  201. {
  202. final Map<String,String> debugMap = new LinkedHashMap<>();
  203. debugMap.put("lastPublishTime", JavaHelper.toIsoDate(lastPublishTime));
  204. debugMap.put("lastError", lastError.toDebugStr());
  205. return new ServiceInfoBean(null,Collections.unmodifiableMap(debugMap));
  206. }
  207. private TelemetryPublishBean generatePublishableBean()
  208. throws URISyntaxException, IOException, PwmUnrecoverableException
  209. {
  210. final StatisticsBundle bundle = pwmApplication.getStatisticsManager().getStatBundleForKey(StatisticsManager.KEY_CUMULATIVE);
  211. final Configuration config = pwmApplication.getConfig();
  212. final Map<String,String> statData = new TreeMap<>();
  213. for (final Statistic loopStat : Statistic.values()) {
  214. statData.put(loopStat.getKey(),bundle.getStatistic(loopStat));
  215. }
  216. final List<String> configuredSettings = new ArrayList<>();
  217. for (final PwmSetting pwmSetting : config.nonDefaultSettings()) {
  218. if (!pwmSetting.getCategory().hasProfiles() && !config.isDefaultValue(pwmSetting)) {
  219. configuredSettings.add(pwmSetting.getKey());
  220. }
  221. }
  222. final Set<ChaiProvider.DIRECTORY_VENDOR> ldapVendors = new LinkedHashSet<>();
  223. for (final LdapProfile ldapProfile : config.getLdapProfiles().values()) {
  224. try {
  225. ldapVendors.add(ldapProfile.getProxyChaiProvider(pwmApplication).getDirectoryVendor());
  226. } catch (Exception e) {
  227. LOGGER.trace(SessionLabel.TELEMETRY_SESSION_LABEL, "unable to read ldap vendor type for stats publication: " + e.getMessage());
  228. }
  229. }
  230. final TelemetryPublishBean.TelemetryPublishBeanBuilder builder = TelemetryPublishBean.builder();
  231. builder.timestamp(Instant.now());
  232. builder.id(makeId(pwmApplication));
  233. builder.instanceID(pwmApplication.getInstanceID());
  234. builder.installTime(pwmApplication.getInstallTime());
  235. builder.siteDescription(config.readSettingAsString(PwmSetting.PUBLISH_STATS_SITE_DESCRIPTION));
  236. builder.versionBuild(PwmConstants.BUILD_NUMBER);
  237. builder.versionVersion(PwmConstants.BUILD_VERSION);
  238. builder.ldapVendor(Collections.unmodifiableList(new ArrayList<>(ldapVendors)));
  239. builder.statistics(Collections.unmodifiableMap(statData));
  240. builder.configuredSettings(Collections.unmodifiableList(configuredSettings));
  241. final TelemetryPublishBean.Environment environment = TelemetryPublishBean.Environment.builder()
  242. .appliance(pwmApplication.getPwmEnvironment().getFlags().contains(PwmEnvironment.ApplicationFlag.Appliance))
  243. .javaVendor(System.getProperty("java.vm.vendor"))
  244. .javaName(System.getProperty("java.vm.name"))
  245. .javaVersion(System.getProperty("java.vm.version"))
  246. .osName(System.getProperty("os.name"))
  247. .osVersion(System.getProperty("os.version"))
  248. .build();
  249. builder.environment(environment);
  250. return builder.build();
  251. }
  252. private static String makeId(final PwmApplication pwmApplication) throws PwmUnrecoverableException
  253. {
  254. final String SEPARATOR = "-";
  255. final String DATETIME_PATTERN = "yyyyMMdd-HHmmss'Z'";
  256. final String timestamp = DateTimeFormatter.ofPattern(DATETIME_PATTERN).format(ZonedDateTime.now(ZoneId.of("Zulu")));
  257. return PwmConstants.PWM_APP_NAME.toLowerCase()
  258. + SEPARATOR + instanceHash(pwmApplication)
  259. + SEPARATOR + timestamp;
  260. }
  261. private static String instanceHash(final PwmApplication pwmApplication) throws PwmUnrecoverableException
  262. {
  263. final int MAX_HASH_LENGTH = 64;
  264. final String instanceID = pwmApplication.getInstanceID();
  265. final String hash = pwmApplication.getSecureService().hash(instanceID);
  266. return hash.length() > 64
  267. ? hash.substring(0, MAX_HASH_LENGTH)
  268. : hash;
  269. }
  270. @Getter
  271. @Builder
  272. private static class Settings {
  273. private TimeDuration publishFrequency;
  274. private int minimumAuthentications;
  275. private String senderImplementation;
  276. private String senderSettings;
  277. static Settings fromConfig(final Configuration config) {
  278. return Settings.builder()
  279. .minimumAuthentications(Integer.parseInt(config.readAppProperty(AppProperty.TELEMETRY_MIN_AUTHENTICATIONS)))
  280. .publishFrequency(new TimeDuration(Integer.parseInt(config.readAppProperty(AppProperty.TELEMETRY_SEND_FREQUENCY_SECONDS)),TimeUnit.SECONDS))
  281. .senderImplementation(config.readAppProperty(AppProperty.TELEMETRY_SENDER_IMPLEMENTATION))
  282. .senderSettings(config.readAppProperty(AppProperty.TELEMETRY_SENDER_SETTINGS))
  283. .build();
  284. }
  285. }
  286. private TimeDuration durationUntilNextPublish() {
  287. final Instant nextPublishTime = settings.getPublishFrequency().incrementFromInstant(lastPublishTime);
  288. final Instant minuteFromNow = TimeDuration.MINUTE.incrementFromInstant(Instant.now());
  289. return nextPublishTime.isBefore(minuteFromNow)
  290. ? TimeDuration.fromCurrent(minuteFromNow)
  291. : TimeDuration.fromCurrent(nextPublishTime.toEpochMilli() + (PwmRandom.getInstance().nextInt(600) - 300));
  292. }
  293. }