Top 10 tips for instrumenting Golang with New Relic, part 3
ntcsteve
Posted on November 1, 2023
To read this full New Relic blog, click here.
Are you building modern apps in Golang? This multi-part blog series will help you instrument Golang faster by providing tips, guidance, and best practices with New Relic.
Typically, the content in this blog would be presented in a workshop, but we've made it a blog series so anyone can follow along "hands on," using sample code, Instruqt, this blog, and videos. For the best experience:
- See and run the full code that I'll be walking through in this multi-part series.
- Follow the hands-on lab in Instruqt.
- Read along with this blog and follow the videos below.
The following two videos will give an overview of Instruqt and some instructions about how to use it if you haven't done that before.
Overview of Instruqt
Top 10 tips for instrumenting Golang with New Relic, part 3 | New Relic
Instruqt UI navigation
Top 10 tips for instrumenting Golang with New Relic, part 3 | New Relic
Now that you're set up, let's learn a little more about the instrumentation of Golang.
Tip 7: It MUST be the database layer!
Top 10 tips for instrumenting Golang with New Relic, part 3 | New Relic
At some point during software development, it's not uncommon to experience issues that may seem to stem from the database layer. However, blaming the database layer is not always the best approach. It's possible to instrument and provide more context for database requests in Go, thanks to the use of segments.
By using segments, you can instrument your Go application's datastore calls. These datastore segments will appear in the APM Transactions breakdown table and Databases tab of the Transactions page in New Relic, providing a comprehensive overview of your database performance.
If you're using a MySQL, PostgreSQL, or SQLite database driver, using a pre-built integration package is the easiest way to add datastore segments. This also applies to MongoDB, so you can easily instrument your database calls without writing any custom code. For MySQL Database, here is the package driver, as seen in our docs. You can go to this link for a full code sample with MySQL.
import (
_ "github.com/newrelic/go-agent/v3/integrations/nrmysql"
)
func main() {
db, err := sql.Open("nrmysql", "user@unix(/path/to/socket)/dbname")
}
To provide a context containing a newrelic.Transaction to all Exec and Query methods on sql.DB, sql.Conn, sql.Tx, and sql.Stmt, you need to use the context methods ExecContext, QueryContext, and QueryRowContext in place of Exec, Query, and QueryRow, respectively. This will ensure proper database transaction monitoring with New Relic.
txn := app.StartTransaction("mysqlQuery")
ctx := newrelic.NewContext(context.Background(), txn)
row := db.QueryRowContext(ctx, "SELECT count(*) from tables")
If you're using a MongoDB database, you can simplify the process of creating Datastore Segments by using this package to instrument your MongoDB calls. To do this, simply set the monitor in the connect options using SetMonitor
, as outlined in our documentation.
import "github.com/newrelic/go-agent/v3/integrations/nrmongo"
nrMon := nrmongo.NewCommandMonitor(nil)
client, err := mongo.Connect(ctx, options.Client().SetMonitor(nrMon))
Note that the nrmongo
monitor must be set last to prevent it from being overwritten. If you need to use more than one event.CommandMonitor
, you can pass the original monitor to the nrmongo.NewCommandMonitor
function, as demonstrated in the documentation.
origMon := &event.CommandMonitor{
Started: origStarted,
Succeeded: origSucceeded,
Failed: origFailed,
}
nrMon := nrmongo.NewCommandMonitor(origMon)
client, err := mongo.Connect(ctx, options.Client().SetMonitor(nrMon))
Then add the current transaction to the context used in any MongoDB call.
ctx = newrelic.NewContext(context.Background(), txn)
resp, err := collection.InsertOne(ctx, bson.M{"name": "pi", "value": 3.14159})
Alternatively, you can manually create Datastore segments for each database call. Like basic segments, datastore segments start when the StartTime field is populated and end when the End method is called.
To monitor a datastore segment, you should add the following code snippet to the beginning of the function you want to monitor, as shown in the linked example.
s := newrelic.DatastoreSegment{
StartTime: txn.StartSegmentNow(),
Product: newrelic.DatastoreMySQL,
Collection: "users",
Operation: "INSERT",
ParameterizedQuery: "INSERT INTO users (name, age) VALUES ($1, $2)",
QueryParameters: map[string]interface{}{
"name": "Dracula",
"age": 439,
},
Host: "mysql-server-1",
PortPathOrID: "3306",
DatabaseName: "my_database",
}
defer s.End()
Here is the full sample code of Golang instrumenting a Database function,
func addAlbum(alb Album) (int64, error) {
result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
// Provide a context containing a newrelic. Transaction to all exec
// and query methods on sql.DB, sql.Conn, sql.Tx, and sql.Stmt.
txn := nrApp.StartTransaction("mysqlQuery")
ctx := newrelic.NewContext(context.Background(), txn)
row := db.QueryRowContext(ctx, "INSERT INTO album (title, artist, price) VALUES (?, ?, ?)")
row.Scan()
// Random sleep to simulate delays
randomDelay := rand.Intn(200)
time.Sleep(time.Duration(randomDelay) * time.Millisecond)
// End the DB transaction.
txn.End()
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
return id, nil
}
Tip 8: What about Goroutines?
Top 10 tips for instrumenting Golang with New Relic, part 3 | New Relic
Goroutines are a key feature in Go programming language that allows functions to run concurrently with other functions or methods. Goroutines are a lightweight alternative to traditional threads, making them an attractive option for concurrent programming in Go.
However, to effectively monitor and instrument a goroutine, you need to use the Transaction.NewGoroutine() transaction method. This method allows you to create a new reference to the transaction, which must be called any time you pass the transaction to another goroutine that makes segments.
To gain valuable insights into how your application is performing, it's important to track the performance of your concurrent code by instrumenting your goroutines. This is especially important for larger applications where concurrency can cause performance bottlenecks and other issues. You can see more details here on instrumenting Goroutines.
go func(txn *newrelic.Transaction) {
defer txn.StartSegment("goroutines").End()
time.Sleep(100 * time.Millisecond)
}(txn.NewGoroutine())
One more important suggestion for instrumenting your Golang code is to use a WaitGroup
, which is a valuable tool for managing concurrency. The WaitGroup waits for a collection of goroutines to finish before proceeding. The main goroutine calls Add to set the number of goroutines to wait for. Then, each of the goroutines runs and calls Done when finished. Finally, Wait can be used to block until all goroutines have finished. By using a WaitGroup, you can ensure that all of your goroutines have completed their tasks before moving on.
Here is an example where WaitGroup is used to wait for all the goroutines launched here to finish. Note that if a WaitGroup is explicitly passed into functions, it should be done by a pointer.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
i := i
// Goroutines
go func() {
defer wg.Done()
worker(i)
}()
}
wg.Wait()
}
Here is an example on how to instrument Goroutines with New Relic,
func async(w http.ResponseWriter, r *http.Request) {
// To access the transaction in your handler, use the newrelic.FromContext API.
txn := newrelic.FromContext(r.Context())
// This WaitGroup is used to wait for all the goroutines to finish.
wg := &sync.WaitGroup{}
println("goRoutines created!")
for i := 1; i < 9; i++ {
wg.Add(1)
i := i
// The Transaction.NewGoroutine() allows transactions to create segments
// in multiple goroutines.
defer wg.Done()
defer txn.StartSegment("goroutine" + strconv.Itoa(i)).End()
println("goRoutine " + strconv.Itoa(i))
randomDelay := rand.Intn(500)
time.Sleep(time.Duration(randomDelay) * time.Millisecond)
}(txn.NewGoroutine())
}
// Ensure the WaitGroup is done
segment := txn.StartSegment("WaitGroup")
wg.Wait()
segment.End()
w.Write([]byte("goRoutines success!"))
}
For more information, don't forget to visit the official page for the New Relic Go agent.
To read this full New Relic blog, click here.
Posted on November 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.