Top 10 tips for instrumenting Golang with New Relic, part 3

ntcsteve

ntcsteve

Posted on November 1, 2023

Top 10 tips for instrumenting Golang with New Relic, part 3

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:

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")
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

Image description

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())
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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!"))
}
Enter fullscreen mode Exit fullscreen mode

Image description

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.

💖 💪 🙅 🚩
ntcsteve
ntcsteve

Posted on November 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related