Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
kotlin.code.style=official

probeGroup=plus.sourceplus.probe
projectVersion=0.6.3-SNAPSHOT
projectVersion=0.6.8-SNAPSHOT
2 changes: 1 addition & 1 deletion src/SourcePlusPlus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ namespace SourcePlusPlus {
async function sendConnected(eventBus: EventBus): Promise<void> {
let probeMetadata = {
language: 'nodejs',
probe_version: '1.0.0', // TODO
probe_version: '1.0.3', // TODO
nodejs_version: process.version,
service: config.serviceName,
service_instance: config.serviceInstance,
Expand Down
24 changes: 14 additions & 10 deletions src/control/ContextReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import VariableUtil from "../util/VariableUtil";
import ProbeMemory from "../ProbeMemory";

namespace ContextReceiver {
let logReport = new LogReportServiceClient(
config.collectorAddress,
config.secure ? grpc.credentials.createSsl() : grpc.credentials.createInsecure()
);
let logReport;

export function initialize() {
logReport = new LogReportServiceClient(
config.collectorAddress,
config.secure ? grpc.credentials.createSsl() : grpc.credentials.createInsecure()
);
}

function tryFindVariable(varName, variables) {
for (let scope in variables) {
Expand Down Expand Up @@ -89,17 +93,17 @@ namespace ContextReceiver {
activeSpan.stop();
}

export function applyLog(liveLogId: string, logFormat: string, logArguments: string[], variables) {
export function applyLog(liveLogId: string, logFormat: string, logArguments: any) {
let logTags = new LogTags();
logTags.addData(new KeyStringValuePair().setKey('log_id').setValue(liveLogId));
logTags.addData(new KeyStringValuePair().setKey('level').setValue('Live'));
logTags.addData(new KeyStringValuePair().setKey('thread').setValue('n/a'));

for (let varName of logArguments) {
let variable = tryFindVariable(varName, variables);
let value = variable ? variable.value : "null"; // TODO: Properly toString the variable (or encode it)
if (variable) {
logTags.addData(new KeyStringValuePair().setKey(`argument.${varName}`).setValue(value));
if (logArguments) {
for (const varName in logArguments) {
logTags.addData(new KeyStringValuePair()
.setKey(`argument.${varName}`)
.setValue(logArguments[varName]));
}
}

Expand Down
154 changes: 84 additions & 70 deletions src/control/LiveInstrumentRemote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export default class LiveInstrumentRemote {
}

this.sourceMapper = new SourceMapper(this.scriptLoaded.bind(this));

ContextReceiver.initialize();
}

async start(): Promise<void> {
Expand Down Expand Up @@ -93,84 +95,87 @@ export default class LiveInstrumentRemote {
}));
}

let instrumentIds = this.breakpointIdToInstrumentIds.get(message.params.hitBreakpoints[0]); // TODO: Handle multiple hit breakpoints
if (!instrumentIds) {
this.removeBreakpoint(message.params.hitBreakpoints[0]);
return;
}
message.params.hitBreakpoints.forEach(breakpointId => { // There should only be a single breakpoint, but handle multiple just in case
let instrumentIds = this.breakpointIdToInstrumentIds.get(breakpointId);
if (!instrumentIds) {
this.removeBreakpoint(message.params.hitBreakpoints[0]);
return;
}

let instruments = instrumentIds.map(id => this.instruments.get(id));
let conditionsSatisfied = Promise.all(instruments.map(instrument => {
if (instrument.condition === undefined || instrument.condition === null)
return true;

return new Promise<boolean>((resolve, reject) => {
this.session.post("Debugger.evaluateOnCallFrame", {
callFrameId: frame.callFrameId,
expression: instrument.condition,
silent: false,
throwOnSideEffect: true
}, (err, res) => {
if (err) {
console.log(`Error evaluating condition (${instrument.condition}): ${err}`);
resolve(false);
} else {
if (res.result.type === 'object' && res.result.subtype === 'error') {
if (res.result.className === 'EvalError') {
console.log(`Could not evaluate condition (${instrument.condition}) due to possible side effects`);
let instruments = instrumentIds.map(id => this.instruments.get(id));
let dataGathered = Promise.all(instruments.map(instrument => {
return new Promise<any>((resolve, reject) => {
this.session.post("Debugger.evaluateOnCallFrame", {
callFrameId: frame.callFrameId,
expression: instrument.createExpression(),
silent: false, // In case of an exception, don't affect the program flow
throwOnSideEffect: true, // Disallow side effects
returnByValue: true // Return the entire JSON object rather than just the remote id
}, (err, res) => {
if (err) {
this.handleConditionalFailed(instrument,
`Error evaluating condition (${instrument.condition}): ${err}`);
resolve({success: false});
} else {
if (res.result.type === 'object' && res.result.subtype === 'error') {
if (res.result.className === 'EvalError') {
this.handleConditionalFailed(instrument,
`Could not evaluate condition (${instrument.condition}) due to possible side effects`);
} else {
this.handleConditionalFailed(instrument,
`Error evaluating condition (${instrument.condition}): ${res.result.description}`);
}
resolve({success: false});
} else if (res.result.type !== 'object') {
this.handleConditionalFailed(instrument,
`Invalid condition for instrument id: ${instrument.id}: ${instrument.condition} ==> ${res.result}`);
resolve({success: false});
} else {
console.log(`Error evaluating condition (${instrument.condition}): ${res.result.description}`);
resolve(res.result.value);
}
resolve(false);
} else if (res.result.type !== 'boolean') {
console.log("Invalid condition for instrument id: " + instrument.id + ": " + instrument.condition, res.result);
resolve(false);
} else {
resolve(res.result.value);
}
}
});
});
});
}));

Promise.all(promises).then(() => conditionsSatisfied)
.then(conditions => {
for (let i = 0; i < instruments.length; i++) {
if (conditions[i]) {
let instrument = instruments[i];
if (!instrument) {
continue;
}
}));

Promise.all(promises).then(() => dataGathered)
.then(data => {
for (let i = 0; i < instruments.length; i++) {
if (data[i].success) {
let instrument = instruments[i];
if (!instrument) {
continue;
}

if (instrument.type == LiveInstrumentType.BREAKPOINT) {
ContextReceiver.applyBreakpoint(
instrument.id,
instrument.location.source,
instrument.location.line,
message.params.callFrames,
variables
);
} else if (instrument.type == LiveInstrumentType.LOG) {
let logInstrument = <LiveLog>instrument;
ContextReceiver.applyLog(
instrument.id,
logInstrument.logFormat,
logInstrument.logArguments,
variables
);
} else if (instrument.type == LiveInstrumentType.METER) {
let meterInstrument = <LiveMeter>instrument;
ContextReceiver.applyMeter(
instrument.id,
variables
);
}
if (instrument.isFinished()) {
this.removeBreakpoint(instrument.id);
if (instrument.type == LiveInstrumentType.BREAKPOINT) {
ContextReceiver.applyBreakpoint(
instrument.id,
instrument.location.source,
instrument.location.line,
message.params.callFrames,
variables
);
} else if (instrument.type == LiveInstrumentType.LOG) {
let logInstrument = <LiveLog>instrument;
ContextReceiver.applyLog(
instrument.id,
logInstrument.logFormat,
data[i].logArguments
);
} else if (instrument.type == LiveInstrumentType.METER) {
let meterInstrument = <LiveMeter>instrument;
ContextReceiver.applyMeter(
instrument.id,
variables
);
}
if (instrument.isFinished()) {
this.removeBreakpoint(instrument.id);
}
}
}
}
});
});
});
});
}

Expand Down Expand Up @@ -332,6 +337,15 @@ export default class LiveInstrumentRemote {
}
}

handleConditionalFailed(instrument: LiveInstrument, error: string) {
this.removeInstrument(instrument.id);
this.eventBus.publish("spp.processor.status.live-instrument-removed", {
occurredAt: Date.now(),
instrument: JSON.stringify(instrument.toJson()),
cause: `EventBusException:LiveInstrumentException[CONDITIONAL_FAILED]: ${error}`
});
}

// TODO: Call this regularly to clean up old instruments
// TODO: Ensure the cache doesn't get too large
private cleanCache() {
Expand Down
7 changes: 7 additions & 0 deletions src/model/LiveInstrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,11 @@ export default class LiveInstrument {
meta: this.meta
};
}

createExpression() {
if (this.condition == null) {
return `(() => { return {success: true} })()`;
}
return `(() => { return {success: ${this.condition}} })()`;
}
}
13 changes: 13 additions & 0 deletions src/model/error/LiveInstrumentException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class LiveInstrumentException {
type: ErrorType;
message: string;

toEventBusString(): string {
return `EventBusException:LiveInstrumentException[${ErrorType[this.type]}]: ${this.message}`;
}
}

export enum ErrorType {
CLASS_NOT_FOUND,
CONDITIONAL_FAILED
}
26 changes: 23 additions & 3 deletions src/model/instruments/LiveLog.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import LiveInstrument from "../LiveInstrument";
import LiveSourceLocation from "../LiveSourceLocation";
import InstrumentThrottle from "../throttle/InstrumentThrottle";
import LiveInstrumentType from "../LiveInstrumentType";
import HitThrottle from "../throttle/HitThrottle";

export default class LiveLog extends LiveInstrument {
type = LiveInstrumentType.LOG;
logFormat: string
logArguments: string[]

createExpression(): string {
let logArgumentsExpression = this.logArguments
.map(arg => `data['${arg}'] = ${arg}.toString()`)
.join(';');
if (this.condition == null) {
return `(() => {
let data = {success: true};
(data => {${logArgumentsExpression}})(data);
return data;
})()`;
} else {
return `(() => {
if (${this.condition}) {
let data = {success: true};
(data => {${logArgumentsExpression}})(data);
returndata;
} else {
return {success: false};
}
})()`;
}
}
}
58 changes: 58 additions & 0 deletions test/LiveLogTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const assert = require('assert');
const TestUtils = require("./TestUtils.js");

module.exports = function () {
function simplePrimitives() {
let i = 1
let c = 'h'
let s = "hi"
let f = 1.0
let bool = true
TestUtils.addLineLabel("done", () => TestUtils.getLineNumber())
}

it('add live log', async function () {
simplePrimitives() //setup labels

await TestUtils.addLiveLog({
"source": TestUtils.getFilename()(),
"line": TestUtils.getLineLabelNumber("done")
}, null, 1, "test log", []).then(function (res) {
assert.equal(res.status, 200);
simplePrimitives(); //trigger breakpoint
}).catch(function (err) {
assert.fail(err)
});
});

it('verify log data', async function () {
this.timeout(2000)

setTimeout(() => simplePrimitives(), 1000);

let event = await TestUtils.awaitMarkerEvent("LOG_HIT");
console.log(event);
// assert.equal(event.stackTrace.elements[0].method, 'simplePrimitives');
// let variables = event.stackTrace.elements[0].variables;
//
// let iVar = TestUtils.locateVariable("i", variables);
// assert.equal(iVar.liveClazz, "number");
// assert.equal(iVar.value, 1);
//
// let cVar = TestUtils.locateVariable("c", variables);
// assert.equal(cVar.liveClazz, "string");
// assert.equal(cVar.value, "h");
//
// let sVar = TestUtils.locateVariable("s", variables);
// assert.equal(sVar.liveClazz, "string");
// assert.equal(sVar.value, "hi");
//
// let fVar = TestUtils.locateVariable("f", variables);
// assert.equal(fVar.liveClazz, "number");
// assert.equal(fVar.value, 1.0);
//
// let boolVar = TestUtils.locateVariable("bool", variables);
// assert.equal(boolVar.liveClazz, "boolean");
// assert.equal(boolVar.value, true);
});
};
Loading