Skip to content

Conversation

@mirackara
Copy link
Contributor

@mirackara mirackara commented Jul 1, 2025

Changes

  • Added detection for existing Applications
  • Added detection for existing Transactions
  • Created new stage of tracing preinstrumentation

Details

This PR begins our initiative to upgrade Go Easy from a tool that is really helpful for onboarding new users with no existing New Relic instrumentation to a tool that users can use continuously.

Application Detection

checkForExistingApplicationInMain() -> This function checks for existing application in main. If an application is detected in a function inside of main, we mark that one as a setup function and will not conduct tracing on it.

Transaction Detection

TransactionCache -> TransactionCache is responsible for tracking existing transactions within a Go application. It maintains the following components:

  • Transactions: A map where each key is a transaction name, and the value is a list of expressions that are active within the lifespan of the transaction.
  • Functions: A map that stores already seen functions alongside their declarations. This is useful for tracking transactions that span multiple function calls.
  • TransactionState: A map that tracks whether a transaction is open or has ended, ensuring that no further expressions are added once a transaction is marked as ended.

Transaction Detection requires us to introduce a new stage in the Go Easy lifecycle with the addition of a PreInstrumentationTracingFunction stage.

PreInstrumentationTracingFunctions defines a function that is executed before any instrumentation is applied to a code block. These functions are executed on every node in the DST tree of every function declared in an application.
It is important to note that these tracing functions will NOT be modifying the existing syntax tree, only scanning it.

With the introduction of the PreInstrumentationTracingFunction a new tracing function called DetectTransactions is created. DetectTransactions analyzes the AST to identify and track transactions within function declarations. It updates the TransactionCache with function declarations and expressions related to transactions. Once the TransactionCache contains the lifespan of a transaction, we can then reference the cache within our TraceFunction() to skip creating new transactions if there is an active transaction within a function.

Sample Code

func anotherFunc(nrTxn *newrelic.Transaction) {
	fmt.Println("Hello from anotherFunc!")
	nrTxn.Ignore()
}

func hello(nrTxn *newrelic.Transaction) {
	defer nrTxn.StartSegment("hello").End()
	fmt.Println("Hello, World!")
	anotherFunc(nrTxn)
	nrTxn.AddAttribute("color", "red")
	fmt.Println("test")
	nrTxn.End()
}

func noNRInstrumentation() {
	fmt.Println("hi")
}

func noNRInstrumentation2() {
	fmt.Println("hi")
}
func main() {
	app, err := newrelic.NewApplication(
		newrelic.ConfigAppName("Short Lived App"),
		newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),
		newrelic.ConfigDebugLogger(os.Stdout),
	)
	if nil != err {
		fmt.Println(err)
		os.Exit(1)
	}

	// Wait for the application to connect.
	if err := app.WaitForConnection(5 * time.Second); nil != err {
		fmt.Println(err)
	}

	// Do the tasks at hand.  Perhaps record them using transactions and/or
	// custom events.
	tasks := []string{"white", "black", "red", "blue", "green", "yellow"}
	for _, task := range tasks {
		txn := app.StartTransaction("task")
		time.Sleep(10 * time.Millisecond)
		fmt.Println("Task:", task)
		txn.End()
		app.RecordCustomEvent("task", map[string]interface{}{
			"color": task,
		})
	}

	nrTxn := app.StartTransaction("hello")
	hello(nrTxn)

	noNRInstrumentation()

	noNRInstrumentation2()

will cause the TransactionCache to look like

Transaction: txn
  Call: Sleep
  Call: Println
  Call: txn.End
Transaction: nrTxn
  Call: hello
  Call: nrTxn.StartSegment
  Call: Println
  Call: anotherFunc
  Call: Println
  Call: nrTxn.Ignore
  Call: nrTxn.AddAttribute
  Call: Println
  Call: nrTxn.End

From this, we can determine which sections of code are already instrumented within the TraceFunction

@mirackara mirackara marked this pull request as draft July 2, 2025 20:14
@mirackara mirackara marked this pull request as ready for review August 12, 2025 19:45
@mirackara mirackara changed the title feat: add check for existing New Relic application feat: pre-existing app instance detection Aug 13, 2025
parser/agent.go Outdated
}

}
if assign, ok := cursor.Node().(*dst.AssignStmt); ok {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this block be attempted if the above block fails? Or should a failure of the conditional on 114

if decl, ok := cursor.Node().(*dst.FuncDecl); ok

cause this logic to be skipped?

parser/agent.go Outdated
Comment on lines 114 to 129
if decl, ok := cursor.Node().(*dst.FuncDecl); ok {
// Print out return values before caching them
if decl.Type.Results != nil {
for _, result := range decl.Type.Results.List {
// Checking if return type of function is a new relic application
if starExpr, ok := result.Type.(*dst.StarExpr); ok {
if ident, ok := starExpr.X.(*dst.Ident); ok {
if ident.Path == codegen.NewRelicAgentImportPath && ident.Name == "Application" {
manager.setupFunc = decl
}
}
}
}
}

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, this would benefit from de-nesting, where instead of continuing execution on a successful check, we return early/break on an unsuccessful conditional.

if callExpr, ok := expr.(*dst.CallExpr); ok {
if selExpr, ok := callExpr.Fun.(*dst.SelectorExpr); ok {
if selExpr.Sel.Name == "End" {
// Check if the End method is called directly on the transaction
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add support for defer transactions

for _, rhs := range stmt.Rhs {
if callExpr, ok := rhs.(*dst.CallExpr); ok {
if selExpr, ok := callExpr.Fun.(*dst.SelectorExpr); ok {
if _, ok := selExpr.X.(*dst.Ident); ok && selExpr.Sel.Name == "StartTransaction" {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for net/http we should be checking FromContext and not StartTransaction

@mirackara mirackara changed the title feat: pre-existing app instance detection feat: pre-existing app/transaction instance detection Sep 19, 2025
}

funcCall, ok := ident.Obj.Decl.(*dst.FuncDecl)
if !ok || manager.setupFunc != funcCall {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this handleAssignStmtForAgentVariable function require or assume that a setupFunc has been detected? If manager.setupFunc is nil, should we even ever bother executing this function?


instrumentPackages(m, tracingFunctions...)

return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the current state of the code, and error will never be returned here. Is it expected that this function can never fail? If so, we should remove the expectation of a return value, otherwise we should identify and return possible error states.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants