Skip to content

Commit

Permalink
feat: Add session recording checkpoint
Browse files Browse the repository at this point in the history
Stream all events to a temp file; removing the Memory recorder.

Recording is returned as an object, which can be deleted, or renamed,
or read into a Writer.

ActiveSessionException is sometimes meaningful, but usually it's just
like a RuntimeException. So, a lot of this behavior has been removed.
The main explicit check that remains is to see if a recording is or is
not in progress when certain actions are performed. Other types of
unexpected errors are not client-caused and therefore they are just
runtime exceptions and will probably result in a 500 error to the
client.

Because events are written into a temp file, the name of the AppMap file
(if any), is not needed until the recording is stopped.
  • Loading branch information
kgilpin committed Jul 28, 2021
1 parent ae144a3 commit 04d9293
Show file tree
Hide file tree
Showing 14 changed files with 448 additions and 373 deletions.
21 changes: 12 additions & 9 deletions src/main/java/com/appland/appmap/Agent.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import com.appland.appmap.config.AppMapConfig;
import com.appland.appmap.config.Properties;
import com.appland.appmap.record.IRecordingSession.Metadata;
import com.appland.appmap.record.Recorder;
import com.appland.appmap.record.Recording;
import com.appland.appmap.record.RecordingSession.Metadata;
import com.appland.appmap.transform.ClassFileTransformer;
import com.appland.appmap.util.Logger;

Expand Down Expand Up @@ -47,17 +48,12 @@ public static void premain(String agentArgs, Instrumentation inst) {
}

