Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  ClientID.sys.mjs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { Log } from "resource://gre/modules/Log.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

const LOGGER_NAME = "Toolkit.Telemetry";
const LOGGER_PREFIX = "ClientID::";
// Must match ID in TelemetryUtils
const CANARY_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
const CANARY_PROFILE_GROUP_ID = "decafdec-afde-cafd-ecaf-decafdecafde";
const CANARY_USAGE_PROFILE_ID = "beefbeef-beef-beef-beef-beeefbeefbee";

const DRS_STATE_VERSION = 2;

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  CommonUtils: "resource://services-common/utils.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "CryptoHash", () => {
  return Components.Constructor(
    "@mozilla.org/security/hash;1",
    "nsICryptoHash",
    "initWithString"
  );
});

ChromeUtils.defineLazyGetter(lazy, "gDatareportingPath", () => {
  return PathUtils.join(
    Services.dirsvc.get("ProfD", Ci.nsIFile).path,
    "datareporting"
  );
});

ChromeUtils.defineLazyGetter(lazy, "gStateFilePath", () => {
  return PathUtils.join(lazy.gDatareportingPath, "state.json");
});

const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
const PREF_CACHED_PROFILEGROUPID = "toolkit.telemetry.cachedProfileGroupID";
const PREF_CACHED_USAGE_PROFILEID = "datareporting.dau.cachedUsageProfileID";

/**
 * Checks if the string is a valid UUID (without braces).
 *
 * @param {String} id A string containing an ID.
 * @return {Boolean} True when the ID has valid format, or False otherwise.
 */
function isValidUUID(id) {
  const UUID_REGEX =
    /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
  return UUID_REGEX.test(id);
}

export var ClientID = Object.freeze({
  /**
   * This returns a promise resolving to the stable client ID we use for
   * data reporting.
   *
   * @return {Promise<string>} The stable client ID.
   */
  getClientID() {
    return ClientIDImpl.getClientID();
  },

  /**
   * This returns a promise resolving to the stable profile group ID we use for
   * data reporting.
   *
   * @return {Promise<string>} The stable profile group ID.
   */
  getProfileGroupID() {
    return ClientIDImpl.getProfileGroupID();
  },

  /**
   * This returns a promise resolving to the stable Usage Profile ID we use for
   * DAU reporting.
   *
   * @return {Promise<string>} The stable Usage Profile ID.
   */
  getUsageProfileID() {
    return ClientIDImpl.getUsageProfileID();
  },

  /**
   * Asynchronously updates the stable profile group ID we use for data
   * reporting.
   *
   * @param {String} id A string containing the profile group ID.
   * @return {Promise} Resolves when the ID is updated.
   */
  setProfileGroupID(id) {
    return ClientIDImpl.setProfileGroupID(id);
  },

  /**
   * Asynchronously updates the stable Usage Profile ID we use for DAU
   * reporting.
   *
   * @param {String} id A string containing the Usage Profile ID.
   * @return {Promise} Resolves when the ID is updated.
   */
  setUsageProfileID(id) {
    return ClientIDImpl.setUsageProfileID(id);
  },

  /**
   * Get the client id synchronously without hitting the disk.
   * This returns:
   *  - the current on-disk client id if it was already loaded
   *  - the client id that we cached into preferences (if any)
   *  - null otherwise
   */
  getCachedClientID() {
    return ClientIDImpl.getCachedClientID();
  },

  /**
   * Get the profile group ID synchronously without hitting the disk.
   * This returns:
   *  - the current on-disk profile group ID if it was already loaded
   *  - the profile group ID that we cached into preferences (if any)
   *  - null otherwise
   */
  getCachedProfileGroupID() {
    return ClientIDImpl.getCachedProfileGroupID();
  },

  /**
   * Get the Usage Profile ID synchronously without hitting the disk.
   * This returns:
   *  - the current on-disk Usage Profile ID if it was already loaded
   *  - the Usage Profile ID that we cached into preferences (if any)
   *  - null otherwise
   */
  getCachedUsageProfileID() {
    return ClientIDImpl.getCachedUsageProfileID();
  },

  async getClientIdHash() {
    return ClientIDImpl.getClientIdHash();
  },

  /**
   * Sets the client ID and profile group ID to the canary (known) identifiers,
   * writing it to disk and updating the cached version.
   *
   * Use `resetIdentifiers` to clear the existing identifiers and generate new,
   * random ones if required.
   *
   * @return {Promise<void>}
   */
  setCanaryIdentifiers() {
    return ClientIDImpl.setCanaryIdentifiers();
  },

  /**
   * Sets the Usage Profile ID to the canary (known) identifier,
   * writing it to disk and updating the cached version.
   *
   * Does not touch the client ID or profile group ID.
   *
   * Use `resetUsageProfileIdentifier` to clear the existing identifier
   * and generate a new, random one if required.
   *
   * @return {Promise<void>}
   */
  setCanaryUsageProfileIdentifier() {
    return ClientIDImpl.setCanaryUsageProfileIdentifier();
  },

  /**
   * Assigns new random values to client ID and profile group ID. Should only be
   * used if a reset is explicitly requested by the user.
   *
   * @return {Promise<void>}
   */
  resetIdentifiers() {
    return ClientIDImpl.resetIdentifiers();
  },

  /**
   * Assigns a new random value to the Usage Profile ID.
   * Should only be used if a reset is explicitly requested by the user.
   *
   * Does not touch the client ID or profile group ID.
   *
   * @return {Promise<void>}
   */
  resetUsageProfileIdentifier() {
    return ClientIDImpl.resetUsageProfileIdentifier();
  },

  /**
   * Only used for testing. Invalidates the cached client ID and profile group
   * ID so that they are read again from file, but doesn't remove the existing
   * IDs from disk.
   */
  _reset() {
    return ClientIDImpl._reset();
  },
});

