Fix #8974 by supporting strings in configureLogging for SignalR JS client (#9252)

This commit is contained in:
Andrew Stanton-Nurse 2019-04-17 08:15:55 -07:00 committed by GitHub
parent 01b0c888d2
commit 46db367760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 240 additions and 96 deletions

View File

@ -13,6 +13,31 @@ import { JsonHubProtocol } from "./JsonHubProtocol";
import { NullLogger } from "./Loggers";
import { Arg, ConsoleLogger } from "./Utils";
// tslint:disable:object-literal-sort-keys
const LogLevelNameMapping = {
trace: LogLevel.Trace,
debug: LogLevel.Debug,
info: LogLevel.Information,
information: LogLevel.Information,
warn: LogLevel.Warning,
warning: LogLevel.Warning,
error: LogLevel.Error,
critical: LogLevel.Critical,
none: LogLevel.None,
};
function parseLogLevel(name: string): LogLevel {
// Case-insensitive matching via lower-casing
// Yes, I know case-folding is a complicated problem in Unicode, but we only support
// the ASCII strings defined in LogLevelNameMapping anyway, so it's fine -anurse.
const mapping = LogLevelNameMapping[name.toLowerCase()];
if (typeof mapping !== "undefined") {
return mapping;
} else {
throw new Error(`Unknown log level: ${name}`);
}
}
/** A builder for configuring {@link @aspnet/signalr.HubConnection} instances. */
export class HubConnectionBuilder {
/** @internal */
@ -44,15 +69,26 @@ export class HubConnectionBuilder {
/** Configures custom logging for the {@link @aspnet/signalr.HubConnection}.
*
* @param {LogLevel | ILogger} logging An object implementing the {@link @aspnet/signalr.ILogger} interface or {@link @aspnet/signalr.LogLevel}.
* @param {string} logLevel A string representing a LogLevel setting a minimum level of messages to log.
* See {@link https://docs.microsoft.com/en-us/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details.
*/
public configureLogging(logLevel: string): HubConnectionBuilder;
/** Configures custom logging for the {@link @aspnet/signalr.HubConnection}.
*
* @param {LogLevel | string | ILogger} logging A {@link @aspnet/signalr.LogLevel}, a string representing a LogLevel, or an object implementing the {@link @aspnet/signalr.ILogger} interface.
* See {@link https://docs.microsoft.com/en-us/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details.
* @returns The {@link @aspnet/signalr.HubConnectionBuilder} instance, for chaining.
*/
public configureLogging(logging: LogLevel | ILogger): HubConnectionBuilder;
public configureLogging(logging: LogLevel | ILogger): HubConnectionBuilder {
public configureLogging(logging: LogLevel | string | ILogger): HubConnectionBuilder;
public configureLogging(logging: LogLevel | string | ILogger): HubConnectionBuilder {
Arg.isRequired(logging, "logging");
if (isLogger(logging)) {
this.logger = logging;
} else if (typeof logging === "string") {
const logLevel = parseLogLevel(logging);
this.logger = new ConsoleLogger(logLevel);
} else {
this.logger = new ConsoleLogger(logging);
}
@ -92,7 +128,7 @@ export class HubConnectionBuilder {
// Flow-typing knows where it's at. Since HttpTransportType is a number and IHttpConnectionOptions is guaranteed
// to be an object, we know (as does TypeScript) this comparison is all we need to figure out which overload was called.
if (typeof transportTypeOrOptions === "object") {
this.httpConnectionOptions = {...this.httpConnectionOptions, ...transportTypeOrOptions};
this.httpConnectionOptions = { ...this.httpConnectionOptions, ...transportTypeOrOptions };
} else {
this.httpConnectionOptions = {
...this.httpConnectionOptions,

View File

@ -147,8 +147,17 @@ export class SubjectSubscription<T> implements ISubscription<T> {
export class ConsoleLogger implements ILogger {
private readonly minimumLogLevel: LogLevel;
// Public for testing purposes.
public outputConsole: {
error(message: any): void,
warn(message: any): void,
info(message: any): void,
log(message: any): void,
};
constructor(minimumLogLevel: LogLevel) {
this.minimumLogLevel = minimumLogLevel;
this.outputConsole = console;
}
public log(logLevel: LogLevel, message: string): void {
@ -156,17 +165,17 @@ export class ConsoleLogger implements ILogger {
switch (logLevel) {
case LogLevel.Critical:
case LogLevel.Error:
console.error(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
this.outputConsole.error(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
break;
case LogLevel.Warning:
console.warn(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
this.outputConsole.warn(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
break;
case LogLevel.Information:
console.info(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
this.outputConsole.info(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
break;
default:
// console.debug only goes to attached debuggers in Node, so we use console.log for Trace and Debug
console.log(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
this.outputConsole.log(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
break;
}
}

View File

@ -10,6 +10,7 @@ import { HubMessage, IHubProtocol } from "../src/IHubProtocol";
import { ILogger, LogLevel } from "../src/ILogger";
import { HttpTransportType, TransferFormat } from "../src/ITransport";
import { NullLogger } from "../src/Loggers";
import { ConsoleLogger } from "../src/Utils";
import { VerifyLogger } from "./Common";
import { TestHttpClient } from "./TestHttpClient";
@ -26,15 +27,51 @@ const commonHttpOptions: IHttpConnectionOptions = {
logMessageContent: true,
};
// We use a different mapping table here to help catch any unintentional breaking changes.
// tslint:disable:object-literal-sort-keys
const ExpectedLogLevelMappings = {
trace: LogLevel.Trace,
debug: LogLevel.Debug,
info: LogLevel.Information,
information: LogLevel.Information,
warn: LogLevel.Warning,
warning: LogLevel.Warning,
error: LogLevel.Error,
critical: LogLevel.Critical,
none: LogLevel.None,
};
class CapturingConsole {
public messages: any[] = [];
public error(message: any) {
this.messages.push(CapturingConsole.stripPrefix(message));
}
public warn(message: any) {
this.messages.push(CapturingConsole.stripPrefix(message));
}
public info(message: any) {
this.messages.push(CapturingConsole.stripPrefix(message));
}
public log(message: any) {
this.messages.push(CapturingConsole.stripPrefix(message));
}
private static stripPrefix(input: any): any {
if (typeof input === "string") {
input = input.replace(/\[.*\]\s+/, "");
}
return input;
}
}
registerUnhandledRejectionHandler();
describe("HubConnectionBuilder", () => {
eachMissingValue((val, name) => {
it(`configureLogging throws if logger is ${name}`, () => {
const builder = new HubConnectionBuilder();
expect(() => builder.configureLogging(val!)).toThrow("The 'logging' argument is required.");
});
it(`withUrl throws if url is ${name}`, () => {
const builder = new HubConnectionBuilder();
expect(() => builder.withUrl(val!)).toThrow("The 'url' argument is required.");
@ -111,98 +148,160 @@ describe("HubConnectionBuilder", () => {
});
});
it("allows logger to be replaced", async () => {
let loggedMessages = 0;
const logger = {
log() {
loggedMessages += 1;
},
};
const pollSent = new PromiseSource<HttpRequest>();
const pollCompleted = new PromiseSource<HttpResponse>();
const testClient = createTestClient(pollSent, pollCompleted.promise)
.on("POST", "http://example.com?id=abc123", (req) => {
// Respond from the poll with the handshake response
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
return new HttpResponse(202);
});
const connection = createConnectionBuilder(logger)
.withUrl("http://example.com", {
...commonHttpOptions,
httpClient: testClient,
})
.build();
describe("configureLogging", async () => {
function testLogLevels(logger: ILogger, minLevel: LogLevel) {
const capturingConsole = new CapturingConsole();
(logger as ConsoleLogger).outputConsole = capturingConsole;
try {
await connection.start();
} catch {
// Ignore failures
for (let level = LogLevel.Trace; level < LogLevel.None; level++) {
const message = `Message at LogLevel.${LogLevel[level]}`;
const expectedMessage = `${LogLevel[level]}: Message at LogLevel.${LogLevel[level]}`;
logger.log(level, message);
if (level >= minLevel) {
expect(capturingConsole.messages).toContain(expectedMessage);
} else {
expect(capturingConsole.messages).not.toContain(expectedMessage);
}
}
}
expect(loggedMessages).toBeGreaterThan(0);
});
it("uses logger for both HttpConnection and HubConnection", async () => {
const pollSent = new PromiseSource<HttpRequest>();
const pollCompleted = new PromiseSource<HttpResponse>();
const testClient = createTestClient(pollSent, pollCompleted.promise)
.on("POST", "http://example.com?id=abc123", (req) => {
// Respond from the poll with the handshake response
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
return new HttpResponse(202);
eachMissingValue((val, name) => {
it(`throws if logger is ${name}`, () => {
const builder = new HubConnectionBuilder();
expect(() => builder.configureLogging(val!)).toThrow("The 'logging' argument is required.");
});
const logger = new CaptureLogger();
const connection = createConnectionBuilder(logger)
.withUrl("http://example.com", {
...commonHttpOptions,
httpClient: testClient,
})
.build();
});
try {
await connection.start();
} catch {
// Ignore failures
[
LogLevel.None,
LogLevel.Critical,
LogLevel.Error,
LogLevel.Warning,
LogLevel.Information,
LogLevel.Debug,
LogLevel.Trace,
].forEach((minLevel) => {
const levelName = LogLevel[minLevel];
it(`accepts LogLevel.${levelName}`, async () => {
const builder = new HubConnectionBuilder()
.configureLogging(minLevel);
expect(builder.logger).toBeDefined();
expect(builder.logger).toBeInstanceOf(ConsoleLogger);
testLogLevels(builder.logger!, minLevel);
});
});
const levelNames = Object.keys(ExpectedLogLevelMappings) as Array<keyof typeof ExpectedLogLevelMappings>;
for (const str of levelNames) {
const mapped = ExpectedLogLevelMappings[str];
const mappedName = LogLevel[mapped];
it(`accepts "${str}" as an alias for LogLevel.${mappedName}`, async () => {
const builder = new HubConnectionBuilder()
.configureLogging(str);
expect(builder.logger).toBeDefined();
expect(builder.logger).toBeInstanceOf(ConsoleLogger);
testLogLevels(builder.logger!, mapped);
});
}
// A HubConnection message
expect(logger.messages).toContain("Starting HubConnection.");
it("allows logger to be replaced", async () => {
let loggedMessages = 0;
const logger = {
log() {
loggedMessages += 1;
},
};
const pollSent = new PromiseSource<HttpRequest>();
const pollCompleted = new PromiseSource<HttpResponse>();
const testClient = createTestClient(pollSent, pollCompleted.promise)
.on("POST", "http://example.com?id=abc123", (req) => {
// Respond from the poll with the handshake response
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
return new HttpResponse(202);
});
const connection = createConnectionBuilder(logger)
.withUrl("http://example.com", {
...commonHttpOptions,
httpClient: testClient,
})
.build();
// An HttpConnection message
expect(logger.messages).toContain("Starting connection with transfer format 'Text'.");
});
try {
await connection.start();
} catch {
// Ignore failures
}
it("does not replace HttpConnectionOptions logger if provided", async () => {
const pollSent = new PromiseSource<HttpRequest>();
const pollCompleted = new PromiseSource<HttpResponse>();
const testClient = createTestClient(pollSent, pollCompleted.promise)
.on("POST", "http://example.com?id=abc123", (req) => {
// Respond from the poll with the handshake response
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
return new HttpResponse(202);
});
const hubConnectionLogger = new CaptureLogger();
const httpConnectionLogger = new CaptureLogger();
const connection = createConnectionBuilder(hubConnectionLogger)
.withUrl("http://example.com", {
httpClient: testClient,
logger: httpConnectionLogger,
})
.build();
expect(loggedMessages).toBeGreaterThan(0);
});
try {
await connection.start();
} catch {
// Ignore failures
}
it("configures logger for both HttpConnection and HubConnection", async () => {
const pollSent = new PromiseSource<HttpRequest>();
const pollCompleted = new PromiseSource<HttpResponse>();
const testClient = createTestClient(pollSent, pollCompleted.promise)
.on("POST", "http://example.com?id=abc123", (req) => {
// Respond from the poll with the handshake response
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
return new HttpResponse(202);
});
const logger = new CaptureLogger();
const connection = createConnectionBuilder(logger)
.withUrl("http://example.com", {
...commonHttpOptions,
httpClient: testClient,
})
.build();
// A HubConnection message
expect(hubConnectionLogger.messages).toContain("Starting HubConnection.");
expect(httpConnectionLogger.messages).not.toContain("Starting HubConnection.");
try {
await connection.start();
} catch {
// Ignore failures
}
// An HttpConnection message
expect(httpConnectionLogger.messages).toContain("Starting connection with transfer format 'Text'.");
expect(hubConnectionLogger.messages).not.toContain("Starting connection with transfer format 'Text'.");
// A HubConnection message
expect(logger.messages).toContain("Starting HubConnection.");
// An HttpConnection message
expect(logger.messages).toContain("Starting connection with transfer format 'Text'.");
});
it("does not replace HttpConnectionOptions logger if provided", async () => {
const pollSent = new PromiseSource<HttpRequest>();
const pollCompleted = new PromiseSource<HttpResponse>();
const testClient = createTestClient(pollSent, pollCompleted.promise)
.on("POST", "http://example.com?id=abc123", (req) => {
// Respond from the poll with the handshake response
pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
return new HttpResponse(202);
});
const hubConnectionLogger = new CaptureLogger();
const httpConnectionLogger = new CaptureLogger();
const connection = createConnectionBuilder(hubConnectionLogger)
.withUrl("http://example.com", {
httpClient: testClient,
logger: httpConnectionLogger,
})
.build();
try {
await connection.start();
} catch {
// Ignore failures
}
// A HubConnection message
expect(hubConnectionLogger.messages).toContain("Starting HubConnection.");
expect(httpConnectionLogger.messages).not.toContain("Starting HubConnection.");
// An HttpConnection message
expect(httpConnectionLogger.messages).toContain("Starting connection with transfer format 'Text'.");
expect(hubConnectionLogger.messages).not.toContain("Starting connection with transfer format 'Text'.");
});
});
it("reconnectPolicy undefined by default", () => {
@ -228,7 +327,7 @@ describe("HubConnectionBuilder", () => {
});
it("withAutomaticReconnect uses custom retryDelays when provided", () => {
const customRetryDelays = [ 3, 1, 4, 1, 5, 9 ];
const customRetryDelays = [3, 1, 4, 1, 5, 9];
const builder = new HubConnectionBuilder()
.withAutomaticReconnect(customRetryDelays);
@ -241,7 +340,7 @@ describe("HubConnectionBuilder", () => {
});
it("withAutomaticReconnect uses a custom IReconnectPolicy when provided", () => {
const customRetryDelays = [ 127, 0, 0, 1 ];
const customRetryDelays = [127, 0, 0, 1];
const builder = new HubConnectionBuilder()
.withAutomaticReconnect(new DefaultReconnectPolicy(customRetryDelays));

View File

@ -151,7 +151,7 @@
console.log('http://' + document.location.host + '/' + hubRoute);
var connectionBuilder = new signalR.HubConnectionBuilder()
.configureLogging(signalR.LogLevel.Trace)
.configureLogging("trace")
.withUrl(hubRoute, options)
.withHubProtocol(protocol);