@microsoft/windows-admin-center-sdk
Version:
Microsoft - Windows Admin Center Shell
583 lines (581 loc) • 24.6 kB
JavaScript
import { filter, take } from 'rxjs/operators';
import { Http } from '../data/http';
import { Net } from '../data/net';
import { RpcLogClient } from '../rpc/log/rpc-log-client';
import { RpcLogSubjectServer } from '../rpc/log/rpc-log-subject-server';
import { RpcTelemetryClient } from '../rpc/telemetry/rpc-telemetry-client';
import { RpcTelemetrySubjectServer } from '../rpc/telemetry/rpc-telemetry-subject-server';
import { LogLevel } from './log-level';
import { LoggingConstants } from './logging-constants';
import { TelemetryControlType } from './telemetry-control-type';
/**
* Logging class.
* @dynamic
*/
export class Logging {
static logMaxRecordLength = 20;
static logMaxWaitTimeMs = 30000;
static telemetryMaxRecordLength = 100;
static telemetryMaxWaitTimeMs = 60000;
static testMode = 'test';
static instance;
static errorLogHeaderStyle = 'font-weight: bold; color: #E81123;';
static warnLogHeaderStyle = 'font-weight: bold; color: #FF8C00;';
static successLogHeaderStyle = 'font-weight: bold; color: ##288928;';
static infoLogHeaderStyle = 'font-weight: bold; color: #0078D7;';
static verboseLogHeaderStyle = 'color: #999';
logSubjectServer;
telemetrySubjectServer;
gateway;
http;
rpc;
logSet;
telemetrySet;
thresholdOfLogLevel = LogLevel.Informational;
verboseTelemetry = false;
/**
* Log a logging event.
*
* @param record the log record to send the gateway.
* @return Promise<any> settle to resolve if buffered.
*/
static log(record) {
return Logging.current.logInternal(record);
}
/**
* Log a critical logging event.
*
* @param source The record originator of event within a module.
* @param message The message of event.
* @param params The parameters object which will be serialized.
* @return Promise<any> settle to resolve if buffered.
*/
static logCritical(source, message, params) {
return Logging.current.logInternal({ level: LogLevel.Critical, source, message, params: params });
}
/**
* Log an error logging event.
*
* @param source The record originator of event within a module.
* @param message The message of event.
* @param params The parameters object which will be serialized.
* @return Promise<any> settle to resolve if buffered.
*/
static logError(source, message, params) {
return Logging.current.logInternal({ level: LogLevel.Error, source, message, params: params });
}
/**
* Log a warning logging event.
*
* @param source The record originator of event within a module.
* @param message The message of event.
* @param params The parameters object which will be serialized.
* @return Promise<any> settle to resolve if buffered.
*/
static logWarning(source, message, params) {
return Logging.current.logInternal({ level: LogLevel.Warning, source, message, params: params });
}
/**
* Log a success logging event.
*
* @param source The record originator of event within a module.
* @param message The message of event.
* @param params The parameters object which will be serialized.
* @return Promise<any> settle to resolve if buffered.
*/
static logSuccess(source, message, params) {
return Logging.current.logInternal({ level: LogLevel.Success, source, message, params: params });
}
/**
* Log a informational logging event.
*
* @param source The record originator of event within a module.
* @param message The message of event.
* @param params The parameters object which will be serialized.
* @return Promise<any> settle to resolve if buffered.
*/
static logInformational(source, message, params) {
return Logging.current.logInternal({ level: LogLevel.Informational, source, message, params: params });
}
/**
* Log a verbose logging event.
*
* @param source The record originator of event within a module.
* @param message The message of event.
* @param params The parameters object which will be serialized.
* @return Promise<any> settle to resolve if buffered.
*/
static logVerbose(source, message, params) {
return Logging.current.logInternal({ level: LogLevel.Verbose, source, message, params: params });
}
/**
* Log a debug logging event.
*
* @param source The record originator of event within a module.
* @param message The message of event.
* @param params The parameters object which will be serialized.
* @return Promise<any> settle to resolve if buffered.
*/
static logDebug(source, message, params) {
return Logging.current.logInternal({ level: LogLevel.Debug, source, message, params: params });
}
/**
* Trace a telemetry event.
*
* @param record the telemetry record to send the gateway.
* @return Promise<any> settle to resolve if buffered.
*/
static trace(record) {
return Logging.current.telemetryInternal(record);
}
/**
* Trace a user action telemetry event.
*
* @param data additional data.
* @return Promise<any> settle to resolve if buffered.
*/
static traceUserAction(data) {
return Logging.trace({
view: LoggingConstants.views.smeUIControl,
instance: LoggingConstants.empty,
action: LoggingConstants.actions.click,
data: data
});
}
/**
* Trace a telemetry event for a button click.
*
* @param controlName Describes which control was used.
* ex) For a button that says 'Set up' the control name can be 'Set up' to distinguish that from a cancel button.
* @param controlId Unique identifier when a control appears more than once. Specific to how implementation handles uniqueness.
* @param nodeName the name of the node currently connected to (this.appContextService.activeConnection.nodeName)
* @param nodeType the type of node currently connected to (this.appContextService.connectionManager.activeConnection.type)
* @return Promise<any> settle to resolve if buffered.
*/
static traceButton(controlName, controlId, nodeName, nodeType) {
return Logging.traceButtonClick(TelemetryControlType.Button, controlName, controlId, nodeName, nodeType);
}
/**
* Trace a telemetry event for a action pane button click.
*
* @param controlName Describes which control was used.
* ex) For a button that says 'Set up' the control name can be 'Set up' to distinguish that from a cancel button.
* @param controlId Unique identifier when a control appears more than once. Specific to how implementation handles uniqueness.
* @param nodeName the name of the node currently connected to (this.appContextService.activeConnection.nodeName)
* @param nodeType the type of node currently connected to (this.appContextService.connectionManager.activeConnection.type)
* @return Promise<any> settle to resolve if buffered.
*/
static traceActionPaneButton(controlName, controlId, nodeName, nodeType) {
return Logging.traceButtonClick(TelemetryControlType.ActionPaneButton, controlName, controlId, nodeName, nodeType);
}
/**
* Trace a telemetry event for a action bar button click.
*
* @param controlName Describes which control was used.
* ex) For a button that says 'Set up' the control name can be 'Set up' to distinguish that from a cancel button.
* @param controlId Unique identifier when a control appears more than once. Specific to how implementation handles uniqueness.
* @param nodeName the name of the node currently connected to (this.appContextService.activeConnection.nodeName)
* @param nodeType the type of node currently connected to (this.appContextService.connectionManager.activeConnection.type)
* @return Promise<any> settle to resolve if buffered.
*/
static traceActionBarButton(controlName, controlId, nodeName, nodeType) {
return Logging.traceButtonClick(TelemetryControlType.ActionBarButton, controlName, controlId, nodeName, nodeType);
}
/**
* Trace a telemetry event on a sme control.
*
* @param controlName Describes which control was used.
* ex) For a button that says 'Set up' the control name can be 'Set up' to distinguish that from a cancel button.
* @param controlId Unique identifier when a control appears more than once. Specific to how implementation handles uniqueness.
* @param additionalData Additional key value pairs that can be sent to telemetry.
* @param nodeName the name of the node currently connected to (this.appContextService.activeConnection.nodeName)
* @param nodeType the type of node currently connected to (this.appContextService.connectionManager.activeConnection.type)
* @return Promise<any> settle to resolve if buffered.
*/
static traceControl(controlType, controlName, controlId, additionalData, nodeName, nodeType) {
let data = {};
if (additionalData) {
data = additionalData;
}
data[controlType] = controlType;
data[controlName] = controlName;
data[controlId] = controlId;
const nodeId = Logging.getFormattedNodeId(nodeName, nodeType);
if (nodeId) {
data[LoggingConstants.dataFields.nodeId] = nodeId;
}
return Logging.traceUserAction(data);
}
/**
* Trace a telemetry event on click of an external link.
*
* @param controlName Describes which control was used.
* ex) For a button that says 'Set up' the control name can be 'Set up' to distinguish that from a cancel button.
* @param url External link that was clicked.
* @param controlId Unique identifier when a control appears more than once. Specific to how implementation handles uniqueness.
* @param nodeName the name of the node currently connected to (this.appContextService.activeConnection.nodeName)
* @param nodeType the type of node currently connected to (this.appContextService.connectionManager.activeConnection.type)
* @return Promise<any> settle to resolve if buffered.
*/
static traceExternalLink(controlName, url, controlId, nodeName, nodeType) {
const data = {
controlType: TelemetryControlType.ExternalLink,
controlName: controlName,
controlId: controlId,
url: url
};
const nodeId = Logging.getFormattedNodeId(nodeName, nodeType);
if (nodeId) {
data[LoggingConstants.dataFields.nodeId] = nodeId;
}
return Logging.traceUserAction(data);
}
/**
* Trace a telemetry event on click of an internal link.
*
* @param controlName Describes which control was used.
* ex) For a button that says 'Set up' the control name can be 'Set up' to distinguish that from a cancel button.
* @param route rpc route.
* @param controlId Unique identifier when a control appears more than once. Specific to how implementation handles uniqueness.
* @param nodeName the name of the node currently connected to (this.appContextService.activeConnection.nodeName)
* @param nodeType the type of node currently connected to (this.appContextService.connectionManager.activeConnection.type)
* @return Promise<any> settle to resolve if buffered.
*/
static traceInternalLink(controlName, route, controlId, nodeName, nodeType) {
const data = {
controlType: TelemetryControlType.InternalLink,
controlName: controlName,
controlId: controlId,
route: route
};
const nodeId = Logging.getFormattedNodeId(nodeName, nodeType);
if (nodeId) {
data[LoggingConstants.dataFields.nodeId] = nodeId;
}
return Logging.traceUserAction(data);
}
/**
* Trace a telemetry event when an async task returns.
*
* @param eventLocation UI location of the event being recorded.
* ex) createVmForm, RegisterAadDialog, stopServiceConfirmationDialog...
* @param eventLabel Description of what event is being logged.
* ex) 'Registered with Azure', 'Added a connection', 'Failed Azure backup'
* @param result Result of event.
* @param additionalData Additional key value pairs that can be sent to telemetry.
* @param nodeName the name of the node currently connected to (this.appContextService.activeConnection.nodeName)
* @param nodeType the type of node currently connected to (this.appContextService.connectionManager.activeConnection.type)
* @return Promise<any> settle to resolve if buffered.
*/
static traceAsyncResult(eventLocation, eventLabel, result, additionalData, nodeName, nodeType) {
let data = {};
if (additionalData) {
data = additionalData;
}
data[LoggingConstants.dataFields.eventLocation] = eventLocation;
data[LoggingConstants.dataFields.eventLabel] = eventLabel;
data[LoggingConstants.dataFields.result] = result;
const nodeId = Logging.getFormattedNodeId(nodeName, nodeType);
if (nodeId) {
data[LoggingConstants.dataFields.nodeId] = nodeId;
}
return Logging.traceUserAction(data);
}
/**
* Log a raw object into the console at debug level of mode.
*/
static debug(object) {
if (Logging.current.consoleLogLevel >= LogLevel.Debug) {
console.log(object);
}
}
/**
* Configure logging mode.
*
* @param thresholdOfLogLevel the log level for gateway.
* @param verboseTelemetry if true, optional telemerty will be collected.
*/
static configureLog(thresholdOfLogLevel, verboseTelemetry) {
Logging.current.thresholdOfLogLevel = thresholdOfLogLevel;
Logging.current.verboseTelemetry = verboseTelemetry;
}
/**
* Wrapper method for tracing a telemetry event for a button click.
*
* @param controlType Type of button that is being recorded
* @param controlName Describes which control was used.
* ex) For a button that says 'Set up' the control name can be 'Set up' to distinguish that from a cancel button.
* @param controlId Unique identifier when a control appears more than once. Specific to how implementation handles uniqueness.
* @param nodeName the name of the node currently connected to (this.appContextService.activeConnection.nodeName)
* @param nodeType the type of node currently connected to (this.appContextService.connectionManager.activeConnection.type)
* @return Promise<any> settle to resolve if buffered.
*/
static traceButtonClick(controlType, controlName, controlId, nodeName, nodeType) {
const data = {
controlType: controlType,
controlName: controlName,
controlId: controlId
};
const nodeId = Logging.getFormattedNodeId(nodeName, nodeType);
if (nodeId) {
data[LoggingConstants.dataFields.nodeId] = nodeId;
}
return Logging.traceUserAction(data);
}
/**
* Format node information for telemetry
*
* @param nodeName the name of the node currently connected to (this.appContextService.activeConnection.nodeName)
* @param nodeType the type of node currently connected to (this.appContextService.connectionManager.activeConnection.type)
* @return string
*/
static getFormattedNodeId(nodeName, nodeType) {
if (nodeType && nodeName) {
return nodeType + '!' + nodeName;
}
else {
return null;
}
}
/**
* Gets the level of current logging.
*/
get consoleLogLevel() {
const global = window;
return (global.MsftSme && global.MsftSme.Init && global.MsftSme.Init.logLevel) || LogLevel.Warning;
}
/**
* Gets the session Id of shell.
*/
get sessionId() {
const global = window;
return global.MsftSme && global.MsftSme.Init && global.MsftSme.Init.sessionId || Logging.testMode;
}
/**
* Gets the name of current shell or module.
*/
get nameOfModule() {
const global = window;
return global.MsftSme && global.MsftSme.Init && global.MsftSme.Init.moduleName || Logging.testMode;
}
/**
* Initializes a new instance of the Logging class.
*/
constructor() {
this.http = new Http();
this.logSet = {
maxWaitTimeMs: Logging.logMaxWaitTimeMs,
maxRecordLength: Logging.logMaxRecordLength,
api: '/log'
};
this.telemetrySet = {
maxWaitTimeMs: Logging.telemetryMaxWaitTimeMs,
maxRecordLength: Logging.telemetryMaxRecordLength,
api: '/telemetry'
};
}
/**
* Gets the current logging instance.
*/
static get current() {
if (Logging.instance) {
return Logging.instance;
}
Logging.instance = new Logging();
return Logging.instance;
}
/**
* Flush the regular log set, immediately submitting the logs to the gateway
*/
static flushLogs() {
Logging.current.flush(Logging.current.logSet);
}
/**
* Flush the telemetry log set, immediately submitting the logs to the gateway
*/
static flushTelemetry() {
Logging.current.flush(Logging.current.telemetrySet);
}
/**
* Register Rpc object to logging instance.
*
* @param rpc the rpc instance.
*/
registerRpc(rpc, gateway) {
this.rpc = rpc;
this.gateway = gateway;
// start subscribing once after the rpc is ready on the shell.
this.rpc.stateChanged
.pipe(filter(active => active), take(1))
.subscribe(() => {
if (this.rpc.isShell) {
this.logSubjectServer = new RpcLogSubjectServer(this.rpc);
this.logSet.rpcSubscription = this.logSubjectServer.subject
.subscribe(data => {
this.logGateway(this.logSet, data.data);
data.deferred.resolve();
});
this.telemetrySubjectServer = new RpcTelemetrySubjectServer(this.rpc);
this.telemetrySet.rpcSubscription = this.telemetrySubjectServer.subject
.subscribe(data => {
this.logGateway(this.telemetrySet, data.data);
data.deferred.resolve();
});
}
});
}
/**
* Dispose the set of rpc forwarding pipes.
*/
dispose() {
this.disposeSet(this.logSet);
this.disposeSet(this.telemetrySet);
}
/**
* Log a record.
*
* @param record the log record.
* @return Promise<any> the promise object.
*/
logInternal(record) {
const now = new Date();
if (record.level <= this.consoleLogLevel) {
let headerStyle = Logging.verboseLogHeaderStyle;
if (record.level <= LogLevel.Error) {
headerStyle = Logging.errorLogHeaderStyle;
}
else if (record.level <= LogLevel.Warning) {
headerStyle = Logging.warnLogHeaderStyle;
}
else if (record.level <= LogLevel.Success) {
headerStyle = Logging.successLogHeaderStyle;
}
else if (record.level <= LogLevel.Informational) {
headerStyle = Logging.infoLogHeaderStyle;
}
const logSeparator = '--';
const customHeader = record.consoleGroupHeader ? ` ${logSeparator} ${record.consoleGroupHeader}` : '';
if (MsftSme.isEdge() || MsftSme.isInternetExplorer()) {
// Microsoft Edge and IE don't support styles in group headers. so remove if running on Microsoft Edge
// Since there is no color support, also log the severity
console.groupCollapsed(`${LogLevel[record.level]} ${logSeparator} ${this.nameOfModule} ${logSeparator} ${record.source}${customHeader}`);
}
else {
console.groupCollapsed(`%c${this.nameOfModule} ${logSeparator} ${record.source}${customHeader}`, headerStyle);
}
console.log(`logLevel: ${LogLevel[record.level]}`);
console.log(`timestamp: ${now.toISOString()}`);
if (record.message) {
if (MsftSme.isObject(record.message)) {
console.log('message:');
console.log({ ...record.message });
}
else {
console.log(`message: ${record.message}`);
}
}
if (record.params) {
console.group('params');
console.log({ ...record.params });
console.groupEnd();
}
if (record.localParams) {
console.group('params');
console.log({ ...record.localParams });
console.groupEnd();
delete record['localParams'];
}
if (record.stack) {
console.group('stack');
console.log(record.stack);
console.groupEnd();
}
console.groupEnd();
}
if (record.level <= this.thresholdOfLogLevel && this.sessionId !== Logging.testMode) {
if (MsftSme.isObject(record.message)) {
// ensure message is a string.
record.message = JSON.stringify(record.message);
}
const rpcRecord = {
...record,
...{ sessionId: this.sessionId, timestamp: now.getTime(), sourceName: this.nameOfModule }
};
if (this.rpc && this.rpc.stateActive && !this.rpc.isShell) {
return RpcLogClient.log(this.rpc, rpcRecord);
}
this.logGateway(this.logSet, rpcRecord);
}
return Promise.resolve();
}
/**
* Log a telemerty record.
*
* @param record the telemetry record.
* @return Promise<any> the promise object.
*/
telemetryInternal(record) {
if (((!this.verboseTelemetry && !record.optional) || this.verboseTelemetry) && this.sessionId !== Logging.testMode) {
const rpcRecord = { ...record, ...{ sessionId: this.sessionId, timestamp: Date.now(), sourceName: this.nameOfModule } };
if (this.rpc && this.rpc.stateActive && !this.rpc.isShell) {
return RpcTelemetryClient.telemetry(this.rpc, rpcRecord);
}
this.logGateway(this.telemetrySet, rpcRecord);
}
return Promise.resolve();
}
/**
* Dispose the set.
*
* @param set the logger set.
*/
disposeSet(set) {
if (set.rpcSubscription) {
set.rpcSubscription.unsubscribe();
set.rpcSubscription = null;
}
}
/**
* Log to the gateway.
*
* @param set the logger set.
* @param data the record data.
*/
logGateway(set, data) {
if (set.timer == null) {
set.buffer = [];
set.timer = setTimeout(() => this.submitRecords(set), set.maxWaitTimeMs);
}
set.buffer.push(data);
if (set.buffer.length >= set.maxRecordLength) {
clearTimeout(set.timer);
this.submitRecords(set);
set.timer = setTimeout(() => this.submitRecords(set), set.maxWaitTimeMs);
}
}
/**
* Flush a log set, immediately submitting the logs to the gateway
*/
flush(set) {
if (set && set.buffer && set.buffer.length > 0) {
this.submitRecords(set);
}
}
/**
* Submit records to the gateway.
*/
submitRecords(set) {
// Disabled gateway can't log anything in gateway.
if (set.buffer.length > 0 && this.gateway && !this.gateway.disabled) {
this.gateway.post(set.api, set.buffer, { maxRetryCount: 0 }).subscribe({
error: error => {
const message = MsftSme.getStrings().MsftSmeShell.Core.Error.LoggingUnableSubmit.message;
console.error(message.format(Net.getErrorMessage(error)));
}
});
}
set.timer = null;
set.buffer = [];
}
}
//# sourceMappingURL=logging.js.map