if (Properties.RecordingAuto) {
String appmapName = Properties.RecordingName;
final Date date = new Date();
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddHHmmss");
final String timestamp = dateFormat.format(date);
final Metadata metadata = new Metadata();
final Recorder recorder = Recorder.getInstance();
String fileName = Properties.RecordingFile;
String appmapName = Properties.RecordingName;

if (fileName == null || fileName.trim().isEmpty()) {
fileName = String.format("%s.appmap.json", timestamp);
}

if (appmapName == null || appmapName.trim().isEmpty()) {
appmapName = timestamp;
Expand All @@ -66,10 +62,17 @@ public static void premain(String agentArgs, Instrumentation inst) {
metadata.recorderName = "remote_recording";
metadata.scenarioName = appmapName;

recorder.start(fileName, metadata);
recorder.start(metadata);

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
recorder.stop();
String fileName = Properties.RecordingFile;

if (fileName == null || fileName.trim().isEmpty()) {
fileName = String.format("%s.appmap.json", timestamp);
}

Recording recording = recorder.stop();
recording.moveTo(fileName);
}));
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/appland/appmap/output/v1/CodeObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ public static CodeObject createTree(CtClass classType) {

/**
* Create a tree of CodeObjects from the given CtBehavior. For example, given a method
* "com.appland.demo.MyClass.myMethod", a heirarchy of five CodeObjects would be created: "com",
* "com.appland.demo.MyClass.myMethod", a hierarchy of five CodeObjects will be created: "com",
* "appland", "demo", "MyClass", "myMethod".
* @param method The method to create a hierarchy from
* @return The root of the CodeObject tree
Expand Down
165 changes: 97 additions & 68 deletions src/main/java/com/appland/appmap/process/hooks/ToggleRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import com.appland.appmap.process.ExitEarly;
import com.appland.appmap.process.conditions.RecordCondition;
import com.appland.appmap.record.ActiveSessionException;
import com.appland.appmap.record.IRecordingSession;
import com.appland.appmap.record.Recorder;
import com.appland.appmap.record.Recording;
import com.appland.appmap.record.RecordingSession;
import com.appland.appmap.reflect.FilterChain;
import com.appland.appmap.reflect.HttpServletRequest;
import com.appland.appmap.reflect.HttpServletResponse;
Expand All @@ -18,89 +19,96 @@

import static com.appland.appmap.util.StringUtil.*;

interface HandlerFunction {
void call(HttpServletRequest req, HttpServletResponse res) throws IOException;
}

/**
* Hooks to toggle event recording. This could be either via HTTP or by entering a unit test method.
*/
public class ToggleRecord {
private static boolean debug = Properties.DebugHttp;
private static final Recorder recorder = Recorder.getInstance();
public static final String RecordRoute = "/_appmap/record";
public static final String CheckpointRoute = "/_appmap/record/checkpoint";

private static void doDelete(HttpServletRequest req, HttpServletResponse res) {
private static void doDelete(HttpServletRequest req, HttpServletResponse res) throws IOException {
if (debug) {
Logger.println("ToggleRecord.doDelete");
}

try {
String json = recorder.stop();
res.setContentType("application/json");
res.setContentLength(json.length());

PrintWriter writer = res.getWriter();
writer.write(json);
writer.flush();
} catch (ActiveSessionException e) {
if (!recorder.hasActiveSession()) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
} catch (IOException e) {
Logger.printf("failed to write response: %s\n", e.getMessage());
return;
}

Recording recording = recorder.stop();
res.setContentType("application/json");
res.setContentLength(recording.size());

recording.readFully(true, res.getWriter());
}

private static void doGet(HttpServletRequest req, HttpServletResponse res) {
private static void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
if (debug) {
Logger.println("ToggleRecord.doGet");
}

res.setStatus(HttpServletResponse.SC_OK);

String responseJson = String.format("{\"enabled\":%b}", recorder.hasActiveSession());
res.setContentType("application/json");
res.setContentLength(responseJson.length());
res.setStatus(HttpServletResponse.SC_OK);

try {
PrintWriter writer = res.getWriter();
writer.write(responseJson);
writer.flush();
} catch (IOException e) {
Logger.printf("failed to write response: %s\n", e.getMessage());
}
PrintWriter writer = res.getWriter();
writer.write(responseJson);
writer.flush();
}

private static void doPost(HttpServletRequest req, HttpServletResponse res) {
if (debug) {
Logger.println("ToggleRecord.doPost");
}

IRecordingSession.Metadata metadata = new IRecordingSession.Metadata();
metadata.recorderName = "remote_recording";
try {
recorder.start(metadata);
} catch (ActiveSessionException e) {
if (recorder.hasActiveSession()) {
res.setStatus(HttpServletResponse.SC_CONFLICT);
return;
}

RecordingSession.Metadata metadata = new RecordingSession.Metadata();
metadata.recorderName = "remote_recording";
recorder.start(metadata);
}

private static void service(Object[] args) throws ExitEarly {
if (args.length != 2) {
return;
private static void doCheckpoint(HttpServletRequest req, HttpServletResponse res) {
if (debug) {
Logger.println("ToggleRecord.doCheckpoint");
}

final HttpServletRequest req = new HttpServletRequest(args[0]);
if (!req.getRequestURI().endsWith(RecordRoute)) {
if (!recorder.hasActiveSession()) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}

Recording recording = recorder.checkpoint();
res.setContentType("application/json");
res.setContentLength(recording.size());

try {
recording.readFully(true, res.getWriter());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private static void handleRecordRequest(HttpServletRequest req, HttpServletResponse res, HandlerFunction fn) throws ExitEarly {
if (debug) {
Logger.println("ToggleRecord.service - handling appmap request");
Logger.printf("ToggleRecord.service - handling appmap request for %s\n", req.getRequestURI());
}

final HttpServletResponse res = new HttpServletResponse(args[1]);
if (req.getMethod().equals("GET")) {
doGet(req, res);
} else if (req.getMethod().equals("POST")) {
doPost(req, res);
} else if (req.getMethod().equals("DELETE")) {
doDelete(req, res);
try {
fn.call(req, res);
} catch (IOException e) {
throw new RuntimeException(e);
}

if (debug) {
Expand All @@ -110,6 +118,29 @@ private static void service(Object[] args) throws ExitEarly {
throw new ExitEarly();
}

private static void service(Object[] args) throws ExitEarly {
if (args.length != 2) {
return;
}

final HttpServletRequest req = new HttpServletRequest(args[0]);
final HttpServletResponse res = new HttpServletResponse(args[1]);

if (req.getRequestURI().endsWith(CheckpointRoute)) {
if (req.getMethod().equals("GET")) {
handleRecordRequest(req, res, ToggleRecord::doCheckpoint);
}
} else if (req.getRequestURI().endsWith(RecordRoute)) {
if (req.getMethod().equals("GET")) {
handleRecordRequest(req, res, ToggleRecord::doGet);
} else if (req.getMethod().equals("POST")) {
handleRecordRequest(req, res, ToggleRecord::doPost);
} else if (req.getMethod().equals("DELETE")) {
handleRecordRequest(req, res, ToggleRecord::doDelete);
}
}
}

private static void skipFilterChain(Object[] args) throws ExitEarly {
if (args.length != 3) {
if (debug) {
Expand Down Expand Up @@ -167,8 +198,7 @@ public static void doFilterJakarta(Event event, Object[] args) throws ExitEarly
skipFilterChain(args);
}

private static IRecordingSession.Metadata getMetadata(Event event) {

private static RecordingSession.Metadata getMetadata(Event event) {
// TODO: Obtain this info in the constructor
boolean junit = false;
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
Expand All @@ -177,7 +207,7 @@ private static IRecordingSession.Metadata getMetadata(Event event) {
junit = true;
}
}
IRecordingSession.Metadata metadata = new IRecordingSession.Metadata();
RecordingSession.Metadata metadata = new RecordingSession.Metadata();
if (junit) {
metadata.recorderName = "toggle_record_receiver";
metadata.framework = "junit";
Expand All @@ -188,30 +218,30 @@ private static IRecordingSession.Metadata getMetadata(Event event) {
}

private static void startRecording(Event event) {
Logger.printf("Recording started for %s\n", canonicalName(event));
try {
final String fileName = String.join("_", event.definedClass, event.methodId)
.replaceAll("[^a-zA-Z0-9-_]", "_");
IRecordingSession.Metadata metadata = getMetadata(event);
metadata.feature = identifierToSentence(event.methodId);
metadata.featureGroup = identifierToSentence(event.definedClass);
RecordingSession.Metadata metadata = getMetadata(event);
final String feature = identifierToSentence(event.methodId);
final String featureGroup = identifierToSentence(event.definedClass);
metadata.scenarioName = String.format(
"%s %s",
metadata.featureGroup,
decapitalize(metadata.feature));
"%s %s",
featureGroup,
decapitalize(feature));
metadata.recordedClassName = event.definedClass;
metadata.recordedMethodName = event.methodId;
recorder.start(fileName + ".appmap.json", metadata);
recorder.start(metadata);
} catch (ActiveSessionException e) {
Logger.printf("%s\n", e.getMessage());
}
}

private static void stopRecording(){
try {
recorder.stop();
} catch (ActiveSessionException e) {
Logger.printf("%s\n", e.getMessage());
}
private static void stopRecording(Event event) {
Logger.printf("Recording stopped for %s\n", canonicalName(event));
String filePath = String.join("_", event.definedClass, event.methodId)
.replaceAll("[^a-zA-Z0-9-_]", "_");
filePath += ".appmap.json";
Recording recording = recorder.stop();
recording.moveTo(filePath);
}

@ArgumentArray
Expand All @@ -226,7 +256,7 @@ public static void junit(Event event, Object[] args) {
@ExcludeReceiver
@HookAnnotated("org.junit.Test")
public static void junit(Event event, Object returnValue, Object[] args) {
stopRecording();
stopRecording(event);
}

@ArgumentArray
Expand All @@ -236,7 +266,7 @@ public static void junit(Event event, Object returnValue, Object[] args) {
public static void junit(Event event, Exception exception, Object[] args) {
event.setException(exception);
recorder.add(event);
stopRecording();
stopRecording(event);
}

@ArgumentArray
Expand All @@ -251,7 +281,7 @@ public static void junitJupiter(Event event, Object[] args) {
@ExcludeReceiver
@HookAnnotated("org.junit.jupiter.api.Test")
public static void junitJupiter(Event event, Object returnValue, Object[] args) {
stopRecording();
stopRecording(event);
}

@ArgumentArray
Expand All @@ -261,7 +291,7 @@ public static void junitJupiter(Event event, Object returnValue, Object[] args)
public static void junitJupiter(Event event, Exception exception, Object[] args) {
event.setException(exception);
recorder.add(event);
stopRecording();
stopRecording(event);
}

@ArgumentArray
Expand All @@ -276,7 +306,7 @@ public static void testng(Event event, Object[] args) {
@ExcludeReceiver
@HookAnnotated("org.testng.annotations.Test")
public static void testnt(Event event, Object returnValue, Object[] args) {
stopRecording();
stopRecording(event);
}

@ArgumentArray
Expand All @@ -286,14 +316,13 @@ public static void testnt(Event event, Object returnValue, Object[] args) {
public static void testng(Event event, Exception exception, Object[] args) {
event.setException(exception);
recorder.add(event);
stopRecording();
stopRecording(event);
}

@ArgumentArray
@ExcludeReceiver
@HookCondition(RecordCondition.class)
public static void record(Event event, Object[] args) {
Logger.printf("Recording started for %s\n", canonicalName(event));
startRecording(event);
}

Expand All @@ -302,7 +331,7 @@ public static void record(Event event, Object[] args) {
@ExcludeReceiver
@HookCondition(RecordCondition.class)
public static void record(Event event, Object returnValue, Object[] args) {
stopRecording();
stopRecording(event);
}

@ArgumentArray
Expand All @@ -312,6 +341,6 @@ public static void record(Event event, Object returnValue, Object[] args) {
public static void record(Event event, Exception exception, Object[] args) {
event.setException(exception);
recorder.add(event);
stopRecording();
stopRecording(event);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ public class ActiveSessionException extends RuntimeException {
public ActiveSessionException(String message) {
super(message);
}
public ActiveSessionException(String message, Throwable cause) {
super(message, cause);
}
}
Loading

0 comments on commit 04d9293

Please sign in to comment.