var ClientIDImpl = {
  _clientID: null,
  _clientIDHash: null,
  _profileGroupID: null,
  _usageProfileID: null,
  _loadClientIdTask: null,
  _saveDataReportingStateTask: null,
  _resetIdentifiersTask: null,
  _resetUsageProfileIdentifierTask: null,
  _logger: null,

  _loadDataReportingState() {
    if (this._loadClientIdTask) {
      return this._loadClientIdTask;
    }

    this._loadClientIdTask = this._doLoadDataReportingState();
    let clear = () => (this._loadClientIdTask = null);
    this._loadClientIdTask.then(clear, clear);
    return this._loadClientIdTask;
  },

  /**
   * Load the client ID, profile group ID and Usage Profile ID from the DataReporting Service
   * state file. If any are missing, we generate a new one.
   */
  async _doLoadDataReportingState() {
    this._log.trace(`_doLoadDataReportingState`);
    // If there's a removal in progress, let's wait for it
    await this._resetIdentifiersTask;
    await this._resetUsageProfileIdentifierTask;

    // Try to load the client id from the DRS state file.
    let hasCurrentClientID = false;
    let hasCurrentProfileGroupID = false;
    let hasCurrentUsageProfileID = false;
    try {
      let state = await IOUtils.readJSON(lazy.gStateFilePath);
      if (state) {
        if (!("version" in state)) {
          // Old version, clear out any previously generated profile group ID.
          delete state.profileGroupID;
        }

        hasCurrentClientID = this.updateClientID(state.clientID);
        hasCurrentProfileGroupID = this.updateProfileGroupID(
          state.profileGroupID
        );
        hasCurrentUsageProfileID = this.updateUsageProfileID(
          state.usageProfileID
        );

        if (!hasCurrentProfileGroupID && hasCurrentClientID) {
          // A pre-existing profile should be assigned the existing client ID.
          hasCurrentProfileGroupID = this.updateProfileGroupID(this._clientID);

          if (hasCurrentProfileGroupID) {
            this._saveDataReportingStateTask = this._saveDataReportingState();
            await this._saveDataReportingStateTask;
          }
        }

        if (
          hasCurrentClientID &&
          hasCurrentProfileGroupID &&
          hasCurrentUsageProfileID
        ) {
          this._log.trace(
            `_doLoadDataReportingState: Client ID, Profile Group ID and Usage Profile ID loaded from state.`
          );
          return {
            clientID: this._clientID,
            profileGroupID: this._profileGroupID,
            usageProfileID: this._usageProfileID,
          };
        }
      }
    } catch (e) {
      // fall through to next option
    }

    // Absent or broken state file? Check prefs as last resort.
    if (!hasCurrentClientID) {
      const cachedID = this.getCachedClientID();
      // Calling `updateClientID` with `null` logs an error, which breaks tests.
      if (cachedID) {
        hasCurrentClientID = this.updateClientID(cachedID);
      }
    }

    if (!hasCurrentProfileGroupID) {
      const cachedID = this.getCachedProfileGroupID();
      // Calling `updateProfileGroupID` with `null` logs an error, which breaks tests.
      if (cachedID) {
        hasCurrentProfileGroupID = this.updateProfileGroupID(cachedID);
      }
    }

    if (!hasCurrentUsageProfileID) {
      const cachedID = this.getCachedUsageProfileID();
      // Calling `updateUsageProfileID` with `null` logs an error, which breaks tests.
      if (cachedID) {
        hasCurrentUsageProfileID = this.updateUsageProfileID(cachedID);
      }
    }

    // We're missing the ID from the DRS state file and prefs.
    // Generate a new one.
    if (!hasCurrentClientID) {
      this.updateClientID(lazy.CommonUtils.generateUUID());
    }

    if (!hasCurrentProfileGroupID) {
      this.updateProfileGroupID(lazy.CommonUtils.generateUUID());
    }

    if (!hasCurrentUsageProfileID) {
      this.updateUsageProfileID(lazy.CommonUtils.generateUUID());
    }

    this._saveDataReportingStateTask = this._saveDataReportingState();

    // Wait on persisting the id. Otherwise failure to save the ID would result in
    // the client creating and subsequently sending multiple IDs to the server.
    // This would appear as multiple clients submitting similar data, which would
    // result in orphaning.
    await this._saveDataReportingStateTask;

    this._log.trace(
      "_doLoadDataReportingState: client ID, profile group ID and Usage Profile ID loaded and persisted."
    );
    return {
      clientID: this._clientID,
      profileGroupID: this._profileGroupID,
      usageProfileID: this._usageProfileID,
    };
  },

  /**
   * Save the client ID, profile group ID and Usage Profile ID to the DRS state file.
   *
   * @return {Promise} A promise resolved when the DRS state file is saved to disk.
   */
  async _saveDataReportingState() {
    try {
      this._log.trace(`_saveDataReportingState`);
      let obj = {
        version: DRS_STATE_VERSION,
        clientID: this._clientID,
        profileGroupID: this._profileGroupID,
        usageProfileID: this._usageProfileID,
      };
      await IOUtils.makeDirectory(lazy.gDatareportingPath);
      await IOUtils.writeJSON(lazy.gStateFilePath, obj, {
        tmpPath: `${lazy.gStateFilePath}.tmp`,
      });
      this._saveDataReportingStateTask = null;
    } catch (ex) {
      if (!DOMException.isInstance(ex) || ex.name !== "AbortError") {
        throw ex;
      }
    }
  },

  /**
   * This returns a promise resolving to the stable client ID we use for
   * data reporting (FHR & Telemetry).
   *
   * @return {Promise<string>} The stable client ID.
   */
  async getClientID() {
    if (!this._clientID) {
      let { clientID } = await this._loadDataReportingState();
      if (AppConstants.platform != "android") {
        Glean.legacyTelemetry.clientId.set(clientID);
      }
      return clientID;
    }

    return Promise.resolve(this._clientID);
  },

  /**
   * This returns a promise resolving to the stable profile group ID we use for
   * data reporting (FHR & Telemetry).
   *
   * @return {Promise<string>} The stable profile group ID.
   */
  async getProfileGroupID() {
    if (!this._profileGroupID) {
      let { profileGroupID } = await this._loadDataReportingState();
      if (AppConstants.platform != "android") {
        Glean.legacyTelemetry.profileGroupId.set(profileGroupID);
      }
      return profileGroupID;
    }

    return this._profileGroupID;
  },

  /**
   * This returns a promise resolving to the stable Usage Profile ID we use for
   * DAU reporting.
   *
   * @return {Promise<string>} The stable Usage Profile ID.
   */
  async getUsageProfileID() {
    if (!this._usageProfileID) {
      let { usageProfileID } = await this._loadDataReportingState();
      if (AppConstants.platform != "android") {
        Glean.usage.profileId.set(usageProfileID);
      }
      return usageProfileID;
    }

    return this._usageProfileID;
  },

  /**
   * Asynchronously updates the stable profile group ID we use for data reporting
   * (FHR & Telemetry).
   *
   * @param {String} id A string containing the profile group ID.
   * @return {Promise} Resolves when the ID is updated.
   */
  async setProfileGroupID(id) {
    // Make sure that we have loaded the client ID. Do this before updating the
    // profile group ID as loading would clobber that.
    if (!this._clientID) {
      await this._loadDataReportingState();
    }

    if (!(await this.updateProfileGroupID(id))) {
      this._log.error(
        "setProfileGroupID - invalid profile group ID passed, not updating"
      );
      throw new Error("Invalid profile group ID");
    }

    // If there is already a save in progress, wait for it to complete.
    await this._saveDataReportingStateTask;

    this._saveDataReportingStateTask = this._saveDataReportingState();
    await this._saveDataReportingStateTask;
  },

  /**
   * Asynchronously updates the stable Usage Profile ID we use for DAU reporting.
   *
   * @param {String} id A string containing the Usage Profile ID.
   * @return {Promise} Resolves when the ID is updated.
   */
  async setUsageProfileID(id) {
    // Make sure that we have loaded the client ID. Do this before updating the
    // Usage Profile ID as loading would clobber that.
    if (!this._clientID) {
      await this._loadDataReportingState();
    }

    GleanPings.usageReporting.setEnabled(true);
    if (!(await this.updateUsageProfileID(id))) {
      this._log.error(
        "setUsageProfileID - invalid Usage Profile ID passed, not updating"
      );
      throw new Error("Invalid Usage Profile ID");
    }

    // If there is already a save in progress, wait for it to complete.
    await this._saveDataReportingStateTask;

    this._saveDataReportingStateTask = this._saveDataReportingState();
    await this._saveDataReportingStateTask;
  },

  /**
   * Get the client id synchronously without hitting the disk.
   * This returns:
   *  - the current on-disk client id if it was already loaded
   *  - the client id that we cached into preferences (if any)
   *  - null otherwise
   */
  getCachedClientID() {
    if (this._clientID) {
      // Already loaded the client id from disk.
      return this._clientID;
    }

    // If the client id cache contains a value of the wrong type,
    // reset the pref. We need to do this before |getStringPref| since
    // it will just return |null| in that case and we won't be able
    // to distinguish between the missing pref and wrong type cases.
    if (
      Services.prefs.prefHasUserValue(PREF_CACHED_CLIENTID) &&
      Services.prefs.getPrefType(PREF_CACHED_CLIENTID) !=
        Ci.nsIPrefBranch.PREF_STRING
    ) {
      this._log.error(
        "getCachedClientID - invalid client id type in preferences, resetting"
      );
      Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
    }

    // Not yet loaded, return the cached client id if we have one.
    let id = Services.prefs.getStringPref(PREF_CACHED_CLIENTID, null);
    if (id === null) {
      return null;
    }
    if (!isValidUUID(id)) {
      this._log.error(
        "getCachedClientID - invalid client id in preferences, resetting",
        id
      );
      Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
      return null;
    }
    return id;
  },

  /**
   * Get the profile group ID synchronously without hitting the disk.
   * This returns:
   *  - the current on-disk profile group ID if it was already loaded
   *  - the profile group ID that we cached into preferences (if any)
   *  - null otherwise
   */
  getCachedProfileGroupID() {
    if (this._profileGroupID) {
      // Already loaded the profile group ID from disk.
      return this._profileGroupID;
    }

    // If the profile group ID cache contains a value of the wrong type,
    // reset the pref. We need to do this before |getStringPref| since
    // it will just return |null| in that case and we won't be able
    // to distinguish between the missing pref and wrong type cases.
    if (
      Services.prefs.prefHasUserValue(PREF_CACHED_PROFILEGROUPID) &&
      Services.prefs.getPrefType(PREF_CACHED_PROFILEGROUPID) !=
        Ci.nsIPrefBranch.PREF_STRING
    ) {
      this._log.error(
        "getCachedProfileGroupID - invalid profile group ID type in preferences, resetting"
      );
      Services.prefs.clearUserPref(PREF_CACHED_PROFILEGROUPID);
    }

    // Not yet loaded, return the cached profile group ID if we have one.
    let id = Services.prefs.getStringPref(PREF_CACHED_PROFILEGROUPID, null);
    if (id === null) {
      return null;
    }
    if (!isValidUUID(id)) {
      this._log.error(
        "getCachedProfileGroupID - invalid profile group ID in preferences, resetting",
        id
      );
      Services.prefs.clearUserPref(PREF_CACHED_PROFILEGROUPID);
      return null;
    }
    return id;
  },

  /**
   * Get the Usage Profile ID synchronously without hitting the disk.
   * This returns:
   *  - the current on-disk Usage Profile ID if it was already loaded
   *  - the Usage Profile ID that we cached into preferences (if any)
   *  - null otherwise
   */
  getCachedUsageProfileID() {
    if (this._usageProfileID) {
      // Already loaded the Usage Profile ID from disk.
      return this._usageProfileID;
    }

    // If the Usage Profile ID cache contains a value of the wrong type,
    // reset the pref. We need to do this before |getStringPref| since
    // it will just return |null| in that case and we won't be able
    // to distinguish between the missing pref and wrong type cases.
    if (
      Services.prefs.prefHasUserValue(PREF_CACHED_USAGE_PROFILEID) &&
      Services.prefs.getPrefType(PREF_CACHED_USAGE_PROFILEID) !=
        Ci.nsIPrefBranch.PREF_STRING
    ) {
      this._log.error(
        "getCachedUsageProfileID - invalid Usage Profile ID type in preferences, resetting"
      );
      Services.prefs.clearUserPref(PREF_CACHED_USAGE_PROFILEID);
    }

    // Not yet loaded, return the cached Usage Profile ID if we have one.
    let id = Services.prefs.getStringPref(PREF_CACHED_USAGE_PROFILEID, null);
    if (id === null) {
      return null;
    }
    if (!isValidUUID(id)) {
      this._log.error(
        "getCachedUsageProfileID - invalid Usage Profile ID in preferences, resetting",
        id
      );
      Services.prefs.clearUserPref(PREF_CACHED_USAGE_PROFILEID);
      return null;
    }
    return id;
  },

  async getClientIdHash() {
    if (!this._clientIDHash) {
      let byteArr = new TextEncoder().encode(await this.getClientID());
      let hash = new lazy.CryptoHash("sha256");
      hash.update(byteArr, byteArr.length);
      this._clientIDHash = lazy.CommonUtils.bytesAsHex(hash.finish(false));
    }
    return this._clientIDHash;
  },

  /**
   * Resets the module. This is for testing only.
   */
  async _reset() {
    await this._loadClientIdTask;
    await this._saveDataReportingStateTask;
    this._clientID = null;
    this._clientIDHash = null;
    this._profileGroupID = null;
    this._usageProfileID = null;
  },

  async setCanaryIdentifiers() {
    this._log.trace("setCanaryIdentifiers");
    this.updateClientID(CANARY_CLIENT_ID);
    this.updateProfileGroupID(CANARY_PROFILE_GROUP_ID);

    this._saveDataReportingStateTask = this._saveDataReportingState();
    await this._saveDataReportingStateTask;
    return this._clientID;
  },

  async setCanaryUsageProfileIdentifier() {
    this._log.trace("setCanaryUsageProfileIdentifier");
    this.updateUsageProfileID(CANARY_USAGE_PROFILE_ID);
    GleanPings.usageReporting.setEnabled(false);

    this._saveDataReportingStateTask = this._saveDataReportingState();
    await this._saveDataReportingStateTask;
    return this._usageProfileID;
  },

  async _doResetIdentifiers() {
    this._log.trace("_doResetIdentifiers");

    // Reset the cached client ID.
    this.updateClientID(lazy.CommonUtils.generateUUID());
    this._clientIDHash = null;

    // Reset the cached profile group ID.
    this.updateProfileGroupID(lazy.CommonUtils.generateUUID());

    // If there is a save in progress, wait for it to complete.
    await this._saveDataReportingStateTask;

    // Save the new identifiers to disk.
    this._saveDataReportingStateTask = this._saveDataReportingState();
    await this._saveDataReportingStateTask;
  },

  async _doResetUsageProfileIdentifier() {
    this._log.trace("_doResetUsageProfileIdentifier");

    // Reset the cached Usage Profile ID.
    GleanPings.usageReporting.setEnabled(true);
    this.updateUsageProfileID(lazy.CommonUtils.generateUUID());

    // If there is a save in progress, wait for it to complete.
    await this._saveDataReportingStateTask;

    // Save the new identifiers to disk.
    this._saveDataReportingStateTask = this._saveDataReportingState();
    await this._saveDataReportingStateTask;
  },

  async resetIdentifiers() {
    this._log.trace("resetIdentifiers");

    // Wait for the removal.
    // Asynchronous calls to getClientID will also be blocked on this.
    this._resetIdentifiersTask = this._doResetIdentifiers();
    let clear = () => (this._resetIdentifiersTask = null);
    this._resetIdentifiersTask.then(clear, clear);

    await this._resetIdentifiersTask;
  },

  async resetUsageProfileIdentifier() {
    this._log.trace("resetUsageProfileIdentifier");

    // Wait for the removal.
    // Asynchronous calls to getClientID will also be blocked on this.
    this._resetUsageProfileIdentifierTask =
      this._doResetUsageProfileIdentifier();
    let clear = () => (this._resetUsageProfileIdentifierTask = null);
    this._resetUsageProfileIdentifierTask.then(clear, clear);

    await this._resetUsageProfileIdentifierTask;
  },

  /**
   * Sets the client id to the given value and updates the value cached in
   * preferences only if the given id is a valid UUID.
   *
   * @param {String} id A string containing the client ID.
   * @return {Boolean} True when the client ID has valid format, or False
   * otherwise.
   */
  updateClientID(id) {
    if (!isValidUUID(id)) {
      this._log.error("updateClientID - invalid client ID", id);
      return false;
    }

    this._clientID = id;
    if (AppConstants.platform != "android") {
      Glean.legacyTelemetry.clientId.set(id);
    }

    this._clientIDHash = null;
    Services.prefs.setStringPref(PREF_CACHED_CLIENTID, this._clientID);
    return true;
  },

  /**
   * Sets the profile group ID to the given value and updates the value cached
   * in preferences only if the given id is a valid UUID.
   *
   * @param {String} id A string containing the profile group ID.
   * @return {Boolean} True when the profile group ID has valid format, or False
   * otherwise.
   */
  updateProfileGroupID(id) {
    if (!isValidUUID(id)) {
      this._log.error("updateProfileGroupID - invalid profile group ID", id);
      return false;
    }

    this._profileGroupID = id;
    if (AppConstants.platform != "android") {
      Glean.legacyTelemetry.profileGroupId.set(id);
    }

    Services.prefs.setStringPref(
      PREF_CACHED_PROFILEGROUPID,
      this._profileGroupID
    );
    return true;
  },

  updateUsageProfileID(id) {
    if (!isValidUUID(id)) {
      this._log.error("updateUsageProfileID - invalid Usage Profile ID", id);
      return false;
    }

    this._usageProfileID = id;
    if (AppConstants.platform != "android") {
      Glean.usage.profileId.set(id);
    }

    Services.prefs.setStringPref(
      PREF_CACHED_USAGE_PROFILEID,
      this._usageProfileID
    );
    return true;
  },

  /**
   * A helper for getting access to telemetry logger.
   */
  get _log() {
    if (!this._logger) {
      this._logger = Log.repository.getLoggerWithMessagePrefix(
        LOGGER_NAME,
        LOGGER_PREFIX
      );
    }

    return this._logger;
  },
};

[ Dauer der Verarbeitung: 0.36 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge